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
- -- direction: -1 = up/away, +1 = down/towards
- local Event_PreviewMouseWheel = 4
- local Event_MouseWheel = 5
- 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)
- return self
- 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, bg_colour)
- local obj = NewTypeInstance(Button, readableTypeName or "Button")
- obj.IsPressed = false
- obj.IsToggleButton = false
- obj:SetPropValue(Button.TextProperty, buttonText)
- obj:SetPropValue(UIComponent.IsFocusableProperty, true)
- if (bg_colour == nil) then
- bg_colour = colours.grey
- end
- obj:SetPropValue(UIComponent.BackgroundProperty, bg_colour)
- 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 UIComponent:DoMouseScroll_Tunnel(time, direction, x, y)
- PushFunction("DoMouseScroll_Tunnel", self.TypeName, direction, x, y)
- if (self:IsMousePointOver(x, y)) then
- local args = {dir = direction, x = x, y = y, handled = false}
- self:RaiseEvent(Event_PreviewMouseWheel, 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:DoMouseScroll_Tunnel(time, direction, 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:DoMouseScroll_Bubble(time, direction, x, y)
- PushFunction("DoMouseScroll_Bubble", self.TypeName, direction, x, y)
- local eventArgs = {dir = direction, x = x, y = y, handled = false}
- if (self:RaiseEvent(Event_MouseWheel, time, eventArgs)) then
- return PopFunction(self)
- end
- if (self.Parent ~= nil) then
- x = x + self.RenderPosX
- y = y + self.RenderPosY
- return PopFunction(self.Parent:DoMouseScroll_Bubble(time, direction, 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
- function toolkit.OnMouseScroll(time, direction, absX, absY)
- if (direction ~= 1 and direction ~= -1) then
- return
- end
- local dirStr = "up"
- if (direction == 1) then
- dirStr = "down"
- end
- PushFunction("OnMouseScroll", dirStr, absX, absY)
- local hit,x,y = AppRootComponent:DoMouseScroll_Tunnel(time, direction, absX, absY)
- if (hit == nil) then
- toolkit.FocusComponent(nil)
- else
- local bubbleHit = hit:DoMouseScroll_Bubble(time, direction, x, y)
- if (bubbleHit ~= nil) then
- toolkit.FocusComponent(bubbleHit)
- else
- toolkit.FocusComponent(hit)
- end
- 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 == "mouse_scroll") then
- if (toolkit.OnWakeDisplay(time)) then return end
- toolkit.OnMouseScroll(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
- 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 IndexToColour(theIndex)
- if (theIndex == 0) then return colors.white
- elseif (theIndex == 1) then return colors.orange
- elseif (theIndex == 2) then return colors.magenta
- elseif (theIndex == 3) then return colors.lightBlue
- elseif (theIndex == 4) then return colors.yellow
- elseif (theIndex == 5) then return colors.lime
- elseif (theIndex == 6) then return colors.pink
- elseif (theIndex == 7) then return colors.gray
- elseif (theIndex == 8) then return colors.lightGray
- elseif (theIndex == 9) then return colors.cyan
- elseif (theIndex == 10) then return colors.purple
- elseif (theIndex == 11) then return colors.blue
- elseif (theIndex == 12) then return colors.brown
- elseif (theIndex == 13) then return colors.green
- elseif (theIndex == 14) then return colors.red
- elseif (theIndex == 15) then return colors.black
- else return nil end
- end
- function ColourToIndex(theColour)
- if (theColour == colors.white) then return 0
- elseif (theColour == colors.orange) then return 1
- elseif (theColour == colors.magenta) then return 2
- elseif (theColour == colors.lightBlue) then return 3
- elseif (theColour == colors.yellow) then return 4
- elseif (theColour == colors.lime) then return 5
- elseif (theColour == colors.pink) then return 6
- elseif (theColour == colors.gray) then return 7
- elseif (theColour == colors.lightGray) then return 8
- elseif (theColour == colors.cyan) then return 9
- elseif (theColour == colors.purple) then return 10
- elseif (theColour == colors.blue) then return 11
- elseif (theColour == colors.brown) then return 12
- elseif (theColour == colors.green) then return 13
- elseif (theColour == colors.red) then return 14
- elseif (theColour == colors.black) then return 15
- else return nil end
- end
- function IndexToColourString(theIndex)
- if (theIndex == 0) then return "white"
- elseif (theIndex == 1) then return "orange"
- elseif (theIndex == 2) then return "magenta"
- elseif (theIndex == 3) then return "light blue"
- elseif (theIndex == 4) then return "yellow"
- elseif (theIndex == 5) then return "lime"
- elseif (theIndex == 6) then return "pink"
- elseif (theIndex == 7) then return "gray"
- elseif (theIndex == 8) then return "light gray"
- elseif (theIndex == 9) then return "cyan"
- elseif (theIndex == 10) then return "purple"
- elseif (theIndex == 11) then return "blue"
- elseif (theIndex == 12) then return "brown"
- elseif (theIndex == 13) then return "green"
- elseif (theIndex == 14) then return "red"
- elseif (theIndex == 15) then return "black"
- else return nil end
- end
- function SetComponentColourIndex(component, colourIndex)
- local theColour = IndexToColour(colourIndex)
- component:SetPropValue(UIComponent.BackgroundProperty, theColour)
- component.ColourIndex = colourIndex
- component:InvalidateLayoutAndVisual()
- if (component.RelatedTextBlock1 ~= nil) then
- local tb = component.RelatedTextBlock1
- local colourName = IndexToColourString(colourIndex)
- UIComponent.SetPropValue(tb, TextBlock.TextProperty, colourName)
- UIComponent.InvalidateLayoutAndVisual(tb)
- end
- if (component.RelatedTextBlock2 ~= nil) then
- local tb = component.RelatedTextBlock2
- local str = tostring(colourIndex + 1) .. "/16"
- UIComponent.SetPropValue(tb, TextBlock.TextProperty, str)
- UIComponent.InvalidateLayoutAndVisual(tb)
- end
- end
- -- The colour channels. Background will be the E chest colours
- local Ch1, Ch2, Ch3
- local Ch1Txt, Ch2Txt, Ch3Txt
- local Ch1Txt2, Ch2Txt2, Ch3Txt2
- function OnChannelScrolled(component, time, args)
- local newColourIndex = component.ColourIndex + args.dir
- if (newColourIndex == -1) then
- newColourIndex = 15
- elseif (newColourIndex == 16) then
- newColourIndex = 0
- end
- SetComponentColourIndex(component, newColourIndex)
- end
- function CreateChannelScroller(theIndex)
- local component = UIComponent.new()
- component:SetAlignment("stretch", "stretch")
- component.MyChannelIndex = theIndex
- UIComponent.AddEventHandler(component, Event_MouseWheel, OnChannelScrolled)
- return component
- end
- function CreateMainContent(parent)
- PushFunction("CreateMainContent", parent.TypeName)
- local myDock = NewTypeInstance(DockPanel, "DockPanel")
- myDock:SetPropValue(UIComponent.HorizontalAlignmentProperty, "stretch")
- myDock:SetPropValue(UIComponent.VerticalAlignmentProperty, "stretch")
- local vertColourPalette = VerticalStackPanel.new()
- vertColourPalette.DockValue = "right"
- vertColourPalette:SetAlignment("right", "top"):SetMargin(nil, 1, 1, nil)
- UIComponent.InsertChild(vertColourPalette, TextBlock.new("Available", colours.white, colours.black))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.white))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.orange))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.magenta))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.lightBlue))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.yellow))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.lime))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.pink))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.gray))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.lightGray))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.cyan))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.purple))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.blue))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.brown))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.green))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.red))
- UIComponent.InsertChild(vertColourPalette, UIComponent.new():SetSize(9, 1):SetPropValue(UIComponent.BackgroundProperty, colors.black))
- UIComponent.InsertChild(myDock, vertColourPalette)
- local vertStack = VerticalStackPanel.new():SetAlignment("stretch", "stretch")
- vertStack.DockValue = "top"
- vertStack:SetMargin(1, 1)
- UIComponent.InsertChild(vertStack, TextBlock.new("Use Mouse Wheel to adjust colours", colours.white, colours.black))
- -- Horizontal Line of Channels
- local channelList = UniformPanel.new()
- channelList:SetPropValue(UniformPanel.SpacingProperty, "1")
- channelList:SetSize(38, 3)
- Ch1 = CreateChannelScroller(0)
- Ch2 = CreateChannelScroller(1)
- Ch3 = CreateChannelScroller(2)
- UIComponent.InsertChild(channelList, Ch1)
- UIComponent.InsertChild(channelList, Ch2)
- UIComponent.InsertChild(channelList, Ch3)
- UIComponent.InsertChild(vertStack, channelList)
- -- Horizontal Line of Channel colour readable names and info
- local channelInfoList = UniformPanel.new()
- channelInfoList:SetPropValue(UniformPanel.SpacingProperty, "1")
- channelInfoList:SetSize(38, 1)
- Ch1Txt = TextBlock.new("", colours.white, colours.black):SetAlignment("center", "center")
- Ch2Txt = TextBlock.new("", colours.white, colours.black):SetAlignment("center", "center")
- Ch3Txt = TextBlock.new("", colours.white, colours.black):SetAlignment("center", "center")
- UIComponent.InsertChild(channelInfoList, Ch1Txt)
- UIComponent.InsertChild(channelInfoList, Ch2Txt)
- UIComponent.InsertChild(channelInfoList, Ch3Txt)
- UIComponent.InsertChild(vertStack, channelInfoList)
- Ch1.RelatedTextBlock1 = Ch1Txt
- Ch2.RelatedTextBlock1 = Ch2Txt
- Ch3.RelatedTextBlock1 = Ch3Txt
- -- Horizontal Line of channel/limit things
- local channelInfoList2 = UniformPanel.new()
- channelInfoList2:SetPropValue(UniformPanel.SpacingProperty, "1")
- channelInfoList2:SetSize(38, 1)
- Ch1Txt2 = TextBlock.new("", colours.white, colours.black):SetAlignment("center", "center")
- Ch2Txt2 = TextBlock.new("", colours.white, colours.black):SetAlignment("center", "center")
- Ch3Txt2 = TextBlock.new("", colours.white, colours.black):SetAlignment("center", "center")
- UIComponent.InsertChild(channelInfoList2, Ch1Txt2)
- UIComponent.InsertChild(channelInfoList2, Ch2Txt2)
- UIComponent.InsertChild(channelInfoList2, Ch3Txt2)
- UIComponent.InsertChild(vertStack, channelInfoList2)
- Ch1.RelatedTextBlock2 = Ch1Txt2
- Ch2.RelatedTextBlock2 = Ch2Txt2
- Ch3.RelatedTextBlock2 = Ch3Txt2
- SetComponentColourIndex(Ch1, 0)
- SetComponentColourIndex(Ch2, 0)
- SetComponentColourIndex(Ch3, 0)
- UIComponent.InsertChild(myDock, vertStack)
- local btnList = HorizontalStackPanel.new()
- btnList.DockValue = "bottom"
- btnList:SetMargin(1, 1, 1, 1)
- local applyButton = Button.new("ApplyButton", "Apply Chest(s)")
- applyButton:SetSize(18, 3)
- applyButton:AddEventHandler(Event_Button_OnClick, function (self, time, args)
- for i, sideOrName in ipairs(peripheral.getNames()) do
- local peripType = peripheral.getType(sideOrName)
- if (peripType == "ender_chest" or peripType == "ender_tank") then
- local chest = peripheral.wrap(sideOrName)
- local ch1 = Ch1:GetPropValue(UIComponent.BackgroundProperty) or colours.white
- local ch2 = Ch2:GetPropValue(UIComponent.BackgroundProperty) or colours.white
- local ch3 = Ch3:GetPropValue(UIComponent.BackgroundProperty) or colours.white
- chest.setColours(ch1, ch2, ch3)
- end
- end
- end)
- local readButton = Button.new("ReadButton", "Read Chest(s)", colours.lightGray)
- readButton:SetSize(17, 3):SetMargin(1)
- readButton:AddEventHandler(Event_Button_OnClick, function (self, time, args)
- for i, sideOrName in ipairs(peripheral.getNames()) do
- if (peripheral.getType(sideOrName) == "ender_chest") then
- local chest = peripheral.wrap(sideOrName)
- local ch1,ch2,ch3 = chest.getColours()
- SetComponentColourIndex(Ch1, ColourToIndex(ch1))
- SetComponentColourIndex(Ch2, ColourToIndex(ch2))
- SetComponentColourIndex(Ch3, ColourToIndex(ch3))
- return
- end
- end
- end)
- UIComponent.InsertChild(btnList, applyButton)
- UIComponent.InsertChild(btnList, readButton)
- UIComponent.InsertChild(myDock, btnList)
- UIComponent.InsertChild(parent, myDock)
- 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
- local AutoSleepButton
- function LoadReactorAndDoShit()
- 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"
- AppRootComponent:SetPropValue(UIComponent.BackgroundProperty, colours.black)
- 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