Advertisement
1lann

UI Framework

Nov 21st, 2015
114
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 14.50 KB | None | 0 0
  1. -- UI Library by 1lann
  2.  
  3. -- Some asynchronous operators
  4.  
  5. local timeouts = {}
  6. local oldPullEvent = os.pullEvent
  7.  
  8. function os.pullEvent(...)
  9.     while true do
  10.         local eventData = {oldPullEvent(...)}
  11.         if eventData[1] == "timer" and timeouts[eventData[2]] then
  12.             timeouts[eventData[2]]()
  13.         else
  14.             return unpack(eventData)
  15.         end
  16.     end
  17. end
  18.  
  19. function setTimeout(func, time)
  20.     local id = os.startTimer(time)
  21.     timeouts[id] = func
  22. end
  23.  
  24. function clearTimeout()
  25.     timeouts[id] = nil
  26. end
  27.  
  28.  
  29. -- Coordinate manipulation
  30.  
  31. Coord = {}
  32. Coord.__index = Coord
  33.  
  34. Coord.w, Coord.h = term.getSize()
  35.  
  36. function Coord.new(x, y)
  37.     if type(x) ~= "number" then
  38.         return error("ui: number expected for x value, got " .. type(x))
  39.     end
  40.  
  41.     if type(y) ~= "number" then
  42.         return error("ui: number expected for y value, got " .. type(y))
  43.     end
  44.  
  45.     local self = {}
  46.     setmetatable(self, Coord)
  47.     self.x = x
  48.     self.y = y
  49.     return self
  50. end
  51.  
  52. function Coord:unpack()
  53.     return self.x, self.y
  54. end
  55.  
  56. function Coord:setCursorPos()
  57.     return term.setCursorPos(self.x, self.y)
  58. end
  59.  
  60. function Coord:setPosition(x, y)
  61.     if type(x) ~= "number" then
  62.         return error("ui: number expected for x value, got " .. type(x))
  63.     end
  64.  
  65.     if type(y) ~= "number" then
  66.         return error("ui: number expected for y value, got " .. type(y))
  67.     end
  68.  
  69.     self.x = x
  70.     self.y = y
  71. end
  72.  
  73. function Coord.assert(coord)
  74.     if type(coord) ~= "table" then
  75.         return error("ui: coordinate expected, got " .. type(coord), 2)
  76.     end
  77.  
  78.     if getmetatable(coord) ~= Coord then
  79.         if #coord == 2 then
  80.             if type(coord[1]) ~= "number" or type(coord[2]) ~= "number" then
  81.                 return error("ui: coordinate expected, got unknown value", 2)
  82.             end
  83.  
  84.             return Coord.new(unpack(coord))
  85.         end
  86.  
  87.         if coord.type then
  88.             return error("ui: coordinate expected, got " .. coord.type, 2)
  89.         else
  90.             return error("ui: coordinate expected, got unknown value", 2)
  91.         end
  92.     end
  93.  
  94.     return coord
  95. end
  96.  
  97. function Coord.__add(a, b)
  98.     local a = Coord.assert(a)
  99.     local b = Coord.assert(b)
  100.  
  101.     return Coord.new(a.x + b.x, a.y + b.y)
  102. end
  103.  
  104. function Coord.__tostring(a)
  105.     return "(" .. tostring(a.x) .. ", " .. tostring(a.y) .. ")"
  106. end
  107.  
  108. -- Renderer
  109.  
  110. Renderer = {}
  111. Renderer.__index = Renderer
  112. Renderer.incrementer = 0
  113. Renderer.forceRenderEventPrefix = "ui_renderer_force_update_"
  114. Renderer.stopExecution = "ui_renderer_stop_execution"
  115.  
  116. function Renderer.new()
  117.     local self = {}
  118.     setmetatable(self, Renderer)
  119.     self.tree = {}
  120.     self.position = Coord.new(1, 1)
  121.     self.width, self.height = term.getSize()
  122.     self.defaultBgColor = colors.black
  123.     self.defaultTextColor = colors.white
  124.     self.scrollable = false
  125.     self.scrollableHeight = self.height
  126.     self.scrollPosition = 0
  127.     self.id = Renderer.uid()
  128.     self.window = nil -- TODO
  129.     self.attr = {}
  130.     self.forceRenderEvent = Renderer.forceRenderEventPrefix .. self.id
  131.     return self
  132. end
  133.  
  134. function Renderer:setSize(width, height)
  135.     if type(width) ~= "number" then
  136.         return error("ui: number expected for width, got " .. type(width))
  137.     end
  138.  
  139.     if type(height) ~= "number" then
  140.         return error("ui: number expected for height, got " .. type(height))
  141.     end
  142.  
  143.     self.width = width
  144.     self.height = height
  145. end
  146.  
  147. function Renderer.setPosition(position)
  148.     self.position = Coord.assert(position)
  149. end
  150.  
  151. function Renderer:add(child)
  152.     table.insert(self.tree, child)
  153. end
  154.  
  155. function Renderer:setDefaultBackgroundColor(color)
  156.     if type(color) ~= "number" then
  157.         return error("ui: number expected for color, got " .. type(color))
  158.     end
  159.  
  160.     self.defaultBgColor = color
  161. end
  162.  
  163. function Renderer:setDefaultTextColor(color)
  164.     if type(color) ~= "number" then
  165.         return error("ui: number expected for color, got " .. type(color))
  166.     end
  167.  
  168.     self.defaultTextColor = color
  169. end
  170.  
  171. function Renderer:prepareRender()
  172.  
  173. end
  174.  
  175. function Renderer:render()
  176.     local layers = {}
  177.     local sortedLayers = {}
  178.  
  179.     for k, child in pairs(self.tree) do
  180.         if not layers[child.layer] then
  181.             layers[child.layer] = {}
  182.             table.insert(sortedLayers, child.layer)
  183.         end
  184.  
  185.         table.insert(layers[child.layer], child)
  186.     end
  187.  
  188.     table.sort(sortedLayers, function(a, b) return a > b end)
  189.  
  190.     term.setBackgroundColor(self.defaultBgColor)
  191.     term.clear()
  192.  
  193.     for _, layer in pairs(sortedLayers) do
  194.         for _, child in pairs(layers[layer]) do
  195.             if not child.draw then
  196.                 return error("ui: missing draw function on renderer child element")
  197.             end
  198.  
  199.             term.setBackgroundColor(self.defaultBgColor)
  200.             term.setTextColor(self.defaultTextColor)
  201.  
  202.             child:draw({self.position.x, self.position.y - self.scrollPosition})
  203.         end
  204.     end
  205. end
  206.  
  207. function Renderer:setScrollHeight(height)
  208.     if type(height) ~= "number" then
  209.         return error("ui: number expected for height, got " .. type(height))
  210.     end
  211.  
  212.     self.scrollableHeight = height
  213. end
  214.  
  215. function Renderer:setScrollable(scrollable)
  216.     if type(scrollable) ~= "boolean" then
  217.         return error("ui: boolean expected for scrollable, got " ..
  218.             type(scrollable))
  219.     end
  220.  
  221.     self.scrollable = scrollable
  222. end
  223.  
  224. function Renderer:forceRender()
  225.     os.queueEvent(self.forceRenderEvent)
  226. end
  227.  
  228. function Renderer:execute()
  229.     self:prepareRender()
  230.  
  231.     local shouldRender = true
  232.     while true do
  233.         if shouldRender then
  234.             self:render()
  235.         end
  236.  
  237.         shouldRender = false
  238.  
  239.         local event, p1, p2, p3, p4, p5 = os.pullEvent()
  240.         if event == self.forceRenderEvent then
  241.             shouldRender = true
  242.         elseif event == "mouse_click" then
  243.             local clickCoords = Coord.new(p2, p3)
  244.  
  245.             for _, child in pairs(self.tree) do
  246.                 if child.click then
  247.                     shouldRender = true
  248.  
  249.                     local resp = child:click(clickCoords + {0, self.scrollPosition})
  250.                     if type(resp) == "function" then
  251.                         return resp()
  252.                     elseif type(resp) == "string" and
  253.                         resp == Renderer.stopExecution then
  254.                         return
  255.                     end
  256.                 end
  257.             end
  258.         elseif event == "key" then
  259.             for _, child in pairs(self.tree) do
  260.                 if child.key then
  261.                     shouldRender = true
  262.  
  263.                     local resp = child:key(p1)
  264.                     if type(resp) == "function" then
  265.                         return resp()
  266.                     elseif type(resp) == "string" and
  267.                         resp == Renderer.stopExecution then
  268.                         return
  269.                     end
  270.                 end
  271.             end
  272.         elseif event == "char" then
  273.             for _, child in pairs(self.tree) do
  274.                 if child.char then
  275.                     shouldRender = true
  276.  
  277.                     local resp = child:char(p1)
  278.                     if type(resp) == "function" then
  279.                         return resp()
  280.                     elseif type(resp) == "string" and
  281.                         resp == Renderer.stopExecution then
  282.                         return
  283.                     end
  284.                 end
  285.             end
  286.         elseif event == "mouse_scroll" and self.scrollable then
  287.             self.scrollPosition = self.scrollPosition + p1
  288.             if self.scrollPosition > self.scrollableHeight - self.height then
  289.                 self.scrollPosition = self.scrollableHeight - self.height
  290.             elseif self.scrollPosition < 0 then
  291.                 self.scrollPosition = 0
  292.             end
  293.  
  294.             shouldRender = true
  295.         end
  296.     end
  297. end
  298.  
  299. function Renderer.uid()
  300.     Renderer.incrementer = Renderer.incrementer + 1
  301.     return Renderer.incrementer
  302. end
  303.  
  304. -- Contents
  305.  
  306. Content = {}
  307. Content.__index = Content
  308.  
  309. function Content.new(width, height, initial)
  310.     if type(width) ~= "number" then
  311.         return error("ui: number expected for width, got " .. type(height))
  312.     end
  313.  
  314.     if type(height) ~= "number" then
  315.         return error("ui: number expected for height, got " .. type(width))
  316.     end
  317.  
  318.     local self = {}
  319.     setmetatable(self, Content)
  320.     self.width = width
  321.     self.height = height
  322.     self.layer = 0
  323.  
  324.     if initial then
  325.         if type(initial) ~= "table" then
  326.             return error("ui: instruction list expected, got " .. type(initial))
  327.         end
  328.  
  329.         self.instructions = initial
  330.     else
  331.         self.instructions = {}
  332.     end
  333.  
  334.     return self
  335. end
  336.  
  337. function Content:setLayer(layer)
  338.     if type(layer) ~= "number" then
  339.         return error("ui: number expected for layer, got " .. type(layer))
  340.     end
  341.  
  342.     self.layer = layer
  343. end
  344.  
  345. function Content:setAttr(key, value)
  346.     self.attr[key] = value
  347. end
  348.  
  349. function Content:getAttr(key)
  350.     return self.attr[key]
  351. end
  352.  
  353. function Content:clear()
  354.     self.instructions = {}
  355. end
  356.  
  357. function Content:addLine(text, position)
  358.     if type(text) ~= "string" then
  359.         return error("ui: string expected for text, got " .. type(text))
  360.     end
  361.  
  362.     if type(position) ~= "string" then
  363.         return error("ui: string expected for position, got " .. type(position))
  364.     end
  365.  
  366.     if position == "left" then
  367.         table.insert(self.instructions, {
  368.             ["action"] = "text",
  369.             ["text"] = text
  370.         })
  371.         self:newLine()
  372.     elseif position == "right" then
  373.         self:setXPosition(width - #text)
  374.         table.insert(self.instructions, {
  375.             ["action"] = "text",
  376.             ["text"] = text
  377.         })
  378.         self:newLine()
  379.     elseif position == "center" then
  380.         self:setXPosition(math.ceil((self.width / 2) - (#text / 2)))
  381.         table.insert(self.instructions, {
  382.             ["action"] = "text",
  383.             ["text"] = text
  384.         })
  385.         self:newLine()
  386.     else
  387.         return error("ui: invalid position string")
  388.     end
  389. end
  390.  
  391. function Content:newLine()
  392.     table.insert(self.instructions, {
  393.         ["action"] = "position",
  394.         ["positionType"] = "newline"
  395.     })
  396. end
  397.  
  398. function Content:setPosition(coord)
  399.     local coord = Coord.assert(coord)
  400.     table.insert(self.instructions, {
  401.         ["action"] = "position",
  402.         ["positionType"] = "manual",
  403.         ["position"] = coord
  404.     })
  405. end
  406.  
  407. function Content:setXPosition(x)
  408.     if type(x) ~= "number" then
  409.         return error("ui: number expected for x value, got " .. type(x))
  410.     end
  411.  
  412.     table.insert(self.instructions, {
  413.         ["action"] = "position",
  414.         ["positionType"] = "x",
  415.         ["x"] = x
  416.     })
  417. end
  418.  
  419. function Content:setTextColor(color)
  420.     if type(color) ~= "number" then
  421.         return error("ui: number expected for color, got " .. type(color))
  422.     end
  423.  
  424.     table.insert(self.instructions, {
  425.         ["action"] = "textColor",
  426.         ["color"] = color
  427.     })
  428. end
  429.  
  430. function Content:setBackgroundColor(color)
  431.     if type(color) ~= "number" then
  432.         return error("ui: number expected for color, got " .. type(color))
  433.     end
  434.  
  435.     table.insert(self.instructions, {
  436.         ["action"] = "bgColor",
  437.         ["color"] = color
  438.     })
  439. end
  440.  
  441. function Content:write(...)
  442.     local args = {...}
  443.     local text = ""
  444.  
  445.     for k, v in pairs(args) do
  446.         text = text .. tostring(v)
  447.     end
  448.  
  449.     table.insert(self.instructions, {
  450.         ["action"] = "text",
  451.         ["text"] = text
  452.     })
  453. end
  454.  
  455. function Content:append(instructions)
  456.     if type(instructions) ~= "table" then
  457.         return error("ui: instruction list expected, got " ..
  458.             type(instructions))
  459.     end
  460.  
  461.     for k, v in pairs(instructions) do
  462.         table.insert(self.instructions, v)
  463.     end
  464. end
  465.  
  466. function Content:draw(origin)
  467.     local origin = Coord.assert(origin)
  468.     origin:setCursorPos()
  469.     local currentLine = 0
  470.  
  471.     for k, v in pairs(self.instructions) do
  472.         if type(v) ~= "table" then
  473.             return error("ui: instruction expected, got " .. type(v))
  474.         end
  475.  
  476.         if type(v.action) ~= "string" then
  477.             return error("ui: string expected for action, got " ..
  478.                 type(v.action))
  479.         end
  480.  
  481.         if v.action == "position" then
  482.             if v.positionType == "manual" then
  483.                 local position = Coord.assert(v.position)
  484.                 local absolutePos = origin + position
  485.  
  486.                 absolutePos:setCursorPos()
  487.                 currentLine = absolutePos.y
  488.             elseif v.positionType == "x" then
  489.                 local absolutePos = origin + {v.x, currentLine}
  490.                 absolutePos:setCursorPos()
  491.             elseif v.positionType == "newline" then
  492.                 positionImmunity = false
  493.                 currentLine = currentLine + 1
  494.  
  495.                 local absolutePos = (origin + {0, currentLine})
  496.                 absolutePos:setCursorPos()
  497.             else
  498.                 return error("ui: unknown position type: " .. v.positionType)
  499.             end
  500.         elseif v.action == "textColor" then
  501.             term.setTextColor(v.color)
  502.         elseif v.action == "bgColor" then
  503.             term.setBackgroundColor(v.color)
  504.         elseif v.action == "text" and not positionImmunity then
  505.             local x = term.getCursorPos()
  506.             if #v.text + x > origin.x + self.width then
  507.                 if origin.x + self.width - x > 0 then
  508.                     local text = v.text:sub(origin.x + self.width - x)
  509.                     term.write(text)
  510.                 end
  511.             else
  512.                 term.write(v.text)
  513.             end
  514.         else
  515.             return error("ui: unknown action type: " .. v.action)
  516.         end
  517.     end
  518. end
  519.  
  520. -- Button
  521.  
  522. Button = {}
  523. Button.__index = Button
  524.  
  525. function Button.new(onclick)
  526.     local self = {}
  527.     setmetatable(self, Button)
  528.     self.height = 0
  529.     self.width = 0
  530.     self.position = Coord.new(0, 0)
  531.     self.content = Content.new(0, 0)
  532.     self.transparent = false
  533.     self.color = colors.white
  534.     self.enterAsClick = false
  535.     self.attr = {}
  536.     self.layer = 0
  537.  
  538.     if onclick then
  539.         if type(onclick) == "function" then
  540.             self.onclick = onclick
  541.         else
  542.             return error("ui: function expected for onclick, got " ..
  543.                 type(onclick))
  544.         end
  545.     else
  546.         self.onclick = function() end
  547.     end
  548.  
  549.     return self
  550. end
  551.  
  552. function Button:setSize(width, height)
  553.     if type(width) ~= "number" then
  554.         return error("ui: number expected for width, got " .. type(width))
  555.     end
  556.  
  557.     if type(height) ~= "number" then
  558.         return error("ui: number expected for height, got " .. type(height))
  559.     end
  560.  
  561.     self.width = width
  562.     self.height = height
  563.     self.content = Content.new(self.width, self.height,
  564.         self.content.instructions)
  565. end
  566.  
  567. function Button:setOnClick(onclick)
  568.     if type(onclick) ~= "function" then
  569.         return error("ui: function expected for onclick, got " .. type(onclick))
  570.     end
  571.  
  572.     self.onclick = onclick
  573. end
  574.  
  575. function Button:setLayer(layer)
  576.     if type(layer) ~= "number" then
  577.         return error("ui: number expected for layer, got " .. type(layer))
  578.     end
  579.  
  580.     self.layer = layer
  581. end
  582.  
  583. function Button:setColor(color)
  584.     if type(color) ~= "number" then
  585.         return error("ui: number expected for color, got " .. type(color))
  586.     end
  587.  
  588.     self.color = color
  589. end
  590.  
  591. function Button:setPosition(position)
  592.     self.position = Coord.assert(position)
  593. end
  594.  
  595. function Button:getContent()
  596.     return self.content
  597. end
  598.  
  599. function Button:setEnterAsClick(enterAsClick)
  600.     if type(enterAsClick) ~= "boolean" then
  601.         return error("ui: boolean expected, got " .. type(enterAsClick))
  602.     end
  603.  
  604.     self.enterAsClick = enterAsClick
  605. end
  606.  
  607. function Button:click(coord)
  608.     local coord = Coord.assert(coord)
  609.  
  610.     if coord.y > self.position.y and
  611.         coord.y <= self.position.y + self.height and
  612.         coord.x > self.position.x and
  613.         coord.x <= self.position.x + self.width then
  614.         self.onclick(coord.x - self.position.x, coord.y - self.position.y)
  615.     end
  616. end
  617.  
  618. function Button:key(key)
  619.     if key == keys.enter and self.enterAsClick then
  620.         return self.onclick()
  621.     end
  622. end
  623.  
  624. function Button:setAttr(key, value)
  625.     self.attr[key] = value
  626. end
  627.  
  628. function Button:getAttr(key)
  629.     return self.attr[key]
  630. end
  631.  
  632. function Button:draw(origin)
  633.     local origin = Coord.assert(origin)
  634.     local position = origin + self.position
  635.  
  636.     term.setBackgroundColor(self.color)
  637.  
  638.     for i = 0, self.height - 1 do
  639.         local absolutePos = position + {0, i}
  640.         absolutePos:setCursorPos()
  641.  
  642.         term.write(string.rep(" ", self.width))
  643.     end
  644.  
  645.     self.content:draw(position)
  646. end
  647.  
  648. -- Text Field
  649.  
  650.  
  651.  
  652. -- Rect
  653.  
  654. Rect = Button
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement