diff --git a/examples/states.lua b/examples/states.lua index a31cf9f..9c12d25 100644 --- a/examples/states.lua +++ b/examples/states.lua @@ -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() \ No newline at end of file diff --git a/src/elements/BaseFrame.lua b/src/elements/BaseFrame.lua index 492fa94..52667d5 100644 --- a/src/elements/BaseFrame.lua +++ b/src/elements/BaseFrame.lua @@ -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() diff --git a/src/elements/Display.lua b/src/elements/Display.lua index 3af6402..9d88b90 100644 --- a/src/elements/Display.lua +++ b/src/elements/Display.lua @@ -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() diff --git a/src/main.lua b/src/main.lua index 7a11ae1..1b41efc 100644 --- a/src/main.lua +++ b/src/main.lua @@ -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