os.loadAPI("/lib/util") function prettyPrintSize(tokens) if type(tokens) == "number" then return tostring(tokens) end local str = "( " for key, token in pairs(tokens) do if token.tokentype == "list" then str = str .. prettyPrintSize(token) elseif token.tokentype == "num" then str = str .. token.val .. " " elseif token.tokentype == "percent" then str = str .. token.val .. "% " elseif token.tokentype == "selfpercent" then str = str .. token.val .. "$ " elseif token.tokentype == "auto" then str = str .. "& " elseif token.tokentype == "add" then str = str .. "+ " elseif token.tokentype == "sub" then str = str .. "- " elseif token.tokentype == "multiply" then str = str .. "* " elseif token.tokentype == "divide" then str = str .. "/ " end end return str .. ")" end function parseSize(size) if type(size) == "number" then return size end size = size .. " " size = string.gsub(size, "center", "50%% - 50$") print(size) local res = { tokentype = "list" } local current = "" local parsing = true local i = 1 while i <= #size do local c = string.sub(size, i, i) if tonumber(c) ~= nil then current = current .. c elseif current ~= "" then local cur = tonumber(current) if cur == nil or current == "+" or current == "-" then -- Don't do anything because there's no number elseif c == "%" then table.insert(res, { tokentype = "percent"; val = cur; }) elseif c == "$" then table.insert(res, { tokentype = "selfpercent"; val = cur; }) else table.insert(res, { tokentype = "num"; val = cur; }) end current = "" end if c == "(" then local substr = "" local depth = 1 while depth > 0 do i = i + 1 local cc = string.sub(size, i, i) if cc == "(" then depth = depth + 1 elseif cc == ")" then depth = depth - 1 end if depth > 0 then substr = substr .. cc end end table.insert(res, parseSize(substr)) elseif c == "&" then table.insert(res, { tokentype = "auto" }) elseif c == "+" then table.insert(res, { tokentype = "add" }) elseif c == "-" then table.insert(res, { tokentype = "sub" }) elseif c == "*" then table.insert(res, { tokentype = "multiply" }) elseif c == "/" then table.insert(res, { tokentype = "divide" }) end i = i + 1 end return res end function calcSize(tokens, parentSizeArg, selfSizeArg, autoSizeArg) if type(tokens) == "number" then return tokens elseif tokens == nil then return nil end local parentSize = 0 if type(parentSizeArg) == "function" then parentSize = parentSizeArg() elseif type(parentSizeArg) == "number" then parentSize = parentSizeArg end local selfSize = 0 if type(selfSizeArg) == "function" then selfSize = selfSizeArg() elseif type(selfSizeArg) == "number" then selfSize = selfSizeArg end local autoSize = 0 if type(autoSizeArg) == "function" then autoSize = autoSizeArg() elseif type(autoSizeArg) == "number" then autoSize = autoSizeArg end function getVal(token) if token.tokentype == "num" then return token.val elseif token.tokentype == "percent" then return util.percentOf(token.val, parentSize) elseif token.tokentype == "selfpercent" then return util.percentOf(token.val, selfSize) - 1 elseif token.tokentype == "auto" then return autoSize elseif token.tokentype == "list" then return calcSize(token, parentSize, selfSize) else return 0 end end local sum = 0 local opr = "add" local i = 1 while i <= #tokens do local num = getVal(tokens[i]) if opr == "add" then sum = sum + num elseif opr == "sub" then sum = sum - num elseif opr == "multiply" then sum = sum * num elseif opr == "divide" then sum = sum / num else error("Unexpected operator: "..opr) return 0 end local oprToken = tokens[i + 1] if oprToken ~= nil then opr = oprToken.tokentype else opr = "" end i = i + 2 end return math.floor(sum) end function Widget() local self = util.newObject() util.makeEventListener(self) local prog = nil local focused = false local children = {} local parent = nil local top = 1 local bottom = nil local left = 1 local right = nil local width = parseSize("&") local height = parseSize("&") local background = "black" local foreground = "white" local x = 1 local y = 1 self.getset("prog", function() return prog end, function(val) prog = val end) self.getset("focused", function() return focused end) self.getset("children", function() return children end) self.getset("parent", function() return parent end) self.getset("_parent", function() return parent end, function(val) parent = val end) self.getset("width", function() return calcSize(width, parent.width, 0, self.autoWidth) end, function(val) width = parseSize(val) end) self.getset("height", function() return calcSize(height, parent.height, 0, self.autoHeight) end, function(val) height = parseSize(val) end) self.getset("background", function() return background end, function(val) background = val end) self.getset("foreground", function() return foreground end, function(val) foreground = val end) self.getset("top", function() return calcSize(top, parent.height, self.height, 0) end, function(val) top = parseSize(val) end) self.getset("bottom", function() return calcSize(bottom, parent.height, self.height, 0) end, function(val) bottom = parseSize(val) end) self.getset("left", function() return calcSize(left, parent.width, self.width, 0) end, function(val) left = parseSize(val) end) self.getset("right", function() return calcSize(right, parent.width, self.width, 0) end, function(val) right = parseSize(val) end) self.getset("x", function() return x end, function(val) x = val end) self.getset("y", function() return y end, function(val) y = val end) self.focus = function() focused = true self.emit("focus", self) end self.unfocus = function() focused = false self.emit("unfocus", self) end self.onclick = function(x, y) for key, child in pairs(self.children()) do local cx = child.x() local cy = child.y() local cw = child.width() local ch = child.height() if x >= cx and x < cx + cw and y >= cy and y < cy + ch then child.onclick(x, y) end end end self.onchar = function(char) end self.onkey = function(key) end self.setColors = function(screen) screen.setTextColor(colors[foreground]) screen.setBackgroundColor(colors[background]) local w = self.width() local h = self.height() local str = "" for i = 1, w do str = str .. " " end local x = self.x() local y = self.y() for i = 1, h do screen.setCursorPos(x, y + i - 1) screen.write(str) end screen.setCursorPos(x, y) end self.addChild = function(widget) if widget.parent() ~= nil then error("Widget already has a parent") return end table.insert(children, widget) widget._parent(self) widget.prog(prog) widget.on("mustreflow", function() self.emit("mustreflow") end) widget.on("mustredraw", function(elem) self.emit("mustredraw", elem) end) widget.on("focus", function(elem) self.emit("focus", elem) end) widget.on("unfocus", function(elem) self.emit("unfocus", elem) end) self.emit("mustreflow") end self.removeChild = function(child) local removed = false for key, val in pairs(children) do if val == child then removed = true child.unfocus() table.remove(children, key) end end if removed then self.emit("mustreflow") end end self.reflow = util.abstract self.draw = util.abstract return self end function LinearLayout() local self = Widget() local orientation = "vertical" local reversed = false self.getset("orientation", function() return orientation end, function(val) orientation = val end) self.getset("reversed", function() return reversed end, function(val) reversed = val end) self.draw = function(screen) self.setColors(screen) for key, child in pairs(self.children()) do child.draw(screen) end end self.reflow = function(screen, x, y) self.x(x) self.y(y) local selfw = self.width() local selfh = self.height() local offset = 0 for key, child in pairs(self.children()) do local cx = x local cy = y local height = child.height() local width = child.width() local top = child.top() local bottom = child.bottom() local left = child.left() local right = child.right() if right ~= nil then cx = cx + selfw - width - right + 1 else cx = cx + top - 1 end if bottom ~= nil then cy = cy + selfh - height - bottom + 1 else cy = cy + top - 1 end if orientation == "vertical" then cy = y + offset + top - 1 offset = offset + top - 1 + height + (bottom or 1) - 1 if reversed then cy = selfh - cy - height + 2 end elseif orientation == "horizontal" then cx = x + offset + left - 1 offset = offset + left - 1 + width + (right or 1) - 1 if reversed then cx = selfw - cx - width + 2 end end child.reflow(screen, cx, cy) end end return self end function FloatingLayout() local self = Widget() self.draw = function(screen) self.setColors(screen) for key, child in pairs(self.children()) do child.draw(screen) end end self.reflow = function(screen, x, y) self.x(x) self.y(y) for key, child in pairs(self.children()) do local cx = x local cy = y if child.right() ~= nil then cx = cx + self.width() - child.width() - child.right() + 1 else cx = cx + child.left() - 1 end if child.bottom() ~= nil then cy = cy + self.height() - child.height() - child.bottom() + 1 else cy = cy + child.top() - 1 end child.reflow(screen, cx, cy) end end self.autoWidth = function() return self.parent().width() end self.autoHeight = function() return self.parent().height() end return self end function TextView() local self = Widget() local text = "" self.getset("text", function() return text end, function(val) local pre = text text = tostring(val) if #pre == #text then self.emit("mustredraw", self) else self.emit("mustreflow") end end) self.reflow = function(screen, x, y) self.x(x) self.y(y) end self.draw = function(screen) self.setColors(screen) screen.setCursorPos(self.x(), self.y()) local w = self.width() local h = self.height() local offy = math.floor(h / 2) local offx = math.floor(w - #text - ((w - #text) / 2)) screen.setCursorPos(self.x() + offx, self.y() + offy) screen.write(text) end self.autoWidth = function() return #text end self.autoHeight = function() return 1 end self.onclick = function(x, y) -- TextViews don't have any interesting onclick actions end return self end function TextInput() local self = Widget() local backgroundInactive = "blue" local backgroundActive = "red" local foregroundInactive = "white" local foregroundActive = "white" local key_backspace = 14 local key_left = 203 local key_right = 205 local key_delete = 211 local text = "" local cursor = 0 self.getset("text", function() return text end, function(val) local pre = text text = tostring(val) if #pre == #text then self.emit("mustredraw", self) else self.emit("mustreflow") end end) self.getset("backgroundInactive", function() return backgroundInactive end, function(val) backgroundInactive = val end) self.getset("backgroundActive", function() return backgroundActive end, function(val) backgroundActive = val end) self.getset("foregroundInactive", function() return foregroundInactive end, function(val) foregroundInactive = val end) self.getset("foregroundActive", function() return foregroundActive end, function(val) foregroundActive = val end) self.reflow = function(screen, x, y) self.x(x) self.y(y) end self.draw = function(screen) if self.focused() then self.background(backgroundActive) self.foreground(foregroundActive) else self.background(backgroundInactive) self.foreground(foregroundInactive) end self.setColors(screen) screen.setCursorPos(self.x(), self.y()) screen.write("|") screen.setCursorPos(self.x() + self.width() - 1, self.y()) screen.write("|") local len = self.width() - 2 screen.setCursorPos(self.x() + 1, self.y()) screen.write(string.sub(text, 1, len)) if self.focused() then screen.setCursorPos(self.x() + 1 + cursor, self.y()) screen.setBackgroundColor(colors.white) screen.write(" ") end end self.autoWidth = function() return #text + 2 end self.autoHeight = function() return 1 end self.onclick = function(x, y) local w = self.width() if x == self.x() or x == w then cursor = #text else cursor = math.min(x - self.x() - 1, #text) end self.focus() end self.onchar = function(char) local pre = string.sub(text, 1, cursor) local post = string.sub(text, cursor + 1, #text) text = pre .. char .. post cursor = cursor + 1 self.emit("mustreflow") end self.onkey = function(key) if key == key_backspace then local pre = string.sub(text, 1, cursor - 1) local post = string.sub(text, cursor + 1, #text) text = pre .. post if cursor > 0 then cursor = cursor - 1 end self.emit("mustreflow") elseif key == key_delete then local pre = string.sub(text, 1, cursor) local post = string.sub(text, cursor + 2, #text) text = pre .. post self.emit("mustreflow") elseif key == key_left then if cursor > 0 then cursor = cursor - 1 end self.emit("mustredraw", self) elseif key == key_right then if cursor < #text then cursor = cursor + 1 end self.emit("mustredraw", self) end end self.on("focus", function() self.emit("mustredraw", self) end) self.on("unfocus", function() self.emit("mustredraw", self) end) return self end function Button() local self = TextView() local backgroundInactive = "blue" local backgroundActive = "red" local foregroundInactive = "white" local foregroundActive = "white" self.getset("backgroundInactive", function() return backgroundInactive end, function(val) backgroundInactive = val end) self.getset("backgroundActive", function() return backgroundActive end, function(val) backgroundActive = val end) self.getset("foregroundInactive", function() return foregroundInactive end, function(val) foregroundInactive = val end) self.getset("foregroundActive", function() return foregroundActive end, function(val) foregroundActive = val end) self.background(backgroundInactive) self.foreground(foregroundInactive) local resetTimeout self.onclick = function(x, y) if resetTimeout ~= nil then self.prog().cancelTimeout(resetTimeout) end self.background(backgroundActive) self.foreground(foregroundActive) self.emit("mustredraw", self) self.emit("focus", self) self.emit("click") resetTimeout = self.prog().setTimeout(0.3, function() self.background(backgroundInactive) self.foreground(foregroundInactive) self.emit("mustredraw", self) end) end return self end function DummyScreen() local self = {} self.getSize = function() return term.getSize() end self.clear = function() end self.write = function() end self.setCursorPos = function() end self.setTextColor = function() end self.setBackgroundColor = function() end return self end function Gui(prog, screen) local self = util.newObject() if screen == nil then screen = term end local focusedWidget = nil local rootWidget = nil self.getset("width", function() local x, y = screen.getSize() return x end) self.getset("height", function() local x, y = screen.getSize(); return y end) self.getset("screen", function() return screen end, function(val) screen = val end) self.getset("focused", function() return focusedWidget end) -- Handle focusing and unfocusing of widgets local function onRootChildFocus(widget) if focusedWidget ~= nil then focusedWidget.unfocus() end focusedWidget = widget end local function onRootChildUnfocus(widget) if widget == focusedWidget then focusedWidget = nil end end local function onRootChildRedraw(widget) widget.draw(screen) end self.setRoot = function(root) if rootWidget ~= nil then rootWidget.removeListener("focus", onRootChildFocus) rootWidget.removeListener("unfocus", onRootChildUnfocus) rootWidget.removeListener("mustreflow", self.reflow) rootWidget.removeListener("mustredraw", onRootChildRedraw) end rootWidget = root root._parent(self) root.on("focus", onRootChildFocus) root.on("unfocus", onRootChildUnfocus) root.on("mustreflow", self.reflow) root.on("mustredraw", onRootChildRedraw) root.prog(prog) end self.reflow = function() if rootWidget == nil then return end screen.clear() screen.setCursorPos(1, 1) rootWidget.reflow(screen, 1, 1) rootWidget.draw(screen) end self.exit = function() prog.exit() end prog.on("init", self.draw) prog.on("exit", function() screen.setTextColor(colors.white) screen.setBackgroundColor(colors.black) screen.clear() screen.setCursorPos(1, 1) end) prog.on("mouse_click", function(button, x, y) if focusedWidget ~= nil then focusedWidget.unfocus() end if rootWidget ~= nil then rootWidget.onclick(x, y) end end) prog.on("char", function(char) if focusedWidget ~= nil then focusedWidget.onchar(char) end end) prog.on("key", function(key) if focusedWidget ~= nil then focusedWidget.onkey(key) end end) return self end