Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- local cliEnabled = false
- -- ReactiveUI: A React-like UI framework for ComputerCraft
- -- Requires: animgraph library for animations
- local ReactiveUI = {}
- local AnimGraph = {}
- local currentApp = nil
- -- ============================================================================
- -- ANIMGRAPH INTEGRATION
- -- ============================================================================
- -- Constants
- local FPS = 20 -- ComputerCraft runs at 20 ticks per second
- -- Helper function to create animation graph objects
- local function createGraph(duration, easingFunc, startVal, endVal)
- local totalFrames = math.floor(duration * FPS)
- if totalFrames < 1 then totalFrames = 1 end
- local hasRange = (startVal ~= nil and endVal ~= nil)
- local graph = {}
- -- Metatable to handle indexing
- setmetatable(graph, {
- __index = function(t, frame)
- if frame == 0 then
- -- Return total frames when indexed with 0
- return totalFrames
- elseif frame < 0 or frame > totalFrames then
- -- Clamp to valid range
- frame = math.max(0, math.min(frame, totalFrames))
- end
- -- Calculate progress (0 to 1)
- local progress = frame / totalFrames
- -- Apply easing function
- local easedProgress = easingFunc(progress)
- -- If range specified, interpolate between start and end values
- if hasRange then
- return startVal + (endVal - startVal) * easedProgress
- else
- return easedProgress
- end
- end
- })
- return graph
- end
- -- Easing Functions (all return values from 0 to 1)
- -- Linear
- local function linear(t)
- return t
- end
- -- Ease In (Quadratic)
- local function easeInQuad(t)
- return t * t
- end
- -- Ease Out (Quadratic)
- local function easeOutQuad(t)
- return t * (2 - t)
- end
- -- Ease In-Out (Quadratic)
- local function easeInOutQuad(t)
- if t < 0.5 then
- return 2 * t * t
- else
- return -1 + (4 - 2 * t) * t
- end
- end
- -- Ease In (Cubic)
- local function easeInCubic(t)
- return t * t * t
- end
- -- Ease Out (Cubic)
- local function easeOutCubic(t)
- local t1 = t - 1
- return t1 * t1 * t1 + 1
- end
- -- Ease In-Out (Cubic)
- local function easeInOutCubic(t)
- if t < 0.5 then
- return 4 * t * t * t
- else
- local t1 = 2 * t - 2
- return (t1 * t1 * t1 + 2) / 2
- end
- end
- -- Ease In (Quartic)
- local function easeInQuart(t)
- return t * t * t * t
- end
- -- Ease Out (Quartic)
- local function easeOutQuart(t)
- local t1 = t - 1
- return 1 - t1 * t1 * t1 * t1
- end
- -- Ease In-Out (Quartic)
- local function easeInOutQuart(t)
- if t < 0.5 then
- return 8 * t * t * t * t
- else
- local t1 = t - 1
- return 1 - 8 * t1 * t1 * t1 * t1
- end
- end
- -- Ease In (Quintic)
- local function easeInQuint(t)
- return t * t * t * t * t
- end
- -- Ease Out (Quintic)
- local function easeOutQuint(t)
- local t1 = t - 1
- return t1 * t1 * t1 * t1 * t1 + 1
- end
- -- Ease In-Out (Quintic)
- local function easeInOutQuint(t)
- if t < 0.5 then
- return 16 * t * t * t * t * t
- else
- local t1 = 2 * t - 2
- return (t1 * t1 * t1 * t1 * t1 + 2) / 2
- end
- end
- -- Ease In (Sine)
- local function easeInSine(t)
- return 1 - math.cos(t * math.pi / 2)
- end
- -- Ease Out (Sine)
- local function easeOutSine(t)
- return math.sin(t * math.pi / 2)
- end
- -- Ease In-Out (Sine)
- local function easeInOutSine(t)
- return -(math.cos(math.pi * t) - 1) / 2
- end
- -- Ease In (Exponential)
- local function easeInExpo(t)
- if t == 0 then return 0 end
- return math.pow(2, 10 * (t - 1))
- end
- -- Ease Out (Exponential)
- local function easeOutExpo(t)
- if t == 1 then return 1 end
- return 1 - math.pow(2, -10 * t)
- end
- -- Ease In-Out (Exponential)
- local function easeInOutExpo(t)
- if t == 0 then return 0 end
- if t == 1 then return 1 end
- if t < 0.5 then
- return math.pow(2, 20 * t - 10) / 2
- else
- return (2 - math.pow(2, -20 * t + 10)) / 2
- end
- end
- -- Ease In (Circular)
- local function easeInCirc(t)
- return 1 - math.sqrt(1 - t * t)
- end
- -- Ease Out (Circular)
- local function easeOutCirc(t)
- local t1 = t - 1
- return math.sqrt(1 - t1 * t1)
- end
- -- Ease In-Out (Circular)
- local function easeInOutCirc(t)
- if t < 0.5 then
- return (1 - math.sqrt(1 - 4 * t * t)) / 2
- else
- local t1 = -2 * t + 2
- return (math.sqrt(1 - t1 * t1) + 1) / 2
- end
- end
- -- Ease In (Back - overshoots)
- local function easeInBack(t)
- local c = 1.70158
- return t * t * ((c + 1) * t - c)
- end
- -- Ease Out (Back - overshoots)
- local function easeOutBack(t)
- local c = 1.70158
- local t1 = t - 1
- return 1 + t1 * t1 * ((c + 1) * t1 + c)
- end
- -- Ease In-Out (Back - overshoots)
- local function easeInOutBack(t)
- local c = 1.70158 * 1.525
- if t < 0.5 then
- local t2 = 2 * t
- return (t2 * t2 * ((c + 1) * t2 - c)) / 2
- else
- local t2 = 2 * t - 2
- return (t2 * t2 * ((c + 1) * t2 + c) + 2) / 2
- end
- end
- -- Ease In (Elastic - bouncy)
- local function easeInElastic(t)
- if t == 0 then return 0 end
- if t == 1 then return 1 end
- local c = (2 * math.pi) / 3
- return -math.pow(2, 10 * t - 10) * math.sin((t * 10 - 10.75) * c)
- end
- -- Ease Out (Elastic - bouncy)
- local function easeOutElastic(t)
- if t == 0 then return 0 end
- if t == 1 then return 1 end
- local c = (2 * math.pi) / 3
- return math.pow(2, -10 * t) * math.sin((t * 10 - 0.75) * c) + 1
- end
- -- Ease In-Out (Elastic - bouncy)
- local function easeInOutElastic(t)
- if t == 0 then return 0 end
- if t == 1 then return 1 end
- local c = (2 * math.pi) / 4.5
- if t < 0.5 then
- return -(math.pow(2, 20 * t - 10) * math.sin((20 * t - 11.125) * c)) / 2
- else
- return (math.pow(2, -20 * t + 10) * math.sin((20 * t - 11.125) * c)) / 2 + 1
- end
- end
- -- Ease Out (Bounce)
- local function easeOutBounce(t)
- local n1 = 7.5625
- local d1 = 2.75
- if t < 1 / d1 then
- return n1 * t * t
- elseif t < 2 / d1 then
- t = t - 1.5 / d1
- return n1 * t * t + 0.75
- elseif t < 2.5 / d1 then
- t = t - 2.25 / d1
- return n1 * t * t + 0.9375
- else
- t = t - 2.625 / d1
- return n1 * t * t + 0.984375
- end
- end
- -- Ease In (Bounce)
- local function easeInBounce(t)
- return 1 - easeOutBounce(1 - t)
- end
- -- Ease In-Out (Bounce)
- local function easeInOutBounce(t)
- if t < 0.5 then
- return (1 - easeOutBounce(1 - 2 * t)) / 2
- else
- return (1 + easeOutBounce(2 * t - 1)) / 2
- end
- end
- -- Public API
- AnimGraph.linear = function(duration, startVal, endVal) return createGraph(duration, linear, startVal, endVal) end
- AnimGraph.easein = function(duration, startVal, endVal) return createGraph(duration, easeInQuad, startVal, endVal) end
- AnimGraph.easeout = function(duration, startVal, endVal) return createGraph(duration, easeOutQuad, startVal, endVal) end
- AnimGraph.easeinout = function(duration, startVal, endVal) return createGraph(duration, easeInOutQuad, startVal, endVal) end
- AnimGraph.easeinCubic = function(duration, startVal, endVal) return createGraph(duration, easeInCubic, startVal, endVal) end
- AnimGraph.easeoutCubic = function(duration, startVal, endVal) return createGraph(duration, easeOutCubic, startVal, endVal) end
- AnimGraph.easeinoutCubic = function(duration, startVal, endVal)
- return createGraph(duration, easeInOutCubic, startVal,
- endVal)
- end
- AnimGraph.easeinQuart = function(duration, startVal, endVal) return createGraph(duration, easeInQuart, startVal, endVal) end
- AnimGraph.easeoutQuart = function(duration, startVal, endVal) return createGraph(duration, easeOutQuart, startVal, endVal) end
- AnimGraph.easeinoutQuart = function(duration, startVal, endVal)
- return createGraph(duration, easeInOutQuart, startVal,
- endVal)
- end
- AnimGraph.easeinQuint = function(duration, startVal, endVal) return createGraph(duration, easeInQuint, startVal, endVal) end
- AnimGraph.easeoutQuint = function(duration, startVal, endVal) return createGraph(duration, easeOutQuint, startVal, endVal) end
- AnimGraph.easeinoutQuint = function(duration, startVal, endVal)
- return createGraph(duration, easeInOutQuint, startVal,
- endVal)
- end
- AnimGraph.easeinSine = function(duration, startVal, endVal) return createGraph(duration, easeInSine, startVal, endVal) end
- AnimGraph.easeoutSine = function(duration, startVal, endVal) return createGraph(duration, easeOutSine, startVal, endVal) end
- AnimGraph.easeinoutSine = function(duration, startVal, endVal)
- return createGraph(duration, easeInOutSine, startVal,
- endVal)
- end
- AnimGraph.easeinExpo = function(duration, startVal, endVal) return createGraph(duration, easeInExpo, startVal, endVal) end
- AnimGraph.easeoutExpo = function(duration, startVal, endVal) return createGraph(duration, easeOutExpo, startVal, endVal) end
- AnimGraph.easeinoutExpo = function(duration, startVal, endVal)
- return createGraph(duration, easeInOutExpo, startVal,
- endVal)
- end
- AnimGraph.easeinCirc = function(duration, startVal, endVal) return createGraph(duration, easeInCirc, startVal, endVal) end
- AnimGraph.easeoutCirc = function(duration, startVal, endVal) return createGraph(duration, easeOutCirc, startVal, endVal) end
- AnimGraph.easeinoutCirc = function(duration, startVal, endVal)
- return createGraph(duration, easeInOutCirc, startVal,
- endVal)
- end
- AnimGraph.easeinBack = function(duration, startVal, endVal) return createGraph(duration, easeInBack, startVal, endVal) end
- AnimGraph.easeoutBack = function(duration, startVal, endVal) return createGraph(duration, easeOutBack, startVal, endVal) end
- AnimGraph.easeinoutBack = function(duration, startVal, endVal)
- return createGraph(duration, easeInOutBack, startVal,
- endVal)
- end
- AnimGraph.easeinElastic = function(duration, startVal, endVal)
- return createGraph(duration, easeInElastic, startVal,
- endVal)
- end
- AnimGraph.easeoutElastic = function(duration, startVal, endVal)
- return createGraph(duration, easeOutElastic, startVal,
- endVal)
- end
- AnimGraph.easeinoutElastic = function(duration, startVal, endVal)
- return createGraph(duration, easeInOutElastic, startVal,
- endVal)
- end
- AnimGraph.easeinBounce = function(duration, startVal, endVal) return createGraph(duration, easeInBounce, startVal, endVal) end
- AnimGraph.easeoutBounce = function(duration, startVal, endVal)
- return createGraph(duration, easeOutBounce, startVal,
- endVal)
- end
- AnimGraph.easeinoutBounce = function(duration, startVal, endVal)
- return createGraph(duration, easeInOutBounce, startVal,
- endVal)
- end
- -- Utility: Create custom easing function
- function AnimGraph.custom(duration, easingFunc, startVal, endVal)
- return createGraph(duration, easingFunc, startVal, endVal)
- end
- -- Utility: Set FPS (default is 20 for ComputerCraft)
- function AnimGraph.setFPS(i)
- FPS = fps
- end
- ReactiveUI.animGraph = AnimGraph
- -- ============================================================================
- -- CORE STATE MANAGEMENT
- -- ============================================================================
- local currentComponent = nil
- local componentStack = {}
- local globalState = {}
- local globalStore = {}
- local routerState = {
- currentRoute = "/",
- routes = {},
- history = {}
- }
- local roundedEnabled = false
- local colorMap = {
- black = colors.black,
- white = colors.white,
- gray = colors.gray,
- grey = colors.gray,
- red = colors.red,
- green = colors.green,
- yellow = colors.yellow,
- blue = colors.blue,
- magenta = colors.magenta,
- cyan = colors.cyan,
- orange = colors.orange,
- lightBlue = colors.lightBlue,
- lightGray = colors.lightGray,
- brown = colors.brown,
- lime = colors.lime,
- pink = colors.pink
- }
- -- ============================================================================
- -- STYLING ENGINE
- -- ============================================================================
- local function mergeStyles(base, override)
- local result = {}
- for k, v in pairs(base or {}) do
- result[k] = v
- end
- for k, v in pairs(override or {}) do
- result[k] = v
- end
- return result
- end
- local function parseClass(cls, isMd, isHover)
- local style = {}
- if cls == "rounded" then
- style.rounded = true
- elseif cls:match("^bg%-(%w+)$") then
- local colName = cls:match("^bg%-(%w+)$")
- style.bg = colorMap[colName] or colors.black
- elseif cls:match("^text%-(%w+)$") then
- local colName = cls:match("^text%-(%w+)$")
- style.fg = colorMap[colName] or colors.white
- elseif cls:match("^w%-(%d+)$") then
- local sz = tonumber(cls:match("^w%-(%d+)$"))
- style.width = sz
- elseif cls:match("^h%-(%d+)$") then
- local sz = tonumber(cls:match("^h%-(%d+)$"))
- style.height = sz
- elseif cls:match("^p%-(%d+)$") then
- local sz = tonumber(cls:match("^p%-(%d+)$"))
- style.padding = sz
- elseif cls:match("^animate%-pulse$") then
- style.animate = "pulse"
- end
- return style
- end
- local function parseClasses(classStr, screenW)
- local base = {}
- local hover = {}
- local focus = {}
- local active = {}
- if classStr then
- for cls in classStr:gmatch("%S+") do
- local target = base
- if cls:match("^md:") then
- if screenW < 40 then goto continue end
- cls = cls:sub(4)
- elseif cls:match("^hover:") then
- target = hover
- cls = cls:sub(7)
- end
- local s = parseClass(cls, true, target == hover)
- for k, v in pairs(s) do
- target[k] = v
- end
- ::continue::
- end
- end
- return base, hover, focus, active
- end
- local function applyStyle(x, y, w, h, style)
- local borderSize = style.border and 1 or 0
- local pad = style.padding or 0
- local innerX = x + borderSize + pad
- local innerY = y + borderSize + pad
- local innerW = w - 2 * borderSize - 2 * pad
- local innerH = h - 2 * borderSize - 2 * pad
- if innerW < 1 then innerW = 1 end
- if innerH < 1 then innerH = 1 end
- if style.bg then
- term.setBackgroundColor(style.bg)
- end
- if style.fg then
- term.setTextColor(style.fg)
- end
- -- Fill background
- if style.bg then
- for dy = 0, h - 1 do
- term.setCursorPos(x, y + dy)
- term.write(string.rep(" ", w))
- end
- end
- -- Border
- if style.border then
- term.setTextColor(style.borderColor or colors.gray)
- local topStr, bottomStr, sideLeft, sideRight
- if style.rounded and roundedEnabled then
- topStr = "╭" .. string.rep("─", w - 2) .. "╮"
- bottomStr = "╰" .. string.rep("─", w - 2) .. "╯"
- sideLeft = "│"
- sideRight = "│"
- else
- topStr = string.rep("-", w)
- bottomStr = string.rep("-", w)
- sideLeft = "|"
- sideRight = "|"
- end
- term.setCursorPos(x, y)
- term.write(topStr)
- term.setCursorPos(x, y + h - 1)
- term.write(bottomStr)
- for dy = 1, h - 2 do
- term.setCursorPos(x, y + dy)
- term.write(sideLeft)
- term.setCursorPos(x + w - 1, y + dy)
- term.write(sideRight)
- end
- end
- return innerX, innerY, innerW, innerH
- end
- -- ============================================================================
- -- ELEMENT SYSTEM
- -- ============================================================================
- local Element = {}
- Element.__index = Element
- function Element.new(type, props)
- local self = setmetatable({}, Element)
- self.type = type
- self.props = props or {}
- self.id = props.id
- self.transitions = props.transitions or {}
- self.activeAnimations = {}
- local screenW, _ = term.getSize()
- local baseCls, hoverCls, focusCls, activeCls = parseClasses(props.class, screenW)
- self.style = mergeStyles(props.style or baseCls, baseCls)
- if type == "input" and (not self.style.fg) then
- self.style.fg = colors.black
- end
- self.hoverStyle = mergeStyles(props.hoverStyle or {}, hoverCls)
- self.activeStyle = mergeStyles(props.activeStyle or {}, activeCls)
- self.focusStyle = mergeStyles(props.focusStyle or {}, focusCls)
- self.children = props.children or {}
- self.dragging = false
- self.dragType = nil
- self.parent = nil
- self.mounted = false
- self.hovered = false
- self.active = false
- self.focused = false
- self.cursorPos = (type == "input") and 0 or nil
- self.animations = {}
- self.targetScroll = nil
- -- Initialize current values from props
- self.x = props.x or 1
- self.y = props.y or 1
- self.width = self.style.width or props.width or 10
- self.height = self.style.height or props.height or 3
- -- Set parent for children
- for _, child in ipairs(self.children) do
- if type(child) == "table" and child.setParent then
- child:setParent(self)
- end
- end
- return self
- end
- function Element:setParent(parent)
- self.parent = parent
- self:updateAbsolutePosition()
- end
- function Element:updateAbsolutePosition()
- if self.parent then
- -- Calculate absolute position based on parent
- local parentBorder = self.parent.style.border and 1 or 0
- local parentPad = self.parent.style.padding or 0
- self.x = self.parent.x + parentBorder + parentPad + (self.props.x or 0)
- self.y = self.parent.y + parentBorder + parentPad + (self.props.y or 0)
- else
- self.x = self.props.x or 1
- self.y = self.props.y or 1
- end
- -- Update children positions
- for _, child in ipairs(self.children) do
- if type(child) == "table" and child.updateAbsolutePosition then
- child:updateAbsolutePosition()
- end
- end
- end
- function Element:updateFromProps(props)
- self.props = props
- local screenW, _ = term.getSize()
- local baseCls, hoverCls, focusCls, activeCls = parseClasses(props.class, screenW)
- self.style = mergeStyles(props.style or baseCls, baseCls)
- if self.type == "input" and (not self.style.fg) then
- self.style.fg = colors.black
- end
- self.hoverStyle = mergeStyles(props.hoverStyle or {}, hoverCls)
- self.activeStyle = mergeStyles(props.activeStyle or {}, activeCls)
- self.focusStyle = mergeStyles(props.focusStyle or {}, focusCls)
- self.width = self.style.width or props.width or 10
- self.height = self.style.height or props.height or 3
- end
- function Element:isPointInside(px, py)
- return px >= self.x and px < self.x + self.width and
- py >= self.y and py < self.y + self.height
- end
- function Element:getCurrentStyle()
- local style = self.style
- if self.focused and next(self.focusStyle) then
- style = mergeStyles(style, self.focusStyle)
- elseif self.active and next(self.activeStyle) then
- style = mergeStyles(style, self.activeStyle)
- elseif self.hovered and next(self.hoverStyle) then
- style = mergeStyles(style, self.hoverStyle)
- end
- return style
- end
- function Element:render(scrollOffset, offsetX, offsetY)
- scrollOffset = scrollOffset or 0
- offsetX = offsetX or 0
- offsetY = offsetY or 0
- local style = self:getCurrentStyle()
- -- Apply scroll offset to rendering position
- local renderY = math.floor(self.y + 0.5) + offsetY - scrollOffset
- local renderX = math.floor(self.x + 0.5) + offsetX
- local innerX, innerY, innerW, innerH = applyStyle(renderX, renderY, self.width, self.height, style)
- if self.type == "box" or self.type == "container" or self.type == "scrollview" then
- elseif self.type == "text" then
- term.setCursorPos(renderX, renderY)
- term.setTextColor(style.fg or colors.white)
- term.setBackgroundColor(style.bg or colors.black)
- term.write(self.props.text or "")
- elseif self.type == "button" then
- local text = self.props.text or "Button"
- local textX = innerX + math.floor((innerW - #text) / 2)
- local textY = innerY + math.floor(innerH / 2)
- term.setCursorPos(textX, textY)
- term.setTextColor(style.fg or colors.white)
- term.write(text)
- elseif self.type == "input" then
- local text = self.props.value or ""
- local displayText = text
- local textColor = style.fg or colors.white
- if text == "" then
- displayText = self.props.placeholder or ""
- textColor = colors.gray
- else
- if self.props.password then
- displayText = string.rep("*", #text)
- end
- end
- displayText = string.sub(displayText, 1, innerW)
- term.setCursorPos(innerX, innerY)
- term.setTextColor(textColor)
- term.setBackgroundColor(style.bg or colors.white)
- term.write(displayText)
- if #displayText < innerW then
- term.write(string.rep(" ", innerW - #displayText))
- end
- elseif self.type == "checkbox" then
- local checked = self.props.checked or false
- local checkChar = checked and "X" or " "
- local label = self.props.label or ""
- term.setCursorPos(innerX, innerY)
- term.setTextColor(style.fg or colors.white)
- term.write("[" .. checkChar .. "] " .. label)
- elseif self.type == "radio" then
- local selected = self.props.selected or false
- local selectChar = selected and "*" or " "
- local label = self.props.label or ""
- term.setCursorPos(innerX, innerY)
- term.setTextColor(style.fg or colors.white)
- term.write("(" .. selectChar .. ") " .. label)
- elseif self.type == "slider" then
- local value = self.props.value or 0
- local min = self.props.min or 0
- local max = self.props.max or 100
- local sliderWidth = innerW
- local normalizedValue = (value - min) / (max - min)
- local sliderPos = math.floor(normalizedValue * sliderWidth)
- term.setCursorPos(innerX, innerY)
- term.setTextColor(style.fg or colors.white)
- term.write("[" .. string.rep("-", sliderPos) .. "O" .. string.rep("-", sliderWidth - sliderPos - 1) .. "]")
- if self.props.showValue then
- term.setCursorPos(innerX + innerW - #tostring(value) - 1, innerY)
- term.write(tostring(value))
- end
- elseif self.type == "file" then
- local filename = self.props.value or ""
- local displayText = filename ~= "" and filename or (self.props.placeholder or "No file selected")
- local textColor = filename ~= "" and (style.fg or colors.white) or colors.gray
- displayText = string.sub(displayText, 1, innerW - 8)
- term.setCursorPos(innerX, innerY)
- term.setTextColor(textColor)
- term.write(displayText)
- -- Browse button
- local browseX = renderX + self.width - 8
- term.setCursorPos(browseX, innerY)
- term.setTextColor(colors.white)
- term.setBackgroundColor(colors.blue)
- term.write("Browse")
- elseif self.type == "list" then
- if self.targetScroll then
- local current = self.props.scroll or 0
- local delta = self.targetScroll - current
- if math.abs(delta) < 1 then
- self.props.scroll = self.targetScroll
- self.targetScroll = nil
- else
- self.props.scroll = current + math.floor(delta * 0.3)
- end
- end
- local scroll = self.props.scroll or 0
- local borderOffset = (style.border and 2 or 0)
- local visibleHeight = innerH
- local displayWidth = innerW - 1
- for i = 1, visibleHeight do
- local itemIndex = scroll + i
- if itemIndex <= # (self.props.items or {}) then
- local item = (self.props.items or {})[itemIndex]
- local itemY = innerY + i - 1
- term.setCursorPos(innerX, itemY)
- local selectedIndex = self.props.selectedIndex or nil
- if itemIndex == selectedIndex then
- term.setBackgroundColor(style.selectedBg or colors.blue)
- term.setTextColor(style.selectedFg or colors.white)
- else
- term.setBackgroundColor(style.bg or colors.black)
- term.setTextColor(style.fg or colors.white)
- end
- local displayText = tostring(item)
- if #displayText > displayWidth then
- displayText = string.sub(displayText, 1, displayWidth - 3) .. "..."
- else
- displayText = displayText .. string.rep(" ", displayWidth - #displayText)
- end
- term.write(displayText)
- end
- end
- local totalItems = #(self.props.items or {})
- if totalItems > visibleHeight then
- local barX = renderX + self.width - 1
- local maxScroll = totalItems - visibleHeight
- local thumbHeight = math.max(1, math.floor((visibleHeight / totalItems) * visibleHeight))
- local thumbTop = math.floor((scroll / maxScroll) * (visibleHeight - thumbHeight)) + 1
- for i = 1, visibleHeight do
- term.setCursorPos(barX, innerY + i - 1)
- if i >= thumbTop and i < thumbTop + thumbHeight then
- term.setBackgroundColor(colors.lightGray)
- term.setTextColor(colors.white)
- term.write("█")
- else
- term.setBackgroundColor(colors.black)
- term.setTextColor(colors.gray)
- term.write("│")
- end
- end
- end
- end
- -- Render children with the same scroll offset and offsets
- for _, child in ipairs(self.children) do
- if child.render then
- child:render(scrollOffset, offsetX, offsetY)
- end
- end
- end
- function Element:focus()
- self.focused = true
- if self.type == "input" then
- local text = self.props.value or ""
- self.cursorPos = #text
- end
- if self.props.onFocus then
- self.props.onFocus(self)
- end
- end
- function Element:blur()
- if not self.focused then return end
- self.focused = false
- if self.props.onBlur then
- self.props.onBlur(self)
- end
- end
- function Element:handleClick(x, y, button, app)
- -- Check children first (top to bottom)
- for i = #self.children, 1, -1 do
- local child = self.children[i]
- if child.handleClick and child:handleClick(x, y, button, app) then
- return true
- end
- end
- if self:isPointInside(x, y) then
- self.active = true
- -- Handle focus for focusable elements
- if self.type == "input" then
- -- Unfocus all other elements
- if app then
- local allElements = app:getAllElements()
- for _, el in ipairs(allElements) do
- if el ~= self and el.focused then
- el:blur()
- end
- end
- end
- -- Focus this element
- if not self.focused then
- self:focus()
- end
- -- Set cursor position
- local text = self.props.value or ""
- local relPos = math.max(0, math.min(#text, x - self.x - 1))
- self.cursorPos = relPos
- elseif self.type == "checkbox" then
- self.props.checked = not (self.props.checked or false)
- if self.props.onChange then
- self.props.onChange(self.props.checked)
- end
- elseif self.type == "radio" then
- if not (self.props.selected or false) then
- if self.props.group and app then
- local allEls = app:getAllElements()
- for _, other in ipairs(allEls) do
- if other.type == "radio" and other.props.group == self.props.group and other.props.selected then
- other.props.selected = false
- end
- end
- end
- self.props.selected = true
- if self.props.onChange then
- self.props.onChange(self.props.value)
- end
- end
- elseif self.type == "slider" then
- local innerW = self.width - 4
- local clickPos = x - self.x - 2
- if clickPos >= 0 and clickPos <= innerW then
- local min = self.props.min or 0
- local max = self.props.max or 100
- local value = min + (clickPos / innerW) * (max - min)
- self.props.value = math.floor(value + 0.5)
- self.dragging = true
- self.dragType = "slider"
- if self.props.onChange then
- self.props.onChange(self.props.value)
- end
- end
- elseif self.type == "file" then
- -- Check if browse button was clicked
- if x >= self.x + self.width - 8 and x < self.x + self.width - 1 then
- if self.props.onBrowse then
- self.props.onBrowse(self)
- end
- end
- elseif self.type == "list" then
- local innerH = self.height - (self.style.border and 2 or 0)
- local clickY = y - self.y - (self.style.border and 1 or 0)
- local scroll = self.props.scroll or 0
- local itemIndex = scroll + clickY
- if itemIndex >= 1 and itemIndex <= #(self.props.items or {}) then
- self.props.selectedIndex = itemIndex
- if self.props.onSelect then
- self.props.onSelect(itemIndex, self.props.items[itemIndex])
- end
- end
- end
- -- Call custom onClick if provided (allows user override)
- if self.props.onClick then
- self.props.onClick(self, x, y, button)
- end
- return true
- end
- return false
- end
- function Element:handleHover(x, y)
- -- Update children hover state
- if self.dragging and self.dragType == "slider" and self.type == "slider" then
- local innerW = self.width - 4
- local clickPos = x - self.x - 2
- clickPos = math.max(0, math.min(clickPos, innerW))
- local min = self.props.min or 0
- local max = self.props.max or 100
- local value = min + (clickPos / innerW) * (max - min)
- self.props.value = math.floor(value + 0.5)
- if self.props.onChange then
- self.props.onChange(self.props.value)
- end
- end
- -- Update children hover state
- for _, child in ipairs(self.children) do
- if child.handleHover then
- child:handleHover(x, y)
- end
- end
- local wasHovered = self.hovered
- self.hovered = self:isPointInside(x, y)
- if self.hovered and not wasHovered and self.props.onHover then
- self.props.onHover(self)
- elseif not self.hovered and wasHovered and self.props.onHoverEnd then
- self.props.onHoverEnd(self)
- end
- end
- function Element:handleKey(key)
- if self.focused and self.type == "input" then
- local text = self.props.value or ""
- local handled = false
- if key == keys.backspace then
- if self.cursorPos > 0 then
- text = text:sub(1, self.cursorPos - 1) .. text:sub(self.cursorPos + 1)
- self.cursorPos = self.cursorPos - 1
- self.props.value = text
- handled = true
- end
- elseif key == keys.delete then
- if self.cursorPos < #text then
- text = text:sub(1, self.cursorPos) .. text:sub(self.cursorPos + 2)
- self.props.value = text
- handled = true
- end
- elseif key == keys.left then
- self.cursorPos = math.max(0, self.cursorPos - 1)
- handled = true
- elseif key == keys.right then
- self.cursorPos = math.min(#text, self.cursorPos + 1)
- handled = true
- elseif key == keys.home then
- self.cursorPos = 0
- handled = true
- elseif key == keys["end"] then
- self.cursorPos = #text
- handled = true
- elseif key == keys.enter then
- if self.props.onSubmit then
- self.props.onSubmit(text)
- end
- -- Unfocus on Enter
- self:blur()
- handled = true
- elseif key == keys.tab then
- -- Tab to next focusable element (handled by app)
- handled = true
- end
- if handled and self.props.onChange then
- self.props.onChange(self.props.value)
- end
- return handled
- end
- return false
- end
- function Element:handleChar(char)
- if self.focused and self.type == "input" then
- local text = self.props.value or ""
- local newText = text:sub(1, self.cursorPos) .. char .. text:sub(self.cursorPos + 1)
- self.cursorPos = self.cursorPos + 1
- self.props.value = newText
- if self.props.onChange then
- self.props.onChange(newText)
- end
- return true
- end
- return false
- end
- function Element:handleScroll(direction, x, y)
- if self.type == "list" and self:isPointInside(x, y) then
- local current = self.props.scroll or 0
- local items = self.props.items or {}
- local visibleHeight = self.height - (self.style.border and 2 or 0)
- local maxScroll = math.max(0, #items - visibleHeight)
- local newScroll = math.max(0, math.min(current + direction * 1, maxScroll))
- self.targetScroll = newScroll
- return true
- end
- return false
- end
- -- ============================================================================
- -- COMPONENT SYSTEM
- -- ============================================================================
- local Component = {}
- Component.__index = Component
- function Component.new(config)
- local self = setmetatable({}, Component)
- self.name = config.name or "Component"
- self.render = config.render
- self.onMount = config.onMount
- self.onUnmount = config.onUnmount
- local internalState = {}
- self.state = {} --proxy
- setmetatable(self.state, {
- __index = internalState,
- __newindex = function(t, key, value)
- -- 1. Update the actual value in our internal table.
- internalState[key] = value
- -- 2. AUTOMATICALLY mark the component as dirty to trigger a re-render.
- if not self.isRendering then
- self.dirty = true
- end
- end
- })
- self.props = {}
- self.elements = {}
- self.emitters = {}
- self.dirty = false
- self.loop = nil
- self.isRendering = false
- return self
- end
- function Component:setState(newState)
- if self.isRendering then return end
- for k, v in pairs(newState) do
- self.state[k] = v
- end
- self.dirty = true
- end
- function Component:emit(event, data)
- if self.emitters[event] then
- for _, callback in ipairs(self.emitters[event]) do
- callback(data)
- end
- end
- end
- function Component:on(event, callback)
- if not self.emitters[event] then
- self.emitters[event] = {}
- end
- table.insert(self.emitters[event], callback)
- end
- function Component:mount(props)
- self.props = props or {}
- if self.onMount then
- self.onMount(self)
- end
- self.isRendering = true
- self.elements = self:render() or {}
- self.isRendering = false
- self.dirty = false
- return self.elements
- end
- function Component:unmount()
- if self.onUnmount then
- self.onUnmount(self)
- end
- self.elements = {}
- end
- function Component:update()
- self.isRendering = true
- local newElements = self:render() or {}
- self.isRendering = false
- self.dirty = false
- return newElements
- end
- -- ============================================================================
- -- ROUTER SYSTEM
- -- ============================================================================
- local Router = {}
- Router.__index = Router
- function Router.new()
- local self = setmetatable({}, Router)
- self.routes = {}
- self.currentRoute = "/"
- self.currentComponent = nil
- self.middleware = {}
- return self
- end
- function Router:addRoute(path, component)
- self.routes[path] = component
- end
- function Router:addMiddleware(middleware)
- table.insert(self.middleware, middleware)
- end
- local function safeSerialize(tbl, indent)
- indent = indent or 0
- local str = "{\n"
- for k, v in pairs(tbl) do
- local key = tostring(k)
- local value
- if type(v) == "table" then
- value = safeSerialize(v, indent + 2)
- elseif type(v) == "function" then
- value = "\"<function>\""
- else
- value = textutils.serialize(v)
- end
- str = str .. string.rep(" ", indent + 2) .. key .. " = " .. value .. ",\n"
- end
- return str .. string.rep(" ", indent) .. "}"
- end
- function Router:navigate(path, props, app, transition)
- -- Run middleware
- for _, middleware in ipairs(self.middleware) do
- local result = middleware(path, props)
- if result == false then
- return self.currentComponent and self.currentComponent.elements or {}
- elseif type(result) == "string" then
- path = result
- end
- end
- -- Unfocus all elements on route change
- if app then
- local allElements = app:getAllElements()
- for _, el in ipairs(allElements) do
- if el.focused then
- el:blur()
- end
- end
- end
- if self.currentComponent and self.currentComponent.unmount then
- self.currentComponent:unmount()
- end
- table.insert(routerState.history, self.currentRoute)
- self.currentRoute = path
- routerState.currentRoute = path
- term.setCursorPos(1, 1)
- term.setBackgroundColor(colors.black)
- term.setTextColor(colors.white)
- local componentConfig = self.routes[path]
- local newElements
- if componentConfig then
- if componentConfig.middleware then
- for _, mw in ipairs(componentConfig.middleware) do
- local result = mw(path, props)
- if result == false then
- return self.currentComponent and self.currentComponent.elements or {}
- elseif type(result) == "string" then
- return self:navigate(result, props, app, transition)
- end
- end
- end
- self.currentComponent = Component.new(componentConfig)
- newElements = self.currentComponent:mount(props)
- else
- local notFound = ReactiveUI.createElement("text", {
- x = 1,
- y = 1,
- text = "Page not found: " .. (path or "(path is nil)"),
- style = { fg = colors.white }
- })
- newElements = { notFound }
- end
- if transition and transition.type then
- app.transition = {
- type = transition.type,
- dir = transition.dir or "right",
- oldElements = app.elements,
- newElements = newElements,
- startTime = os.epoch("utc"),
- duration = (transition.duration or 0.5) * 1000,
- progress = 0
- }
- else
- app:setElements(newElements)
- end
- return newElements
- end
- function Router:back(app, transition)
- if #routerState.history > 0 then
- local previousRoute = table.remove(routerState.history)
- return self:navigate(previousRoute, nil, app, transition)
- end
- end
- function Router:getCurrentComponent()
- return self.currentComponent
- end
- -- ============================================================================
- -- APPLICATION SYSTEM
- -- ============================================================================
- local App = {}
- App.__index = App
- function App.new()
- local self = setmetatable({}, App)
- self.router = Router.new()
- self.elements = {}
- self.running = false
- self.needsRedraw = true
- self.components = {}
- self.pageRoot = nil
- self.compRoot = nil
- self.modTimes = { pages = {}, components = {} }
- self.focusedElement = nil
- self.scrollY = 0
- self.targetScrollY = nil
- self.contentHeight = 0
- self.transition = nil
- return self
- end
- function App:setElements(newElements)
- self.elements = newElements or {}
- for _, root in ipairs(self.elements) do
- root:updateAbsolutePosition()
- end
- self:recalculateContentHeight()
- -- Restore focus if needed
- if self.focusedElement then
- local restored = false
- for _, el in ipairs(self:getAllElements()) do
- if el.id == self.focusedElement.id then
- el.focused = true
- el.cursorPos = self.focusedElement.cursorPos
- self.focusedElement = el
- restored = true
- break
- end
- end
- if not restored then
- self.focusedElement = nil
- end
- end
- end
- function App:findElementById(element, id)
- if element.id == id then
- return element
- end
- for _, child in ipairs(element.children or {}) do
- local found = self:findElementById(child, id)
- if found then
- return found
- end
- end
- return nil
- end
- function App:getAllElements(elements)
- local all = {}
- elements = elements or self.elements
- for _, el in ipairs(elements) do
- table.insert(all, el)
- for _, child in ipairs(self:getAllElements(el.children)) do
- table.insert(all, child)
- end
- end
- return all
- end
- function App:getFocusableElements()
- local focusable = {}
- for _, el in ipairs(self:getAllElements()) do
- if el.type == "input" or el.type == "button" then
- table.insert(focusable, el)
- end
- end
- return focusable
- end
- function App:focusNext()
- local focusable = self:getFocusableElements()
- if #focusable == 0 then return end
- local currentIndex = 0
- for i, el in ipairs(focusable) do
- if el.focused then
- currentIndex = i
- el:blur()
- break
- end
- end
- local nextIndex = (currentIndex % #focusable) + 1
- focusable[nextIndex]:focus()
- self.focusedElement = focusable[nextIndex]
- end
- function App:loadPages(pathOrDir)
- local normalized = pathOrDir:gsub("/+$", "")
- self.pageRoot = normalized
- self.modTimes.pages = {}
- local isDir = fs.isDir(normalized)
- if isDir then
- if not fs.exists(normalized) or not fs.isDir(normalized) then
- print("Pages directory not found: " .. normalized)
- return
- end
- local files = fs.list(normalized)
- local hasIndex = false
- for _, file in ipairs(files) do
- if file:match("%.lua$") then
- local fullpath = fs.combine(normalized, file)
- local f = fs.open(fullpath, "r")
- local cont = f.readAll()
- f.close()
- local env = { ReactiveUI = ReactiveUI }
- setmetatable(env, { __index = _G })
- local chunk, err = load(cont, file, "t", env)
- if not chunk then
- print("Failed to load page: " .. file)
- print("Error: " .. tostring(err))
- else
- local success, pageConfig = pcall(chunk)
- if success and type(pageConfig) == "table" then
- local routeName = file:gsub("%.lua$", "")
- local routePath = routeName == "index" and "/" or "/" .. routeName
- self.router:addRoute(routePath, pageConfig)
- print("Loaded page: " .. routePath)
- local attrs = fs.attributes(fullpath)
- self.modTimes.pages[fullpath] = attrs.modified
- if routePath == "/" then
- hasIndex = true
- end
- else
- print("Failed to execute page: " .. file)
- if not success then
- print("Error: " .. tostring(pageConfig))
- end
- end
- end
- end
- end
- if not hasIndex then
- local firstRoute = next(self.router.routes)
- if firstRoute then
- self.router.currentRoute = firstRoute
- print("No index page, starting at: " .. firstRoute)
- end
- end
- else
- if not fs.exists(normalized) then
- print("Page file not found: " .. normalized)
- return
- end
- local f = fs.open(normalized, "r")
- local cont = f.readAll()
- f.close()
- local env = { ReactiveUI = ReactiveUI }
- setmetatable(env, { __index = _G })
- local chunk, err = load(cont, fs.getName(normalized), "t", env)
- if not chunk then
- print("Failed to load page: " .. normalized)
- print("Error: " .. tostring(err))
- else
- local success, pageConfig = pcall(chunk)
- if success and type(pageConfig) == "table" then
- local routeName = fs.getName(normalized):gsub("%.lua$", "")
- local routePath = routeName == "index" and "/" or "/" .. routeName
- self.router:addRoute(routePath, pageConfig)
- if self.router.currentRoute == "/" then
- self.router.currentRoute = routePath
- end
- print("Loaded page: " .. routePath)
- local attrs = fs.attributes(normalized)
- self.modTimes.pages[normalized] = attrs.modified
- else
- print("Failed to execute page: " .. normalized)
- if not success then
- print("Error: " .. tostring(pageConfig))
- end
- end
- end
- end
- end
- function App:loadPagesFromStrings(pagesTbl)
- for route, cont in pairs(pagesTbl) do
- local env = { ReactiveUI = ReactiveUI }
- setmetatable(env, { __index = _G })
- local chunk, err = load(cont, route .. ".lua", "t", env)
- if not chunk then
- print("Failed to load page from string: " .. route)
- print("Error: " .. tostring(err))
- else
- local success, pageConfig = pcall(chunk)
- if success and type(pageConfig) == "table" then
- self.router:addRoute(route, pageConfig)
- print("Loaded page from string: " .. route)
- else
- print("Failed to execute page from string: " .. route)
- if not success then
- print("Error: " .. tostring(pageConfig))
- end
- end
- end
- end
- end
- function App:loadComponents(dir)
- local normalized = dir:gsub("/+$", "")
- self.components = {}
- self.modTimes.components = {}
- self.compRoot = normalized
- if not fs.exists(normalized) or not fs.isDir(normalized) then
- print("Components directory not found: " .. normalized)
- return
- end
- local files = fs.list(normalized)
- for _, file in ipairs(files) do
- if file:match("%.lua$") then
- local fullpath = fs.combine(normalized, file)
- local f = fs.open(fullpath, "r")
- local cont = f.readAll()
- f.close()
- local env = { ReactiveUI = ReactiveUI }
- setmetatable(env, { __index = _G })
- local chunk, err = load(cont, file, "t", env)
- if not chunk then
- print("Failed to load component: " .. file)
- print("Error: " .. tostring(err))
- else
- local success, component = pcall(chunk)
- if success and type(component) == "table" then
- local componentName = file:gsub("%.lua$", "")
- self.components[componentName] = component
- local attrs = fs.attributes(fullpath)
- self.modTimes.components[fullpath] = attrs.modified
- print("Loaded component: " .. componentName)
- else
- print("Failed to execute component: " .. file)
- if not success then
- print("Error: " .. tostring(component))
- end
- end
- end
- end
- end
- end
- function App:loadComponentsFromStrings(compsTbl)
- for name, cont in pairs(compsTbl) do
- local env = { ReactiveUI = ReactiveUI }
- setmetatable(env, { __index = _G })
- local chunk, err = load(cont, name .. ".lua", "t", env)
- if not chunk then
- print("Failed to load component from string: " .. name)
- print("Error: " .. tostring(err))
- else
- local success, component = pcall(chunk)
- if success and type(component) == "table" then
- self.components[name] = component
- print("Loaded component from string: " .. name)
- else
- print("Failed to execute component from string: " .. name)
- if not success then
- print("Error: " .. tostring(component))
- end
- end
- end
- end
- end
- function App:getComponent(name)
- return self.components[name]
- end
- function App:render()
- if self.needsRedraw then
- local screenW, screenH = term.getSize()
- term.setBackgroundColor(colors.black)
- term.clear()
- -- Handle transition rendering
- if self.transition then
- local now = os.epoch("utc")
- local elapsed = now - self.transition.startTime
- local prog = math.min(1, elapsed / self.transition.duration)
- self.transition.progress = prog
- local eased = ReactiveUI.animGraph.easeinout(prog)
- local oldOffsetX, oldOffsetY = 0, 0
- local newOffsetX, newOffsetY = 0, 0
- local transType = self.transition.type
- local dir = self.transition.dir
- local deltaX = screenW * eased
- local deltaY = screenH * eased
- if transType == "fade" then
- if prog < 0.5 then
- for _, el in ipairs(self.transition.oldElements) do
- el:render(self.scrollY, 0, 0)
- end
- else
- for _, el in ipairs(self.transition.newElements) do
- el:render(self.scrollY, 0, 0)
- end
- end
- elseif transType == "carousel" then
- if dir == "left" then
- oldOffsetX = -deltaX
- newOffsetX = screenW - deltaX
- elseif dir == "right" then
- oldOffsetX = deltaX
- newOffsetX = -screenW + deltaX
- elseif dir == "up" then
- oldOffsetY = -deltaY
- newOffsetY = screenH - deltaY
- elseif dir == "down" then
- oldOffsetY = deltaY
- newOffsetY = -screenH + deltaY
- end
- for _, el in ipairs(self.transition.oldElements) do
- el:render(self.scrollY, oldOffsetX, oldOffsetY)
- end
- for _, el in ipairs(self.transition.newElements) do
- el:render(self.scrollY, newOffsetX, newOffsetY)
- end
- elseif transType == "slideOnTop" then
- for _, el in ipairs(self.transition.oldElements) do
- el:render(self.scrollY, 0, 0)
- end
- newOffsetY = screenH * (1 - eased)
- for _, el in ipairs(self.transition.newElements) do
- el:render(self.scrollY, 0, newOffsetY)
- end
- end
- if prog >= 1 then
- self.transition = nil
- self:setElements(self.transition.newElements)
- end
- else
- -- Normal rendering
- -- Find focused element for cursor rendering
- local focused = nil
- for _, el in ipairs(self:getAllElements()) do
- if el.focused then
- focused = el
- break
- end
- end
- -- Render all elements with scroll offset and viewport culling
- for _, element in ipairs(self.elements) do
- local displayY = element.y - self.scrollY
- local elemBottom = displayY + element.height - 1
- -- Only render if element is within viewport (not fully outside)
- if elemBottom >= 1 and displayY <= screenH then
- element:render(self.scrollY, 0, 0)
- end
- end
- -- Render cursor for focused input
- if focused and focused.type == "input" then
- local text = focused.props.value or ""
- local cursorPos = focused.cursorPos or #text
- local maxVisible = focused.width - 2
- local displayCursor = math.min(cursorPos, maxVisible)
- local cursorX = focused.x + 1 + displayCursor
- local cursorY = focused.y + 1 - self.scrollY
- -- Only show cursor if it's in viewport
- if cursorY >= 1 and cursorY <= screenH then
- term.setCursorPos(cursorX, cursorY)
- term.setCursorBlink(true)
- else
- term.setCursorBlink(false)
- end
- else
- term.setCursorBlink(false)
- end
- -- Render scrollbar if content is scrollable
- if self.contentHeight > screenH then
- local trackX = screenW
- local trackHeight = screenH
- local maxScroll = self.contentHeight - screenH
- local thumbHeight = math.max(1, math.floor((screenH / self.contentHeight) * trackHeight))
- local thumbTop = math.floor((self.scrollY / maxScroll) * (trackHeight - thumbHeight)) + 1
- for i = 1, trackHeight do
- term.setCursorPos(trackX, i)
- if i >= thumbTop and i < thumbTop + thumbHeight then
- term.setBackgroundColor(colors.lightGray)
- term.setTextColor(colors.white)
- term.write(" ")
- else
- term.setBackgroundColor(colors.black)
- term.setTextColor(colors.gray)
- term.write("|")
- end
- end
- end
- end
- self.needsRedraw = false
- end
- end
- function App:reload()
- local currentRoute = self.router.currentRoute
- self.router.routes = {}
- local normalized = self.pageRoot:gsub("/+$", "")
- local isPageDir = fs.isDir(normalized)
- if isPageDir then
- self:loadPages(normalized)
- else
- local pagePath = next(self.modTimes.pages)
- if pagePath then
- local f = fs.open(pagePath, "r")
- local cont = f.readAll()
- f.close()
- local env = { ReactiveUI = ReactiveUI }
- setmetatable(env, { __index = _G })
- local chunk, err = load(cont, fs.getName(pagePath), "t", env)
- if chunk then
- local success, pageConfig = pcall(chunk)
- if success and type(pageConfig) == "table" then
- local routeName = fs.getName(pagePath):gsub("%.lua$", "")
- local routePath = routeName == "index" and "/" or "/" .. routeName
- self.router:addRoute(routePath, pageConfig)
- if self.router.currentRoute == "/" then
- self.router.currentRoute = routePath
- end
- print("Reloaded page: " .. routePath)
- local attrs = fs.attributes(pagePath)
- self.modTimes.pages[pagePath] = attrs.modified
- end
- end
- end
- end
- local normalizedComp = self.compRoot and self.compRoot:gsub("/+$", "") or nil
- if normalizedComp and fs.exists(normalizedComp) and fs.isDir(normalizedComp) then
- self:loadComponents(normalizedComp)
- end
- local elements = self.router:navigate(currentRoute, nil, self)
- self:setElements(elements or {})
- self.needsRedraw = true
- print("Reload complete")
- end
- function App:updateAnimations()
- local allElements = self:getAllElements()
- local isAnimating = false
- for _, el in ipairs(allElements) do
- -- Check for new animations to start
- for prop, config in pairs(el.transitions) do
- if el.props[prop] ~= nil and el.props[prop] ~= el[prop] and not el.activeAnimations[prop] then
- local anim = ReactiveUI.animGraph[config.ease or "easeinout"](
- config.duration or 0.5,
- el[prop],
- el.props[prop]
- )
- el.activeAnimations[prop] = {
- graph = anim,
- frame = 1
- }
- end
- end
- -- Update currently running animations
- if next(el.activeAnimations) then
- isAnimating = true
- for prop, animData in pairs(el.activeAnimations) do
- local anim = animData.graph
- local frame = animData.frame
- el[prop] = anim[frame]
- if frame >= anim[0] then
- el[prop] = el.props[prop]
- el.activeAnimations[prop] = nil
- else
- animData.frame = frame + 1
- end
- end
- end
- end
- return isAnimating
- end
- function App:reconcile(oldElements, newElements)
- local finalElements = {}
- local oldElementsById = {}
- for _, oldEl in ipairs(oldElements) do
- if oldEl.id then
- oldElementsById[oldEl.id] = oldEl
- end
- end
- for _, newEl in ipairs(newElements) do
- local oldEl = newEl.id and oldElementsById[newEl.id] or nil
- if oldEl then
- oldEl:updateFromProps(newEl.props)
- if oldEl.children or newEl.children then
- oldEl.children = self:reconcile(oldEl.children or {}, newEl.children or {})
- end
- table.insert(finalElements, oldEl)
- else
- table.insert(finalElements, newEl)
- end
- end
- return finalElements
- end
- function App:recalculateContentHeight()
- local _, screenH = term.getSize()
- self.contentHeight = screenH
- local all = self:getAllElements()
- for _, el in ipairs(all) do
- -- Find the lowest point occupied by any element
- self.contentHeight = math.max(self.contentHeight, el.y + el.height - 1)
- end
- end
- function App:run(watch)
- watch = watch or false
- currentApp = self
- self.running = true
- local elements = self.router:navigate(self.router.currentRoute, nil, self)
- self:setElements(elements or {})
- self:render()
- local tickTimer = os.startTimer(0)
- local watchTimer
- if watch then
- watchTimer = os.startTimer(1)
- end
- while self.running do
- local event, p1, p2, p3 = os.pullEventRaw()
- local needsRedraw = false
- if event == "terminate" then
- self:stop()
- break
- elseif event == "timer" and p1 == tickTimer then
- tickTimer = os.startTimer(0)
- elseif event == "reload" then
- self:reload()
- needsRedraw = true
- elseif watch and event == "timer" and p1 == watchTimer then
- local changed = false
- for path, oldMtime in pairs(self.modTimes.pages) do
- if fs.exists(path) then
- local attrs = fs.attributes(path)
- local mtime = attrs.modified
- if mtime > oldMtime then
- changed = true
- self.modTimes.pages[path] = mtime
- end
- end
- end
- for path, oldMtime in pairs(self.modTimes.components) do
- if fs.exists(path) then
- local attrs = fs.attributes(path)
- local mtime = attrs.modified
- if mtime > oldMtime then
- changed = true
- self.modTimes.components[path] = mtime
- end
- end
- end
- if changed then
- os.queueEvent("reload")
- print("Detected changes, reloading...")
- end
- watchTimer = os.startTimer(1)
- else
- if self.transition then
- needsRedraw = true
- elseif event == "mouse_click" then
- local button, x, y = p1, p2, p3
- local adjustedY = y + self.scrollY
- local allElements = self:getAllElements()
- local clickedSomething = false
- for i = #self.elements, 1, -1 do
- if self.elements[i]:handleClick(x, adjustedY, button, self) then
- clickedSomething = true
- break
- end
- end
- if not clickedSomething then
- for _, el in ipairs(allElements) do
- if el.focused then
- el:blur()
- self.focusedElement = nil
- end
- end
- end
- for _, element in ipairs(allElements) do
- element.active = false
- end
- needsRedraw = true
- elseif event == "mouse_drag" or event == "mouse_move" then
- local x, y = p2, p3
- local adjustedY = y + self.scrollY
- for _, element in ipairs(self.elements) do
- element:handleHover(x, adjustedY)
- end
- needsRedraw = true
- elseif event == "mouse_up" then
- local allElements = self:getAllElements()
- for _, element in ipairs(allElements) do
- element.dragging = false
- element.dragType = nil
- end
- elseif event == "mouse_scroll" then
- local direction, x, y = p1, p2, p3
- local handled = false
- local adjustedY = y + self.scrollY
- local allElements = self:getAllElements()
- for _, element in ipairs(allElements) do
- if element:handleScroll(direction, x, adjustedY) then
- handled = true
- break
- end
- end
- if not handled then
- local screenW, screenH = term.getSize()
- local maxScroll = math.max(0, self.contentHeight - screenH)
- if maxScroll > 0 then
- local scrollAmount = direction * 1
- self.scrollY = math.max(0, math.min(self.scrollY + scrollAmount, maxScroll))
- handled = true
- end
- end
- if handled then
- needsRedraw = true
- end
- elseif event == "key" then
- local key = p1
- local handled = false
- if key == keys.tab then
- self:focusNext()
- needsRedraw = true
- handled = true
- end
- if not handled then
- for _, element in ipairs(self:getAllElements()) do
- if element:handleKey(key) then
- needsRedraw = true
- if element.focused and element.id then
- self.focusedElement = element
- elseif not element.focused and self.focusedElement == element then
- self.focusedElement = nil
- end
- break
- end
- end
- end
- elseif event == "char" then
- local char = p1
- for _, element in ipairs(self:getAllElements()) do
- if element:handleChar(char) then
- needsRedraw = true
- if element.id then
- self.focusedElement = element
- end
- break
- end
- end
- elseif event == "term_resize" then
- needsRedraw = true
- end
- end
- self.needsRedraw = needsRedraw or self.needsRedraw
- local comp = self.router:getCurrentComponent()
- if comp and comp.dirty then
- local newElements = comp:update()
- self.elements = self:reconcile(self.elements, newElements)
- for _, root in ipairs(self.elements) do
- root:updateAbsolutePosition()
- end
- self:recalculateContentHeight()
- self.needsRedraw = true
- end
- if comp and comp.loop and coroutine.status(comp.loop) == "suspended" then
- local ok, err = coroutine.resume(comp.loop)
- if not ok then
- print("Loop error: " .. tostring(err))
- comp.loop = nil
- end
- if coroutine.status(comp.loop) == "dead" then
- comp.loop = nil
- end
- end
- if self.targetScrollY then
- local current = self.scrollY or 0
- local delta = self.targetScrollY - current
- if math.abs(delta) < 1 then
- self.scrollY = self.targetScrollY
- self.targetScrollY = nil
- else
- self.scrollY = current + math.floor(delta * 0.3)
- self.needsRedraw = true
- end
- end
- local isAnimating = self:updateAnimations()
- if isAnimating then
- self.needsRedraw = true
- end
- if self.needsRedraw then
- self:render()
- end
- end
- end
- function App:stop()
- self.running = false
- end
- -- ============================================================================
- -- HELPER FUNCTIONS
- -- ============================================================================
- function ReactiveUI.createElement(type, props)
- return Element.new(type, props)
- end
- function ReactiveUI.createComponent(config)
- return Component.new(config)
- end
- function ReactiveUI.createApp()
- return App.new()
- end
- function ReactiveUI.log(wait, logs)
- term.setCursorPos(1, 1)
- term.setTextColor(colors.white)
- term.setBackgroundColor(colors.black)
- for i, log in ipairs(logs) do
- print(log)
- end
- if wait then os.pullEvent("key") end
- end
- function ReactiveUI.normalizePath(path)
- if type(path) ~= "string" then
- print("!!! Warning: tried to navigate with non-string path:", tostring(path))
- return "/"
- end
- path = path:gsub("/+", "/")
- path = path:gsub("/$", "")
- if path == "" then path = "/" end
- return path
- end
- function ReactiveUI.navigate(path, props, transition)
- if not path or path == "" then
- path = "/"
- end
- local app = currentApp or ReactiveUI.createApp()
- app.router:navigate(path, props, app, transition or {})
- end
- function ReactiveUI.setGlobal(key, value)
- globalStore[key] = value
- end
- function ReactiveUI.getGlobal(key, default)
- return globalStore[key] or default
- end
- -- Middleware helpers
- function ReactiveUI.createAuthMiddleware(checkAuth)
- return function(path, props)
- if not checkAuth() then
- return "/login"
- end
- return true
- end
- end
- function ReactiveUI.createLoggerMiddleware()
- return function(path, props)
- print("[Router] Navigating to: " .. path)
- return true
- end
- end
- -- ============================================================================
- -- EXAMPLE USAGE
- -- ============================================================================
- function ReactiveUI.exampleApp()
- local app = ReactiveUI.createApp()
- app:loadPages("pages")
- app.router:addRoute("/page", {
- name = "TestPage",
- render = function(self)
- local w, h = term.getSize()
- local y = 1
- local elements = {}
- table.insert(elements,
- ReactiveUI.createElement("text",
- { x = 1, y = y, text = "ReactiveUI Component Showcase", style = { fg = colors.white } }))
- y = y + 2
- table.insert(elements,
- ReactiveUI.createElement("text", { x = 1, y = y, text = "Box:", style = { fg = colors.yellow } }))
- y = y + 1
- table.insert(elements,
- ReactiveUI.createElement("box",
- { x = 1, y = y, width = 20, height = 3, style = { bg = colors.lightBlue, border = true, rounded = true } }))
- y = y + 4
- table.insert(elements,
- ReactiveUI.createElement("text", { x = 1, y = y, text = "Text:", style = { fg = colors.yellow } }))
- y = y + 1
- table.insert(elements,
- ReactiveUI.createElement("text",
- { x = 1, y = y, text = "This is a text element.", style = { fg = colors.green } }))
- y = y + 2
- table.insert(elements,
- ReactiveUI.createElement("text", { x = 1, y = y, text = "Button:", style = { fg = colors.yellow } }))
- y = y + 1
- table.insert(elements,
- ReactiveUI.createElement("button",
- {
- x = 1,
- y = y,
- width = 10,
- height = 3,
- text = "Click Me",
- class = "bg-green-500 text-white border rounded hover:bg-blue-500",
- style = { bg = colors.green, fg = colors.white, border = true },
- onClick = function()
- print("Clicked!")
- end
- }))
- y = y + 4
- table.insert(elements,
- ReactiveUI.createElement("text", { x = 1, y = y, text = "Input:", style = { fg = colors.yellow } }))
- y = y + 1
- local input = ReactiveUI.createElement("input",
- { x = 1, y = y, width = 20, height = 3, value = "", placeholder = "Type here...", class = "border", style = { border = true } })
- table.insert(elements, input)
- y = y + 4
- table.insert(elements,
- ReactiveUI.createElement("text", { x = 1, y = y, text = "Checkbox:", style = { fg = colors.yellow } }))
- y = y + 1
- local checkbox = ReactiveUI.createElement("checkbox",
- {
- x = 1,
- y = y,
- width = 20,
- height = 3,
- checked = false,
- label = "Check me",
- class = "border",
- style = { border = true },
- onChange = function(
- checked)
- print("Checked: " .. tostring(checked))
- end
- })
- table.insert(elements, checkbox)
- y = y + 4
- table.insert(elements,
- ReactiveUI.createElement("text", { x = 1, y = y, text = "Radio:", style = { fg = colors.yellow } }))
- y = y + 1
- table.insert(elements,
- ReactiveUI.createElement("radio",
- {
- x = 1,
- y = y,
- width = 20,
- height = 3,
- selected = true,
- label = "Option 1",
- class = "border",
- style = { border = true },
- onChange = function(
- val)
- print("Selected: " .. val)
- end,
- value = "opt1",
- group = "test"
- }))
- y = y + 3
- table.insert(elements,
- ReactiveUI.createElement("radio",
- {
- x = 1,
- y = y,
- width = 20,
- height = 3,
- selected = false,
- label = "Option 2",
- class = "border",
- style = { border = true },
- group =
- "test",
- onChange = function(val) print("Selected: " .. val) end,
- value = "opt2"
- }))
- y = y + 4
- table.insert(elements,
- ReactiveUI.createElement("text", { x = 1, y = y, text = "Slider:", style = { fg = colors.yellow } }))
- y = y + 1
- local slider = ReactiveUI.createElement("slider",
- {
- x = 1,
- y = y,
- width = 20,
- height = 3,
- value = 50,
- min = 0,
- max = 100,
- showValue = true,
- class = "border",
- style = { border = true },
- onChange = function(
- val)
- print("Slider: " .. val)
- end
- })
- table.insert(elements, slider)
- y = y + 4
- table.insert(elements,
- ReactiveUI.createElement("text", { x = 1, y = y, text = "File Input:", style = { fg = colors.yellow } }))
- y = y + 1
- local file = ReactiveUI.createElement("file",
- {
- x = 1,
- y = y,
- width = 20,
- height = 3,
- value = "",
- placeholder = "No file",
- class = "border",
- style = { border = true },
- onBrowse = function()
- print("Browse clicked")
- end
- })
- table.insert(elements, file)
- y = y + 4
- table.insert(elements,
- ReactiveUI.createElement("text", { x = 1, y = y, text = "List:", style = { fg = colors.yellow } }))
- y = y + 1
- local listItems = { "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", "Item 9",
- "Item 10", "Item 11", "Item 12" }
- local list = ReactiveUI.createElement("list",
- {
- x = 1,
- y = y,
- width = 20,
- height = 10,
- items = listItems,
- scroll = 0,
- selectedIndex = 1,
- class = "border",
- style = { border = true },
- onSelect = function(
- idx, item)
- print("Selected: " .. item)
- end
- })
- table.insert(elements, list)
- y = y + 12
- table.insert(elements,
- ReactiveUI.createElement("text",
- { x = 1, y = y, text = "End of showcase. Scroll to see all.", style = { fg = colors.white } }))
- return elements
- end
- })
- return app
- end
- function ReactiveUI.cli(arg)
- if #arg > 0 and arg[1] then
- local cmd = arg[1]
- if cmd == "server" then
- local watchMode = false
- local path
- if #arg >= 3 and arg[2] == "-w" then
- watchMode = true
- path = arg[3]
- elseif #arg >= 2 then
- path = arg[2]
- end
- if not path then
- print("Usage: server [-w] <path> | build [dir]")
- return
- end
- local normalizedPath = path:gsub("/+$", "")
- local app = ReactiveUI.createApp()
- app:loadPages(normalizedPath)
- local isPageDir = fs.isDir(normalizedPath)
- local compRoot
- if isPageDir then
- compRoot = fs.combine(normalizedPath, "components")
- else
- local fileDir = fs.getDir(normalizedPath) or shell.dir()
- compRoot = fs.combine(fileDir, "components")
- end
- if fs.exists(compRoot) and fs.isDir(compRoot) then
- app:loadComponents(compRoot)
- end
- if watchMode then
- app:run(true)
- else
- app:run()
- end
- elseif cmd == "build" then
- local dir = arg[2] or "."
- local libFile = "react.lua"
- if not fs.exists(libFile) then
- error("Library file not found: " .. libFile)
- end
- local f = fs.open(libFile, "r")
- local libCode = f.readAll()
- f.close()
- -- disable CLI
- local lines = {}
- for line in libCode:gmatch("[^\r\n]+") do
- table.insert(lines, line)
- end
- if #lines > 0 then
- lines[1] = "local cliEnabled = false"
- end
- libCode = table.concat(lines, "\n")
- local pages = {}
- local components = {}
- local pagesDir = fs.combine(dir, "pages")
- if fs.exists(pagesDir) and fs.isDir(pagesDir) then
- local files = fs.list(pagesDir)
- for _, file in ipairs(files) do
- if file:match("%.lua$") then
- local path = fs.combine(pagesDir, file)
- local f2 = fs.open(path, "r")
- local cont = f2.readAll()
- f2.close()
- local routeName = file:gsub("%.lua$", "")
- local routePath = routeName == "index" and "/" or "/" .. routeName
- pages[routePath] = cont
- end
- end
- end
- local compsDir = fs.combine(dir, "components")
- if fs.exists(compsDir) and fs.isDir(compsDir) then
- local files = fs.list(compsDir)
- for _, file in ipairs(files) do
- if file:match("%.lua$") then
- local path = fs.combine(compsDir, file)
- local f2 = fs.open(path, "r")
- local cont = f2.readAll()
- f2.close()
- local compName = file:gsub("%.lua$", "")
- components[compName] = cont
- end
- end
- end
- local function escapeLuaString(str)
- return str:gsub("\\", "\\\\"):gsub('"', '\\"'):gsub("\n", "\\n"):gsub("\r", "\\r")
- end
- local builtCode = libCode .. "\n\n-- Built Application\nlocal pages = {\n"
- for route, cont in pairs(pages) do
- local esc = escapeLuaString(cont)
- builtCode = builtCode .. " ['" .. route .. "'] = \"" .. esc .. "\",\n"
- end
- builtCode = builtCode .. "}\nlocal components = {\n"
- for name, cont in pairs(components) do
- local esc = escapeLuaString(cont)
- builtCode = builtCode .. " ['" .. name .. "'] = \"" .. esc .. "\",\n"
- end
- builtCode = builtCode ..
- "}\nlocal app = ReactiveUI.createApp()\napp:loadPagesFromStrings(pages)\napp:loadComponentsFromStrings(components)\napp:run()\n"
- local output = fs.combine(dir, "ui.lua")
- local outF = fs.open(output, "w")
- outF.write(builtCode)
- outF.close()
- print("Built to " .. output)
- else
- print("Usage: server [-w] <path> | build [dir]")
- end
- end
- end
- if shell and cliEnabled then
- local args = { ... }
- local program = shell.getRunningProgram()
- local resolved = shell.resolveProgram("react") or shell.resolveProgram("react.lua")
- if program == resolved then
- ReactiveUI.cli(args)
- else
- return ReactiveUI
- end
- elseif cliEnabled then
- return ReactiveUI
- end
- -- Built Application
- local pages = {
- ['/profile'] = "-- pages/profile.lua\nreturn {\n name = \"ProfilePage\",\n\n onMount = function(self)\n self.state.name = \"\"\n self.state.email = \"\"\n end,\n\n render = function(self)\n local w, h = term.getSize()\n\n local elements = {\n -- Background\n ReactiveUI.createElement(\"box\", {\n x = 1,\n y = 1,\n width = w,\n height = h,\n style = { bg = colors.lightBlue }\n }),\n\n -- Title\n ReactiveUI.createElement(\"text\", {\n x = 2,\n y = 2,\n text = \"Profile Page\",\n style = { fg = colors.white, bg = colors.lightBlue }\n }),\n\n -- Name label and input\n ReactiveUI.createElement(\"text\", {\n x = 2,\n y = 4,\n text = \"Name:\",\n style = { fg = colors.white, bg = colors.lightBlue }\n }),\n ReactiveUI.createElement(\"input\", {\n x = 8,\n y = 4,\n width = 20,\n height = 3,\n id = \"name_input\",\n value = self.state.name,\n placeholder = \"Enter your name\",\n style = { bg = colors.white, fg = colors.black, border = true, rounded = true },\n focusStyle = { borderColor = colors.blue },\n onChange = function(value)\n self.state.name = value\n end\n }),\n\n -- Email label and input\n ReactiveUI.createElement(\"text\", {\n x = 2,\n y = 8,\n text = \"Email:\",\n style = { fg = colors.white, bg = colors.lightBlue }\n }),\n ReactiveUI.createElement(\"input\", {\n x = 9,\n y = 8,\n width = 20,\n height = 3,\n id = \"email_input\",\n value = self.state.email,\n placeholder = \"Enter your email\",\n style = { bg = colors.white, fg = colors.black, border = true, rounded = true },\n focusStyle = { borderColor = colors.blue },\n onChange = function(value)\n self.state.email = value\n end\n }),\n\n -- Greeting\n ReactiveUI.createElement(\"text\", {\n x = 2,\n y = 12,\n text = \"Hello, \" .. (self.state.name ~= \"\" and self.state.name or \"stranger\") .. \"!\",\n style = { fg = colors.green, bg = colors.lightBlue }\n }),\n\n -- Back button\n ReactiveUI.createElement(\"button\", {\n x = 2,\n y = 15,\n width = 16,\n height = 3,\n text = \"Back to Home\",\n style = { bg = colors.red, fg = colors.white, border = true, rounded = true },\n hoverStyle = { bg = colors.orange },\n onClick = function()\n ReactiveUI.navigate(\"/home\")\n end\n })\n }\n\n return elements\n end\n}\n",
- ['/'] = "-- pages/test.lua\nreturn {\n name = \"TestPage\",\n render = function(self)\n local w, h = term.getSize()\n local y = 1\n local elements = {}\n table.insert(elements,\n ReactiveUI.createElement(\"text\",\n { x = 1, y = y, text = \"ReactiveUI Component Showcase\", style = { fg = colors.white } }))\n y = y + 2\n table.insert(elements,\n ReactiveUI.createElement(\"text\", { x = 1, y = y, text = \"Box:\", style = { fg = colors.yellow } }))\n y = y + 1\n table.insert(elements,\n ReactiveUI.createElement(\"box\",\n { x = 1, y = y, width = 20, height = 3, style = { bg = colors.lightBlue, border = true, rounded = true } }))\n y = y + 4\n table.insert(elements,\n ReactiveUI.createElement(\"text\", { x = 1, y = y, text = \"Text:\", style = { fg = colors.yellow } }))\n y = y + 1\n table.insert(elements,\n ReactiveUI.createElement(\"text\",\n { x = 1, y = y, text = \"This is a text element.\", style = { fg = colors.green } }))\n y = y + 2\n table.insert(elements,\n ReactiveUI.createElement(\"text\", { x = 1, y = y, text = \"Button:\", style = { fg = colors.yellow } }))\n y = y + 1\n table.insert(elements,\n ReactiveUI.createElement(\"button\",\n {\n x = 1,\n y = y,\n width = 10,\n height = 3,\n text = \"Click Me\",\n style = { bg = colors.green, fg = colors.white, border = true },\n onClick = function()\n ReactiveUI.navigate(\"/home\")\n end\n }))\n y = y + 4\n table.insert(elements,\n ReactiveUI.createElement(\"text\", { x = 1, y = y, text = \"Input:\", style = { fg = colors.yellow } }))\n y = y + 1\n local input = ReactiveUI.createElement(\"input\",\n { x = 1, y = y, width = 20, height = 3, value = \"\", placeholder = \"Type here...\", style = { border = true, fg = colors.black } })\n table.insert(elements, input)\n y = y + 4\n table.insert(elements,\n ReactiveUI.createElement(\"text\", { x = 1, y = y, text = \"Checkbox:\", style = { fg = colors.yellow } }))\n y = y + 1\n local checkbox = ReactiveUI.createElement(\"checkbox\",\n {\n x = 1,\n y = y,\n width = 20,\n height = 3,\n checked = false,\n label = \"Check me\",\n style = { border = true },\n onChange = function(\n checked)\n print(\"Checked: \" .. tostring(checked))\n end\n })\n table.insert(elements, checkbox)\n y = y + 4\n table.insert(elements,\n ReactiveUI.createElement(\"text\", { x = 1, y = y, text = \"Radio:\", style = { fg = colors.yellow } }))\n y = y + 1\n table.insert(elements,\n ReactiveUI.createElement(\"radio\",\n {\n x = 1,\n y = y,\n width = 20,\n height = 3,\n selected = true,\n label = \"Option 1\",\n style = { border = true },\n onChange = function(\n val)\n print(\"Selected: \" .. val)\n end,\n value = \"opt1\",\n group = \"test\"\n }))\n y = y + 3\n table.insert(elements,\n ReactiveUI.createElement(\"radio\",\n {\n x = 1,\n y = y,\n width = 20,\n height = 3,\n selected = false,\n label = \"Option 2\",\n style = { border = true },\n group =\n \"test\",\n onChange = function(val) print(\"Selected: \" .. val) end,\n value = \"opt2\"\n }))\n y = y + 4\n table.insert(elements,\n ReactiveUI.createElement(\"text\", { x = 1, y = y, text = \"Slider:\", style = { fg = colors.yellow } }))\n y = y + 1\n local slider = ReactiveUI.createElement(\"slider\",\n {\n x = 1,\n y = y,\n width = 20,\n height = 3,\n value = 50,\n min = 0,\n max = 100,\n showValue = true,\n style = { border = true },\n onChange = function(\n val)\n print(\"Slider: \" .. val)\n end\n })\n table.insert(elements, slider)\n y = y + 4\n table.insert(elements,\n ReactiveUI.createElement(\"text\", { x = 1, y = y, text = \"File Input:\", style = { fg = colors.yellow } }))\n y = y + 1\n local file = ReactiveUI.createElement(\"file\",\n {\n x = 1,\n y = y,\n width = 20,\n height = 3,\n value = \"\",\n placeholder = \"No file\",\n style = { border = true },\n onBrowse = function()\n print(\"Browse clicked\")\n end\n })\n table.insert(elements, file)\n y = y + 4\n table.insert(elements,\n ReactiveUI.createElement(\"text\", { x = 1, y = y, text = \"List:\", style = { fg = colors.yellow } }))\n y = y + 1\n local listItems = { \"Item 1\", \"Item 2\", \"Item 3\", \"Item 4\", \"Item 5\", \"Item 6\", \"Item 7\", \"Item 8\", \"Item 9\",\n \"Item 10\", \"Item 11\", \"Item 12\" }\n local list = ReactiveUI.createElement(\"list\",\n {\n x = 1,\n y = y,\n width = 20,\n height = 10,\n items = listItems,\n scroll = 0,\n selectedIndex = 1,\n style = { border = true },\n onSelect = function(\n idx, item)\n print(\"Selected: \" .. item)\n end\n })\n table.insert(elements, list)\n y = y + 12\n table.insert(elements,\n ReactiveUI.createElement(\"text\",\n { x = 1, y = y, text = \"End of showcase. Scroll to see all.\", style = { fg = colors.white } }))\n return elements\n end\n}\n",
- ['/home'] = "return {\n name = \"HomePage\",\n onMount = function(self)\n self.state.x = 2\n self.state.y = 10\n end,\n render = function(self)\n local w, h = term.getSize()\n return {\n ReactiveUI.createElement(\"box\", {\n x = 1,\n y = 1,\n width = w,\n height = h,\n style = { bg = colors.blue }\n }),\n ReactiveUI.createElement(\"button\", {\n id = \"moveBtn\",\n x = self.state.x, -- This is the TARGET x value\n y = self.state.y,\n width = 12,\n height = 3,\n text = \"Move me!\",\n style = {\n bg = colors.pink,\n fg = colors.white\n },\n\n transitions = {\n x = { duration = 1, ease = \"easeoutCubic\" },\n y = { duration = 1, ease = \"easeoutCubic\" }\n },\n\n onClick = function()\n local newX\n local newY\n local sx, sy = term.getSize()\n repeat\n newX = math.random(1, sx - 10)\n until newX ~= self.state.x\n repeat\n newY = math.random(1, sy - 3)\n until newY ~= self.state.y\n self.state.x = newX\n self.state.y = newY\n end\n }),\n ReactiveUI.createElement(\"text\", {\n x = 2,\n y = 2,\n text = \"Home Page\",\n style = { fg = colors.white }\n }),\n ReactiveUI.createElement(\"button\", {\n x = 2,\n y = 5,\n width = 16,\n height = 3,\n text = \"Go to Profile\",\n style = { bg = colors.orange, fg = colors.white, border = true },\n onClick = function()\n ReactiveUI.navigate(\"/profile\")\n end\n }),\n ReactiveUI.createElement(\"button\", {\n x = 20,\n y = 5,\n width = 14,\n height = 3,\n text = \"Go to Test\",\n style = { bg = colors.black, fg = colors.white, border = true },\n onClick = function()\n ReactiveUI.navigate(\"/\")\n end\n })\n }\n end\n}\n",
- }
- local components = {
- }
- local app = ReactiveUI.createApp()
- app:loadPagesFromStrings(pages)
- app:loadComponentsFromStrings(components)
- app:run()
Advertisement
Add Comment
Please, Sign In to add comment