- 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")
-- Create main frame
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()
:setSize("{parent.width - 4}", "{parent.height - 4}")
: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
form:addLabel()
:setText("Registration Form")
:setPosition(2, 2)
:setForeground(colors.black)
-- Add computed validation state
form:computed("isValid", function(self)
local username = self:getState("username")
local password = self:getState("password")
local confirmPass = self:getState("confirmPassword")
return #username >= 3 and #password >= 6 and password == confirmPass
end)
-- Username input with state binding
local usernameInput = form:addInput()
:setPosition(2, 4)
:setSize(20, 1)
:setBackground(colors.white)
:setForeground(colors.black)
-- Update username state when input changes
:onChange(function(self, value)
form:setState("username", value)
end)
-- Create labels
form:addLabel({text="Username:", x = 2, y = 2, foreground = colors.lightGray})
form:addLabel({text="Password:", x = 2, y = 4, foreground = colors.lightGray})
form:addLabel({text="Confirm:", x = 2, y = 6, foreground = colors.lightGray})
form:addLabel()
:setText("Username:")
:setPosition(2, 3)
:setForeground(colors.black)
local userInput = form:addInput({x = 11, y = 2, width = 20, height = 1}):bind("text", "username")
local passwordInput = form:addInput({x = 11, y = 4, width = 20, height = 1}):bind("text", "password")
local confirmInput = form:addInput({x = 11, y = 6, width = 20, height = 1}):bind("text", "confirmPassword")
-- Email input
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
-- Submit button
local submitBtn = form:addButton()
:setPosition(2, 10)
:setSize(10, 1)
:setText("Submit")
-- Button color changes based on form validity
:onStateChange("isValid", function(self, isValid)
submitBtn:setBackground(isValid and colors.lime or colors.gray)
end)
:onMouseClick(function()
if form.computedStates.isValid() then
form:setState("submitted", true)
end
end)
:setPosition(2, 8)
:setSize(29, 1)
-- Status message that updates when form is submitted
-- Status label
local statusLabel = form:addLabel()
:setPosition(2, 12)
:setForeground(colors.black)
:setPosition(2, 10)
:setSize(29, 1)
-- Listen for form submission
form:onStateChange("submitted", function(self, submitted)
if submitted then
local username = self:getState("username")
local email = self:getState("email")
local age = self:getState("age")
statusLabel:setText(string.format(
"Submitted: %s (%s) - Age: %d",
username, email, age
))
form:onStateChange("isValid", function(self, isValid)
if isValid then
statusLabel:setText("Form is valid!")
:setForeground(colors.green)
submitBtn:setBackground(colors.green)
else
statusLabel:setText("Please fill all fields correctly")
:setForeground(colors.red)
submitBtn:setBackground(colors.red)
end
end)
-- Run basalt
basalt.run()
basalt.run()

View File

@@ -1,5 +1,6 @@
local elementManager = require("elementManager")
local Container = elementManager.getElement("Container")
local errorManager = require("errorManager")
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.
@@ -8,14 +9,39 @@ local Render = require("render")
---@class BaseFrame : Container
---@field _render Render The render object
---@field _renderUpdate boolean Whether the render object needs to be updated
---@field _peripheralName string The name of a peripheral
local BaseFrame = setmetatable({}, Container)
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)
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
return value
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._renderUpdate = true
local width, height = value.getSize()
@@ -105,10 +131,39 @@ end
--- @param y number The y position to set the cursor to
--- @param blink boolean Whether the cursor should blink
function BaseFrame:setCursor(x, y, blink, color)
local term = self.get("term")
local _term = self.get("term")
self._render:setCursor(x, y, blink, color)
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
--- @param button number The button that was released
--- @param x number The x position of the mouse
@@ -156,6 +211,17 @@ function BaseFrame:char(char)
Container.char(self, char)
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
--- @protected
function BaseFrame:render()

View File

@@ -2,7 +2,8 @@ local elementManager = require("elementManager")
local VisualElement = elementManager.getElement("VisualElement")
local getCenteredPosition = require("libraries/utils").getCenteredPosition
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
--- 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._window = window.create(basalt.getActiveFrame():getTerm(), 1, 1, self.get("width"), self.get("height"), false)
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.set("x", x)
self.set("y", y)
@@ -49,6 +52,14 @@ function Display:init(props, basalt)
self._window.isVisible = function(self)
return self.get("visible")
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)
local window = self._window
@@ -71,6 +82,30 @@ function Display:getWindow()
return self._window
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
--- @protected
function Display:render()

View File

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