commit e7433b8f9afcfeb27283c3fe0b9b0f24c5211b84 Author: Corbin Bartsch Date: Sat Nov 23 21:02:21 2024 -0500 Initial commit diff --git a/Cubicle.lua b/Cubicle.lua new file mode 100644 index 0000000..5eefabc --- /dev/null +++ b/Cubicle.lua @@ -0,0 +1,1024 @@ +-- Cubicle - A Minecraft ComputerCraft Window Compositor API + +--- The @{Cubicle} API provides methods for compositing and window +-- management. +-- +-- @module Cubicle + +Cubicle = {} + +--- Whether an object is an instance of a class +-- +-- @tparam table o Object to compare +-- @tparam table class Class to compare +-- @treturn boolean Whether the object is an instance of class +local function instanceOf(o, class) + while o do + o = getmetatable(o) + if o == class then return true end + end + return false +end + +local function min(a, b) + if a < b then return a + else return b end +end + +--- Wrap text to a limited length +-- +-- @tparam string text The text to wrap +-- @tparam number length The length limit of each line +-- @treturn table Array of lines +-- @usage wrapText("A very long string to wrap", 10) +local function wrapText(text, length) + _, text = assert(pcall(tostring, text)) + assert(text, + "bad argument #1 for text (expected string got "..type(text)..")") + assert(length and type(length) == "number", + "bad argument #2 for length (expected number got "..type(length)..")") + local lineCount = #text/length + local wrappedText = {} + for i=0,lineCount do + table.insert(wrappedText, string.sub(text, i*length+1, (i+1)*length)) + end + return wrappedText +end + +--- Base class inherited by Button, Text, and Dialog +-- @class GraphicsElement +local GraphicsElement = {} + +--- Instantiate a new GraphicsElement +-- +-- While optional, creating a GraphicsElement without passing a definition is +-- not very useful. +-- +-- @tparam[opt] table o Definition of the GraphicsElement +-- @treturn table Instance of class GraphicsElement +-- @usage GraphicsElement:new { +-- name = "graphicsElement", +-- title = "A GraphicsElement", +-- align = "center", +-- x = 1, y = 2, +-- width = 20, height = 3, +-- textColor = colors.black, bgColor = colors.lime, +-- visible = true +-- } +function GraphicsElement:new(o) + o = o or { + name = "graphicsElement", + title = "graphicsElement", + align = "center", + x = 1, y = 1, + width = 10, height = 1, + textColor = colors.black, bgColor = colors.white, + visible = true + } + + setmetatable(o, self) + self.__index = self + return o +end + +--- Set the x position of the GraphicsElement +-- +-- @tparam number x New x position of the GraphicsElement +function GraphicsElement:setX(x) self.x = x end + +--- Set the y position of the GraphicsElement +-- +-- @tparam number y New y position of the GraphicsElement +function GraphicsElement:setY(y) self.y = y end + +--- Set both the x and y positions of the GraphicsElement +-- +-- @tparam number x New x position of the GraphicsElement +-- @tparam number y New x position of the GraphicsElement +function GraphicsElement:move(x, y) self.x, self.y = x, y end + +--- Set width of the GraphicsElement +-- +-- @tparam number width New width of the GraphicsElement +function GraphicsElement:setWidth(w) self.width = w end + +--- Set height of the GraphicsElement +-- +-- @tparam number height New height of the GraphicsElement +function GraphicsElement:setHeight(h) self.height = h end + +--- Set visibility of the GraphicsElement +-- +-- @tparam boolean visibility Whether the GraphicsElement should be rendered +function GraphicsElement:setVisible(visibility) self.visible = visibility end + +--- Set the text color of the GraphicsElement +-- +-- @tparam number color New color of the GraphicsElement text +function GraphicsElement:setTextColor(color) self.textColor = color end + +--- Set the background color of the GraphicsElement +-- +-- @tparam number color New color of the GraphicsElement background +function GraphicsElement:setBackgroundColor(color) self.bgColor = color end + +--- Set the title of the GraphicsElement +-- +-- @tparam string title New title of the GraphicsElement +function GraphicsElement:setTitle(title) self.title = tostring(title) end + +--- Render the GraphicsElement +-- +-- @tparam table terminal Terminal on which to render +-- @tparam number parentX X position of the parent Window +-- @tparam number parentY Y position of the parent Window +-- @usage Render the GraphicsElement in a Window on the current terminal +-- +-- local window = Window:new() +-- GraphicsElement:render(term.current(), window.x, window.y) +-- @usage Render the GraphicsElement on a monitor connected to the right of the +-- computer +-- +-- local mon = peripheral.wrap("right") +-- GraphicsElement:render(mon, 1, 1) +function GraphicsElement:render(terminal, parentX, parentY) + assert(terminal and type(terminal) == "table", + "bad argument #1 to terminal (expected table, got "..type(terminal)) + assert(parentX and type(parentX) == "number", + "bad argument #2 to parentX (expected number, got "..type(parentX)) + assert(parentX and type(parentX) == "number", + "bad argument #3 to parentY (expected number, got "..type(parentY)) + + if self.visible then + paintutils.drawFilledBox( + parentX + self.x - 1, parentY + self.y, + parentX + self.x + self.width - 2, + parentY + self.y + self.height - 1, + self.bgColor + ) + + terminal.setTextColor(self.textColor) + terminal.setBackgroundColor(self.bgColor) + + local wrappedText = wrapText(self.title, self.width+1) + + for k,v in ipairs(wrappedText) do + if self.align == "left" then + terminal.setCursorPos( + parentX + self.x - 1, + parentY + self.y + self.height/2 + k-1 -- - #wrappedText/2 + ) + elseif self.align == "center" then + terminal.setCursorPos( + parentX + self.x + self.width/2 - #v/2 - 1, + parentY + self.y + + math.fmod(k-math.floor(#wrappedText/2), self.height/2) + ) + elseif self.align == "right" then + terminal.setCursorPos( + parentX + self.x + self.width - #v - 1, + parentY + self.y + self.height/2 + k-1 -- - #wrappedText/2 + ) + end + + terminal.write(v) + end + end +end + +-- Button, Text, Input, and Window inherit base class GraphicsElement +-- @class Text +Text = GraphicsElement:new() + +-- @class Button +Button = GraphicsElement:new() + +--- Instantiate a new Button +-- +-- Inherits @{GraphicsElement} with addition of action +-- +-- While optional, creating a Button without passing a definition is not very +-- useful. +-- +-- @tparam[opt] table o Definition of Button object +-- @treturn table Instance of class Button +-- @usage Button:new { +-- name = "buttonElement", +-- title = "A Button", +-- align = "center", +-- x = 1, y = 2, +-- width = 20, height = 3, +-- textColor = colors.black, bgColor = colors.lime, +-- visible = true, +-- action = function() return true end +-- } +function Button:new(o) + o = o or { + name = "buttonElement", + title = "buttonElement", + align = "center", + x = 1, y = 1, + width = 10, height = 1, + textColor = colors.black, bgColor = colors.white, + visible = true, + action = function() return true end + } + + setmetatable(o, self) + self.__index = self + return o +end + +-- @class Input +Input = GraphicsElement:new() + +--- Instantiate a new Input +-- +-- Inherits @{GraphicsElement} with addition of action and cursor rendering +-- +-- While optional, creating an Input without passing a definition is not very +-- useful. +-- +-- @tparam[opt] table o Definition of Input object +-- @treturn table Instance of class Input +-- @usage Input:new { +-- name = "inputElement", +-- value = "An Input", +-- x = 1, y = 2, +-- width = 20, height = 3, +-- textColor = colors.black, bgColor = colors.lime, +-- visible = true, +-- action = function() return true end +-- } +function Input:new(o) + o = o or { + name = "inputElement", + value = "inputElement", + x = 1, y = 1, + width = 10, height = 1, + textColor = colors.black, bgColor = colors.white, + visible = true, + action = function() return true end + } + + setmetatable(o, self) + self.__index = self + return o +end + +--- Get the value of the Input +-- +-- @treturn string Current value of the Input +function Input:getValue(value) + return self.value +end + +--- Set the value of the Input +-- +-- @tparam string value New value of the Input +function Input:setValue(value) + self.value = tostring(value) +end + +--- Render the Input +-- +-- Overrides inherited @{GraphicsElement:render} method while scrolling title to +-- the left and constraining to bounds of Input width. +-- +-- @tparam table terminal Terminal on which to render +-- @tparam number parentX X position of the parent Window +-- @tparam number parentY Y position of the parent Window +-- @usage Render the GraphicsElement in a Window on the current terminal +-- +-- local window = Window:new() +-- Input:render(term.current(), window.x, window.y) +-- @usage Render the GraphicsElement on a monitor connected to the right of the computer +-- +-- local mon = peripheral.wrap("right") +-- Input:render(mon, 1, 1) +function Input:render(terminal, parentX, parentY) + assert(terminal and type(terminal) == "table", + "bad argument #1 to terminal (expected table, got "..type(terminal)) + assert(parentX and type(parentX) == "number", + "bad argument #2 to parentX (expected number, got "..type(parentX)) + assert(parentX and type(parentX) == "number", + "bad argument #3 to parentY (expected number, got "..type(parentY)) + + if self.visible then + paintutils.drawFilledBox( + parentX + self.x - 1, parentY + self.y, + parentX + self.x + self.width - 2, + parentY + self.y + self.height - 1, + self.bgColor + ) + + terminal.setTextColor(self.textColor) + terminal.setBackgroundColor(self.bgColor) + + terminal.setCursorPos( + parentX + self.x - 1, + parentY + self.y + self.height/2 + ) + + if #self.value <= self.width - 1 then + terminal.write(self.value) + else + terminal.write( + self.value:sub(#self.value - self.width + 2, #self.value) + ) + end + end +end + +-- @class ProgressBar +ProgressBar = GraphicsElement:new() + +--- Instantiate a new ProgressBar +-- +-- Inherits @{GraphicsElement} with additon of progress and completeColor +-- +-- While optional, creating a ProgressBar without passing a defition is not +-- very useful. +-- +-- @tparam[opt] table o Definition of ProgressBar object +-- @treturn table Instance of class ProgressBar +function ProgressBar:new(o) + o = o or { + name = "progressBar", + x = 1, y = 1, + width = 20, height = 1, + textColor = colors.black, bgColor = colors.gray, + completeColor = colors.lime, + -- Progress as a number between 0 and 1 + progress = 0, + visible = true, + -- By default, show a percentage of progress completion + progressText = true + } + + setmetatable(o, self) + self.__index = self + return o +end + +function ProgressBar:setProgress(progress) + assert(progress and type(progress) == "number", + "bad argument #1 to progress (expected number, got "..type(progress)..")") + assert(progress >= 0 and progress <= 1, + "bad argument #1 to progress: progress should be between 0 and 1") + + self.progress = progress +end + +function ProgressBar:render(terminal, parentX, parentY) + assert(terminal and type(terminal) == "table", + "bad argument #1 to terminal (expected table, got "..type(terminal)..")") + assert(parentX and type(parentX) == "number", + "bad argument #2 to parentX (expected number, got "..type(parentX)..")") + assert(parentX and type(parentX) == "number", + "bad argument #3 to parentY (expected number, got "..type(parentY)..")") + + terminal.setCursorPos( + parentX + self.x + self.width - 3, + parentY + self.y + ) + terminal.setTextColor(self.textColor) + terminal.write(tostring(self.progress*100).."%") + + if self.visible then + if self.progressText then + -- Background + paintutils.drawFilledBox( + parentX + self.x - 1, parentY + self.y, + parentX + self.x + self.width - 5, + parentY + self.y + self.height - 1, + self.bgColor + ) + if self.progress > 0 then + -- Progress + paintutils.drawFilledBox( + parentX + self.x - 1, parentY + self.y, + parentX + self.x + self.width*self.progress - 2 - 2*self.progress, + parentY + self.y + self.height - 1, + self.completeColor + ) + end + else + -- Background + paintutils.drawFilledBox( + parentX + self.x - 1, parentY + self.y, + parentX + self.x + self.width - 2, + parentY + self.y + self.height - 1, + self.bgColor + ) + if self.progress > 0 then + -- Progress + paintutils.drawFilledBox( + parentX + self.x - 1, parentY + self.y, + parentX + self.x + self.width*self.progress - 2, + parentY + self.y + self.height - 1, + self.completeColor + ) + end + end + + end +end + +-- @class Dataview +Dataview = GraphicsElement:new() + +--- Instantiate a new Dataview +-- +-- Inherits @{GraphicsElement} with addition of columns and data +-- +-- While optional, creating an Dataview without passing a definition is not +-- very useful. +-- +-- @tparam[opt] table o Definition of Dataview object +-- @treturn table Instance of class Dataview +-- @usage Dataview:new { +-- name = "dataview", +-- title = "A Dataview", +-- x = 1, y = 1, +-- scrollX = 0, scrollY = 0, +-- width = 20, height = 10, +-- textColor = colors.black, headerColor = colors.white, +-- bgColor = colors.white, +-- visible = true, +-- columns = { +-- { name = "col1", title = "Column 1", width = 10 }, +-- { name = "col2", title = "Column 2", width = 15 }, +-- { name = "col3", title = "Column 3", width = 10 } +-- }, +-- data = { +-- { col1 = "asdf", col2 = "1234", col3 = "hello, world!"} +-- } +-- } +function Dataview:new(o) + o = o or { + name = "dataview", + title = "Dataview", + x = 1, y = 1, + scrollX = 1, scrollY = 1, + width = 20, height = 10, + titleHeight = 1, + textColor = colors.white, bgColor = colors.black, + headerColor = colors.white, selectColor = colors.lightGray, + visible = true, + data = {}, + columns = {}, + select = 1 + } + + setmetatable(o, self) + self.__index = self + return o +end + +function Dataview:getColumns() return self.columns end + +function Dataview:getData() return self.data end + +function Dataview:setData(data) + assert(data and type(data) == "table", + "bad argument #1 to data (expected table, got "..type(data)..")") + + self.data = data +end + +function Dataview:addRow(row) + assert(row and type(row) == "table", + "bad argument #1 to row (expected table, got "..type(row)..")") + + table.insert(self.data, row) +end + +function Dataview:getRow(id) + assert(id and type(id) == "number", + "bad argument #1 to id (expected number, got "..type(id)..")") + + return self.data[id] +end + +function Dataview:getSelection() return self.select end + +function Dataview:setSelection(id) + assert(id and type(id) == "number", + "bad argument #1 to id (expected number, got "..type(id)..")") + + self.select = id +end + +function Dataview:getSelectedRow() return self.data[self.select] end + +function Dataview:getScrollX(n) return self.scrollX end + +function Dataview:setScrollX(n) + assert(n and type(n) == "number", + "bad argument #1 for n (expected number, got "..type(n)..")") + + self.scrollX = n +end + +function Dataview:getScrollY(n) return self.scrollY end + +function Dataview:setScrollY(n) + assert(n and type(n) == "number", + "bad argument #1 for n (expected number, got "..type(n)..")") + + self.scrollY = n +end + +function Dataview:render(terminal, parentX, parentY) + assert(terminal and type(terminal) == "table", + "bad argument #1 to terminal (expected table, got "..type(terminal)..")") + assert(parentX and type(parentX) == "number", + "bad argument #2 to parentX (expected number, got "..type(parentX)..")") + assert(parentX and type(parentX) == "number", + "bad argument #3 to parentY (expected number, got "..type(parentY)..")") + + if self.visible then + -- Background + paintutils.drawFilledBox( + parentX + self.x - 1, parentY + self.y, + parentX + self.x + self.width - 2, + parentY + self.y + self.height - 1, + self.bgColor + ) + -- Header row + paintutils.drawLine( + parentX + self.x - 1, parentY + self.y, + parentX + self.x + self.width-1 + 2*#self.columns, parentY + self.y, + self.headerColor + ) + + terminal.setTextColor(self.textColor) + terminal.setBackgroundColor(self.headerColor) + + local xPos = 0 + for _,v in ipairs(self.columns) do + terminal.setCursorPos(parentX + self.x + xPos - 1, parentY + self.y) + terminal.write("| "..v.title) + xPos = xPos + v.width + 2 + end + + terminal.setBackgroundColor(self.bgColor) + + -- Only display as many that will fit in the bounds of the dataview body + for i = self.scrollY, min(#self.data, self.scrollY + self.height - self.titleHeight - 1) do + if i == self.select then + paintutils.drawLine( + parentX + self.x, + parentY + self.y + self.titleHeight + i - self.scrollY, + parentX + self.x + self.width-1 + 2*#self.columns, + parentY + self.y + self.titleHeight + i - self.scrollY, + self.selectColor + ) + else + terminal.setBackgroundColor(self.bgColor) + end + local xPos = 0 + for _,v in ipairs(self.columns) do + terminal.setCursorPos( + parentX + self.x + xPos - 1, + parentY + self.y + self.titleHeight + i - self.scrollY + ) + terminal.write("| "..self.data[i][v.name]) + -- Set X position of the next column + xPos = xPos + v.width + 2 + end + -- terminal.setCursorPos( + -- parentX + self.x - 1, + -- parentY + self.y + self.titleHeight + i - self.scrollY + -- ) + -- terminal.write(i) + end + end +end + +Window = GraphicsElement:new() + +--- Instantiate a new Window +-- +-- Inherits @{GraphicsElement} with addition of onLoad, onClose functions, +-- list of child elements, and title bar. +-- +-- Windows can be dragged around the screen by clicking and dragging on the +-- title bar. +-- +-- Title bar and background colors set the same and title text set to empty +-- string will effectively "hide" the title bar. +-- +-- Title text will always be aligned left, and all elements can not currently +-- be aligned by the window. +-- +-- @tparam[opt] table o Window +-- @treturn table Instance of class Window +-- @usage GraphicsElement:new { +-- name = "window", +-- title = "A Window", +-- x = 1, y = 2, +-- width = 20, height = 3, +-- textColor = colors.black, bgColor = colors.lime, +-- titleColor = colors.blue, +-- visible = true, +-- elements = {}, +-- onLoad = function() return true end, +-- onClose = function() return true end +-- } +function Window:new(o) + o = o or { + name = "window", + title = "window", + x = 1, y = 1, + -- TODO: Figure out if the currently displayed is less than the + -- height, if so, allow scrolling + scrollX = 0, scrollY = 0, + -- Default is the resolution of a computer, since we're not passing a + -- terminal object to the Window + width = 51, height = 19, + -- This will need to be handled when creating the window if the + -- terminal does not support color + textColor = colors.white, bgColor = colors.black, + titleColor = colors.white, + elements = {}, + select = 1, + onLoad = function() return true end, + onClose = function() return true end + } + + setmetatable(o, self) + self.__index = self + return o +end + +--- Add an element to the Window +-- +-- @tparam table o Instance of element to add to the Window +function Window:addElement(o) + assert(o and type(o) == "table", + "bad argument #1 for o (expected table, got "..type(o)..")") + + table.insert(self.elements, o) +end + +--- Remove an element from the Window +-- +-- @tparam table o ID of the element to remove from the Window +function Window:removeElement(o) table.remove(self.elements, o) end + +--- Get the table of elements in a Window +-- +-- @treturn table Elements contained in the Window +function Window:getElements() return self.elements end + +--- Get an element contained in the Window by ID +-- +-- @tparam number id ID of the element to return +-- @treturn table The element with ID, if it exists in the window +function Window:getElementById(id) + if self.elements[id] then + return self.elements[id] + else + return nil + end +end + +--- Get an element contained in the Window by name +-- +-- @tparam string name Name of the element to return +-- @treturn table The element with name, if it exists in the window +function Window:getElementByName(name) + for _,v in pairs(self.elements) do + if v.name == name then return v end + end +end + +--- Get the ID of the currently selected element +-- +-- @treturn number ID of the currently selected element +function Window:getSelection() return self.select end + +--- Set the selected element by ID +-- +-- @tparam number id ID of the element to select +function Window:setSelection(id) + -- Check if there is an element with this ID + assert(self.elements[id], "No element ID "..tostring(id)) + self.select = id +end + +--- Get the currently selected element +-- +-- @treturn table Element which is currently selected +function Window:getSelectedElement() return self.elements[self.select] end + +--- Render the Window and it's elements +-- +-- @tparam table terminal Terminal on which to render the Window +function Window:render(terminal) + -- Background + paintutils.drawFilledBox( + self.x, self.y, + self.x + self.width-1, self.y + self.height, + self.bgColor + ) + -- Title bar + paintutils.drawLine( + self.x, self.y, + self.x + self.width-1, self.y, + self.titleColor + ) + + terminal.setTextColor(self.textColor) + terminal.setBackgroundColor(self.titleColor) + + terminal.setCursorPos(self.x, self.y) + terminal.write(self.title) + + -- Render each element on the window + for k,v in pairs(self.elements) do + v:render(terminal, self.x, self.y, self.width, self.height) + + -- Add a mark to selected button + if self.select == k then + if instanceOf(v, Button) then + -- Qualify instances of buttons, because we don't want to draw + -- this selector when it's an Input + terminal.setCursorPos(self.x + v.x - 1, self.y + v.y + v.height/21) + terminal.write("*") + end + end + end + + -- If an input is selected, place the cursor in it and enable cursor + -- Because inputs are not always the last to render (and therefore + -- won't be able to set the cursor where they want) we have to do this + -- here. + local selectedElement = self:getSelectedElement() + if instanceOf(selectedElement, Input) then + if #selectedElement:getValue() <= selectedElement.width - 1 then + terminal.setCursorPos( + self.x + selectedElement.x - 1 + #selectedElement:getValue(), + self.y + selectedElement.y + ) + else + terminal.setCursorPos( + self.x + selectedElement.x - 1 + selectedElement.width - 1, + self.y + selectedElement.y + ) + end + terminal.setCursorBlink(true) + else + terminal.setCursorBlink(false) + end + +end + +--- Instantiate a new Compositor +-- +-- @{Compositor} is the primary class in which @{Window}s reside +-- +-- @tparam table terminal Terminal on which to render Windows +-- @tparam[opt] table o Compositor object +-- @treturn table Instance of class Compositor +function Compositor:new(terminal, o) + o = o or { + -- The last element in this table is always going to be the focused + -- window. Focused windows will be drawn last over all other windows. + windows = {}, + terminal = terminal, + running = true + } + + setmetatable(o, self) + self.__index = self + return o +end + +--- Add a window +-- +-- @tparam table o Window to add/open +function Compositor:addWindow(o) return table.insert(self.windows, o) end + +--- Remove a window +-- +-- @tparam number id ID of the Window to remove/close +function Compositor:removeWindow(id) return table.remove(self.windows, id) end + +--- Get all Windows +-- +-- @treturn table Table of Windows +function Compositor:getWindows() return self.windows end + +--- Get a Window by its ID +-- +-- @treturn table An instance of class Window +function Compositor:getWindow(id) return self.windows[id] end + +--- Get a Window by its name +-- +-- @treturn table An instance of class Window +function Compositor:getWindowByName(name) + for k,v in pairs(self.windows) do + if v.name == name then + return v + end + end +end + +--- Get the focused Windows +-- +-- @treturn table An instance of class Window +function Compositor:getFocusedWindow() return self.windows[#self.windows] end + +--- Set the focused window +-- +-- @tparam number windowId ID of the window to focus +function Compositor:setFocusedWindow(windowId) + -- Move the self.windows[windowId] to the end of the table + self:addWindow(self:removeWindow(windowId)) + assert(pcall(self:getFocusedWindow().onLoad)) +end + +--- Remove / close the focused Window +function Compositor:removeFocusedWindow() + assert(pcall(self:getFocusedWindow().onClose)) + self:removeWindow(#self.windows) +end + +--- Stop the compositor +-- +-- The compositor handles "terminate" events internally +function Compositor:stop() self.running = false end + +--- Run the compositor, rendering @{Window}s and listening and handling events +-- to navigate and interact with @{GraphicsElement}s +-- +-- Only the focused window (and all of it's contained elements) will be +-- rendered each pass to reduce screen flickering. +-- +-- The compositor handles the following events internally for window and +-- element interaction and will not pass them to the eventHandler function: +-- mouse_click, mouse_up, mouse_drag, key, char, and terminate +-- +-- When the last Window has been closed (is removed), the compositor will stop. +-- +-- @tparam[opt] Function to be run before rendering the Windows +-- @tparam[opt] Function to be run after rendering the Windows +-- @tparam[opt] Function to handle events not handled by the compositor +function Compositor:run(preRender, postRender, eventHandler) + preRender = preRender or function(compositor) end + postRender = postRender or function(compositor) end + eventHandler = eventHandler or function(compositor, event) end + + local window = self:getFocusedWindow() + + -- Run the first window's onLoad function + if window.onLoad and type(window.onLoad) == "function" then + assert(pcall(window.onLoad)) + end + + while self.running do + + local window = self:getFocusedWindow() + + -- Pre render + if preRender and type(preRender) == "function" then + assert(pcall(preRender, self)) + end + + -- Render windows + -- We will only care about rending the focused window for now + -- Windows behind the focused window should be ignored and overwritten + -- by the focused window + -- Eventually, we'll want to be able to drag smaller windows, so we + -- will need to draw what's behind the focused window, as well, to + -- prevent artifacting. + -- window:render(self.terminal) + + -- Render each window + -- Lower IDs will be rendered first. Higher IDs are + window:render(self.terminal) + -- for k,v in pairs(self:getWindows()) do + -- v:render(self.terminal) + -- end + + -- Post render + if postRender and type(postRender) == "function" then + assert(pcall(postRender, self)) + end + + local selectedElement = window:getSelectedElement() + + -- Listen for events + local event = {os.pullEventRaw()} + if event[1] == "mouse_click" or event[1] == "monitor_touch" then + -- Check if the mouse is down on the title bar of a Window + -- If it is, then when a drag event comes we will reposition the window + -- Do this *before* doing any button actions, in case the button action is to *close this Window* + if event[2] == 1 and event[3] >= window.x and event[3] <= window.x + window.width + and event[4] == window.y then + window.dragging = true + end + -- Check to see if click falls within any element's boundaries + for id, element in pairs(window:getElements()) do + if event[3] >= window.x + element.x - 1 + and event[3] <= window.x + element.x + element.width - 2 + and event[4] >= window.y + element.y + and event[4] <= window.y + element.y + element.height - 1 then + -- Element clicked, run the element's action function + window:setSelection(id) + -- Only want to run the action if it's a button + if instanceOf(element, Button) then + assert(pcall(element.action)) + elseif instanceOf(element, Dataview) then + if event[4] <= #element:getData() then + element:setSelection(event[4] - element.y + element:getScrollY() - 2) + end + end + end + + end + elseif event[1] == "mouse_drag" then + if window.dragging then + window:move(event[3], event[4]) + end + elseif event[1] == "mouse_up" then + window.dragging = false + elseif event[1] == "mouse_scroll" then + if instanceOf(selectedElement, Dataview) then + -- event[2] scroll direction -1: up, 1: down + if event[2] == -1 and selectedElement:getScrollY() > 1 then + selectedElement:setScrollY(selectedElement:getScrollY() - 1) + elseif event[2] == 1 and selectedElement:getScrollY() < #selectedElement:getData() - selectedElement.height then + selectedElement:setScrollY(selectedElement:getScrollY() + 1) + end + end + elseif event[1] == "key" then + if event[2] == keys.enter then + -- Enter key pressed, run the element's action function + if selectedElement.action + and type(selectedElement.action) == "function" then + assert(pcall(selectedElement.action)) + end + elseif event[2] == keys.up and window:getSelection() > 1 then + -- Select previous option + local selection = window:getSelection()-1 + -- Loop through the options that come before the one that is + -- currently selected + -- elements = { [...], selection, ... } + for i = 1,selection do + -- If it's a button or input, set the selection to that + -- element's ID + if instanceOf(window:getElementById(i), Button) + or instanceOf(window:getElementById(i), Input) then + window:setSelection(i) + end + end + elseif event[2] == keys.down then + -- Select next option + local selection = window:getSelection()+1 + -- elements = { ..., selection, [...] } + for i = selection,#window:getElements() do + if instanceOf(window:getElementById(i), Button) + or instanceOf(window:getElementById(i), Input) then + window:setSelection(i) + break + end + end + elseif event[2] == keys.backspace + and instanceOf(selectedElement, Input) then + -- If backspace is pressed and we've selected an Input element, + -- remove the last character from the element title + window:getSelectedElement():setValue( + selectedElement:getValue():sub(1, -2) + ) + end + elseif event[1] == "char" and instanceOf(selectedElement, Input) then + -- Qualify with having an input selected, because otherwise we want + -- to pass the char event to the event handler function. + selectedElement:setValue(selectedElement:getValue()..event[2]) + elseif event[1] == "terminate" then + -- Stop the menu + self.running = false + else + -- Pass any other events to the custom event handler function + if eventHandler and type(eventHandler) == "function" then + assert(pcall(eventHandler, self, event)) + end + end + + -- Stop if the last window has been closed/removed + if #self:getWindows() < 1 then self.running = false end + end + + -- Reset terminal + term.setBackgroundColor(colors.black) + term.setTextColor(colors.white) + term.clear() + term.setCursorPos(1, 1) +end + +-- vim: syntax=lua diff --git a/README.md b/README.md new file mode 100644 index 0000000..db58dfe --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Cubicle + +A Minecraft ComputerCraft window manager. + +