- Added monitor support

- Fixed states example
- Small changes to Display element
This commit is contained in:
Robert Jelic
2025-03-24 20:19:48 +01:00
parent cecabcddf6
commit c2507b5486
4 changed files with 213 additions and 126 deletions

View File

@@ -1,111 +1,54 @@
local basalt = require("basalt") local basalt = require("basalt")
-- Create main frame
local main = basalt.getMainFrame() local main = basalt.getMainFrame()
-- Initialize form states
:initializeState("username", "", true) -- make them persistent
:initializeState("password", "", true) -- make them persistent
:initializeState("confirmPassword", "", true) -- make them persistent
-- Create a complex form using states for managing its data
local form = main:addFrame() local form = main:addFrame()
:setSize("{parent.width - 4}", "{parent.height - 4}") :setSize("{parent.width - 4}", "{parent.height - 4}")
:setPosition(3, 3) :setPosition(3, 3)
:setBackground(colors.lightGray)
-- Initialize form states with default values
:initializeState("username", "", true) -- true = triggers render update
:initializeState("email", "")
:initializeState("age", 0)
:initializeState("submitted", false)
-- Add computed state for form validation
:computed("isValid", function(self)
local username = self:getState("username")
local email = self:getState("email")
local age = self:getState("age")
return #username > 0 and email:match(".+@.+") and age > 0
end)
-- Create form title -- Add computed validation state
form:addLabel() form:computed("isValid", function(self)
:setText("Registration Form") local username = self:getState("username")
:setPosition(2, 2) local password = self:getState("password")
:setForeground(colors.black) local confirmPass = self:getState("confirmPassword")
return #username >= 3 and #password >= 6 and password == confirmPass
end)
-- Username input with state binding -- Create labels
local usernameInput = form:addInput() form:addLabel({text="Username:", x = 2, y = 2, foreground = colors.lightGray})
:setPosition(2, 4) form:addLabel({text="Password:", x = 2, y = 4, foreground = colors.lightGray})
:setSize(20, 1) form:addLabel({text="Confirm:", x = 2, y = 6, foreground = colors.lightGray})
:setBackground(colors.white)
:setForeground(colors.black)
-- Update username state when input changes
:onChange(function(self, value)
form:setState("username", value)
end)
form:addLabel() local userInput = form:addInput({x = 11, y = 2, width = 20, height = 1}):bind("text", "username")
:setText("Username:") local passwordInput = form:addInput({x = 11, y = 4, width = 20, height = 1}):bind("text", "password")
:setPosition(2, 3) local confirmInput = form:addInput({x = 11, y = 6, width = 20, height = 1}):bind("text", "confirmPassword")
:setForeground(colors.black)
-- Email input -- Submit button
local emailInput = form:addInput()
:setPosition(2, 6)
:setSize(20, 1)
:setBackground(colors.white)
:setForeground(colors.black)
:onChange(function(self, value)
form:setState("email", value)
end)
form:addLabel()
:setText("Email:")
:setPosition(2, 5)
:setForeground(colors.black)
-- Age input
local ageInput = form:addInput()
:setPosition(2, 8)
:setSize(5, 1)
:setBackground(colors.white)
:setForeground(colors.black)
:onChange(function(self, value)
-- Convert to number and update state
form:setState("age", tonumber(value) or 0)
end)
form:addLabel()
:setText("Age:")
:setPosition(2, 7)
:setForeground(colors.black)
-- Submit button that reacts to form validity
local submitBtn = form:addButton() local submitBtn = form:addButton()
:setPosition(2, 10)
:setSize(10, 1)
:setText("Submit") :setText("Submit")
-- Button color changes based on form validity :setPosition(2, 8)
:onStateChange("isValid", function(self, isValid) :setSize(29, 1)
submitBtn:setBackground(isValid and colors.lime or colors.gray)
end)
:onMouseClick(function()
if form.computedStates.isValid() then
form:setState("submitted", true)
end
end)
-- Status message that updates when form is submitted -- Status label
local statusLabel = form:addLabel() local statusLabel = form:addLabel()
:setPosition(2, 12) :setPosition(2, 10)
:setForeground(colors.black) :setSize(29, 1)
-- Listen for form submission
form:onStateChange("submitted", function(self, submitted) form:onStateChange("isValid", function(self, isValid)
if submitted then if isValid then
local username = self:getState("username") statusLabel:setText("Form is valid!")
local email = self:getState("email") :setForeground(colors.green)
local age = self:getState("age") submitBtn:setBackground(colors.green)
statusLabel:setText(string.format( else
"Submitted: %s (%s) - Age: %d", statusLabel:setText("Please fill all fields correctly")
username, email, age :setForeground(colors.red)
)) submitBtn:setBackground(colors.red)
end end
end) end)
-- Run basalt basalt.run()
basalt.run()

