Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- By TheRareCarrot/REghZy >:)
- -- This is an implementation of the WPF layout system in lua
- -- The measure phase takes a constraint and measures the component (and child components too) to calculate the desired size
- -- The arrange phase positions the component (and child components) on screen, relative to the parent component
- -- The layout is a recursive operation, so it can affect performance if a layout is requested for a fairly top level component
- -- Rendering is pretty bad, as implementing a pixel buffer in computercraft would be expensive and so the entire component tree is re-drawn (but only when required (InvalidateVisual))
- local USE_DEBUG_TRACE = true
- local TRACE = {}
- local AppTimerId = 0
- local AppTimerTickRate = 10
- local AppTimerInterval = 1 / AppTimerTickRate
- local IsAppRunning = true
- local AppDispatcherId = 0
- ---@class AppRootComponent : UIComponent
- local AppRootComponent
- -- display sleep utils, to help with server performance
- local CanDisplayAutoSleep = true
- local IsDisplayHidden = false
- local LastDisplayWakeTime = 0
- -- ONLY USE IN EXCEPTIONAL CASES!!! THIS WILL EAT UP A MEGATON OF DISK
- local ALLOW_PRINT_DEBUG = false
- local ENABLE_RENDERING = true
- local IsRootRenderDirty = false
- local DebugFileHandle = nil
- local IsProcessingDispatcherQueue = false
- local TheLayoutUpdateTask = nil
- local ui = {}
- local maths = {}
- local toolkit = {}
- local CompositionTarget = term
- local CompositionOffsetX = 0
- local CompositionOffsetY = 0
- local CompositionWidth = 0
- local CompositionHeight = 0
- local CompositionComponentSource = nil
- local FocusedComponent = nil
- local MouseCapturedComponent = nil
- local NextDispatcherOperationId = 1
- local PriorityHighest = 1
- local PriorityLayout = 2
- local PriorityRender = 3
- local PriorityBackground = 4
- local Event_PreviewMouseDown = 1
- local Event_MouseDown = 2
- local Event_MouseDrag = 3
- local Event_Button_OnClick = 20
- local Event_TabItem_OnNowActive = 40
- local Event_PropertyChanged = 100
- -- stores the application root component objects
- local ForceArrangeOrMeasure = false
- local layout_arrange_queue = {}
- local layout_measure_queue = {}
- local dispatcher_queue = {
- [PriorityHighest] = {},
- [PriorityLayout] = {},
- [PriorityRender] = {},
- [PriorityBackground] = {},
- }
- local delayedTaskQueue = {}
- local ConfigTable = {}
- ---Defines a new type, using the given base type class
- ---@param baseType table? The base class
- ---@param readableTypeName string? A readable type name (usually the name of the table returned)
- ---@return table table The actual type
- function DefineType(baseType, readableTypeName)
- local theTable = {}
- theTable.__index = theTable
- if (baseType ~= nil) then
- setmetatable(theTable, baseType)
- theTable.__TheBaseType__ = baseType
- end
- theTable.ActualTypeString = readableTypeName or "<no typename>"
- if (baseType ~= nil and baseType.SetupDerivedType ~= nil) then
- baseType.SetupDerivedType(baseType, theTable)
- end
- return theTable
- end
- ---Creates a new instance of the given type
- ---@param theType table The type
- ---@param readableTypeName string? The readable type name (key as TypeName). Defaults to the type's ActualTypeString
- ---@return table table The new instance
- function NewTypeInstance(theType, readableTypeName)
- local baseType = theType.__TheBaseType__
- local obj
- if (baseType == nil or baseType.new == nil) then
- obj = {}
- else
- obj = baseType.new()
- end
- setmetatable(obj, theType)
- obj.TypeName = readableTypeName or theType.ActualTypeString
- obj.__TheType__ = theType
- obj.__TheBaseType__ = baseType
- return obj
- end
- --region Base Object
- ---@class BaseRZObject
- ---@field TypeName string?
- ---@field __TheType__ table
- ---@field __TheBaseType__ table
- local BaseRZObject = DefineType(nil, "BaseRZObject")
- ---@class DependencyObject : BaseRZObject
- ---@field PropValueTable table
- DependencyObject = DefineType(BaseRZObject, "DependencyObject")
- DependencyObject.PropTable = {}
- DependencyObject.PropTable.TotalEntries = 0
- function DependencyObject:SetupDerivedType(derivedType)
- derivedType.PropTable = {}
- derivedType.PropTable.TotalEntries = 0
- -- error("New derived: " .. self.ActualTypeString .. "->" .. derivedType.ActualTypeString .. ". " .. tostring(self.PropTable) .. " -> " .. tostring(derivedType.PropTable))
- end
- local PropertyNullWrapper = {}
- function DependencyObject.new()
- local obj = NewTypeInstance(DependencyObject, "DependencyObject")
- obj.PropValueTable = {}
- return obj
- end
- ---@alias PropertyChangeFlags
- ---| "AffectsMeasure"
- ---| "AffectsArrange"
- ---| "AffectsRender"
- ---@alias HorizontalAlignment
- ---| "left"
- ---| "center"
- ---| "right"
- ---| "stretch"
- ---@alias VerticalAlignment
- ---| "top"
- ---| "center"
- ---| "bottom"
- ---| "stretch"
- ---@alias Visibility
- ---| "visible"
- ---| "hidden"
- ---| "collapsed"
- ---@alias DockSide
- ---| "left"
- ---| "top"
- ---| "right"
- ---| "bottom"
- ---@alias EventHandler fun(self: any, time: number, args: table):boolean?
- ---Defines a new property for this type. DO NOT REGISTER FOR EACH INSTANCE OF OBJECT!
- ---@param name string The property name
- ---@param theType type The type of value stored
- ---@param defValue any The default value
- ---@param ... PropertyChangeFlags AffectsRender, AffectsMeasure, AffectsArrange
- function DependencyObject:RegProp(name, theType, defValue, ...)
- if (self.PropTable == nil) then
- error("Not a DependencyObject: " .. (self.TypeName or "<unknown type>"))
- end
- if (self.PropTable[name] ~= nil) then
- error("Property already exists: " .. name .. ". Self = " .. self.ActualTypeString)
- end
- if (self.__TheType__ ~= nil) then
- error("Properties must be registered on the object type table, not the instance")
- end
- local property = {
- Name = name,
- ValueType = theType,
- DefaultValue = defValue,
- Flags = {},
- ChangeHandlers = {}
- }
- if (arg ~= nil) then
- for i = 1, arg.n do
- property.Flags[arg[i]] = true
- end
- end
- self.PropTable[name] = property
- self.PropTable.TotalEntries = self.PropTable.TotalEntries + 1
- return property
- end
- ---Finds a property with a string name, or returns the property passed as a parameter
- ---@param self DependencyObject Ourself
- ---@param thePropertyOrName table|string
- ---@return table
- function DependencyObject:FindProperty(thePropertyOrName)
- if (type(thePropertyOrName) == "string") then
- local obj = self
- local property
- while (true) do
- property = obj.PropTable[thePropertyOrName]
- if (property ~= nil) then
- return property
- end
- obj = obj.__TheBaseType__
- if (obj == nil) then
- break
- end
- end
- error("No such property: " .. thePropertyOrName)
- elseif (thePropertyOrName == nil) then
- error("Property (or name) cannot be null")
- elseif (thePropertyOrName.Name == nil) then
- error("Invalid property object")
- end
- return thePropertyOrName
- end
- ---Adds a property changed handler. This has the same method signature as a
- ---regular event. These are fired before Event_PropertyChanged
- ---@param propName string Property Name
- ---@param onPropertyChangedHandler EventHandler The event handler function
- function DependencyObject:AddPropertyHandler(propName, onPropertyChangedHandler)
- local definition = self.PropTable[propName]
- if (definition == nil) then
- error("No such property: " .. propName)
- end
- table.insert(definition.ChangeHandlers, onPropertyChangedHandler)
- end
- function DependencyObject:GetPropValue(propertyOrName)
- local property = self:FindProperty(propertyOrName);
- local value = self.PropValueTable[property]
- if (value == nil) then
- return property.DefaultValue
- elseif (value == PropertyNullWrapper) then
- return nil
- else
- return value
- end
- end
- function DependencyObject:SetPropValue(propertyOrName, newValue)
- local property = self:FindProperty(propertyOrName);
- local oldValue = self.PropValueTable[property]
- if (oldValue == nil) then
- oldValue = property.DefaultValue
- end
- if (oldValue == newValue) then
- return
- end
- self.PropValueTable[property] = newValue or PropertyNullWrapper
- self:InternalOnPropValueChanged(property, oldValue, newValue, false)
- end
- function DependencyObject:ClearPropValue(propertyOrName)
- local property = self:FindProperty(propertyOrName);
- local oldValue = self.PropValueTable[property]
- if (oldValue == nil) then
- return
- end
- if (oldValue == PropertyNullWrapper) then
- oldValue = nil
- end
- self.PropValueTable[property] = nil
- self:InternalOnPropValueChanged(property, oldValue, nil, true)
- end
- function DependencyObject:InternalOnPropValueChanged(property, oldValue, newValue, isClearing)
- end
- --endregion
- --region UI Methods
- local function IndexForPixel(x, y)
- return (y - 1) * CompositionWidth + x
- end
- ---Sets the background colour
- ---@param the_bg integer? The colour
- function ui.setBackgroundColour(the_bg)
- if (the_bg ~= nil) then
- CompositionTarget.setBackgroundColour(the_bg)
- end
- end
- ---Sets the text colour
- ---@param the_tc integer? The colour
- function ui.setTextColour(the_tc)
- if (the_tc ~= nil) then
- CompositionTarget.setTextColour(the_tc)
- end
- end
- ---Clears the target
- function ui.clear()
- CompositionTarget.setBackgroundColour(colours.black)
- CompositionTarget.setTextColour(colours.white)
- CompositionTarget.clear()
- CompositionTarget.setCursorPos(1, 1)
- end
- ---Raw function for writing text at a position (just calls setCursorPos and write)
- ---@param x integer The X pos to write text at
- ---@param y integer The Y pos to write text at
- ---@param text string The text to write
- function ui.writeTextAt(x, y, text)
- CompositionTarget.setCursorPos(x + CompositionOffsetX, y + CompositionOffsetY)
- CompositionTarget.write(text)
- end
- ---Fills a region with a colour
- ---@param x integer The X pos to begin drawing at
- ---@param y integer The Y pos to begin drawing at
- ---@param width integer The width of the square
- ---@param height integer The height of the square
- ---@param bg_colour integer? The colour to draw
- function ui.fill(x, y, width, height, bg_colour)
- ui.setBackgroundColour(bg_colour)
- local text = string.rep(" ", width)
- for i = 0, height - 1 do
- ui.writeTextAt(x, y + i, text)
- end
- end
- ---Writes text at the given position with a given text and background colour
- ---@param x integer The X pos to write text at
- ---@param y integer The Y pos to write text at
- ---@param text string The text to write
- ---@param text_colour integer? The text colour
- ---@param bg_colour integer? The background colour
- function ui.drawText(x, y, text, text_colour, bg_colour)
- ui.setTextColour(text_colour)
- ui.setBackgroundColour(bg_colour)
- ui.writeTextAt(x, y, text)
- end
- ---Writes text at the given position with a given text and background colour
- ---@param x integer The X pos to write text at
- ---@param y integer The Y pos to write text at
- ---@param text string The text to write
- ---@param text_colour integer? The text colour
- ---@param bg_colour integer? The background colour
- function ui.drawTextCentered(x, y, width, height, text, text_colour, bg_colour)
- local absX = math.floor((width / 2) - (#text / 2))
- local absY = math.floor(height / 2)
- ui.drawText(x + absX, y + absY, text, text_colour, bg_colour)
- end
- ---Draws a horizontal line
- ---@param x integer The X pos to start the line at
- ---@param y integer The Y pos to start the line at
- ---@param length integer The length of the line
- ---@param colour integer? The colour of the line
- function ui.drawLineH(x, y, length, colour)
- ui.setBackgroundColour(colour)
- ui.writeTextAt(x, y, string.rep(" ", length))
- end
- ---Draws a vertical line
- ---@param x integer The X pos to start the line at
- ---@param y integer The Y pos to start the line at
- ---@param height integer The height of the line
- ---@param colour integer? The colour of the line
- function ui.drawLineV(x, y, height, colour)
- x = x + CompositionOffsetX
- y = y + CompositionOffsetY
- ui.setBackgroundColour(colour)
- for i = 1, height, 1 do
- CompositionTarget.setCursorPos(x, y + i)
- CompositionTarget.write(" ")
- end
- end
- local function internal_calc_progress_bar(length, min, max, value)
- return math.floor((maths.clamp(value, min, max) / (max - min)) * length)
- end
- ---Draws a progress bar
- ---@param x integer The X pos to start drawing
- ---@param y integer The Y pos to start drawing
- ---@param length integer The length of the progress bar
- ---@param min number The minimum value
- ---@param max number The maximum value
- ---@param value number The value
- ---@param bg_colour integer? The background colour (non-value)
- ---@param val_colour integer? The foreground aka value colour
- ---@param rightToLeft boolean? True to draw the progress right to left instead of left to right
- function ui.drawProgressBarH(x, y, length, min, max, value, bg_colour, val_colour, rightToLeft)
- local draw_len = internal_calc_progress_bar(length, min, max, value)
- ui.drawLineH(draw_len + 1, y, length - draw_len, bg_colour)
- if (rightToLeft == true) then
- ui.drawLineH(x + length - draw_len, y, draw_len, val_colour)
- else
- ui.drawLineH(x, y, draw_len, val_colour)
- end
- end
- ---Draws a progress bar
- ---@param x integer The X pos to start drawing
- ---@param y integer The Y pos to start drawing
- ---@param height integer The height of the progress bar
- ---@param min number The minimum value
- ---@param max number The maximum value
- ---@param value number The value
- ---@param bg_colour integer? The background colour (non-value)
- ---@param val_colour integer? The foreground aka value colour
- ---@param bottomToTop boolean? True to draw the progress bottom to top instead of top to bottom
- function ui.drawProgressBarV(x, y, height, min, max, value, bg_colour, val_colour, bottomToTop)
- ui.drawLineV(x, y, height, bg_colour)
- local draw_len = internal_calc_progress_bar(height, min, max, value)
- if (bottomToTop ~= nil and bottomToTop == true) then
- ui.drawLineV(x, y + height - draw_len, draw_len, val_colour)
- else
- ui.drawLineV(x, y, draw_len, val_colour)
- end
- end
- --endregion
- --region Utils
- function PushFunction(funcName, ...)
- if (USE_DEBUG_TRACE) then
- table.insert(TRACE, {FunctionName = funcName, Args = {...}})
- end
- end
- function PushFunctionAndInvoke(funcName, func, ...)
- PushFunction(funcName)
- return PopFunction(func(...))
- end
- function PopFunction(...)
- if (USE_DEBUG_TRACE and #TRACE > 0) then
- table.remove(TRACE, #TRACE)
- end
- if (... ~= nil) then
- return ...
- end
- end
- function PrintStackTrace()
- for i = 1, #TRACE, 1 do
- local info = TRACE[i]
- local text = info.FunctionName
- if (#info.Args > 0) then
- local strtab = {}
- for key, value in pairs(info.Args) do
- if (type(key) == "number") then
- table.insert(strtab, tostring(value))
- else
- table.insert(strtab, tostring(key) .. "=" .. tostring(value))
- end
- end
- text = text .. ": " .. table.concat(strtab, ", ")
- end
- print(text)
- PrintDebug(text)
- end
- end
- function PrintDebug(msg, ...)
- if (ALLOW_PRINT_DEBUG and DebugFileHandle ~= nil) then
- local args = {...}
- if (args ~= nil and #args > 0) then
- for i = 1, #args do
- local theArg = args[i]
- if (theArg == nil) then
- args[i] = "nil"
- else
- local argType = type(theArg)
- if (argType == "boolean" or argType == "function" or argType == "table") then
- args[i] = tostring(theArg)
- end
- end
- end
- msg = string.format(msg, unpack(args))
- end
- DebugFileHandle.writeLine(msg)
- end
- end
- function maths.min(...)
- local min = 0
- for i = 1, arg.n, 1 do
- min = math.min(#arg[i], min)
- end
- return min
- end
- function maths.max(...)
- local max = 0
- for i = 1, arg.n, 1 do
- max = math.max(#arg[i], max)
- end
- return max
- end
- function maths.clamp(value, min, max)
- if (value < min) then return min end
- if (value > max) then return max end
- return value
- end
- function maths.isNaN(val)
- return val ~= val
- end
- function UpdateCachedIndices(children)
- for k, obj in pairs(children) do
- obj.IndexInParent = k
- end
- end
- function ClearTable(theTable)
- for i = #theTable, 1, -1 do
- table.remove(theTable, i)
- end
- end
- --endregion
- --region Dispatcher utils
- local DispatcherOperation = DefineType(BaseRZObject, "DispatcherOperation")
- function DispatcherOperation.new(theMethod, ...)
- local id = NextDispatcherOperationId
- NextDispatcherOperationId = id + 1
- local operation = NewTypeInstance(DispatcherOperation)
- operation.Id = id
- operation.IsCompleted = false
- operation.Method = theMethod
- operation.Args = {...}
- return operation
- end
- function InvokeAsync(theMethod, priority, ...)
- local operation = DispatcherOperation.new(theMethod, ...)
- if (priority == nil) then
- priority = PriorityBackground
- end
- table.insert(dispatcher_queue[priority], operation)
- if (AppDispatcherId == 0) then
- AppDispatcherId = os.startTimer(0)
- end
- return operation
- end
- function InvokeAsyncWithDelay(theMethod, delay_seconds, ...)
- local operation = DispatcherOperation.new(theMethod, ...)
- operation.TimeUntilExecution = os.clock() + delay_seconds
- table.insert(delayedTaskQueue, operation)
- end
- --endregion
- --region UI Toolkit
- ---@class UIComponent : DependencyObject
- ---@field DesiredWidth number
- ---@field DesiredHeight number
- ---@field RenderPosX number
- ---@field RenderPosY number
- ---@field RenderWidth number
- ---@field RenderHeight number
- ---@field _internal_Visibility Visibility
- ---@field _internal_FinalVisibility Visibility
- ---@field IsVisualDirty boolean
- ---@field IsMeasureDirty boolean
- ---@field IsArrangeDirty boolean
- ---@field NeverMeasured boolean
- ---@field NeverArranged boolean
- ---@field MeasureInProgress boolean
- ---@field ArrangeInProgress boolean
- ---@field MeasureDuringArrange boolean
- ---@field LastMeasureAvailableWidth number
- ---@field LastMeasureAvailableHeight number
- ---@field LastArrangeFinalX number
- ---@field LastArrangeFinalY number
- ---@field LastArrangeFinalW number
- ---@field LastArrangeFinalH number
- ---@field IsVisualPositionValid boolean
- ---@field Children UIComponent[]
- ---@field IndexInParent integer
- ---@field Parent UIComponent
- ---@field IdToChildTable table<string,UIComponent>
- ---@field EventHandlers table<EventHandler>
- UIComponent = DefineType(DependencyObject, "UIComponent")
- UIComponent.MarginLProperty = DependencyObject.RegProp(UIComponent, "MarginL", "number", 0, "AffectsArrange")
- UIComponent.MarginTProperty = DependencyObject.RegProp(UIComponent, "MarginT", "number", 0, "AffectsArrange")
- UIComponent.MarginRProperty = DependencyObject.RegProp(UIComponent, "MarginR", "number", 0, "AffectsArrange")
- UIComponent.MarginBProperty = DependencyObject.RegProp(UIComponent, "MarginB", "number", 0, "AffectsArrange")
- UIComponent.HorizontalAlignmentProperty = DependencyObject.RegProp(UIComponent, "HorizontalAlignment", "string", "left", "AffectsArrange")
- UIComponent.VerticalAlignmentProperty = DependencyObject.RegProp(UIComponent, "VerticalAlignment", "string", "top", "AffectsArrange")
- UIComponent.WidthProperty = DependencyObject.RegProp(UIComponent, "Width", "number", nil, "AffectsMeasure")
- UIComponent.HeightProperty = DependencyObject.RegProp(UIComponent, "Height", "number", nil, "AffectsMeasure")
- UIComponent.MinWidthProperty = DependencyObject.RegProp(UIComponent, "MinWidth", "number", 0, "AffectsMeasure")
- UIComponent.MinHeightProperty = DependencyObject.RegProp(UIComponent, "MinHeight", "number", 0, "AffectsMeasure")
- UIComponent.MaxWidthProperty = DependencyObject.RegProp(UIComponent, "MaxWidth", "number", math.huge, "AffectsMeasure")
- UIComponent.MaxHeightProperty = DependencyObject.RegProp(UIComponent, "MaxHeight", "number", math.huge, "AffectsMeasure")
- UIComponent.IsFocusableProperty = DependencyObject.RegProp(UIComponent, "IsFocusable", "boolean", false)
- UIComponent.IsFocusedProperty = DependencyObject.RegProp(UIComponent, "IsFocused", "boolean", false)
- UIComponent.BackgroundProperty = DependencyObject.RegProp(UIComponent, "Background", "number", nil, "AffectsRender")
- ---@class Button : UIComponent
- ---@field IsPressed boolean
- ---@field IsToggleButton boolean
- ---@field Text string
- ---@field DoNotProcessButtonClickLogic boolean
- Button = DefineType(UIComponent, "Button")
- Button.TextProperty = DependencyObject.RegProp(Button, "Text", "string", "Text", "AffectsMeasure", "AffectsRender")
- Button.PressedBackgroundProperty = DependencyObject.RegProp(Button, "PressedBackground", "number", colours.lightBlue, "AffectsRender")
- ---@class TextBlock : UIComponent
- TextBlock = DefineType(UIComponent, "TextBlock")
- TextBlock.TextProperty = DependencyObject.RegProp(TextBlock, "Text", "string", "Text", "AffectsMeasure", "AffectsRender")
- TextBlock.TextColourProperty = DependencyObject.RegProp(TextBlock, "TextColour", "string", colours.white, "AffectsRender")
- ---@class UniformPanel : UIComponent
- UniformPanel = DefineType(UIComponent, "UniformPanel")
- UniformPanel.OrientationProperty = DependencyObject.RegProp(UniformPanel, "Orientation", "string", "horizontal")
- UniformPanel.SpacingProperty = DependencyObject.RegProp(UniformPanel, "Spacing", "number", 0)
- ---@class HorizontalStackPanel : UIComponent
- HorizontalStackPanel = DefineType(UIComponent, "HorizontalStackPanel")
- ---@class VerticalStackPanel : UIComponent
- VerticalStackPanel = DefineType(UIComponent, "VerticalStackPanel")
- ---@class DockPanel : UIComponent
- DockPanel = DefineType(UIComponent, "DockPanel")
- ---@class ContentPresenter : UIComponent
- ContentPresenter = DefineType(UIComponent, "ContentPresenter")
- ---@class ContentPresenter : UIComponent
- ---@field MyContentPresenter ContentPresenter
- local TabControl = DefineType(UIComponent, "TabControl")
- ---@class ProgressBar : UIComponent
- ProgressBar = DefineType(UIComponent, "ProgressBar")
- ProgressBar.ValueColourProperty = DependencyObject.RegProp(ProgressBar, "ValueColour", "number", colours.lightBlue, "AffectsRender")
- ProgressBar.ValueProperty = DependencyObject.RegProp(ProgressBar, "Value", "number", 0, "AffectsRender")
- ProgressBar.MinimumProperty = DependencyObject.RegProp(ProgressBar, "Minimum", "number", 0, "AffectsRender")
- ProgressBar.MaximumProperty = DependencyObject.RegProp(ProgressBar, "Maximum", "number", 1, "AffectsRender")
- ---@class DualColourTextBlock : UIComponent
- ---@field LeftText string
- ---@field RightText string
- ---@field LeftColour number
- ---@field RightColour number
- local DualColourTextBlock = DefineType(UIComponent, "DualColourTextBlock")
- ---@class BasePageComponent : UIComponent
- local BasePageComponent = DefineType(UIComponent, "BasePageComponent")
- ---@class HomePageComponent : BasePageComponent
- ---@field PowerLabel DualColourTextBlock
- ---@field FuelLevelLabel DualColourTextBlock
- ---@field FuelLevelProgBar ProgressBar
- ---@field FuelTempLabel DualColourTextBlock
- ---@field FuelTempProgBar ProgressBar
- ---@field CasingTempLabel DualColourTextBlock
- ---@field CasingTempProgBar ProgressBar
- ---@field EnergyLabel DualColourTextBlock
- ---@field StoredLabel DualColourTextBlock
- ---@field StoredProgBar ProgressBar
- ---@field ControlRodsLabel DualColourTextBlock
- ---@field ControlBtn_Auto Button
- ---@field ControlBtn_On Button
- ---@field ControlBtn_Off Button
- local HomePageComponent = DefineType(BasePageComponent, "HomePageComponent")
- ---@class RodsPageComponent : BasePageComponent
- local RodsPageComponent = DefineType(BasePageComponent, "RodsPageComponent")
- ---@class AutoPageComponent : BasePageComponent
- local AutoPageComponent = DefineType(BasePageComponent, "AutoPageComponent")
- ---@class InfoPageComponent : BasePageComponent
- local InfoPageComponent = DefineType(BasePageComponent, "InfoPageComponent")
- ---Adds an event handler (of the given event type) to this component
- ---@param type number The event type
- ---@param handler EventHandler The handler. Parameters are self,time,args
- function UIComponent:AddEventHandler(type, handler)
- PushFunction("AddEventHandler", self.TypeName)
- local myHandlerTable = self.EventHandlers[type]
- if (myHandlerTable == nil) then
- myHandlerTable = {}
- self.EventHandlers[type] = myHandlerTable
- end
- table.insert(myHandlerTable, handler)
- PopFunction()
- end
- ---Raises an event of the given type, using the given time and args as parameters (and ourself obviously)
- ---@param type number The event type
- ---@param time number The os clock time
- ---@param args table The event args
- ---@return boolean boolean The event args handled state
- function UIComponent:RaiseEvent(type, time, args)
- PushFunction("RaiseEvent", self.TypeName)
- local tab = self.EventHandlers[type]
- if (tab ~= nil) then
- if (time == nil) then time = os.clock() end
- for i, handler in ipairs(tab) do
- if (handler(self, time, args) == true) then
- args.handled = true
- end
- end
- end
- return PopFunction(args.handled)
- end
- function UIComponent:InternalOnPropValueChanged(property, oldValue, newValue, isClearing)
- if (property.Flags["AffectsMeasure"] == true) then
- self:InvalidateMeasure()
- end
- if (property.Flags["AffectsArrange"] == true) then
- self:InvalidateArrange()
- end
- if (property.Flags["AffectsRender"] == true) then
- self:InvalidateVisual()
- end
- DependencyObject.InternalOnPropValueChanged(self, property, oldValue, newValue, isClearing)
- local time = os.clock()
- local args = {OldValue = oldValue, NewValue = newValue, Property = property}
- for i, handler in ipairs(property.ChangeHandlers) do
- handler(self, time, args)
- end
- self:RaiseEvent(Event_PropertyChanged, time, args)
- end
- ---new UIComponent
- function UIComponent.new()
- local obj = NewTypeInstance(UIComponent, "UIComponent")
- obj.DesiredWidth = 0 -- measured width based on Width and children
- obj.DesiredHeight = 0 -- measured height based on Height and children
- obj.RenderPosX = 0 -- arranged render X (relative to parent duh)
- obj.RenderPosY = 0 -- arranged render Y (relative to parent duh)
- obj.RenderWidth = 0 -- arranged render width
- obj.RenderHeight = 0 -- arranged render height
- obj._internal_Visibility = "visible" -- our target visibility
- obj._internal_FinalVisibility = "visible" -- out final visibility, based on self and parent tree
- obj.IsVisualDirty = false -- true when this component and its hierarchy requires drawing
- obj.IsMeasureDirty = false
- obj.IsArrangeDirty = false
- obj.NeverMeasured = true
- obj.NeverArranged = true
- obj.MeasureInProgress = false
- obj.ArrangeInProgress = false
- obj.MeasureDuringArrange = false
- obj.LastMeasureAvailableWidth = 0
- obj.LastMeasureAvailableHeight = 0
- obj.LastArrangeFinalX = 0
- obj.LastArrangeFinalY = 0
- obj.LastArrangeFinalW = 0
- obj.LastArrangeFinalH = 0
- obj.IsVisualPositionValid = false
- obj.Children = {} -- a table of child components
- obj.IndexInParent = -1 -- the index of this component in its parent
- obj.Parent = nil -- our parent object
- obj.IdToChildTable = {}
- obj.EventHandlers = {}
- return obj
- end
- function UIComponent:SetVisibility(visible)
- self._internal_Visibility = visible
- self:UpdateVisibility()
- self:InvalidateLayoutAndVisual()
- end
- function UIComponent:GetVisibility(useNonInherited)
- if (useNonInherited == true) then
- return self._internal_Visibility
- else
- return self._internal_FinalVisibility
- end
- end
- function UIComponent:UpdateVisibility()
- if (self.Parent == nil) then
- self._internal_FinalVisibility = self._internal_Visibility
- else
- local parent_vis = self.Parent._internal_FinalVisibility
- if (parent_vis == "visible") then
- self._internal_FinalVisibility = self._internal_Visibility
- else
- self._internal_FinalVisibility = parent_vis
- end
- end
- for i, child in ipairs(self.Children) do
- child:UpdateVisibility()
- end
- end
- ---Sets the component's margins
- ---@generic T: UIComponent
- ---@param self T
- ---@param left number? The left margin
- ---@param top number? The top margin
- ---@param right number? The right margin
- ---@param bottom number? The bottom margin
- ---@return T
- function UIComponent:SetMargin(left, top, right, bottom)
- if (left ~= nil) then DependencyObject.SetPropValue(self, UIComponent.MarginLProperty, left) end
- if (top ~= nil) then DependencyObject.SetPropValue(self, UIComponent.MarginTProperty, top) end
- if (right ~= nil) then DependencyObject.SetPropValue(self, UIComponent.MarginRProperty, right) end
- if (bottom ~= nil) then DependencyObject.SetPropValue(self, UIComponent.MarginBProperty, bottom) end
- return self
- end
- ---Sets the alignments for this component
- ---@generic T: UIComponent
- ---@param self T
- ---@param horizontal HorizontalAlignment? The horizontal alignment
- ---@param vertical VerticalAlignment? The horizontal alignment
- ---@return T
- function UIComponent:SetAlignment(horizontal, vertical)
- if (horizontal ~= nil) then DependencyObject.SetPropValue(self, UIComponent.HorizontalAlignmentProperty, horizontal) end
- if (vertical ~= nil) then DependencyObject.SetPropValue(self, UIComponent.VerticalAlignmentProperty, vertical) end
- return self
- end
- ---Sets the width and height of this component
- ---@generic T: UIComponent
- ---@param self T
- ---@param width number? The new width
- ---@param height number? The new height
- ---@return T
- function UIComponent:SetSize(width, height)
- if (width ~= nil) then DependencyObject.SetPropValue(self, UIComponent.WidthProperty, width) end
- if (height ~= nil) then DependencyObject.SetPropValue(self, UIComponent.HeightProperty, height) end
- return self
- end
- ---Creates a new uniform panel
- ---@return UniformPanel
- function UniformPanel.new()
- local obj = NewTypeInstance(UniformPanel, "UniformPanel")
- return obj
- end
- function HorizontalStackPanel.new()
- return NewTypeInstance(HorizontalStackPanel, "HorizontalStackPanel")
- end
- ---Creates a new vertical stack panel
- ---@return VerticalStackPanel panel The stack panel returned
- function VerticalStackPanel.new()
- return NewTypeInstance(VerticalStackPanel, "VerticalStackPanel")
- end
- function ProgressBar.new(min, max, val, bg_colour, val_colour)
- local progBar = NewTypeInstance(ProgressBar, "ProgressBar")
- progBar:SetPropValue(ProgressBar.MinimumProperty, min or 0)
- progBar:SetPropValue(ProgressBar.MaximumProperty, max or 1)
- progBar:SetPropValue(ProgressBar.ValueProperty, val or 0)
- DependencyObject.SetPropValue(progBar, UIComponent.BackgroundProperty, bg_colour or colours.grey)
- DependencyObject.SetPropValue(progBar, ProgressBar.ValueColourProperty, val_colour or colours.lightBlue)
- progBar:SetSize(nil, 1)
- return progBar
- end
- function ProgressBar:SetValue(new_value, val_colour)
- if (val_colour ~= nil) then
- self:SetPropValue(ProgressBar.ValueColourProperty, val_colour)
- end
- self:SetPropValue(ProgressBar.ValueProperty, new_value)
- end
- function ProgressBar:OnRender()
- local min = self:GetPropValue(ProgressBar.MinimumProperty) or 0
- local max = self:GetPropValue(ProgressBar.MaximumProperty) or 1
- local value = self:GetPropValue(ProgressBar.ValueProperty) or 0
- ui.drawProgressBarH(1, 1, self.RenderWidth, min, max, value, self:GetPropValue(UIComponent.BackgroundProperty), self:GetPropValue(ProgressBar.ValueColourProperty))
- end
- function DockPanel.new()
- local obj = NewTypeInstance(DockPanel, "DockPanel")
- obj.LastChildFill = true
- return obj
- end
- function ContentPresenter.new()
- local obj = NewTypeInstance(ContentPresenter, "ContentPresenter")
- return obj
- end
- function TabControl:SetActiveTabItem(tabItem)
- if (tabItem ~= nil and tabItem.IsActive == true) then
- return
- end
- local presenter = self.MyContentPresenter
- local lastItem = TabControl.ActiveTabItem
- if (lastItem ~= nil) then
- lastItem:SetPropValue(UIComponent.BackgroundProperty, lastItem.BackgroundBeforeNotActive)
- lastItem.BackgroundBeforeNotActive = nil
- lastItem.IsActive = false
- if (lastItem.PageContent ~= nil) then
- lastItem.PageContent:SetVisibility("collapsed")
- end
- lastItem:InvalidateLayoutAndVisual()
- TabControl.ActiveTabItem = nil
- end
- if (tabItem ~= nil) then
- tabItem.BackgroundBeforeNotActive = tabItem:GetPropValue(UIComponent.BackgroundProperty)
- tabItem:SetPropValue(UIComponent.BackgroundProperty, colours.lightBlue)
- tabItem.IsActive = true
- tabItem:InvalidateLayoutAndVisual()
- TabControl.ActiveTabItem = tabItem
- if (tabItem.PageContent ~= nil) then
- presenter:ClearChildren()
- presenter:InsertChild(tabItem.PageContent)
- tabItem.PageContent:SetVisibility("visible")
- end
- tabItem:RaiseEvent(Event_TabItem_OnNowActive, os.clock(), {})
- end
- self:InvalidateLayoutAndVisual()
- end
- function OnTabItemClicked(self, time, args)
- local tabPanel = self.Parent
- if (tabPanel == nil) then
- return
- end
- local tabControl = tabPanel.Parent
- if (tabControl == nil) then
- return
- end
- tabControl:SetActiveTabItem(self)
- end
- ---Creates a new button
- ---@param readableTypeName string? Readable type name
- ---@param buttonText string? Button text
- ---@return Button
- function Button.new(readableTypeName, buttonText)
- local obj = NewTypeInstance(Button, readableTypeName or "Button")
- obj.IsPressed = false
- obj.IsToggleButton = false
- obj:SetPropValue(Button.TextProperty, buttonText)
- obj:SetPropValue(UIComponent.IsFocusableProperty, true)
- obj:SetPropValue(UIComponent.BackgroundProperty, colours.grey)
- obj:SetPropValue(Button.PressedBackgroundProperty, colours.green)
- obj.DoNotProcessButtonClickLogic = false
- UIComponent.AddEventHandler(obj, Event_MouseDown,Button.OnMouseDown)
- return obj
- end
- function NewTabItem(text, pageContent)
- local btn = Button.new("TabItem:" .. text, text)
- btn:SetPropValue(UIComponent.HorizontalAlignmentProperty, "stretch")
- btn:AddEventHandler(Event_Button_OnClick, OnTabItemClicked)
- btn:SetPropValue(UIComponent.BackgroundProperty, colours.blue)
- btn.PageContent = pageContent
- btn.DoNotProcessButtonClickLogic = true
- return btn
- end
- function Button:SetPressed(isPressed)
- if (self.IsPressed == isPressed) then
- return
- end
- self.IsPressed = isPressed
- self:InvalidateVisual()
- end
- function TextBlock.new(text, text_colour, bg_colour)
- local obj = NewTypeInstance(TextBlock, "TextBlock")
- obj:SetPropValue(TextBlock.TextProperty, text)
- if (text_colour ~= nil) then
- obj:SetPropValue(TextBlock.TextColourProperty, text_colour)
- end
- if (bg_colour ~= nil) then
- obj:SetPropValue(UIComponent.BackgroundProperty, bg_colour)
- end
- return obj
- end
- function TextBlock:MeasureOverride(available_width, available_height)
- local text = self:GetPropValue(TextBlock.TextProperty)
- if (text ~= nil) then
- local charCount = #text
- if (charCount <= available_width) then
- return charCount,1
- else
- return available_width,math.ceil(charCount / available_width)
- end
- else
- return 0, 1
- end
- end
- function TextBlock:ArrangeOverride(width, height)
- local text = self:GetPropValue(TextBlock.TextProperty)
- if (text == nil) then
- self.InternalLines = nil
- return width,height
- end
- local lines = {}
- local j = 1
- local k = width
- local line = 0
- while true do
- line = line + 1
- if (line > height) then
- break
- end
- table.insert(lines, string.sub(text, j, k))
- j = k + 1
- k = k + width
- end
- self.InternalLines = lines
- return width, height
- end
- function TextBlock:OnRender()
- UIComponent.OnRender(self)
- if (self.InternalLines ~= nil) then
- local txt_col = self:GetPropValue(TextBlock.TextColourProperty) or colours.white
- -- local bg_col = self:GetPropValue(UIComponent.BackgroundProperty) or colours.black
- for i, str in ipairs(self.InternalLines) do
- ui.drawText(1, i, str, txt_col, nil)
- end
- end
- end
- ---Gets the final minimum and maximum size bounds, taking into account the width and height values
- ---@return number number min width
- ---@return number number max width
- ---@return number number min height
- ---@return number number max height
- function UIComponent:GetMinMax()
- local min_w,max_w = self:GetPropValue(UIComponent.MinWidthProperty),self:GetPropValue(UIComponent.MaxWidthProperty)
- local min_h,max_h = self:GetPropValue(UIComponent.MinHeightProperty),self:GetPropValue(UIComponent.MaxHeightProperty)
- local w,h = self:GetPropValue(UIComponent.WidthProperty), self:GetPropValue(UIComponent.HeightProperty)
- max_w = math.max(math.min(w or math.huge, max_w), min_w)
- min_w = math.max(math.min(max_w, w or 0), min_w)
- max_h = math.max(math.min(h or math.huge, max_h), min_h)
- min_h = math.max(math.min(max_h, h or 0), min_h)
- return min_w, max_w, min_h, max_h
- end
- function UIComponent:GetRenderPositon()
- PushFunction("GetRenderPositon", self.TypeName)
- if (self.IsVisualPositionValid) then
- return PopFunction(self.AbsVisualPosX or 1,self.AbsVisualPosY or 1)
- end
- if (self.IsArrangeDirty) then
- error("Arrangement is dirty; cannot get absolute position")
- end
- local chain = {}
- local next_component = self
- while (next_component ~= nil) do
- table.insert(chain, 1, next_component)
- next_component = next_component.Parent
- end
- local x,y = 0,0
- for i, component in ipairs(chain) do
- x = x + component.RenderPosX
- y = y + component.RenderPosY
- end
- self.AbsVisualPosX = x
- self.AbsVisualPosY = y
- self.IsVisualPositionValid = true
- return PopFunction(x,y)
- end
- function UIComponent:OnRender()
- local bg = self:GetPropValue(UIComponent.BackgroundProperty)
- if (bg ~= nil) then
- ui.fill(1, 1, self.RenderWidth, self.RenderHeight, bg)
- end
- end
- ---Measures this component (and any child components), and updates (and also returns) the desired size of this component
- ---@param available_width number The constrated width
- ---@param available_height number The constrated height
- ---@return number Desired Width
- ---@return number Desired Height
- function UIComponent:Measure(available_width, available_height)
- if (available_width == nil) then error("Cannot measure with nil width") end
- if (available_height == nil) then error("Cannot measure with nil height") end
- if (self:GetVisibility() == "collapsed") then
- self.LastMeasureAvailableWidth = available_width
- self.LastMeasureAvailableHeight = available_height
- if (ALLOW_PRINT_DEBUG) then
- PrintDebug("Skipping measure for %s as it is collapsed", self.TypeName)
- end
- else
- -- no need to measure again if it's already the same
- local isEqualToLastSize = available_width == self.LastMeasureAvailableWidth and available_height == self.LastMeasureAvailableHeight
- if (self.IsMeasureDirty or self.NeverMeasured or not isEqualToLastSize) then
- if (ALLOW_PRINT_DEBUG) then
- PrintDebug("Begin Measure '%s' (forced = %s, dirty = %s, NeverMeasured = %s, similar = %s)", self.TypeName, ForceArrangeOrMeasure, self.IsMeasureDirty, self.NeverMeasured, isEqualToLastSize)
- end
- self.NeverMeasured = false
- self.LastMeasureAvailableWidth = available_width
- self.LastMeasureAvailableHeight = available_height
- self.MeasureInProgress = true
- local w, h = self:MeasureCore(available_width, available_height)
- self.MeasureInProgress = false
- if (w == nil) then error("MeasureCore returned null width") end
- if (h == nil) then error("MeasureCore returned null height") end
- self.DesiredWidth = w
- self.DesiredHeight = h
- self.IsMeasureDirty = false
- if (ALLOW_PRINT_DEBUG) then
- PrintDebug("End Measure '%s' (%f x %f)", self.TypeName, w, h)
- end
- else
- if (ALLOW_PRINT_DEBUG) then
- PrintDebug("Skipping measure for %s", self.TypeName)
- end
- end
- end
- return self.DesiredWidth,self.DesiredHeight
- end
- function UIComponent:MeasureCore(available_width, available_height)
- local margin_width = self:GetPropValue(UIComponent.MarginLProperty) + self:GetPropValue(UIComponent.MarginRProperty)
- local margin_height = self:GetPropValue(UIComponent.MarginTProperty) + self:GetPropValue(UIComponent.MarginBProperty)
- local min_width, max_width, min_height, max_height = self:GetMinMax()
- local desired_width = maths.clamp(math.max(available_width - margin_width, 0), min_width, max_width)
- local desired_height = maths.clamp(math.max(available_height - margin_height, 0), min_height, max_height)
- desired_width, desired_height = self:MeasureOverride(desired_width, desired_height)
- desired_width, desired_height = math.max(desired_width, min_width), math.max(desired_height, min_height)
- local real_measured_width,real_measured_height = desired_width,desired_height
- local isClipRequired = false
- if (desired_width > max_width) then
- desired_width = max_width
- isClipRequired = true
- end
- if (desired_height > max_height) then
- desired_height = max_height
- isClipRequired = true
- end
- local final_width, final_height = desired_width + margin_width, desired_height + margin_height
- if (final_width > available_width) then
- final_width = available_width
- isClipRequired = true
- end
- if (final_height > available_height) then
- final_height = available_height
- isClipRequired = true
- end
- local unclipped_desired_size = self.UnclippedDesiredSize
- if (isClipRequired or final_width < 0 or final_height < 0) then
- if (unclipped_desired_size == nil) then
- unclipped_desired_size = {}
- self.UnclippedDesiredSize = unclipped_desired_size
- end
- unclipped_desired_size.w = real_measured_width
- unclipped_desired_size.h = real_measured_height
- elseif (unclipped_desired_size ~= nil) then
- self.UnclippedDesiredSize = nil
- end
- final_width = math.max(final_width, 0)
- final_height = math.max(final_height, 0)
- return final_width,final_height
- end
- ---The overridable method for measuring this component. Returns 0,0 for an empty component
- ---@param available_width number The maximum width
- ---@param available_height number The maximum height
- ---@return number number Desired width
- ---@return number number Desired height
- function UIComponent:MeasureOverride(available_width, available_height)
- local w, h = 0,0
- for i, child in ipairs(self.Children) do
- local cW, cH = child:Measure(available_width, available_height)
- if (cW > w) then w = cW end
- if (cH > h) then h = cH end
- end
- return w, h
- end
- function UIComponent:SetLastArrangeRect(final_x, final_y, final_w, final_h)
- self.LastArrangeFinalX = final_x
- self.LastArrangeFinalY = final_y
- self.LastArrangeFinalW = final_w
- self.LastArrangeFinalH = final_h
- end
- ---Arranges the position and calculates the render size for this component and any child components
- ---@param posX number The rendering X offset (0 by default)
- ---@param posY number The rendering Y offset (0 by default)
- ---@param finalWidth number
- ---@param finalHeight number
- function UIComponent:Arrange(posX, posY, finalWidth, finalHeight)
- if (self:GetVisibility() == "collapsed") then
- if (ALLOW_PRINT_DEBUG) then
- PrintDebug("Skipping arrange for %s as it is collapsed", self.TypeName)
- end
- self:SetLastArrangeRect(posX, posY, finalWidth, finalHeight)
- return
- end
- if (self.IsMeasureDirty or self.NeverMeasured) then
- if (ALLOW_PRINT_DEBUG) then
- PrintDebug("Measuring %s during arrange (Dirty = %s, NeverMeasured = %s)", self.TypeName, self.IsMeasureDirty, self.NeverMeasured)
- end
- self.MeasureDuringArrange = true
- if (self.NeverMeasured) then
- self:Measure(finalWidth, finalHeight)
- else
- self:Measure(self.LastMeasureAvailableWidth, self.LastMeasureAvailableHeight)
- end
- self.MeasureDuringArrange = false
- end
- local isSimilarToOldArrange = self.LastArrangeFinalX == posX and self.LastArrangeFinalY == posY and
- self.LastArrangeFinalW == finalWidth and self.LastArrangeFinalH == finalHeight
- if (ForceArrangeOrMeasure or self.IsArrangeDirty or self.NeverArranged or not isSimilarToOldArrange) then
- if (ALLOW_PRINT_DEBUG) then
- PrintDebug("Begin Arrange '%s' (forced = %s, dirty = %s, NeverArranged = %s, similar = %s)", self.TypeName, ForceArrangeOrMeasure, self.IsArrangeDirty, self.NeverArranged, isSimilarToOldArrange)
- end
- self.IsVisualPositionValid = false
- self.NeverArranged = false
- self.ArrangeInProgress = true
- self:ArrangeCore(posX, posY, finalWidth, finalHeight)
- self.ArrangeInProgress = false
- self:SetLastArrangeRect(posX, posY, finalWidth, finalHeight)
- self.IsArrangeDirty = false
- if (ALLOW_PRINT_DEBUG) then
- PrintDebug("End Arrange '%s' at %f,%f (%f x %f)", self.TypeName, self.RenderPosX, self.RenderPosY, self.RenderWidth, self.RenderHeight)
- end
- else
- if (ALLOW_PRINT_DEBUG) then
- PrintDebug("Skipping arrange for %s", self.TypeName)
- end
- end
- end
- function UIComponent:ArrangeCore(posX, posY, availableWidth, availableHeight)
- local mL,mT,mR,mB = self:GetPropValue(UIComponent.MarginLProperty),self:GetPropValue(UIComponent.MarginTProperty),self:GetPropValue(UIComponent.MarginRProperty),self:GetPropValue(UIComponent.MarginBProperty)
- availableWidth = math.max(availableWidth - (mL + mR), 0)
- availableHeight = math.max(availableHeight - (mT + mB), 0)
- local cpyAvailableW, cpyAvailableH = availableWidth, availableHeight
- local desiredWidth, desiredHeight
- if (self.UnclippedDesiredSize == nil) then
- desiredWidth = math.max(self.DesiredWidth - (mL + mR), 0)
- desiredHeight = math.max(self.DesiredHeight - (mT + mB), 0)
- else
- desiredWidth = self.UnclippedDesiredSize.w
- desiredHeight = self.UnclippedDesiredSize.h
- end
- -- Check if there isn't enough space available, and if so, require clipping
- self.NeedClipBounds = false
- if (desiredWidth > availableWidth) then
- availableWidth = desiredWidth
- self.NeedClipBounds = true
- end
- if (desiredHeight > availableHeight) then
- availableHeight = desiredHeight
- self.NeedClipBounds = true
- end
- -- If alignment is stretch, then arrange using all of the available size
- -- Otherwise, only use our desired size, leaving extra space for other components
- local alignHorz = DependencyObject.GetPropValue(self, UIComponent.HorizontalAlignmentProperty)
- local alignVert = DependencyObject.GetPropValue(self, UIComponent.VerticalAlignmentProperty)
- if (alignHorz ~= "stretch") then availableWidth = desiredWidth end
- if (alignVert ~= "stretch") then availableHeight = desiredHeight end
- local _, max_width, _, max_height = self:GetMinMax()
- local maxOrDesiredWidth = math.max(desiredWidth, max_width)
- if (availableWidth > maxOrDesiredWidth) then
- availableWidth = maxOrDesiredWidth
- self.NeedClipBounds = true
- end
- local maxOrDesiredHeight = math.max(desiredHeight, max_height)
- if (availableHeight > maxOrDesiredHeight) then
- availableHeight = maxOrDesiredHeight
- self.NeedClipBounds = true
- end
- local arrangeWidth, arrangeHeight = self:ArrangeOverride(availableWidth, availableHeight)
- if (arrangeWidth == nil) then error("ArrangeOverride Width cannot be null") end
- if (arrangeHeight == nil) then error("ArrangeOverride Height cannot be null") end
- self.RenderWidth = arrangeWidth
- self.RenderHeight = arrangeHeight
- -- The actual arranged width/height exceeds our max width/height, so clip
- local finalArrangeWidth,finalArrangeHeight = math.min(arrangeWidth, max_width),math.min(arrangeHeight, max_height)
- if (not self.NeedClipBounds and ((finalArrangeWidth < arrangeWidth) or (finalArrangeHeight < arrangeHeight))) then
- self.NeedClipBounds = true
- end
- if (not self.NeedClipBounds and ((cpyAvailableW < finalArrangeWidth) or (cpyAvailableH < finalArrangeHeight))) then
- self.NeedClipBounds = true
- end
- if (finalArrangeWidth > cpyAvailableW) then alignHorz = "left" end
- if (finalArrangeHeight > cpyAvailableH) then alignVert = "top" end
- local vecX, vecY = 0,0
- if (alignHorz == "center" or alignHorz == "stretch") then
- vecX = (cpyAvailableW - finalArrangeWidth) / 2
- elseif (alignHorz == "right") then
- vecX = cpyAvailableW - finalArrangeWidth
- end
- if (alignVert == "center" or alignVert == "stretch") then
- vecY = (cpyAvailableH - finalArrangeHeight) / 2
- elseif (alignVert == "bottom") then
- vecY = cpyAvailableH - finalArrangeHeight
- end
- self.RenderPosX = vecX + posX + mL
- self.RenderPosY = vecY + posY + mT
- end
- ---The overridable method for arranging this component
- ---@param width number The final width of this component
- ---@param height number The final height of this component
- ---@return number The render width, typically the final width parameter
- ---@return number The render height, typically the final height parameter
- function UIComponent:ArrangeOverride(width, height)
- for i, child in ipairs(self.Children) do
- child:Arrange(0, 0, width, height)
- if (child.IsArrangeDirty) then
- error("Child arrange was dirty after Arrange() call")
- end
- end
- return width, height
- end
- function UIComponent:IsMousePointOver(x, y)
- if (x < 0 or x >= self.RenderWidth) then
- return false
- end
- if (y < 0 or y >= self.RenderHeight) then
- return false
- end
- return true
- end
- function toolkit.RenderComponent(component)
- local absX,absY = component:GetRenderPositon()
- CompositionOffsetX = absX
- CompositionOffsetY = absY
- CompositionComponentSource = component
- component:OnRender()
- component.IsVisualDirty = false
- end
- function toolkit.RenderComponentTreeInternal(component)
- if (component:GetVisibility() ~= "visible") then
- return
- end
- if (component.IsArrangeDirty) then
- error("Component's arrangement is dirty; cannot render")
- end
- toolkit.RenderComponent(component)
- for i, child in ipairs(component.Children) do
- toolkit.RenderComponentTreeInternal(child)
- end
- end
- function toolkit.RenderComponentTree(component)
- PushFunction("DoRenderComponent", component.TypeName)
- if (ENABLE_RENDERING) then
- toolkit.RenderComponentTreeInternal(component)
- CompositionComponentSource = nil
- CompositionOffsetX = 0
- CompositionOffsetY = 0
- end
- PopFunction()
- end
- function toolkit.GetComponentDepth(component, depth)
- if (depth == nil) then depth = 0 end
- while component ~= nil do
- depth = depth + 1
- component = component.Parent
- end
- return depth
- end
- function toolkit.FindHighestComponentForRender(renderList)
- if (#renderList < 1) then
- return nil
- end
- local hIndex, hObj = toolkit.GetComponentDepth(renderList[1]), renderList[1]
- for i = 2, #renderList, 1 do
- local obj = renderList[i]
- local depth = toolkit.GetComponentDepth(obj)
- if (depth < hIndex) then
- hIndex = depth
- hObj = obj
- end
- end
- return hObj
- end
- function ThrowForDirtyArrangeWhenUnexpected(component)
- PushFunction("ThrowForDirtyArrangeWhenUnexpected: " .. component.TypeName)
- if (component:GetVisibility() ~= "collapsed" and component.IsArrangeDirty) then
- error("Child arrange was dirty after Arrange() call")
- end
- for i, child in ipairs(component.Children) do
- ThrowForDirtyArrangeWhenUnexpected(child)
- end
- PopFunction()
- end
- function toolkit.InternalCanProcessItem(component, isMeasure)
- if (isMeasure) then
- return component.IsMeasureDirty
- else
- return component.IsArrangeDirty
- end
- end
- function toolkit.DoLayoutUpdate()
- PushFunction("DoLayoutUpdate")
- if (ALLOW_PRINT_DEBUG) then
- PrintDebug("Doing layout update: %d measures and %d arranges", #layout_measure_queue, #layout_arrange_queue)
- end
- -- workaround for measuring/arranging scanning top-level components. If a child deep in the hierarchy
- -- is invalidated but a few parents above but before target_component are not invalid,
- -- then the child is never arranged. This is mostly a LUA performance limitation, since WPF runs on .NET
- -- which can afford to do the tree scan while invalidating each parent stage
- ForceArrangeOrMeasure = true
- if (#layout_measure_queue > 0) then
- for i, component in ipairs(layout_measure_queue) do
- local last_dirty = nil
- local scan_parent = component.Parent
- while (scan_parent ~= nil) do
- if (scan_parent.IsMeasureDirty) then
- last_dirty = scan_parent
- end
- scan_parent = scan_parent.Parent
- end
- local target_component
- if (last_dirty ~= nil) then
- target_component = last_dirty
- else
- target_component = component
- end
- local w,h
- if (not target_component.NeverMeasured) then
- w = target_component.LastMeasureAvailableWidth
- h = target_component.LastMeasureAvailableHeight
- elseif (target_component.Parent == nil) then
- w,h = toolkit.GetCompositionRenderSize()
- else
- local p = target_component.Parent
- w = p.LastMeasureAvailableWidth
- h = p.LastMeasureAvailableHeight
- end
- local lastDw,lastDh = component.DesiredWidth,component.DesiredHeight
- component:Measure(w, h)
- if (lastDw ~= component.DesiredWidth or lastDh ~= component.DesiredHeight) then
- component:InvalidateArrange()
- end
- end
- ClearTable(layout_measure_queue)
- end
- if (#layout_arrange_queue > 0) then
- for i, component in ipairs(layout_arrange_queue) do
- local last_dirty = nil
- local scan_parent = component.Parent
- while (scan_parent ~= nil) do
- if (scan_parent.IsArrangeDirty) then
- last_dirty = scan_parent
- end
- scan_parent = scan_parent.Parent
- end
- local c -- target component
- if (last_dirty ~= nil) then
- c = last_dirty
- else
- c = component
- end
- local x,y,w,h
- if (not c.NeverArranged) then
- x,y,w,h = c.LastArrangeFinalX, c.LastArrangeFinalY, c.LastArrangeFinalW, c.LastArrangeFinalH
- elseif (c.Parent == nil) then
- x,y,w,h = 0, 0, toolkit.GetCompositionRenderSize()
- else
- -- as long as arrangement happens in the correct order, the parent will have
- -- always been arranged at least once due to the code in AppMain()
- local p = c.Parent
- x,y,w,h = p.LastArrangeFinalX, p.LastArrangeFinalY, p.LastArrangeFinalW, p.LastArrangeFinalH
- end
- local lastX, lastY = c.LastArrangeFinalX,c.LastArrangeFinalY
- local lastW, lastH = c.LastArrangeFinalW,c.LastArrangeFinalH
- c:Arrange(x, y, w, h)
- -- use when arrangement isn't working correctly
- -- ThrowForDirtyArrangeWhenUnexpected(target_component)
- component.IsVisualPositionValid = false
- local hasPosChanged = c.LastArrangeFinalX ~= lastX or c.LastArrangeFinalY ~= lastY
- local hasSizeChanged = c.LastArrangeFinalW ~= lastW or c.LastArrangeFinalH ~= lastH
- if (hasPosChanged or hasSizeChanged) then
- UIComponent.InvalidateVisual(c)
- end
- end
- ClearTable(layout_arrange_queue)
- end
- ForceArrangeOrMeasure = false
- if (IsRootRenderDirty) then
- IsRootRenderDirty = false
- if (not IsDisplayHidden) then
- toolkit.RenderComponentTree(AppRootComponent)
- end
- end
- PopFunction()
- end
- function toolkit.TryScheduleLayoutUpdate()
- if (TheLayoutUpdateTask == nil or TheLayoutUpdateTask.IsCompleted) then
- TheLayoutUpdateTask = InvokeAsync(toolkit.DoLayoutUpdate, PriorityLayout)
- end
- end
- function UIComponent:InvalidateArrange()
- if (self.IsArrangeDirty or self.ArrangeInProgress) then
- return
- end
- if (ALLOW_PRINT_DEBUG) then
- PrintDebug("Arrange invalidated for '%s'", self.TypeName)
- end
- if (not self.NeverArranged) then
- table.insert(layout_arrange_queue, self)
- end
- self.IsArrangeDirty = true
- toolkit.TryScheduleLayoutUpdate()
- end
- function UIComponent:InvalidateMeasure()
- if (self.IsMeasureDirty or self.MeasureInProgress) then
- return
- end
- if (ALLOW_PRINT_DEBUG) then
- PrintDebug("Measure invalidated for '%s'", self.TypeName)
- end
- if (not self.NeverMeasured) then
- table.insert(layout_measure_queue, self)
- end
- self.IsMeasureDirty = true
- toolkit.TryScheduleLayoutUpdate()
- end
- function UIComponent:InvalidateVisual()
- toolkit.TryScheduleLayoutUpdate()
- if (not self.IsVisualDirty) then
- self.IsVisualDirty = true
- IsRootRenderDirty = true
- if (ALLOW_PRINT_DEBUG) then
- PrintDebug("Visual invalidated for '%s'", self.TypeName)
- end
- end
- end
- function UIComponent:InvalidateLayoutAndVisual()
- self:InvalidateMeasure()
- self:InvalidateArrange()
- self:InvalidateVisual()
- end
- -- ---@class ClassA
- -- ---@field Name string
- -- ---@field Type string
- -- local ClassA = { Name = "joe", Type = "joemama" }
- --
- -- ---Adds an item
- -- ---@generic T: ClassA
- -- ---@param item T the
- -- function ClassA:AddChild(item)
- -- item.Name = "jo"
- -- end
- ---Inserts a child component
- ---@generic T : UIComponent
- ---@param child T The child to add
- ---@param id string? A unique identifier for this component (relative to the component in which a child is being added to)
- ---@return T
- function UIComponent:InsertChild(child, id)
- PushFunction("InsertChild")
- assert(child ~= nil, "Child cannot be null")
- if (child.IndexInParent ~= -1) then
- error("Child component already added to another component: " .. child.IndexInParent)
- end
- if (self == child) then
- error("Cannot add self as a child")
- end
- child.Parent = self
- table.insert(self.Children, child)
- if (id ~= nil) then
- child.IdInParent = id
- self.IdToChildTable[id] = child
- end
- UpdateCachedIndices(self.Children)
- child:UpdateVisibility()
- self:InvalidateLayoutAndVisual()
- return PopFunction(child)
- end
- function UIComponent:RemoveChild(child, useDeferredCacheUpdate)
- if (child.IndexInParent == -1 or child.Parent ~= self) then
- return
- end
- self:RemoveChildAt(child.IndexInParent, useDeferredCacheUpdate)
- end
- function UIComponent:GetChildById(id)
- return self.IdToChildTable[id]
- end
- function UIComponent:RemoveChildAt(index, useDeferredCacheUpdate)
- PushFunction("RemoveChildAt", index, useDeferredCacheUpdate)
- local child = self.Children[index]
- child.Parent = nil
- child.IndexInParent = -1
- table.remove(self.Children, index)
- if (child.IdInParent ~= nil) then
- self.IdToChildTable[child.IdInParent] = nil
- end
- if (useDeferredCacheUpdate ~= true) then
- UpdateCachedIndices(self.Children)
- end
- child:UpdateVisibility()
- PopFunction()
- end
- function UIComponent:RemoveFromParent()
- if (self.IndexInParent == -1 or self.Parent == nil) then
- return
- end
- self.Parent:RemoveChildAt(self.IndexInParent)
- end
- function UIComponent:ClearChildren()
- PushFunction("ClearChildren")
- for i = #self.Children, 1, -1 do
- self:RemoveChildAt(i, true)
- end
- self.IdToChildTable = {}
- UpdateCachedIndices(self.Children)
- PopFunction()
- end
- function UIComponent:OnGotFocus() end
- function UIComponent:OnLostFocus() end
- function UniformPanel:CountVisibleChildren()
- local count = 0
- for i,child in ipairs(self.Children) do
- if (child:GetVisibility() ~= "collapsed") then
- count = count + 1
- end
- end
- return count
- end
- function UniformPanel:GetTotalGap(numElements, spacing)
- if (numElements > 1) then
- return (numElements - 1) * spacing
- end
- return 0
- end
- function UniformPanel:GetSlotPerElement(width, height, count)
- local totalGap = self:GetTotalGap(count, self:GetPropValue(UniformPanel.SpacingProperty))
- if (self:GetPropValue(UniformPanel.OrientationProperty) == "vertical") then
- return width, math.ceil((height - totalGap) / count)
- else
- return math.ceil((width - totalGap) / count), height
- end
- end
- function UniformPanel:MeasureOverride(max_w, max_h)
- local count = self:CountVisibleChildren()
- local slotW,slotH = self:GetSlotPerElement(max_w, max_h, count)
- local finalW, finalH = 0,0
- for i, child in ipairs(self.Children) do
- child:Measure(slotW, slotH)
- if (finalW < child.DesiredWidth) then finalW = child.DesiredWidth end
- if (finalH < child.DesiredHeight) then finalH = child.DesiredHeight end
- end
- return finalW,finalH
- -- if (self.Orientation == "horizontal") then
- -- return finalW * count, finalH
- -- else
- -- return finalW, finalH * count
- -- end
- end
- function UniformPanel:ArrangeOverride(arrangeW, arrangeH)
- local finalX,finalY = 0,0
- local count = self:CountVisibleChildren()
- local orientation = self:GetPropValue(UniformPanel.OrientationProperty)
- local spacing = self:GetPropValue(UniformPanel.SpacingProperty)
- local finalW,finalH = self:GetSlotPerElement(arrangeW, arrangeH, count)
- for i, child in ipairs(self.Children) do
- child:Arrange(finalX, finalY, finalW, finalH)
- if (child:GetVisibility() ~= "collapsed") then
- if (orientation == "vertical") then
- finalY = finalY + finalH + spacing
- else
- finalX = finalX + finalW + spacing
- end
- end
- end
- return arrangeW, arrangeH
- end
- -- function UniformPanel:MeasureOverride(max_w, max_h)
- -- local limitW = math.ceil(max_w / #self.Children)
- -- local wid, hei = 0, 0
- -- for i, child in ipairs(self.Children) do
- -- local cW, cH = child:Measure(limitW, max_h)
- -- if (wid < cW) then wid = cW end
- -- if (hei < cH) then hei = cH end
- -- end
- -- return wid * #self.Children, hei
- -- end
- --
- -- function UniformPanel:ArrangeOverride(arrangeW, arrangeH)
- -- local frX, frY, frW, frH = 0, 0, math.ceil(arrangeW / #self.Children), arrangeH
- -- local width = frW
- -- local numEx = arrangeW - 1
- -- for i, child in ipairs(self.Children) do
- -- child:Arrange(frX, frY, frW, frH)
- -- frX = frX + width
- -- if (frX >= numEx) then
- -- frY = frY + frH
- -- frX = 0
- -- end
- -- end
- -- return arrangeW,arrangeH
- -- end
- function HorizontalStackPanel:MeasureOverride(max_w, max_h)
- local new_max_w, new_max_h = 0, 0
- for i, child in ipairs(self.Children) do
- local desired_w, desired_h = child:Measure(max_w, max_h)
- new_max_w = new_max_w + desired_w
- if (new_max_h < desired_h) then new_max_h = desired_h end
- end
- return new_max_w, new_max_h
- end
- function HorizontalStackPanel:ArrangeOverride(arrangeW, arrangeH)
- local finalX = 0
- for i, child in ipairs(self.Children) do
- child:Arrange(finalX, 0, child.DesiredWidth, arrangeH)
- finalX = finalX + child.DesiredWidth
- end
- return arrangeW,arrangeH
- end
- function VerticalStackPanel:MeasureOverride(max_w, max_h)
- local new_max_w, new_max_h = 0, 0
- for i, child in ipairs(self.Children) do
- local desired_w, desired_h = child:Measure(max_w, max_h)
- new_max_h = new_max_h + desired_h
- if (new_max_w < desired_w) then new_max_w = desired_w end
- end
- return new_max_w, new_max_h
- end
- function VerticalStackPanel:ArrangeOverride(arrangeW, arrangeH)
- local finalY = 0
- for i, child in ipairs(self.Children) do
- child:Arrange(0, finalY, arrangeW, child.DesiredHeight)
- finalY = finalY + child.DesiredHeight
- end
- return arrangeW,arrangeH
- end
- function DockPanel:MeasureOverride(max_w, max_h)
- local actual_max_w,actual_max_h,total_w,total_h = 0,0,0,0
- for i, child in ipairs(self.Children) do
- child:Measure(math.max(0, max_w - total_w), math.max(0, max_h - total_h))
- local dock = child.DockValue or "left"
- if (dock == "left" or dock == "right") then
- actual_max_h = math.max(actual_max_h, total_h + child.DesiredHeight)
- total_w = total_w + child.DesiredWidth
- elseif (dock == "top" or dock == "bottom") then
- actual_max_w = math.max(actual_max_w, total_w + child.DesiredWidth)
- total_h = total_h + child.DesiredHeight
- end
- end
- return math.max(actual_max_w, total_w),math.max(actual_max_h, total_h)
- end
- function DockPanel:ArrangeOverride(arr_w, arr_h)
- local lastIndex = #self.Children
- if (self.LastChildFill) then
- lastIndex = lastIndex - 1
- end
- local x,y,total_w,total_h = 0,0,0,0
- for i, child in ipairs(self.Children) do
- local fx,fy = x,y
- local fw,fh = math.max(0, arr_w - (x + total_w)), math.max(0, arr_h - (y + total_h))
- local dock = child.DockValue or "left"
- if ((i - 1) < lastIndex) then
- if (dock == "left") then
- x = x + child.DesiredWidth
- fw = child.DesiredWidth
- elseif (dock == "top") then
- y = y + child.DesiredHeight
- fh = child.DesiredHeight
- elseif (dock == "right") then
- total_w = total_w + child.DesiredWidth
- fx = math.max(0, arr_w - total_w)
- fw = child.DesiredWidth
- elseif (dock == "bottom") then
- total_h = total_h + child.DesiredHeight
- fy = math.max(0, arr_h - total_h)
- fh = child.DesiredHeight
- end
- end
- child:Arrange(fx, fy, fw, fh)
- end
- return arr_w,arr_h
- end
- function Button.OnMouseDown(self, time, args)
- args.handled = true
- if (not self.DoNotProcessButtonClickLogic) then
- if (self.IsToggleButton) then
- self.IsPressed = not self.IsPressed
- self:InvalidateVisual()
- elseif (not self.IsPressed) then
- self.IsPressed = true
- self:InvalidateVisual()
- InvokeAsyncWithDelay(function ()
- self.IsPressed = false
- self:InvalidateVisual()
- end, 0.5)
- end
- end
- self:RaiseEvent(Event_Button_OnClick, time, args)
- end
- function Button:MeasureOverride(max_w, max_h)
- local w, h = UIComponent.MeasureOverride(self, max_w, max_h)
- local text = self:GetPropValue(Button.TextProperty)
- if (text ~= nil and #text > w) then
- w = #text
- end
- if (h < 1) then h = 1 end
- return w, h
- end
- function Button:OnRender()
- local bg_colour
- if (self.IsPressed) then
- bg_colour = self:GetPropValue(Button.PressedBackgroundProperty)
- else
- bg_colour = self:GetPropValue(UIComponent.BackgroundProperty)
- end
- ui.fill(1, 1, self.RenderWidth, self.RenderHeight, bg_colour)
- ui.drawTextCentered(1, 1, self.RenderWidth, self.RenderHeight, self:GetPropValue(Button.TextProperty) or "", colours.white, nil)
- end
- function toolkit.FocusComponent(component)
- PushFunction("FocusComponent", component)
- if (component ~= nil and not component:GetPropValue(UIComponent.IsFocusableProperty)) then
- PopFunction()
- return
- end
- if (FocusedComponent ~= nil and FocusedComponent:GetPropValue(UIComponent.IsFocusedProperty)) then
- FocusedComponent:SetPropValue(UIComponent.IsFocusedProperty, false)
- FocusedComponent:OnLostFocus()
- end
- FocusedComponent = component
- if (component ~= nil) then
- component:SetPropValue(UIComponent.IsFocusedProperty, true)
- component:OnGotFocus()
- end
- PopFunction()
- end
- ---Sets the target peripheral object that will be used for arrangement and rendering
- ---@param target table The new target. May only be null while app functions are not being used
- function toolkit.SetComponsitionTarget(target)
- CompositionTarget = target
- if (target == nil) then
- CompositionWidth = 0
- CompositionHeight = 0
- else
- CompositionWidth, CompositionHeight = target.getSize()
- end
- end
- function toolkit.GetCompositionRenderSize()
- return CompositionWidth,CompositionHeight
- end
- --endregion
- --region Application
- function UIComponent:DoMouseClick_Tunnel(time, button, x, y)
- PushFunction("DoMouseClick_Tunnel", self.TypeName, button, x, y)
- if (self:IsMousePointOver(x, y)) then
- local args = {btn = button, x = x, y = y, handled = false}
- self:RaiseEvent(Event_PreviewMouseDown, time, args)
- if (not args.handled) then
- for i = #self.Children, 1, -1 do
- local child = self.Children[i]
- local posX, posY = x - child.RenderPosX, y - child.RenderPosY
- local hit = child:DoMouseClick_Tunnel(time, button, posX, posY)
- if (hit ~= nil) then
- return PopFunction(hit,posX,posY)
- end
- end
- end
- if (args.handled or self:GetPropValue(UIComponent.IsFocusableProperty) or self:GetPropValue(UIComponent.BackgroundProperty) ~= nil) then
- return PopFunction(self,x,y)
- end
- end
- return PopFunction(nil,nil,nil)
- end
- function UIComponent:DoMouseClick_Bubble(time, button, x, y)
- PushFunction("DoMouseClick_Bubble", self.TypeName, button, x, y)
- local eventArgs = {btn = button, x = x, y = y, handled = false}
- if (self:RaiseEvent(Event_MouseDown, time, eventArgs)) then
- return PopFunction(self)
- end
- if (self.Parent ~= nil) then
- x = x + self.RenderPosX
- y = y + self.RenderPosY
- return PopFunction(self.Parent:DoMouseClick_Bubble(time, button, x, y))
- end
- return PopFunction(nil)
- end
- function toolkit.OnWakeDisplay(time)
- LastDisplayWakeTime = time
- if (IsDisplayHidden) then
- IsDisplayHidden = false
- if (AppRootComponent ~= nil) then
- AppRootComponent:InvalidateVisual()
- end
- return true
- else
- return false
- end
- end
- function toolkit.OnTickDisplaySleep(time)
- if (CanDisplayAutoSleep and (not IsDisplayHidden) and (time - LastDisplayWakeTime) > 300) then -- 5 minutes
- IsDisplayHidden = true
- ui.clear()
- local w,h = toolkit.GetCompositionRenderSize()
- ui.drawTextCentered(1, 1, w, h, "Display is asleep. Click to wake", colours.grey, colours.black)
- end
- end
- ---Invoked when the user clicks a specific point on the screen
- ---@param time number The OS clock time
- ---@param btn integer The mouse button (1=LMB,2=RMB,3=MWB)
- ---@param absX integer The mouse pos X
- ---@param absY integer The mouse pos Y
- function toolkit.OnMouseClick(time, btn, absX, absY)
- if (AppRootComponent == nil) then
- return
- end
- PushFunction("OnMouseClick", btn, absX, absY)
- local hit,x,y = AppRootComponent:DoMouseClick_Tunnel(time, btn, absX, absY)
- if (hit == nil) then
- toolkit.FocusComponent(nil)
- else
- local bubbleHit = hit:DoMouseClick_Bubble(time, btn, x, y)
- if (bubbleHit ~= nil) then
- toolkit.FocusComponent(bubbleHit)
- else
- toolkit.FocusComponent(hit)
- end
- end
- PopFunction()
- end
- function toolkit.OnMouseDrag(time, btn, absX, absY)
- PushFunction("OnMouseDrag", btn, absX, absY)
- if (FocusedComponent ~= nil and FocusedComponent.CanHandleDrag and not FocusedComponent.IsArrangeDirty) then
- local rpX,rpY = FocusedComponent:GetRenderPositon()
- local eventArgs = {btn = btn, x = absX - rpX, y = absY - rpY, absX = absX, absY = absY, handled = false}
- FocusedComponent:RaiseEvent(Event_MouseDrag, time, eventArgs)
- end
- return PopFunction()
- end
- -- Sets up a timer for the next application tick
- function SetupTimer(delay)
- AppTimerId = os.startTimer(delay)
- end
- local function OnApplicationEvent(time, eventType, p1, p2, p3, p4, p5)
- PushFunction("OnApplicationEvent", eventType, p1, p2, p3, p4, p5)
- if (eventType == "timer") then
- if (p1 == AppTimerId) then
- SetupTimer(AppTimerInterval)
- if (#delayedTaskQueue > 0) then
- local operationList = {}
- local removalList = {}
- for i, operation in ipairs(delayedTaskQueue) do
- if (ALLOW_PRINT_DEBUG) then
- PrintDebug(tostring(time) .. " >= " .. operation.TimeUntilExecution .. ": " .. tostring(time >= operation.TimeUntilExecution))
- end
- if (time >= operation.TimeUntilExecution) then
- table.insert(operationList, operation)
- table.insert(removalList, i)
- end
- end
- -- Remove back to front. Always faster than front to back
- for i = #removalList, 1, -1 do
- table.remove(delayedTaskQueue, removalList[i])
- end
- -- Invoke operations
- for i, operation in ipairs(operationList) do
- operation.Method(unpack(operation.Args))
- end
- end
- toolkit.OnTickDisplaySleep(time)
- elseif (p1 == AppDispatcherId) then
- AppDispatcherId = 0
- -- accumulate operations to invoke
- local invocationList = {}
- for i, list in ipairs(dispatcher_queue) do
- for j = 1, #list do
- table.insert(invocationList, list[j])
- end
- ClearTable(list)
- end
- -- execute dispatcher queue
- if (#invocationList > 0) then
- IsProcessingDispatcherQueue = true
- for i = 1, #invocationList do
- local operation = invocationList[i]
- operation.Method(unpack(operation.Args))
- operation.IsCompleted = true
- end
- IsProcessingDispatcherQueue = false
- end
- end
- else
- if (eventType == "mouse_click") then
- if (toolkit.OnWakeDisplay(time)) then return end
- toolkit.OnMouseClick(time, p1, p2 - 1, p3 - 1)
- elseif (eventType == "monitor_touch") then
- if (toolkit.OnWakeDisplay(time)) then return end
- toolkit.OnMouseClick(time, 1, p2 - 1, p3 - 1)
- elseif (eventType == "mouse_drag") then
- if (toolkit.OnWakeDisplay(time)) then return end
- toolkit.OnMouseDrag(time, p1, p2 - 1, p3 - 1)
- elseif (eventType == "key") then
- -- app.OnKeyPress(time, p1)
- elseif (eventType == "char") then
- -- app.OnCharPress(time, p1)
- else
- return
- end
- end
- PopFunction()
- end
- function DualColourTextBlock.new(leftText, rightText, leftColour, rightColour, bg_colour)
- local obj = NewTypeInstance(DualColourTextBlock, "DualColourTextBlock")
- obj.LeftText = leftText or ""
- obj.RightText = rightText or ""
- obj.LeftColour = leftColour or colours.white
- obj.RightColour = rightColour or colours.white
- DependencyObject.SetPropValue(obj, UIComponent.BackgroundProperty, bg_colour or colours.black)
- return obj
- end
- function DualColourTextBlock:MeasureOverride(max_w, max_h)
- local w, h = UIComponent.MeasureOverride(self, max_w, max_h)
- local textLen = 0
- if (self.LeftText ~= nil) then textLen = textLen + #self.LeftText end
- if (self.RightText ~= nil) then textLen = textLen + #self.RightText end
- if (w < textLen) then
- w = textLen
- end
- if (h < 1) then h = 1 end
- return w, h
- end
- function DualColourTextBlock:OnRender()
- UIComponent.OnRender(self)
- ui.drawText(1, 1, self.LeftText, self.LeftColour, nil)
- ui.drawText(1 + #self.LeftText, 1, self.RightText, self.RightColour, nil)
- end
- function DualColourTextBlock:SetRightText(theText, theColour)
- if (theText == nil) then
- theText = ""
- elseif (type(theText) ~= "string") then
- theText = tostring(theText)
- end
- self.RightText = theText
- self.RightColour = theColour
- self:InvalidateMeasure()
- self:InvalidateVisual()
- end
- local TabItem_Home
- local TabItem_Rods
- local TabItem_Auto
- local ReactorInfo = {
- Handle = nil,
- IsOnline = false,
- PowerLevel = 0,
- MaxFuelAmount = 0,
- CurFuelAmount = 0,
- FuelTemp = 0,
- CaseTemp = 0,
- EnergyProducedLastTick = 0,
- EnergyStored = 0,
- AvgRodInsertion = 0,
- TotalControlRodCount = 0,
- Automation = {
- Minimum = 40,
- Maximum = 60,
- IsEnabled = false
- },
- UI = {
- HomePage = {},
- RodsPage = {},
- AutoPage = {}
- }
- }
- function toolkit.LoadConfig()
- local hFile = fs.open("config", "r")
- if (hFile == nil) then
- return
- end
- while true do
- local nextLine = hFile.readLine()
- if (nextLine == nil) then
- break
- end
- for key, value in string.gmatch(nextLine, "([^=]+)=([^=]+)") do
- ConfigTable[key] = value
- end
- end
- hFile.close()
- ReactorInfo.Automation.Minimum = tonumber(ConfigTable["Automation_Minimum"]) or 30
- ReactorInfo.Automation.Maximum = tonumber(ConfigTable["Automation_Maximum"]) or 70
- local isAutoEnabled = (ConfigTable["Automation_IsEnabled"] == "true") or false
- CanDisplayAutoSleep = (ConfigTable["CanDisplayAutoSleep"] == "true") or false
- SetAutomationState(isAutoEnabled)
- end
- function toolkit.SaveConfig()
- local hFile = fs.open("config", "w")
- if (hFile == nil) then
- return
- end
- ConfigTable["Automation_Minimum"] = tostring(ReactorInfo.Automation.Minimum)
- ConfigTable["Automation_Maximum"] = tostring(ReactorInfo.Automation.Maximum)
- ConfigTable["Automation_IsEnabled"] = tostring(ReactorInfo.Automation.IsEnabled)
- ConfigTable["CanDisplayAutoSleep"] = tostring(CanDisplayAutoSleep)
- for key, value in pairs(ConfigTable) do
- hFile.writeLine(key .. "=" .. value)
- end
- hFile.close()
- end
- function ReactorInfo.SetRodInsertion(percent)
- if (ReactorInfo.Handle ~= nil) then
- ReactorInfo.Handle.setAllControlRodLevels(percent)
- end
- ReactorInfo.AvgRodInsertion = percent
- end
- function GetColourForReactorTemp(fuel_temp)
- if (fuel_temp < 500) then
- return colors.lime
- elseif (fuel_temp < 1000) then
- return colors.yellow
- elseif (fuel_temp < 1500) then
- return colors.orange
- else
- return colors.red
- end
- end
- function SetAutomationState(isAutomationEnabled)
- if (ReactorInfo.Automation.IsEnabled == isAutomationEnabled) then
- return false
- end
- ReactorInfo.Automation.IsEnabled = isAutomationEnabled
- toolkit.SaveConfig()
- local info = ReactorInfo.Automation
- local page = ReactorInfo.UI.HomePage
- if (info.IsEnabled) then
- page.ControlBtn_On:SetPropValue(Button.PressedBackgroundProperty, colours.cyan)
- page.ControlBtn_Off:SetPropValue(Button.PressedBackgroundProperty, colours.cyan)
- page.ControlBtn_Auto.IsPressed = true
- else
- page.ControlBtn_On:SetPropValue(Button.PressedBackgroundProperty, colours.green)
- page.ControlBtn_Off:SetPropValue(Button.PressedBackgroundProperty, colours.green)
- page.ControlBtn_Auto.IsPressed = false
- end
- DoReactorTick(false)
- return true
- end
- local app_ticks = 1
- function DoReactorTick(canScheduleNextTick)
- PushFunction("DoReactorTick::" .. tostring(canScheduleNextTick))
- local reactor = ReactorInfo.Handle
- if (reactor ~= nil) then
- ReactorInfo.IsOnline = reactor.getActive()
- ReactorInfo.PowerLevel = reactor.getEnergyStored()
- ReactorInfo.MaxFuelAmount = reactor.getFuelAmountMax()
- ReactorInfo.CurFuelAmount = reactor.getFuelAmount()
- ReactorInfo.FuelTemp = reactor.getFuelTemperature()
- ReactorInfo.CaseTemp = reactor.getCasingTemperature()
- ReactorInfo.EnergyProducedLastTick = reactor.getEnergyProducedLastTick()
- ReactorInfo.EnergyStored = reactor.getEnergyStored()
- ReactorInfo.AvgRodInsertion = reactor.getControlRodLevel(0)
- ReactorInfo.TotalControlRodCount = reactor.getNumberOfControlRods()
- local auto = ReactorInfo.Automation
- if (auto.IsEnabled) then
- local power_percent = math.floor((ReactorInfo.EnergyStored / 10000000) * 100)
- if (auto.Minimum == auto.Maximum) then
- if (power_percent < auto.Minimum) then
- reactor.setActive(true)
- elseif (power_percent > auto.Minimum) then
- reactor.setActive(false)
- end
- else
- if (power_percent <= auto.Minimum) then
- reactor.setActive(true)
- elseif (power_percent >= auto.Maximum) then
- reactor.setActive(false)
- end
- end
- end
- end
- if (TabItem_Home.IsActive) then
- local page = ReactorInfo.UI.HomePage
- if (ReactorInfo.IsOnline) then
- page.PowerLabel:SetRightText("ONLINE", colours.green)
- else
- page.PowerLabel:SetRightText("OFFLINE", colours.red)
- end
- local fuelLevelPercent = math.floor((ReactorInfo.CurFuelAmount/ReactorInfo.MaxFuelAmount)*100)
- page.FuelLevelLabel:SetRightText(tostring(fuelLevelPercent) .. "%", colours.white)
- page.FuelLevelProgBar:SetValue(fuelLevelPercent, colours.lime)
- local fuel_temp = ReactorInfo.FuelTemp
- page.FuelTempLabel:SetRightText(tostring(math.floor(fuel_temp)) .. "/4000", colours.white)
- page.FuelTempProgBar:SetValue(fuel_temp, GetColourForReactorTemp(fuel_temp))
- local case_temp = ReactorInfo.CaseTemp
- page.CasingTempLabel:SetRightText(tostring(math.floor(case_temp)) .. "/4000", colours.white)
- page.CasingTempProgBar:SetValue(case_temp, GetColourForReactorTemp(case_temp))
- page.EnergyLabel:SetRightText(tostring(math.floor(ReactorInfo.EnergyProducedLastTick)) .. " RF/t", colours.white)
- page.StoredLabel:SetRightText(tostring(math.floor((ReactorInfo.EnergyStored/10000000)*100)) .. "% (" .. tostring(math.floor(ReactorInfo.EnergyStored)) .. " RF)", colours.white)
- page.StoredProgBar:SetValue(ReactorInfo.EnergyStored)
- page.ControlRodsLabel:SetRightText(ReactorInfo.AvgRodInsertion .. "%", colours.white)
- page.ControlBtn_On:SetPressed(ReactorInfo.IsOnline)
- page.ControlBtn_Off:SetPressed(not ReactorInfo.IsOnline)
- page.ControlBtn_Auto:SetPressed(ReactorInfo.Automation.IsEnabled)
- elseif (TabItem_Rods.IsActive) then
- local page = ReactorInfo.UI.RodsPage
- page.InsertionTextBlock:SetRightText(tostring(ReactorInfo.AvgRodInsertion) .. "%", colours.lightBlue)
- page.InsertionProgBar:SetValue(ReactorInfo.AvgRodInsertion)
- page.TotalControlRodLabel:SetRightText(tostring(ReactorInfo.TotalControlRodCount))
- elseif (TabItem_Auto.IsActive) then
- local page = ReactorInfo.UI.AutoPage
- UpdateReactorAutomationLabels()
- end
- if (canScheduleNextTick == nil or canScheduleNextTick == true) then
- InvokeAsyncWithDelay(DoReactorTick, 0.4, true)
- end
- PopFunction()
- end
- ---Creates a basic page
- ---@generic T : BasePageComponent
- ---@param theComponentType T
- ---@param footerMessage string
- ---@return T
- function CreateBasicPage(theComponentType, footerMessage)
- local page = NewTypeInstance(theComponentType, "BasicPage")
- DependencyObject.SetPropValue(page, UIComponent.BackgroundProperty, colours.black)
- page:SetPropValue(UIComponent.HorizontalAlignmentProperty, "stretch")
- page:SetPropValue(UIComponent.VerticalAlignmentProperty, "stretch")
- local label = TextBlock.new(footerMessage, colours.white, colours.blue)
- label:SetPropValue(UIComponent.HorizontalAlignmentProperty, "center")
- local bottom_strip = UIComponent.new()
- DependencyObject.SetPropValue(bottom_strip, UIComponent.BackgroundProperty, colours.blue)
- bottom_strip:SetPropValue(UIComponent.HorizontalAlignmentProperty, "stretch")
- bottom_strip:SetPropValue(UIComponent.VerticalAlignmentProperty, "bottom")
- bottom_strip:InsertChild(label)
- page:InsertChild(bottom_strip)
- return page
- end
- function CreateHomePage()
- local page_Home = CreateBasicPage(HomePageComponent, "Info about your reactor")
- local vert_stack = page_Home:InsertChild(VerticalStackPanel.new():SetMargin(1, 1, 1, 2))
- vert_stack:SetPropValue(UIComponent.HorizontalAlignmentProperty, "stretch")
- -- define page layout
- page_Home.PowerLabel = vert_stack:InsertChild(DualColourTextBlock.new("Power: ", "OFFLINE", colours.yellow, colours.red))
- vert_stack:InsertChild(UIComponent.new():SetSize(0, 1))
- page_Home.FuelLevelLabel = vert_stack:InsertChild(DualColourTextBlock.new("Fuel Level: ", "0", colours.yellow, colours.white))
- page_Home.FuelLevelProgBar = vert_stack:InsertChild(ProgressBar.new(0, 100, 0):SetAlignment("stretch", "top"))
- vert_stack:InsertChild(UIComponent.new():SetSize(0, 1))
- page_Home.FuelTempLabel = vert_stack:InsertChild(DualColourTextBlock.new("Fuel Temp: ", "0", colours.yellow, colours.white))
- page_Home.FuelTempProgBar = vert_stack:InsertChild(ProgressBar.new(0, 4000, 0):SetAlignment("stretch", "top"))
- vert_stack:InsertChild(UIComponent.new():SetSize(0, 1))
- page_Home.CasingTempLabel = vert_stack:InsertChild(DualColourTextBlock.new("Casing Temp: ", "0", colours.yellow, colours.white))
- page_Home.CasingTempProgBar = vert_stack:InsertChild(ProgressBar.new(0, 4000, 0):SetAlignment("stretch", "top"))
- vert_stack:InsertChild(UIComponent.new():SetSize(0, 1))
- local dock_panel = vert_stack:InsertChild(DockPanel.new():SetAlignment("stretch"))
- local btn_controls = dock_panel:InsertChild(HorizontalStackPanel.new())
- btn_controls.DockValue = "right"
- local otherInfoStack = dock_panel:InsertChild(VerticalStackPanel.new():SetMargin(0, 0, 1, 0))
- otherInfoStack:SetPropValue(UIComponent.HorizontalAlignmentProperty, "stretch")
- page_Home.EnergyLabel = otherInfoStack:InsertChild(DualColourTextBlock.new("Energy: ", "0 RF/t", colours.yellow, colours.white))
- page_Home.StoredLabel = otherInfoStack:InsertChild(DualColourTextBlock.new("Stored: ", "0 RF", colours.yellow, colours.white))
- page_Home.StoredProgBar = otherInfoStack:InsertChild(ProgressBar.new(0, 10000000, 0, colours.grey, colours.red):SetAlignment("stretch"))
- page_Home.ControlRodsLabel = otherInfoStack:InsertChild(DualColourTextBlock.new("Control Rods: ", "0%", colours.yellow, colours.white))
- local btn_auto = Button.new("Button:Automatic")
- page_Home.ControlBtn_Auto = btn_auto
- btn_auto.IsToggleButton = true
- btn_auto:SetPropValue(Button.TextProperty, "Automatic")
- btn_auto:SetSize(11, 3)
- btn_auto:AddEventHandler(Event_Button_OnClick, function (self, time, e)
- if (not SetAutomationState(self.IsPressed)) then
- return
- end
- self:InvalidateVisual()
- end)
- local btn_start = Button.new("Button:ON")
- page_Home.ControlBtn_On = btn_start
- btn_start.DoNotProcessButtonClickLogic = true
- btn_start:SetPropValue(Button.TextProperty, "ON")
- btn_start:SetSize(6, 3)
- btn_start:AddEventHandler(Event_Button_OnClick, function (self, time, e)
- if (ReactorInfo.Handle ~= nil) then ReactorInfo.Handle.setActive(true) end
- SetAutomationState(false)
- self:InvalidateVisual()
- end)
- local btn_stop = Button.new("Button:OFF")
- page_Home.ControlBtn_Off = btn_stop
- btn_stop.DoNotProcessButtonClickLogic = true
- btn_stop:SetPropValue(Button.TextProperty, "OFF")
- btn_stop:SetSize(7, 3)
- btn_stop:AddEventHandler(Event_Button_OnClick, function (self, time, e)
- if (ReactorInfo.Handle ~= nil) then ReactorInfo.Handle.setActive(false) end
- SetAutomationState(false)
- self:InvalidateVisual()
- end)
- btn_controls:InsertChild(btn_auto)
- btn_controls:InsertChild(btn_start)
- btn_controls:InsertChild(btn_stop)
- return page_Home
- end
- function AddToRodInsertion(increment)
- ReactorInfo.AvgRodInsertion = ReactorInfo.AvgRodInsertion + increment
- ReactorInfo.SetRodInsertion(maths.clamp(ReactorInfo.AvgRodInsertion, 0, 100))
- local page = ReactorInfo.UI.RodsPage
- page.InsertionTextBlock:SetRightText(tostring(ReactorInfo.AvgRodInsertion) .. "%", colours.lightBlue)
- page.InsertionProgBar:SetValue(ReactorInfo.AvgRodInsertion)
- end
- function CreateRodsPage()
- ---@class RodsPageComponent : BasePageComponent
- local page = CreateBasicPage(RodsPageComponent, "Modify control rods here")
- local vert_stack = page:InsertChild(VerticalStackPanel.new():SetMargin(1, 1, 1, 2):SetAlignment("stretch"))
- DependencyObject.SetPropValue(vert_stack, UIComponent.BackgroundProperty, colours.black)
- page.InsertionTextBlock = vert_stack:InsertChild(DualColourTextBlock.new("Inserted: ", "0%", colours.white, colours.lightBlue, colours.black):SetAlignment("center"))
- vert_stack:InsertChild(UIComponent.SetSize(UIComponent.new(), 0, 1))
- page.InsertionProgBar = vert_stack:InsertChild(ProgressBar.new(0, 100, 0, colours.grey, colours.lightBlue):SetAlignment("stretch"))
- vert_stack:InsertChild(UIComponent.SetSize(UIComponent.new(), 0, 1))
- local button_grid = vert_stack:InsertChild(UIComponent.new():SetAlignment("stretch"))
- local left_side = button_grid:InsertChild(HorizontalStackPanel.new():SetAlignment("left"))
- local decr_b = left_side:InsertChild(Button.new("DecrBigButton", "<<"):SetSize(4, 1))
- local decr_s = left_side:InsertChild(Button.new("DecrSmallButton", "<"):SetSize(3, 1):SetMargin(1))
- local right_side = button_grid:InsertChild(HorizontalStackPanel.new():SetAlignment("right"))
- local incr_s = right_side:InsertChild(Button.new("IncrSmallButton", ">"):SetSize(3, 1))
- local incr_b = right_side:InsertChild(Button.new("IncrBigButton", ">>"):SetSize(4, 1):SetMargin(1))
- decr_b:AddEventHandler(Event_Button_OnClick, function (self, time, e) AddToRodInsertion(-10) end)
- decr_s:AddEventHandler(Event_Button_OnClick, function (self, time, e) AddToRodInsertion(-1) end)
- incr_b:AddEventHandler(Event_Button_OnClick, function (self, time, e) AddToRodInsertion(10) end)
- incr_s:AddEventHandler(Event_Button_OnClick, function (self, time, e) AddToRodInsertion(1) end)
- page.TotalControlRodLabel = page:InsertChild(DualColourTextBlock.new("Total control rods: ", "0", colours.white, colours.green))
- page.TotalControlRodLabel:SetMargin(1, 0, 1, 2):SetAlignment("left", "bottom")
- vert_stack:InsertChild(UIComponent.SetSize(UIComponent.new(), 0, 1))
- return page
- end
- function UpdateReactorAutomationLabels()
- local info = ReactorInfo.Automation
- local page = ReactorInfo.UI.AutoPage
- page.StorageOnTextBlock:SetPropValue(TextBlock.TextProperty, tostring(info.Minimum) .. "% RF")
- page.StorageOffTextBlock:SetPropValue(TextBlock.TextProperty, tostring(info.Maximum) .. "% RF")
- end
- function AddToReactorAutomation(increment, isMinimum)
- local info = ReactorInfo.Automation
- if (isMinimum) then
- info.Minimum = maths.clamp(info.Minimum + increment, 0, info.Maximum)
- else
- info.Maximum = maths.clamp(info.Maximum + increment, info.Minimum, 100)
- end
- UpdateReactorAutomationLabels()
- toolkit.SaveConfig()
- end
- function CreateAutoPage()
- local page = CreateBasicPage(AutoPageComponent, "Modify automation parameters")
- local vert_stack = page:InsertChild(VerticalStackPanel.new():SetMargin(1, 1, 1, 2))
- vert_stack:SetPropValue(UIComponent.HorizontalAlignmentProperty, "stretch")
- DependencyObject.SetPropValue(vert_stack, UIComponent.BackgroundProperty, colours.black)
- vert_stack:InsertChild(TextBlock.new("Configure automatic ON/OFF data"))
- vert_stack:InsertChild(TextBlock.new("You can enable this by clicking 'Automatic' button in the home screen"))
- vert_stack:InsertChild(UIComponent.SetSize(UIComponent.new(), 0, 1))
- vert_stack:InsertChild(TextBlock.new("Turn ON when storage drops below:", colours.white, colours.black))
- local stack_ON = vert_stack:InsertChild(HorizontalStackPanel.new())
- local btn_min_sub_b = stack_ON:InsertChild(Button.new("ON_Decr_Button_b", "--"):SetSize(4):SetMargin(0, 0, 0, 0))
- local btn_min_sub_s = stack_ON:InsertChild(Button.new("ON_Decr_Button_s", "-"):SetSize(3):SetMargin(1, 0, 0, 0))
- page.StorageOnTextBlock = stack_ON:InsertChild(TextBlock.new("0% RF", colours.white, colours.black):SetSize(7):SetMargin(1, 0, 1))
- local btn_min_add_s = stack_ON:InsertChild(Button.new("ON_Incr_Button_s", "+"):SetSize(3):SetMargin(0, 0, 0, 0))
- local btn_min_add_b = stack_ON:InsertChild(Button.new("ON_Incr_Button_b", "++"):SetSize(4):SetMargin(1, 0, 0, 0))
- vert_stack:InsertChild(UIComponent.SetSize(UIComponent.new(), 0, 1))
- vert_stack:InsertChild(TextBlock.new("Turn OFF when storage exceeds:", colours.white, colours.black))
- local stack_OFF = vert_stack:InsertChild(HorizontalStackPanel.new())
- local btn_max_sub_b = stack_OFF:InsertChild(Button.new("OFF_Decr_Button_b", "--"):SetSize(4):SetMargin(0, 0, 0, 0))
- local btn_max_sub_s = stack_OFF:InsertChild(Button.new("OFF_Decr_Button_s", "-"):SetSize(3):SetMargin(1, 0, 0, 0))
- page.StorageOffTextBlock = stack_OFF:InsertChild(TextBlock.new("100% RF", colours.white, colours.black):SetSize(7):SetMargin(1, 0, 1))
- local btn_max_add_s = stack_OFF:InsertChild(Button.new("OFF_Incr_Button_s", "+"):SetSize(3):SetMargin(0, 0, 0, 0))
- local btn_max_add_b = stack_OFF:InsertChild(Button.new("OFF_Incr_Button_b", "++"):SetSize(4):SetMargin(1, 0, 0, 0))
- btn_min_sub_s:AddEventHandler(Event_Button_OnClick, function (self, time, e) AddToReactorAutomation(-1, true) end)
- btn_min_sub_b:AddEventHandler(Event_Button_OnClick, function (self, time, e) AddToReactorAutomation(-10, true) end)
- btn_min_add_s:AddEventHandler(Event_Button_OnClick, function (self, time, e) AddToReactorAutomation(1, true) end)
- btn_min_add_b:AddEventHandler(Event_Button_OnClick, function (self, time, e) AddToReactorAutomation(10, true) end)
- btn_max_sub_s:AddEventHandler(Event_Button_OnClick, function (self, time, e) AddToReactorAutomation(-1, false) end)
- btn_max_sub_b:AddEventHandler(Event_Button_OnClick, function (self, time, e) AddToReactorAutomation(-10, false) end)
- btn_max_add_s:AddEventHandler(Event_Button_OnClick, function (self, time, e) AddToReactorAutomation(1, false) end)
- btn_max_add_b:AddEventHandler(Event_Button_OnClick, function (self, time, e) AddToReactorAutomation(10, false) end)
- return page
- end
- function CreateInfoPage()
- local page = CreateBasicPage(InfoPageComponent, "About the program")
- local vert_stack = page:InsertChild(VerticalStackPanel.new():SetMargin(1, 1, 1, 2))
- DependencyObject.SetPropValue(vert_stack, UIComponent.BackgroundProperty, colours.black)
- vert_stack:InsertChild(TextBlock.new("Made by TheRareCarrot :D", colours.white))
- vert_stack:InsertChild(TextBlock.new("BigReactor controller program v1.0"))
- vert_stack:InsertChild(TextBlock.new("using Lua Presentation Framework (LPF) v1.1"))
- vert_stack:InsertChild(UIComponent.new():SetSize(0, 1))
- vert_stack:InsertChild(TextBlock.new("The 'Z' button (bottom right) toggles auto-sleep after 5 minutes"))
- vert_stack:InsertChild(TextBlock.new("The 'Automatic' button toggles reactor activation automation (configurable in the Auto tab)"))
- vert_stack:InsertChild(UIComponent.new():SetSize(0, 1))
- vert_stack:InsertChild(TextBlock.new("This works best in the computer itself, or a 5x3 monitor, though the monitor can be bigger if you wish", colours.yellow))
- return page
- end
- function CreateMainContent(parent)
- PushFunction("CreateMainContent", parent.TypeName)
- local theTabControl = NewTypeInstance(TabControl, "TabControl")
- theTabControl:SetPropValue(UIComponent.HorizontalAlignmentProperty, "stretch")
- theTabControl:SetPropValue(UIComponent.VerticalAlignmentProperty, "stretch")
- local tabPanel = theTabControl:InsertChild(UniformPanel.new())
- tabPanel:SetPropValue(UIComponent.HorizontalAlignmentProperty, "stretch")
- tabPanel:SetPropValue(UIComponent.VerticalAlignmentProperty, "stretch")
- local page_Home = CreateHomePage()
- local page_Rods = CreateRodsPage()
- local page_Auto = CreateAutoPage()
- local page_Info = CreateInfoPage()
- ReactorInfo.UI.HomePage = page_Home
- ReactorInfo.UI.RodsPage = page_Rods
- ReactorInfo.UI.AutoPage = page_Auto
- TabItem_Home = tabPanel:InsertChild(NewTabItem("Home", page_Home))
- TabItem_Rods = tabPanel:InsertChild(NewTabItem("Rods", page_Rods))
- TabItem_Auto = tabPanel:InsertChild(NewTabItem("Auto", page_Auto))
- TabItem_Info = tabPanel:InsertChild(NewTabItem("Info", page_Info))
- local contentPresenter = ContentPresenter.new()
- contentPresenter:SetPropValue(UIComponent.HorizontalAlignmentProperty, "stretch")
- contentPresenter:SetPropValue(UIComponent.VerticalAlignmentProperty, "stretch")
- contentPresenter:SetPropValue(UIComponent.MarginTProperty, 1)
- theTabControl:InsertChild(contentPresenter)
- theTabControl.MyContentPresenter = contentPresenter
- theTabControl:SetActiveTabItem(TabItem_Home)
- parent:InsertChild(theTabControl)
- local tabChangedHandler = function (self, time, e)
- PushFunctionAndInvoke("DoReactorTick::tabChangedHandler", DoReactorTick, false)
- end
- TabItem_Home:AddEventHandler(Event_TabItem_OnNowActive, tabChangedHandler)
- TabItem_Rods:AddEventHandler(Event_TabItem_OnNowActive, tabChangedHandler)
- TabItem_Auto:AddEventHandler(Event_TabItem_OnNowActive, tabChangedHandler)
- PopFunction()
- end
- function FindPeripheralByType(theType)
- local names = peripheral.getNames()
- for i, name in ipairs(names) do
- if (peripheral.getType(name) == theType) then
- return peripheral.wrap(name)
- end
- end
- return nil
- end
- function FindReactor()
- return FindPeripheralByType("BigReactors-Reactor")
- end
- local AutoSleepButton
- function LoadReactorAndDoShit()
- ReactorInfo.Handle = FindReactor()
- toolkit.LoadConfig()
- AutoSleepButton.IsPressed = CanDisplayAutoSleep
- AutoSleepButton:InvalidateVisual()
- PushFunctionAndInvoke("DoReactorTick::LoadReactorAndDoShit", DoReactorTick, true)
- end
- function OnAppStartup()
- if (AppRootComponent == nil) then
- error("No root component")
- end
- CreateMainContent(AppRootComponent)
- AutoSleepButton = AppRootComponent:InsertChild(Button.new("AutoSleepButton", "Z"):SetAlignment("right", "bottom"))
- AutoSleepButton.IsToggleButton = true
- AutoSleepButton:AddEventHandler(Event_Button_OnClick, function (self, time, e)
- CanDisplayAutoSleep = self.IsPressed
- toolkit.SaveConfig()
- end)
- AppRootComponent:InvalidateLayoutAndVisual()
- InvokeAsync(LoadReactorAndDoShit, PriorityBackground)
- end
- local function SetupRootComponent()
- AppRootComponent:SetAlignment("stretch", "stretch")
- AppRootComponent.TypeName = "RootPanel"
- local w,h = toolkit.GetCompositionRenderSize()
- AppRootComponent:SetSize(w, h)
- AppRootComponent:UpdateVisibility()
- AppRootComponent:Arrange(0, 0, w, h)
- AppRootComponent:InvalidateLayoutAndVisual()
- toolkit.DoLayoutUpdate()
- end
- local function AppMain()
- PushFunction("AppMain")
- LastDisplayWakeTime = os.clock()
- toolkit.SetComponsitionTarget(FindPeripheralByType("monitor") or term) -- term size = 51 x 19
- ui.clear()
- AppRootComponent = UIComponent.new()
- SetupRootComponent();
- InvokeAsync(OnAppStartup, PriorityBackground)
- SetupTimer(AppTimerInterval)
- while IsAppRunning do
- local event, p1, p2, p3, p4, p5 = os.pullEventRaw()
- if (event == "terminate") then
- break
- end
- OnApplicationEvent(os.clock(), event, p1, p2, p3, p4, p5)
- end
- PopFunction()
- end
- --endregion
- local function Main()
- DebugFileHandle = fs.open("debug.log", "w")
- local is_main_success, errMsg = pcall(AppMain)
- toolkit.SetComponsitionTarget(term)
- ui.setBackgroundColour(colours.black)
- ui.setTextColour(colours.white)
- if (not is_main_success) then
- PrintStackTrace()
- print("The app has crashed! " .. errMsg)
- PrintDebug(tostring(errMsg))
- end
- DebugFileHandle.close()
- if (is_main_success) then
- print("Application has exited without error")
- end
- end
- Main()
Add Comment
Please, Sign In to add comment