View File

@@ -1,5 +1,6 @@
local elementManager = require("elementManager") local elementManager = require("elementManager")
local Container = elementManager.getElement("Container") local Container = elementManager.getElement("Container")
local errorManager = require("errorManager")
local Render = require("render") local Render = require("render")
---@configDescription This is the base frame class. It is the root element of all elements and the only element without a parent. ---@configDescription This is the base frame class. It is the root element of all elements and the only element without a parent.
@@ -8,14 +9,39 @@ local Render = require("render")
---@class BaseFrame : Container ---@class BaseFrame : Container
---@field _render Render The render object ---@field _render Render The render object
---@field _renderUpdate boolean Whether the render object needs to be updated ---@field _renderUpdate boolean Whether the render object needs to be updated
---@field _peripheralName string The name of a peripheral
local BaseFrame = setmetatable({}, Container) local BaseFrame = setmetatable({}, Container)
BaseFrame.__index = BaseFrame BaseFrame.__index = BaseFrame
---@property text term nil The terminal object to render to local function isPeripheral(t)
local ok, result = pcall(function()
return peripheral.getType(t)
end)
if ok then
return true
end
return false
end
---@property term term|peripheral term.current() The terminal or (monitor) peripheral object to render to
BaseFrame.defineProperty(BaseFrame, "term", {default = nil, type = "table", setter = function(self, value) BaseFrame.defineProperty(BaseFrame, "term", {default = nil, type = "table", setter = function(self, value)
self._peripheralName = nil
if self.basalt.getActiveFrame(self._values.term)==self then
self.basalt.setActiveFrame(self, false)
end
if value == nil or value.setCursorPos == nil then if value == nil or value.setCursorPos == nil then
return value return value
end end
if(isPeripheral(value)) then
self._peripheralName = peripheral.getName(value)
end
self._values.term = value
if self.basalt.getActiveFrame(value) == nil then
self.basalt.setActiveFrame(self)
end
self._render = Render.new(value) self._render = Render.new(value)
self._renderUpdate = true self._renderUpdate = true
local width, height = value.getSize() local width, height = value.getSize()
@@ -105,10 +131,39 @@ end
--- @param y number The y position to set the cursor to --- @param y number The y position to set the cursor to
--- @param blink boolean Whether the cursor should blink --- @param blink boolean Whether the cursor should blink
function BaseFrame:setCursor(x, y, blink, color) function BaseFrame:setCursor(x, y, blink, color)
local term = self.get("term") local _term = self.get("term")
self._render:setCursor(x, y, blink, color) self._render:setCursor(x, y, blink, color)
end end
--- @shortDescription Handles monitor touch events
--- @param name string The name of the monitor that was touched
--- @param x number The x position of the mouse
--- @param y number The y position of the mouse
--- @protected
function BaseFrame:monitor_touch(name, x, y)
local _term = self.get("term")
if _term == nil then return end
if(isPeripheral(_term))then
if self._peripheralName == name then
self:mouse_click(0, x, y)
self.basalt.schedule(function()
sleep(0.1)
self:mouse_up(0, x, y)
end)
end
end
end
--- @shortDescription Handles mouse click events
--- @param button number The button that was clicked
--- @param x number The x position of the mouse
--- @param y number The y position of the mouse
--- @protected
function BaseFrame:mouse_click(button, x, y)
Container.mouse_click(self, button, x, y)
self.basalt.setFocus(self)
end
--- @shortDescription Handles mouse up events --- @shortDescription Handles mouse up events
--- @param button number The button that was released --- @param button number The button that was released
--- @param x number The x position of the mouse --- @param x number The x position of the mouse
@@ -156,6 +211,17 @@ function BaseFrame:char(char)
Container.char(self, char) Container.char(self, char)
end end
function BaseFrame:dispatchEvent(event, ...)
local _term = self.get("term")
if _term == nil then return end
if(isPeripheral(_term))then
if event == "mouse_click" then
return
end
end
Container.dispatchEvent(self, event, ...)
end
--- @shortDescription Renders the Frame --- @shortDescription Renders the Frame
--- @protected --- @protected
function BaseFrame:render() function BaseFrame:render()

View File

@@ -2,7 +2,8 @@ local elementManager = require("elementManager")
local VisualElement = elementManager.getElement("VisualElement") local VisualElement = elementManager.getElement("VisualElement")
local getCenteredPosition = require("libraries/utils").getCenteredPosition local getCenteredPosition = require("libraries/utils").getCenteredPosition
local deepcopy = require("libraries/utils").deepcopy local deepcopy = require("libraries/utils").deepcopy
---@cofnigDescription The Display is a special element which uses the cc window API which you can use. local colorHex = require("libraries/colorHex")
---@configDescription The Display is a special element which uses the cc window API which you can use.
---@configDefault false ---@configDefault false
--- The Display is a special element where you can use the window (term) API to draw on a element, useful when you need to use external APIs. --- The Display is a special element where you can use the window (term) API to draw on a element, useful when you need to use external APIs.
@@ -30,6 +31,8 @@ function Display:init(props, basalt)
self.set("type", "Display") self.set("type", "Display")
self._window = window.create(basalt.getActiveFrame():getTerm(), 1, 1, self.get("width"), self.get("height"), false) self._window = window.create(basalt.getActiveFrame():getTerm(), 1, 1, self.get("width"), self.get("height"), false)
local reposition = self._window.reposition local reposition = self._window.reposition
local blit = self._window.blit
local write = self._window.write
self._window.reposition = function(self, x, y, width, height) self._window.reposition = function(self, x, y, width, height)
self.set("x", x) self.set("x", x)
self.set("y", y) self.set("y", y)
@@ -49,6 +52,14 @@ function Display:init(props, basalt)
self._window.isVisible = function(self) self._window.isVisible = function(self)
return self.get("visible") return self.get("visible")
end end
self._window.blit = function(self, x, y, text, fg, bg)
blit(self, x, y, text, fg, bg)
self:updateRender()
end
self._window.write = function(self, x, y, text)
write(self, x, y, text)
self:updateRender()
end
self:observe("width", function(self, width) self:observe("width", function(self, width)
local window = self._window local window = self._window
@@ -71,6 +82,30 @@ function Display:getWindow()
return self._window return self._window
end end
--- Writes text to the display at the given position with the given foreground and background colors
--- @shortDescription Writes text to the display
--- @param x number The x position to write to
--- @param y number The y position to write to
--- @param text string The text to write
--- @param fg? colors The foreground color (optional)
--- @param bg? colors The background color (optional)
--- @return Display self The display instance
function Display:write(x, y, text, fg, bg)
local window = self._window
if window then
if fg then
window.setTextColor(fg)
end
if bg then
window.setBackgroundColor(bg)
end
window.setCursorPos(x, y)
window.write(text)
end
self:updateRender()
return self
end
--- @shortDescription Renders the Display --- @shortDescription Renders the Display
--- @protected --- @protected
function Display:render() function Display:render()

View File

@@ -29,8 +29,9 @@ else
basalt.path = fs.getDir(select(2, ...)) basalt.path = fs.getDir(select(2, ...))
end end
local mainFrame = nil local main = nil
local activeFrame = nil local focusedFrame = nil
local activeFrames = {}
local _type = type local _type = type
local lazyElements = {} local lazyElements = {}
@@ -94,52 +95,79 @@ end
--- Creates and returns a new BaseFrame --- Creates and returns a new BaseFrame
--- @shortDescription Creates a new BaseFrame --- @shortDescription Creates a new BaseFrame
--- @return BaseFrame BaseFrame The created frame instance --- @return BaseFrame BaseFrame The created frame instance
--- @usage local mainFrame = basalt.createFrame()
function basalt.createFrame() function basalt.createFrame()
local frame = basalt.create("BaseFrame") local frame = basalt.create("BaseFrame")
frame:postInit() frame:postInit()
if(mainFrame==nil)then
mainFrame = frame
activeFrame = frame
end
return frame return frame
end end
--- Returns the element manager instance --- Returns the element manager instance
--- @shortDescription Returns the element manager --- @shortDescription Returns the element manager
--- @return table ElementManager The element manager --- @return table ElementManager The element manager
--- @usage local manager = basalt.getElementManager()
function basalt.getElementManager() function basalt.getElementManager()
return elementManager return elementManager
end end
--- Returns the error manager instance
--- @shortDescription Returns the error manager
--- @return table ErrorManager The error manager
function basalt.getErrorManager()
return errorManager
end
--- Gets or creates the main frame --- Gets or creates the main frame
--- @shortDescription Gets or creates the main frame --- @shortDescription Gets or creates the main frame
--- @return BaseFrame BaseFrame The main frame instance --- @return BaseFrame BaseFrame The main frame instance
--- @usage local frame = basalt.getMainFrame()
function basalt.getMainFrame() function basalt.getMainFrame()
if(mainFrame == nil)then local _main = tostring(term.current())
mainFrame = basalt.createFrame() if(activeFrames[_main] == nil)then
activeFrame = mainFrame main = _main
basalt.createFrame()
end end
return mainFrame return activeFrames[_main]
end end
--- Sets the active frame --- Sets the active frame
--- @shortDescription Sets the active frame --- @shortDescription Sets the active frame
--- @param frame BaseFrame The frame to set as active --- @param frame BaseFrame The frame to set as active
--- @usage basalt.setActiveFrame(myFrame) --- @param setActive? boolean Whether to set the frame as active (default: true)
function basalt.setActiveFrame(frame) function basalt.setActiveFrame(frame, setActive)
activeFrame = frame local t = frame:getTerm()
activeFrame:updateRender() if(setActive==nil)then setActive = true end
if(t~=nil)then
activeFrames[tostring(t)] = setActive and frame or nil
frame:updateRender()
end
end end
--- Returns the active frame --- Returns the active frame
--- @shortDescription Returns the active frame --- @shortDescription Returns the active frame
--- @param t? term The term to get the active frame for (default: current term)
--- @return BaseFrame? BaseFrame The frame to set as active --- @return BaseFrame? BaseFrame The frame to set as active
--- @usage local frame = basalt.getActiveFrame() function basalt.getActiveFrame(t)
function basalt.getActiveFrame() if(t==nil)then t = term.current() end
return activeFrame return activeFrames[tostring(t)]
end
--- Sets a frame as focused
--- @shortDescription Sets a frame as focused
--- @param frame BaseFrame The frame to set as focused
function basalt.setFocus(frame)
if(focusedFrame==frame)then return end
if(focusedFrame~=nil)then
focusedFrame:dispatchEvent("blur")
end
focusedFrame = frame
if(focusedFrame~=nil)then
focusedFrame:dispatchEvent("focus")
end
end
--- Returns the focused frame
--- @shortDescription Returns the focused frame
--- @return BaseFrame? BaseFrame The focused frame
function basalt.getFocus()
return focusedFrame
end end
--- Schedules a function to run in a coroutine --- Schedules a function to run in a coroutine
@@ -147,7 +175,6 @@ end
--- @function scheduleUpdate --- @function scheduleUpdate
--- @param func function The function to schedule --- @param func function The function to schedule
--- @return thread func The scheduled function --- @return thread func The scheduled function
--- @usage local id = basalt.scheduleUpdate(myFunction)
function basalt.schedule(func) function basalt.schedule(func)
expect(1, func, "function") expect(1, func, "function")
@@ -167,7 +194,6 @@ end
--- @function removeSchedule --- @function removeSchedule
--- @param func thread The scheduled function to remove --- @param func thread The scheduled function to remove
--- @return boolean success Whether the scheduled function was removed --- @return boolean success Whether the scheduled function was removed
--- @usage basalt.removeSchedule(scheduleId)
function basalt.removeSchedule(func) function basalt.removeSchedule(func)
for i, v in ipairs(basalt._schedule) do for i, v in ipairs(basalt._schedule) do
if(v.coroutine==func)then if(v.coroutine==func)then
@@ -178,15 +204,36 @@ function basalt.removeSchedule(func)
return false return false
end end
local mouseEvents = {
mouse_click = true,
mouse_up = true,
mouse_scroll = true,
mouse_drag = true,
}
local keyEvents = {
key = true,
key_up = true,
char = true,
}
local function updateEvent(event, ...) local function updateEvent(event, ...)
if(event=="terminate")then basalt.stop() end if(event=="terminate")then basalt.stop() end
if lazyElementsEventHandler(event, ...) then return end if lazyElementsEventHandler(event, ...) then return end
if(activeFrame)then if(mouseEvents[event])then
activeFrame:dispatchEvent(event, ...) activeFrames[main]:dispatchEvent(event, ...)
elseif(keyEvents[event])then
if(focusedFrame~=nil)then
focusedFrame:dispatchEvent(event, ...)
end
else
for _, frame in pairs(activeFrames) do
frame:dispatchEvent(event, ...)
end
end end
for k, func in ipairs(basalt._schedule) do for _, func in ipairs(basalt._schedule) do
if(event==func.filter)or(func.filter==nil)then if(event==func.filter)or(func.filter==nil)then
local ok, result = coroutine.resume(func.coroutine, event, ...) local ok, result = coroutine.resume(func.coroutine, event, ...)
if(not ok)then if(not ok)then
@@ -208,15 +255,14 @@ local function updateEvent(event, ...)
end end
local function renderFrames() local function renderFrames()
if(activeFrame)then for _, frame in pairs(activeFrames)do
activeFrame:render() frame:render()
end end
end end
--- Runs basalt once, can be used to update the UI manually, but you have to feed it the events --- Runs basalt once, can be used to update the UI manually, but you have to feed it the events
--- @shortDescription Runs basalt once --- @shortDescription Runs basalt once
--- @vararg any The event to run with --- @vararg any The event to run with
--- @usage basalt.update()
function basalt.update(...) function basalt.update(...)
local f = function(...) local f = function(...)
updateEvent(...) updateEvent(...)
@@ -231,7 +277,6 @@ end
--- Stops the Basalt runtime --- Stops the Basalt runtime
--- @shortDescription Stops the Basalt runtime --- @shortDescription Stops the Basalt runtime
--- @usage basalt.stop()
function basalt.stop() function basalt.stop()
basalt.isRunning = false basalt.isRunning = false
term.clear() term.clear()
@@ -241,8 +286,6 @@ end
--- Starts the Basalt runtime --- Starts the Basalt runtime
--- @shortDescription Starts the Basalt runtime --- @shortDescription Starts the Basalt runtime
--- @param isActive? boolean Whether to start active (default: true) --- @param isActive? boolean Whether to start active (default: true)
--- @usage basalt.run()
--- @usage basalt.run(false)
function basalt.run(isActive) function basalt.run(isActive)
if(basalt.isRunning)then errorManager.error("Basalt is already running") end if(basalt.isRunning)then errorManager.error("Basalt is already running") end
if(isActive==nil)then if(isActive==nil)then