diff --git a/sys/apis/class.lua b/sys/apis/class.lua index f01a9e0..bb94b63 100644 --- a/sys/apis/class.lua +++ b/sys/apis/class.lua @@ -7,6 +7,9 @@ return function(base) local c = { } -- a new class instance if type(base) == 'table' then -- our new class is a shallow copy of the base class! + if base._preload then + base = base._preload(base) + end for i,v in pairs(base) do c[i] = v end diff --git a/sys/apis/terminal.lua b/sys/apis/terminal.lua index fbc860c..fc9f49f 100644 --- a/sys/apis/terminal.lua +++ b/sys/apis/terminal.lua @@ -1,205 +1,202 @@ +local Canvas = require('ui.canvas') + local colors = _G.colors local term = _G.term local _gsub = string.gsub -local _rep = string.rep -local _sub = string.sub local Terminal = { } --- add scrolling functions to a window -function Terminal.scrollable(win, maxScroll) - local lines = { } - local scrollPos = 0 - local oblit, oreposition = win.blit, win.reposition - - local palette = { } - for n = 1, 16 do - palette[2 ^ (n - 1)] = _sub("0123456789abcdef", n, n) +-- Replacement for window api with scrolling and buffering +function Terminal.window(parent, sx, sy, w, h, isVisible) + isVisible = isVisible ~= false + if not w or not h then + w, h = parent.getSize() end - maxScroll = maxScroll or 100 + local win = { } + local maxScroll = 100 + local cx, cy = 1, 1 + local blink = false + local bg, fg = parent.getBackgroundColor(), parent.getTextColor() - -- should only do if window is visible... - local function redraw() - local _, h = win.getSize() - local x, y = win.getCursorPos() - for i = 1, h do - local line = lines[i + scrollPos] - if line and line.dirty then - win.setCursorPos(1, i) - oblit(line.text, line.fg, line.bg) - line.dirty = false - end - end - win.setCursorPos(x, y) - end + local canvas = Canvas({ + x = sx, + y = sy, + width = w, + height = h, + isColor = parent.isColor(), + }) + canvas.offy = 0 - local function scrollTo(p, forceRedraw) - local _, h = win.getSize() - local ms = #lines - h -- max scroll - p = math.min(math.max(p, 0), ms) -- normalize - - if p ~= scrollPos or forceRedraw then - scrollPos = p - for _, line in pairs(lines) do - line.dirty = true - end + local function update() + if isVisible then + canvas:render(parent) + win.setCursorPos(cx, cy) end end - function win.write(text) - local _, h = win.getSize() + local function scrollTo(y) + y = math.max(0, y) + y = math.min(#canvas.lines - canvas.height, y) - text = tostring(text) or '' - scrollTo(#lines - h) - win.blit(text, - _rep(palette[win.getTextColor()], #text), - _rep(palette[win.getBackgroundColor()], #text)) - local x, y = win.getCursorPos() - win.setCursorPos(x + #text, y) - end - - function win.clearLine() - local w, h = win.getSize() - local _, y = win.getCursorPos() - - scrollTo(#lines - h) - lines[y + scrollPos] = { - text = _rep(' ', w), - fg = _rep(palette[win.getTextColor()], w), - bg = _rep(palette[win.getBackgroundColor()], w), - dirty = true, - } - redraw() - end - - function win.blit(text, fg, bg) - local x, y = win.getCursorPos() - local w, h = win.getSize() - - if y > 0 and y <= h and x <= w then - local width = #text - - -- fix ffs - if x < 1 then - text = _sub(text, 2 - x) - if bg then - bg = _sub(bg, 2 - x) - end - if bg then - fg = _sub(fg, 2 - x) - end - width = width + x - 1 - x = 1 - end - - if x + width - 1 > w then - text = _sub(text, 1, w - x + 1) - if bg then - bg = _sub(bg, 1, w - x + 1) - end - if bg then - fg = _sub(fg, 1, w - x + 1) - end - width = #text - end - - if width > 0 then - local function replace(sstr, pos, rstr) - if pos == 1 and width == w then - return rstr - elseif pos == 1 then - return rstr .. _sub(sstr, pos+width) - elseif pos + width > w then - return _sub(sstr, 1, pos-1) .. rstr - end - return _sub(sstr, 1, pos-1) .. rstr .. _sub(sstr, pos+width) - end - - local line = lines[y + scrollPos] - line.dirty = true - line.text = replace(line.text, x, text, width) - if fg then - line.fg = replace(line.fg, x, fg, width) - end - if bg then - line.bg = replace(line.bg, x, bg, width) - end - end + if y ~= canvas.offy then + canvas.offy = y + canvas:dirty() + update() end - redraw() + end + + function win.write(str) + str = tostring(str) or '' + canvas:write(cx, cy + canvas.offy, str, bg, fg) + win.setCursorPos(cx + #str, cy) + update() + end + + function win.blit(str, fg, bg) + canvas:blit(cx, cy + canvas.offy, str, bg, fg) + win.setCursorPos(cx + #str, cy) + update() end function win.clear() - local w, h = win.getSize() - - local text = _rep(' ', w) - local fg = _rep(palette[win.getTextColor()], w) - local bg = _rep(palette[win.getBackgroundColor()], w) - lines = { } - for y = 1, h do - lines[y] = { - dirty = true, - text = text, - fg = fg, - bg = bg, - } + canvas.offy = 0 + canvas:clear(bg, fg) + for i = #canvas.lines, canvas.height + 1, -1 do + canvas.lines[i] = nil end - scrollPos = 0 - redraw() + update() + end + + function win.clearLine() + canvas:clearLine(cy, bg, fg) + win.setCursorPos(cx, cy) + update() + end + + function win.getCursorPos() + return cx, cy + end + + function win.setCursorPos(x, y) + cx, cy = x, y + parent.setCursorPos(x + canvas.x - 1, y + canvas.y - 1) + end + + function win.setCursorBlink(b) + blink = b + parent.setCursorBlink(b) + end + + function win.isColor() + return canvas.isColor + end + win.isColour = win.isColor + + function win.setTextColor(c) + fg = c + end + win.setTextColour = win.setTextColor + + function win.setBackgroundColor(c) + bg = c + end + win.setBackgroundColour = win.setBackgroundColor + + function win.getSize() + return canvas.width, canvas.height end - -- doesn't support negative scrolling... function win.scroll(n) - local w = win.getSize() - - for _ = 1, n do - lines[#lines + 1] = { - text = _rep(' ', w), - fg = _rep(palette[win.getTextColor()], w), - bg = _rep(palette[win.getBackgroundColor()], w), - } + n = n or 1 + if n > 0 then + for _ = 1, n do + canvas.lines[#canvas.lines + 1] = { } + canvas:clearLine(#canvas.lines, bg, fg) + end + while #canvas.lines > maxScroll do + table.remove(canvas.lines, 1) + end + scrollTo(#canvas.lines - canvas.height) + canvas.offy = #canvas.lines - canvas.height + canvas:dirty() + update() end + end - while #lines > maxScroll do - table.remove(lines, 1) + function win.getTextColor() + return fg + end + win.getTextColour = win.getTextColor + + function win.getBackgroundColor() + return bg + end + win.getBackgroundColour = win.getBackgroundColor + + function win.setVisible(visible) + if visible ~= isVisible then + isVisible = visible + if isVisible then + canvas:dirty() + update() + end end + end - scrollTo(maxScroll, true) - redraw() + function win.redraw() + if isVisible then + canvas:dirty() + canvas:render(parent) + end + end + + function win.restoreCursor() + win.setCursorPos(cx, cy) + win.setCursorBlink(blink) + end + + function win.getPosition() + return canvas.x, canvas.y + end + + function win.reposition(x, y, width, height) + canvas.x, canvas.y = x, y + canvas:resize(width or canvas.width, height or canvas.height) + end + + --[[ Additional methods ]]-- + function win.scrollDown() + scrollTo(canvas.offy + 1) end function win.scrollUp() - scrollTo(scrollPos - 1) - redraw() + scrollTo(canvas.offy - 1) end - function win.scrollDown() - scrollTo(scrollPos + 1) - redraw() + function win.scrollTop() + scrollTo(0) end - function win.reposition(x, y, nw, nh) - local w, h = win.getSize() - local D = (nh or h) - h - - if D > 0 then - for _ = 1, D do - lines[#lines + 1] = { - text = _rep(' ', w), - fg = _rep(palette[win.getTextColor()], w), - bg = _rep(palette[win.getBackgroundColor()], w), - } - end - elseif D < 0 then - for _ = D, -1 do - lines[#lines] = nil - end - end - return oreposition(x, y, nw, nh) + function win.scrollBottom() + scrollTo(#canvas.lines) end - win.clear() + function win.setMaxScroll(ms) + maxScroll = ms + end + + function win.getCanvas() + return canvas + end + + function win.getParent() + return parent + end + + canvas:clear() + + return win end -- get windows contents diff --git a/sys/apis/ui.lua b/sys/apis/ui.lua index 2e4597f..36e4bc1 100644 --- a/sys/apis/ui.lua +++ b/sys/apis/ui.lua @@ -3,7 +3,6 @@ local class = require('class') local Event = require('event') local Input = require('input') local Peripheral = require('peripheral') -local Sound = require('sound') local Transition = require('ui.transition') local Util = require('util') @@ -14,7 +13,6 @@ local device = _G.device local fs = _G.fs local os = _G.os local term = _G.term -local window = _G.window --[[ Using the shorthand window definition, elements are created from @@ -23,14 +21,6 @@ local window = _G.window On :init(), elements do not know the parent or can calculate sizing. ]] -local function safeValue(v) - local t = type(v) - if t == 'string' or t == 'number' then - return v - end - return tostring(v) -end - -- need to add offsets to this test local function getPosition(element) local x, y = 1, 1 @@ -225,11 +215,7 @@ function Manager:loadTheme(filename) if not theme then error(err) end - for k,v in pairs(theme) do - if self[k] and self[k].defaults then - Util.merge(self[k].defaults, v) - end - end + self.theme = theme end end @@ -479,7 +465,7 @@ function UI.Window:initChildren() end end -local function setSize(self) +function UI.Window:layout() if self.x < 0 then self.x = self.parent.width + self.x + 1 end @@ -525,7 +511,7 @@ function UI.Window:setParent() self.oh, self.ow = self.height, self.width self.ox, self.oy = self.x, self.y - setSize(self) + self:layout() self:initChildren() end @@ -534,7 +520,7 @@ function UI.Window:resize() self.height, self.width = self.oh, self.ow self.x, self.y = self.ox, self.oy - setSize(self) + self:layout() if self.children then for _,child in ipairs(self.children) do @@ -626,13 +612,13 @@ end function UI.Window:write(x, y, text, bg, tc) bg = bg or self.backgroundColor tc = tc or self.textColor - -- TODO: get rid of offx/y - scroll canvas instead - x = x - self.offx - y = y - self.offy - if y <= self.height and y > 0 then - if self.canvas then - self.canvas:write(x, y, text, bg, tc) - else + + if self.canvas then + self.canvas:write(x, y, text, bg, tc) + else + x = x - self.offx + y = y - self.offy + if y <= self.height and y > 0 then self.parent:write( self.x + x - 1, self.y + y - 1, tostring(text), bg, tc) end @@ -759,6 +745,7 @@ function UI.Window:release(child) end function UI.Window:pointToChild(x, y) + -- TODO: get rid of this offx/y mess and scroll canvas instead x = x + self.offx - self.x + 1 y = y + self.offy - self.y + 1 if self.children then @@ -790,13 +777,13 @@ function UI.Window:getFocusables() return a.y < b.y end - local function getFocusable(parent, x, y) + local function getFocusable(parent) for _,child in Util.spairs(parent.children, focusSort) do if child.enabled and child.focus and not child.inactive then table.insert(focusable, child) end if child.children then - getFocusable(child, child.x + x, child.y + y) + getFocusable(child) end end end @@ -907,7 +894,7 @@ function UI.Window:find(uid) end end -function UI.Window:eventHandler(event) +function UI.Window:eventHandler() return false end @@ -995,21 +982,14 @@ function UI.Device:addTransition(effect, args) end function UI.Device:runTransitions(transitions, canvas) - --[[ - for _,t in ipairs(transitions) do - canvas:punch(t.args) -- punch out the effect areas - end - canvas:blitClipped(self.device) -- and blit the remainder - canvas:reset() - ]] - while true do for _,k in ipairs(Util.keys(transitions)) do local transition = transitions[k] - if not transition.update(self.device) then + if not transition.update() then transitions[k] = nil end end + canvas:render(self.device) if Util.empty(transitions) then break end @@ -1018,11 +998,8 @@ function UI.Device:runTransitions(transitions, canvas) end function UI.Device:sync() - local transitions - if self.transitions and self.effectsEnabled then - transitions = self.transitions - self.transitions = nil - end + local transitions = self.effectsEnabled and self.transitions + self.transitions = nil if self:getCursorBlink() then self.device.setCursorBlink(false) @@ -1039,75 +1016,6 @@ function UI.Device:sync() end end ---[[-- StringBuffer --]]-- --- justs optimizes string concatenations -UI.StringBuffer = class() -function UI.StringBuffer:init(bufSize) - self.bufSize = bufSize - self.buffer = {} -end - -function UI.StringBuffer:insert(s, width) - local len = #tostring(s or '') - if len > width then - s = _sub(s, 1, width) - end - table.insert(self.buffer, s) - if len < width then - table.insert(self.buffer, _rep(' ', width - len)) - end -end - -function UI.StringBuffer:insertRight(s, width) - local len = #tostring(s or '') - if len > width then - s = _sub(s, 1, width) - end - if len < width then - table.insert(self.buffer, _rep(' ', width - len)) - end - table.insert(self.buffer, s) -end - -function UI.StringBuffer:get(sep) - return Util.widthify(table.concat(self.buffer, sep or ''), self.bufSize) -end - -function UI.StringBuffer:clear() - self.buffer = { } -end - --- For manipulating text in a fixed width string -local SB = { } -function SB:new(width) - return setmetatable({ - width = width, - buf = _rep(' ', width) - }, { __index = SB }) -end -function SB:insert(x, str, width) - if x < 1 then - x = self.width + x + 1 - end - width = width or #str - if x + width - 1 > self.width then - width = self.width - x - end - if width > 0 then - self.buf = _sub(self.buf, 1, x - 1) .. _sub(str, 1, width) .. _sub(self.buf, x + width) - end -end -function SB:fill(x, ch, width) - width = width or self.width - x + 1 - self:insert(x, _rep(ch, width)) -end -function SB:center(str) - self:insert(math.max(1, math.ceil((self.width - #str + 1) / 2)), str) -end -function SB:get() - return self.buf -end - --[[-- Page (focus manager) --]]-- UI.Page = class(UI.Window) UI.Page.defaults = { @@ -1251,2267 +1159,39 @@ function UI.Page:eventHandler(event) end end ---[[-- Grid --]]-- -UI.Grid = class(UI.Window) -UI.Grid.defaults = { - UIElement = 'Grid', - index = 1, - inverseSort = false, - disableHeader = false, - headerHeight = 1, - marginRight = 0, - textColor = colors.white, - textSelectedColor = colors.white, - backgroundColor = colors.black, - backgroundSelectedColor = colors.gray, - headerBackgroundColor = colors.cyan, - headerTextColor = colors.white, - headerSortColor = colors.yellow, - unfocusedTextSelectedColor = colors.white, - unfocusedBackgroundSelectedColor = colors.gray, - focusIndicator = '>', - sortIndicator = ' ', - inverseSortIndicator = '^', - values = { }, - columns = { }, - accelerators = { - enter = 'key_enter', - [ 'control-c' ] = 'copy', - down = 'scroll_down', - up = 'scroll_up', - home = 'scroll_top', - [ 'end' ] = 'scroll_bottom', - pageUp = 'scroll_pageUp', - [ 'control-b' ] = 'scroll_pageUp', - pageDown = 'scroll_pageDown', - [ 'control-f' ] = 'scroll_pageDown', - }, -} -function UI.Grid:setParent() - UI.Window.setParent(self) - - for _,c in pairs(self.columns) do - c.cw = c.width - if not c.heading then - c.heading = '' +local function loadComponents() + local function load(name) + local s, m = Util.run(_ENV, 'sys/apis/ui/components/' .. name .. '.lua') + if not s then + error(m) end - end - - self:update() - - if not self.pageSize then - if self.disableHeader then - self.pageSize = self.height - else - self.pageSize = self.height - self.headerHeight + if UI[name]._preload then + error('Error loading UI.' .. name) end - end -end - -function UI.Grid:resize() - UI.Window.resize(self) - - if self.disableHeader then - self.pageSize = self.height - else - self.pageSize = self.height - self.headerHeight - end - self:adjustWidth() -end - -function UI.Grid:adjustWidth() - local t = { } -- cols without width - local w = self.width - #self.columns - 1 - self.marginRight -- width remaining - - for _,c in pairs(self.columns) do - if c.width then - c.cw = c.width - w = w - c.cw - else - table.insert(t, c) + if UI.theme[name] and UI[name].defaults then + Util.merge(UI[name].defaults, UI.theme[name]) end + return UI[name] end - if #t == 0 then - return - end + local components = fs.list('sys/apis/ui/components') + for _, f in pairs(components) do + local name = f:match('(.+)%.') - if #t == 1 then - t[1].cw = #(t[1].heading or '') - t[1].cw = math.max(t[1].cw, w) - return - end - - if not self.autospace then - for k,c in ipairs(t) do - c.cw = math.floor(w / (#t - k + 1)) - w = w - c.cw - end - - else - for _,c in ipairs(t) do - c.cw = #(c.heading or '') - w = w - c.cw - end - -- adjust the size to the length of the value - for key,row in pairs(self.values) do - if w <= 0 then - break + UI[name] = setmetatable({ }, { + __call = function(self, ...) + load(name) + setmetatable(self, getmetatable(UI[name])) + return self(...) end - row = self:getDisplayValues(row, key) - for _,col in pairs(t) do - local value = row[col.key] - if value then - value = tostring(value) - if #value > col.cw then - w = w + col.cw - col.cw = math.min(#value, w) - w = w - col.cw - if w <= 0 then - break - end - end - end - end - end - - -- last column does not get padding (right alignment) - if not self.columns[#self.columns].width then - Util.removeByValue(t, self.columns[#self.columns]) - end - - -- got some extra room - add some padding - if w > 0 then - for k,c in ipairs(t) do - local padding = math.floor(w / (#t - k + 1)) - c.cw = c.cw + padding - w = w - padding - end - end - end -end - -function UI.Grid:setPageSize(pageSize) - self.pageSize = pageSize -end - -function UI.Grid:getValues() - return self.values -end - -function UI.Grid:setValues(t) - self.values = t - self:update() -end - -function UI.Grid:setInverseSort(inverseSort) - self.inverseSort = inverseSort - self:update() - self:setIndex(self.index) -end - -function UI.Grid:setSortColumn(column) - self.sortColumn = column -end - -function UI.Grid:getDisplayValues(row, key) - return row -end - -function UI.Grid:getSelected() - if self.sorted then - return self.values[self.sorted[self.index]], self.sorted[self.index] - end -end - -function UI.Grid:setSelected(name, value) - if self.sorted then - for k,v in pairs(self.sorted) do - if self.values[v][name] == value then - self:setIndex(k) - return - end - end - end - self:setIndex(1) -end - -function UI.Grid:focus() - self:drawRows() -end - -function UI.Grid:draw() - if not self.disableHeader then - self:drawHeadings() - end - - if self.index <= 0 then - self:setIndex(1) - elseif self.index > #self.sorted then - self:setIndex(#self.sorted) - end - self:drawRows() -end - --- Something about the displayed table has changed --- resort the table -function UI.Grid:update() - local function sort(a, b) - if not a[self.sortColumn] then - return false - elseif not b[self.sortColumn] then - return true - end - return self:sortCompare(a, b) - end - - local function inverseSort(a, b) - return not sort(a, b) - end - - local order - if self.sortColumn then - order = sort - if self.inverseSort then - order = inverseSort - end - end - - self.sorted = Util.keys(self.values) - if order then - table.sort(self.sorted, function(a,b) - return order(self.values[a], self.values[b]) - end) - end - - self:adjustWidth() -end - -function UI.Grid:drawHeadings() - local x = 1 - local sb = UI.StringBuffer(self.width) - for _,col in ipairs(self.columns) do - local ind = ' ' - if col.key == self.sortColumn then - if self.inverseSort then - ind = self.inverseSortIndicator - else - ind = self.sortIndicator - end - end - sb:insert(ind .. col.heading, col.cw + 1) - --[[ - self:write(x, - 1, - Util.widthify(ind .. col.heading, col.cw + 1), - self.headerBackgroundColor, - col.key == self.sortColumn and self.headerSortColor or self.headerTextColor) - x = x + col.cw + 1 - ]] - end - local y = math.ceil(self.headerHeight / 2) - if self.headerHeight > 1 then - self:clear(self.headerBackgroundColor) - end - self:write(1, y, sb:get(), self.headerBackgroundColor, self.headerTextColor) -end - -function UI.Grid:sortCompare(a, b) - a = safeValue(a[self.sortColumn]) - b = safeValue(b[self.sortColumn]) - if type(a) == type(b) then - return a < b - end - return tostring(a) < tostring(b) -end - -function UI.Grid:drawRows() - local y = 1 - local startRow = math.max(1, self:getStartRow()) - local sb = UI.StringBuffer(self.width) - - if not self.disableHeader then - y = y + self.headerHeight - end - - local lastRow = math.min(startRow + self.pageSize - 1, #self.sorted) - for index = startRow, lastRow do - - local sindex = self.sorted[index] - local rawRow = self.values[sindex] - local key = sindex - local row = self:getDisplayValues(rawRow, key) - - sb:clear() - - local ind = ' ' - if self.focused and index == self.index and not self.inactive then - ind = self.focusIndicator - end - - for _,col in pairs(self.columns) do - if col.justify == 'right' then - sb:insertRight(ind .. safeValue(row[col.key] or ''), col.cw + 1) - else - sb:insert(ind .. safeValue(row[col.key] or ''), col.cw + 1) - end - ind = ' ' - end - - local selected = index == self.index and not self.inactive - - self:write(1, y, sb:get(), - self:getRowBackgroundColor(rawRow, selected), - self:getRowTextColor(rawRow, selected)) - - y = y + 1 - end - - if y <= self.height then - self:clearArea(1, y, self.width, self.height - y + 1) - end -end - -function UI.Grid:getRowTextColor(row, selected) - if selected then - if self.focused then - return self.textSelectedColor - end - return self.unfocusedTextSelectedColor - end - return self.textColor -end - -function UI.Grid:getRowBackgroundColor(row, selected) - if selected then - if self.focused then - return self.backgroundSelectedColor - end - return self.unfocusedBackgroundSelectedColor - end - return self.backgroundColor -end - -function UI.Grid:getIndex() - return self.index -end - -function UI.Grid:setIndex(index) - index = math.max(1, index) - self.index = math.min(index, #self.sorted) - - local selected = self:getSelected() - if selected ~= self.selected then - self:drawRows() - self.selected = selected - if selected then - self:emit({ type = 'grid_focus_row', selected = selected, element = self }) - end - end -end - -function UI.Grid:getStartRow() - return math.floor((self.index - 1) / self.pageSize) * self.pageSize + 1 -end - -function UI.Grid:getPage() - return math.floor(self.index / self.pageSize) + 1 -end - -function UI.Grid:getPageCount() - local tableSize = Util.size(self.values) - local pc = math.floor(tableSize / self.pageSize) - if tableSize % self.pageSize > 0 then - pc = pc + 1 - end - return pc -end - -function UI.Grid:nextPage() - self:setPage(self:getPage() + 1) -end - -function UI.Grid:previousPage() - self:setPage(self:getPage() - 1) -end - -function UI.Grid:setPage(pageNo) - -- 1 based paging - self:setIndex((pageNo-1) * self.pageSize + 1) -end - -function UI.Grid:eventHandler(event) - if event.type == 'mouse_click' or - event.type == 'mouse_rightclick' or - event.type == 'mouse_doubleclick' then - if not self.disableHeader then - if event.y <= self.headerHeight then - local col = 2 - for _,c in ipairs(self.columns) do - if event.x < col + c.cw then - self:emit({ - type = 'grid_sort', - sortColumn = c.key, - inverseSort = self.sortColumn == c.key and not self.inverseSort, - element = self, - }) - break - end - col = col + c.cw + 1 - end - return true - end - end - local row = self:getStartRow() + event.y - 1 - if not self.disableHeader then - row = row - self.headerHeight - end - if row > 0 and row <= Util.size(self.values) then - self:setIndex(row) - if event.type == 'mouse_doubleclick' then - self:emit({ type = 'key_enter' }) - elseif event.type == 'mouse_rightclick' then - self:emit({ type = 'grid_select_right', selected = self.selected, element = self }) - end - return true - end - return false - - elseif event.type == 'grid_sort' then - self.sortColumn = event.sortColumn - self:setInverseSort(event.inverseSort) - self:draw() - elseif event.type == 'scroll_down' then - self:setIndex(self.index + 1) - elseif event.type == 'scroll_up' then - self:setIndex(self.index - 1) - elseif event.type == 'scroll_top' then - self:setIndex(1) - elseif event.type == 'scroll_bottom' then - self:setIndex(Util.size(self.values)) - elseif event.type == 'scroll_pageUp' then - self:setIndex(self.index - self.pageSize) - elseif event.type == 'scroll_pageDown' then - self:setIndex(self.index + self.pageSize) - elseif event.type == 'key_enter' then - if self.selected then - self:emit({ type = 'grid_select', selected = self.selected, element = self }) - end - elseif event.type == 'copy' then - if self.selected then - os.queueEvent('clipboard_copy', self.selected) - end - else - return false - end - return true -end - ---[[-- ScrollingGrid --]]-- -UI.ScrollingGrid = class(UI.Grid) -UI.ScrollingGrid.defaults = { - UIElement = 'ScrollingGrid', - scrollOffset = 0, - marginRight = 1, -} -function UI.ScrollingGrid:postInit() - self.scrollBar = UI.ScrollBar() -end - -function UI.ScrollingGrid:drawRows() - UI.Grid.drawRows(self) - self.scrollBar:draw() -end - -function UI.ScrollingGrid:getViewArea() - local y = 1 - if not self.disableHeader then - y = y + self.headerHeight - end - return { - static = true, -- the container doesn't scroll - y = y, -- scrollbar Y - height = self.pageSize, -- viewable height - totalHeight = Util.size(self.values), -- total height - offsetY = self.scrollOffset, -- scroll offset - } -end - -function UI.ScrollingGrid:getStartRow() - local ts = Util.size(self.values) - if ts < self.pageSize then - self.scrollOffset = 0 - end - return self.scrollOffset + 1 -end - -function UI.ScrollingGrid:setIndex(index) - if index < self.scrollOffset + 1 then - self.scrollOffset = index - 1 - elseif index - self.scrollOffset > self.pageSize then - self.scrollOffset = index - self.pageSize - end - - if self.scrollOffset < 0 then - self.scrollOffset = 0 - else - local ts = Util.size(self.values) - if self.pageSize + self.scrollOffset + 1 > ts then - self.scrollOffset = math.max(0, ts - self.pageSize) - end - end - UI.Grid.setIndex(self, index) -end - ---[[-- Menu --]]-- -UI.Menu = class(UI.Grid) -UI.Menu.defaults = { - UIElement = 'Menu', - disableHeader = true, - columns = { { heading = 'Prompt', key = 'prompt', width = 20 } }, -} -function UI.Menu:postInit() - self.values = self.menuItems - self.pageSize = #self.menuItems -end - -function UI.Menu:setParent() - UI.Grid.setParent(self) - self.itemWidth = 1 - for _,v in pairs(self.values) do - if #v.prompt > self.itemWidth then - self.itemWidth = #v.prompt - end - end - self.columns[1].width = self.itemWidth - - if self.centered then - self:center() - else - self.width = self.itemWidth + 2 - end -end - -function UI.Menu:center() - self.x = (self.width - self.itemWidth + 2) / 2 - self.width = self.itemWidth + 2 -end - -function UI.Menu:eventHandler(event) - if event.type == 'key' then - if event.key == 'enter' then - local selected = self.menuItems[self.index] - self:emit({ - type = selected.event or 'menu_select', - selected = selected - }) - return true - end - elseif event.type == 'mouse_click' then - if event.y <= #self.menuItems then - UI.Grid.setIndex(self, event.y) - local selected = self.menuItems[self.index] - self:emit({ - type = selected.event or 'menu_select', - selected = selected - }) - return true - end - end - return UI.Grid.eventHandler(self, event) -end - ---[[-- Viewport --]]-- -UI.Viewport = class(UI.Window) -UI.Viewport.defaults = { - UIElement = 'Viewport', - backgroundColor = colors.cyan, - accelerators = { - down = 'scroll_down', - up = 'scroll_up', - home = 'scroll_top', - [ 'end' ] = 'scroll_bottom', - pageUp = 'scroll_pageUp', - [ 'control-b' ] = 'scroll_pageUp', - pageDown = 'scroll_pageDown', - [ 'control-f' ] = 'scroll_pageDown', - }, -} -function UI.Viewport:setScrollPosition(offset) - local oldOffset = self.offy - self.offy = math.max(offset, 0) - local max = self.ymax or self.height - if self.children then - for _, child in ipairs(self.children) do - if child ~= self.scrollBar then -- hack ! - max = math.max(child.y + child.height - 1, max) - end - end - end - self.offy = math.min(self.offy, math.max(max, self.height) - self.height) - if self.offy ~= oldOffset then - self:draw() - end -end - -function UI.Viewport:reset() - self.offy = 0 -end - -function UI.Viewport:getViewArea() - return { - y = (self.offy or 0) + 1, - height = self.height, - totalHeight = self.ymax, - offsetY = self.offy or 0, - } -end - -function UI.Viewport:eventHandler(event) - if event.type == 'scroll_down' then - self:setScrollPosition(self.offy + 1) - elseif event.type == 'scroll_up' then - self:setScrollPosition(self.offy - 1) - elseif event.type == 'scroll_top' then - self:setScrollPosition(0) - elseif event.type == 'scroll_bottom' then - self:setScrollPosition(10000000) - elseif event.type == 'scroll_pageUp' then - self:setScrollPosition(self.offy - self.height) - elseif event.type == 'scroll_pageDown' then - self:setScrollPosition(self.offy + self.height) - else - return false - end - return true -end - ---[[-- TitleBar --]]-- -UI.TitleBar = class(UI.Window) -UI.TitleBar.defaults = { - UIElement = 'TitleBar', - height = 1, - textColor = colors.white, - backgroundColor = colors.cyan, - title = '', - frameChar = '-', - closeInd = '*', -} -function UI.TitleBar:draw() - local sb = SB:new(self.width) - sb:fill(2, self.frameChar, sb.width - 3) - sb:center(string.format(' %s ', self.title)) - if self.previousPage or self.event then - sb:insert(-1, self.closeInd) - else - sb:insert(-2, self.frameChar) - end - self:write(1, 1, sb:get()) -end - -function UI.TitleBar:eventHandler(event) - if event.type == 'mouse_click' then - if (self.previousPage or self.event) and event.x == self.width then - if self.event then - self:emit({ type = self.event, element = self }) - elseif type(self.previousPage) == 'string' or - type(self.previousPage) == 'table' then - UI:setPage(self.previousPage) - else - UI:setPreviousPage() - end - return true - end - end -end - ---[[-- Button --]]-- -UI.Button = class(UI.Window) -UI.Button.defaults = { - UIElement = 'Button', - text = 'button', - backgroundColor = colors.lightGray, - backgroundFocusColor = colors.gray, - textFocusColor = colors.white, - textInactiveColor = colors.gray, - textColor = colors.black, - centered = true, - height = 1, - focusIndicator = ' ', - event = 'button_press', - accelerators = { - space = 'button_activate', - enter = 'button_activate', - mouse_click = 'button_activate', - } -} -function UI.Button:setParent() - if not self.width and not self.ex then - self.width = #self.text + 2 - end - UI.Window.setParent(self) -end - -function UI.Button:draw() - local fg = self.textColor - local bg = self.backgroundColor - local ind = ' ' - if self.focused then - bg = self.backgroundFocusColor - fg = self.textFocusColor - ind = self.focusIndicator - elseif self.inactive then - fg = self.textInactiveColor - end - local text = ind .. self.text .. ' ' - if self.centered then - self:clear(bg) - self:centeredWrite(1 + math.floor(self.height / 2), text, bg, fg) - else - self:write(1, 1, Util.widthify(text, self.width), bg, fg) - end -end - -function UI.Button:focus() - if self.focused then - self:scrollIntoView() - end - self:draw() -end - -function UI.Button:eventHandler(event) - if event.type == 'button_activate' then - self:emit({ type = self.event, button = self }) - return true - end - return false -end - ---[[-- MenuItem --]]-- -UI.MenuItem = class(UI.Button) -UI.MenuItem.defaults = { - UIElement = 'MenuItem', - textColor = colors.black, - backgroundColor = colors.lightGray, - textFocusColor = colors.white, - backgroundFocusColor = colors.lightGray, -} - ---[[-- MenuBar --]]-- -UI.MenuBar = class(UI.Window) -UI.MenuBar.defaults = { - UIElement = 'MenuBar', - buttons = { }, - height = 1, - backgroundColor = colors.lightGray, - textColor = colors.black, - spacing = 2, - lastx = 1, - showBackButton = false, - buttonClass = 'MenuItem', -} -UI.MenuBar.spacer = { spacer = true, text = 'spacer', inactive = true } - -function UI.MenuBar:postInit() - self:addButtons(self.buttons) -end - -function UI.MenuBar:addButtons(buttons) - if not self.children then - self.children = { } - end - - for _,button in pairs(buttons) do - if button.UIElement then - table.insert(self.children, button) - else - local buttonProperties = { - x = self.lastx, - width = #button.text + self.spacing, - centered = false, - } - self.lastx = self.lastx + buttonProperties.width - UI:mergeProperties(buttonProperties, button) - - button = UI[self.buttonClass](buttonProperties) - if button.name then - self[button.name] = button - else - table.insert(self.children, button) - end - - if button.dropdown then - button.dropmenu = UI.DropMenu { buttons = button.dropdown } - end - end - end - if self.parent then - self:initChildren() - end -end - -function UI.MenuBar:getActive(menuItem) - return not menuItem.inactive -end - -function UI.MenuBar:eventHandler(event) - if event.type == 'button_press' and event.button.dropmenu then - if event.button.dropmenu.enabled then - event.button.dropmenu:hide() - self:refocus() - return true - else - local x, y = getPosition(event.button) - if x + event.button.dropmenu.width > self.width then - x = self.width - event.button.dropmenu.width + 1 - end - for _,c in pairs(event.button.dropmenu.children) do - if not c.spacer then - c.inactive = not self:getActive(c) - end - end - event.button.dropmenu:show(x, y + 1) - end - return true - end -end - ---[[-- DropMenuItem --]]-- -UI.DropMenuItem = class(UI.Button) -UI.DropMenuItem.defaults = { - UIElement = 'DropMenuItem', - textColor = colors.black, - backgroundColor = colors.white, - textFocusColor = colors.white, - textInactiveColor = colors.lightGray, - backgroundFocusColor = colors.lightGray, -} -function UI.DropMenuItem:eventHandler(event) - if event.type == 'button_activate' then - self.parent:hide() - end - return UI.Button.eventHandler(self, event) -end - ---[[-- DropMenu --]]-- -UI.DropMenu = class(UI.MenuBar) -UI.DropMenu.defaults = { - UIElement = 'DropMenu', - backgroundColor = colors.white, - buttonClass = 'DropMenuItem', -} -function UI.DropMenu:setParent() - UI.MenuBar.setParent(self) - - local maxWidth = 1 - for y,child in ipairs(self.children) do - child.x = 1 - child.y = y - if #(child.text or '') > maxWidth then - maxWidth = #child.text - end - end - for _,child in ipairs(self.children) do - child.width = maxWidth + 2 - if child.spacer then - child.text = string.rep('-', child.width - 2) - end - end - - self.height = #self.children + 1 - self.width = maxWidth + 2 - self.ow = self.width - - self.canvas = self:addLayer() -end - -function UI.DropMenu:enable() -end - -function UI.DropMenu:show(x, y) - self.x, self.y = x, y - self.canvas:move(x, y) - self.canvas:setVisible(true) - - UI.Window.enable(self) - - self:draw() - self:capture(self) - self:focusFirst() -end - -function UI.DropMenu:hide() - self:disable() - self.canvas:setVisible(false) - self:release(self) -end - -function UI.DropMenu:eventHandler(event) - if event.type == 'focus_lost' and self.enabled then - if not Util.contains(self.children, event.focused) then - self:hide() - end - elseif event.type == 'mouse_out' and self.enabled then - self:hide() - self:refocus() - else - return UI.MenuBar.eventHandler(self, event) - end - return true -end - ---[[-- TabBarMenuItem --]]-- -UI.TabBarMenuItem = class(UI.Button) -UI.TabBarMenuItem.defaults = { - UIElement = 'TabBarMenuItem', - event = 'tab_select', - textColor = colors.black, - selectedBackgroundColor = colors.cyan, - unselectedBackgroundColor = colors.lightGray, - backgroundColor = colors.lightGray, -} -function UI.TabBarMenuItem:draw() - if self.selected then - self.backgroundColor = self.selectedBackgroundColor - self.backgroundFocusColor = self.selectedBackgroundColor - else - self.backgroundColor = self.unselectedBackgroundColor - self.backgroundFocusColor = self.unselectedBackgroundColor - end - UI.Button.draw(self) -end - ---[[-- TabBar --]]-- -UI.TabBar = class(UI.MenuBar) -UI.TabBar.defaults = { - UIElement = 'TabBar', - buttonClass = 'TabBarMenuItem', - selectedBackgroundColor = colors.cyan, -} -function UI.TabBar:enable() - UI.MenuBar.enable(self) - if not Util.find(self.children, 'selected', true) then - local menuItem = self:getFocusables()[1] - if menuItem then - menuItem.selected = true - end - end -end - -function UI.TabBar:eventHandler(event) - if event.type == 'tab_select' then - local selected, si = Util.find(self.children, 'uid', event.button.uid) - local previous, pi = Util.find(self.children, 'selected', true) - - if si ~= pi then - selected.selected = true - if previous then - previous.selected = false - self:emit({ type = 'tab_change', current = si, last = pi, tab = selected }) - end - end - UI.MenuBar.draw(self) - end - return UI.MenuBar.eventHandler(self, event) -end - -function UI.TabBar:selectTab(text) - local menuItem = Util.find(self.children, 'text', text) - if menuItem then - menuItem.selected = true - end -end - ---[[-- Tabs --]]-- -UI.Tabs = class(UI.Window) -UI.Tabs.defaults = { - UIElement = 'Tabs', -} -function UI.Tabs:postInit() - self:add(self) -end - -function UI.Tabs:add(children) - local buttons = { } - for _,child in pairs(children) do - if type(child) == 'table' and child.UIElement and child.tabTitle then - child.y = 2 - table.insert(buttons, { - text = child.tabTitle, - event = 'tab_select', - tabUid = child.uid, - }) - end - end - - if not self.tabBar then - self.tabBar = UI.TabBar({ - buttons = buttons, }) - else - self.tabBar:addButtons(buttons) - end - - if self.parent then - return UI.Window.add(self, children) - end -end - -function UI.Tabs:selectTab(tab) - local menuItem = Util.find(self.tabBar.children, 'tabUid', tab.uid) - if menuItem then - self.tabBar:emit({ type = 'tab_select', button = { uid = menuItem.uid } }) - end -end - -function UI.Tabs:setActive(tab, active) - local menuItem = Util.find(self.tabBar.children, 'tabUid', tab.uid) - if menuItem then - menuItem.inactive = not active - end -end - -function UI.Tabs:enable() - self.enabled = true - self.transitionHint = nil - self.tabBar:enable() - - local menuItem = Util.find(self.tabBar.children, 'selected', true) - - for _,child in pairs(self.children) do - if child.uid == menuItem.tabUid then - child:enable() - self:emit({ type = 'tab_activate', activated = child }) - elseif child.tabTitle then - child:disable() + UI[name]._preload = function(self) + return load(name) end end end -function UI.Tabs:eventHandler(event) - if event.type == 'tab_change' then - local tab = self:find(event.tab.tabUid) - if event.current > event.last then - self.transitionHint = 'slideLeft' - else - self.transitionHint = 'slideRight' - end - - for _,child in pairs(self.children) do - if child.uid == event.tab.tabUid then - child:enable() - elseif child.tabTitle then - child:disable() - end - end - self:emit({ type = 'tab_activate', activated = tab }) - tab:draw() - end -end - ---[[-- Tab --]]-- -UI.Tab = class(UI.Window) -UI.Tab.defaults = { - UIElement = 'Tab', - tabTitle = 'tab', - backgroundColor = colors.cyan, - y = 2, -} -function UI.Tab:setParent() - UI.Window.setParent(self) - self.canvas = self:addLayer() -end - -function UI.Tab:enable(...) - self.canvas:setVisible(true) - UI.Window.enable(self, ...) - if self.parent.transitionHint then - self:addTransition(self.parent.transitionHint) - end - self:focusFirst() -end - -function UI.Tab:disable() - self.canvas:setVisible(false) - UI.Window.disable(self) -end - ---[[-- Wizard --]]-- -UI.Wizard = class(UI.Window) -UI.Wizard.defaults = { - UIElement = 'Wizard', - pages = { }, -} -function UI.Wizard:postInit() - self.cancelButton = UI.Button { - x = 2, y = -1, - text = 'Cancel', - event = 'cancel', - } - self.previousButton = UI.Button { - x = -18, y = -1, - text = '< Back', - event = 'previousView', - } - self.nextButton = UI.Button { - x = -9, y = -1, - text = 'Next >', - event = 'nextView', - } - - Util.merge(self, self.pages) - for _, child in pairs(self.pages) do - child.ey = -2 - end -end - -function UI.Wizard:add(pages) - Util.merge(self.pages, pages) - Util.merge(self, pages) - - for _, child in pairs(self.pages) do - child.ey = child.ey or -2 - end - - if self.parent then - self:initChildren() - end -end - -function UI.Wizard:getPage(index) - return Util.find(self.pages, 'index', index) -end - -function UI.Wizard:enable(...) - self.enabled = true - self.index = 1 - local initial = self:getPage(1) - for _,child in pairs(self.children) do - if child == initial or not child.index then - child:enable(...) - else - child:disable() - end - end - self:emit({ type = 'enable_view', next = initial }) -end - -function UI.Wizard:isViewValid() - local currentView = self:getPage(self.index) - return not currentView.validate and true or currentView:validate() -end - -function UI.Wizard:eventHandler(event) - if event.type == 'nextView' then - local currentView = self:getPage(self.index) - if self:isViewValid() then - self.index = self.index + 1 - local nextView = self:getPage(self.index) - currentView:emit({ type = 'enable_view', next = nextView, current = currentView }) - end - - elseif event.type == 'previousView' then - local currentView = self:getPage(self.index) - local nextView = self:getPage(self.index - 1) - if nextView then - self.index = self.index - 1 - currentView:emit({ type = 'enable_view', prev = nextView, current = currentView }) - end - return true - - elseif event.type == 'wizard_complete' then - if self:isViewValid() then - self:emit({ type = 'accept' }) - end - - elseif event.type == 'enable_view' then - if event.current then - if event.next then - self:addTransition('slideLeft') - elseif event.prev then - self:addTransition('slideRight') - end - event.current:disable() - end - - local current = event.next or event.prev - if not current then error('property "index" is required on wizard pages') end - - if self:getPage(self.index - 1) then - self.previousButton:enable() - else - self.previousButton:disable() - end - - if self:getPage(self.index + 1) then - self.nextButton.text = 'Next >' - self.nextButton.event = 'nextView' - else - self.nextButton.text = 'Accept' - self.nextButton.event = 'wizard_complete' - end - -- a new current view - current:enable() - current:emit({ type = 'view_enabled', view = current }) - self:draw() - end -end - ---[[-- SlideOut --]]-- -UI.SlideOut = class(UI.Window) -UI.SlideOut.defaults = { - UIElement = 'SlideOut', - pageType = 'modal', -} -function UI.SlideOut:setParent() - UI.Window.setParent(self) - self.canvas = self:addLayer() -end - -function UI.SlideOut:enable() -end - -function UI.SlideOut:show(...) - self:addTransition('expandUp') - self.canvas:setVisible(true) - UI.Window.enable(self, ...) - self:draw() - self:capture(self) - self:focusFirst() -end - -function UI.SlideOut:disable() - self.canvas:setVisible(false) - UI.Window.disable(self) -end - -function UI.SlideOut:hide() - self:disable() - self:release(self) - self:refocus() -end - -function UI.SlideOut:eventHandler(event) - if event.type == 'slide_show' then - self:show() - return true - - elseif event.type == 'slide_hide' then - self:hide() - return true - end -end - ---[[-- Embedded --]]-- -UI.Embedded = class(UI.Window) -UI.Embedded.defaults = { - UIElement = 'Embedded', - backgroundColor = colors.black, - textColor = colors.white, - accelerators = { - up = 'scroll_up', - down = 'scroll_down', - } -} - -function UI.Embedded:setParent() - UI.Window.setParent(self) - self.win = window.create(UI.term.device, 1, 1, self.width, self.height, false) - Canvas.scrollingWindow(self.win, self.x, self.y) - self.win.setParent(UI.term.device) - self.win.setMaxScroll(100) - - local canvas = self:getCanvas() - self.win.canvas.parent = canvas - table.insert(canvas.layers, self.win.canvas) - self.canvas = self.win.canvas - - self.win.setCursorPos(1, 1) - self.win.setBackgroundColor(self.backgroundColor) - self.win.setTextColor(self.textColor) - self.win.clear() -end - -function UI.Embedded:draw() - self.canvas:dirty() -end - -function UI.Embedded:enable() - self.canvas:setVisible(true) - UI.Window.enable(self) -end - -function UI.Embedded:disable() - self.canvas:setVisible(false) - UI.Window.disable(self) -end - -function UI.Embedded:eventHandler(event) - if event.type == 'scroll_up' then - self.win.scrollUp() - return true - elseif event.type == 'scroll_down' then - self.win.scrollDown() - return true - end -end - -function UI.Embedded:focus() - -- allow scrolling -end - ---[[-- Notification --]]-- -UI.Notification = class(UI.Window) -UI.Notification.defaults = { - UIElement = 'Notification', - backgroundColor = colors.gray, - height = 3, -} -function UI.Notification:draw() -end - -function UI.Notification:enable() -end - -function UI.Notification:error(value, timeout) - self.backgroundColor = colors.red - Sound.play('entity.villager.no', .5) - self:display(value, timeout) -end - -function UI.Notification:info(value, timeout) - self.backgroundColor = colors.gray - self:display(value, timeout) -end - -function UI.Notification:success(value, timeout) - self.backgroundColor = colors.green - self:display(value, timeout) -end - -function UI.Notification:cancel() - if self.canvas then - Event.cancelNamedTimer('notificationTimer') - self.enabled = false - self.canvas:removeLayer() - self.canvas = nil - end -end - -function UI.Notification:display(value, timeout) - self.enabled = true - local lines = Util.wordWrap(value, self.width - 2) - self.height = #lines + 1 - self.y = self.parent.height - self.height + 1 - if self.canvas then - self.canvas:removeLayer() - end - - self.canvas = self:addLayer(self.backgroundColor, self.textColor) - self:addTransition('expandUp', { ticks = self.height }) - self.canvas:setVisible(true) - self:clear() - for k,v in pairs(lines) do - self:write(2, k, v) - end - - Event.addNamedTimer('notificationTimer', timeout or 3, false, function() - self:cancel() - self:sync() - end) -end - ---[[-- Throttle --]]-- -UI.Throttle = class(UI.Window) -UI.Throttle.defaults = { - UIElement = 'Throttle', - backgroundColor = colors.gray, - bordercolor = colors.cyan, - height = 4, - width = 10, - timeout = .075, - ctr = 0, - image = { - ' //) (O )~@ &~&-( ?Q ', - ' //) (O )- @ \\-( ?) && ', - ' //) (O ), @ \\-(?) && ', - ' //) (O ). @ \\-d ) (@ ' - } -} -function UI.Throttle:setParent() - self.x = math.ceil((self.parent.width - self.width) / 2) - self.y = math.ceil((self.parent.height - self.height) / 2) - UI.Window.setParent(self) -end - -function UI.Throttle:enable() - self.c = os.clock() - self.enabled = false -end - -function UI.Throttle:disable() - if self.canvas then - self.enabled = false - self.canvas:removeLayer() - self.canvas = nil - self.ctr = 0 - end -end - -function UI.Throttle:update() - local cc = os.clock() - if cc > self.c + self.timeout then - os.sleep(0) - self.c = os.clock() - self.enabled = true - if not self.canvas then - self.canvas = self:addLayer(self.backgroundColor, self.borderColor) - self.canvas:setVisible(true) - self:clear(self.borderColor) - end - local image = self.image[self.ctr + 1] - local width = self.width - 2 - for i = 0, #self.image do - self:write(2, i + 1, image:sub(width * i + 1, width * i + width), - self.backgroundColor, self.textColor) - end - - self.ctr = (self.ctr + 1) % #self.image - - self:sync() - end -end - ---[[-- StatusBar --]]-- -UI.StatusBar = class(UI.Window) -UI.StatusBar.defaults = { - UIElement = 'StatusBar', - backgroundColor = colors.lightGray, - textColor = colors.gray, - height = 1, - ey = -1, -} -function UI.StatusBar:adjustWidth() - -- Can only have 1 adjustable width - if self.columns then - local w = self.width - #self.columns - 1 - for _,c in pairs(self.columns) do - if c.width then - c.cw = c.width -- computed width - w = w - c.width - end - end - for _,c in pairs(self.columns) do - if not c.width then - c.cw = w - end - end - end -end - -function UI.StatusBar:resize() - UI.Window.resize(self) - self:adjustWidth() -end - -function UI.StatusBar:setParent() - UI.Window.setParent(self) - self:adjustWidth() -end - -function UI.StatusBar:setStatus(status) - if self.values ~= status then - self.values = status - self:draw() - end -end - -function UI.StatusBar:setValue(name, value) - if not self.values then - self.values = { } - end - self.values[name] = value -end - -function UI.StatusBar:getValue(name) - if self.values then - return self.values[name] - end -end - -function UI.StatusBar:timedStatus(status, timeout) - timeout = timeout or 3 - self:write(2, 1, Util.widthify(status, self.width-2), self.backgroundColor) - Event.addNamedTimer('statusTimer', timeout, false, function() - if self.parent.enabled then - self:draw() - self:sync() - end - end) -end - -function UI.StatusBar:getColumnWidth(name) - local c = Util.find(self.columns, 'key', name) - return c and c.cw -end - -function UI.StatusBar:setColumnWidth(name, width) - local c = Util.find(self.columns, 'key', name) - if c then - c.cw = width - end -end - -function UI.StatusBar:draw() - if not self.values then - self:clear() - elseif type(self.values) == 'string' then - self:write(1, 1, Util.widthify(' ' .. self.values, self.width)) - else - local s = '' - for _,c in ipairs(self.columns) do - s = s .. ' ' .. Util.widthify(tostring(self.values[c.key] or ''), c.cw) - end - self:write(1, 1, Util.widthify(s, self.width)) - end -end - ---[[-- ProgressBar --]]-- -UI.ProgressBar = class(UI.Window) -UI.ProgressBar.defaults = { - UIElement = 'ProgressBar', - progressColor = colors.lime, - backgroundColor = colors.gray, - height = 1, - value = 0, -} -function UI.ProgressBar:draw() - self:clear() - local width = math.ceil(self.value / 100 * self.width) - self:clearArea(1, 1, width, self.height, self.progressColor) -end - ---[[-- VerticalMeter --]]-- -UI.VerticalMeter = class(UI.Window) -UI.VerticalMeter.defaults = { - UIElement = 'VerticalMeter', - backgroundColor = colors.gray, - meterColor = colors.lime, - width = 1, - value = 0, -} -function UI.VerticalMeter:draw() - local height = self.height - math.ceil(self.value / 100 * self.height) - self:clear() - self:clearArea(1, height + 1, self.width, self.height, self.meterColor) -end - ---[[-- TextEntry --]]-- -UI.TextEntry = class(UI.Window) -UI.TextEntry.defaults = { - UIElement = 'TextEntry', - value = '', - shadowText = '', - focused = false, - textColor = colors.white, - shadowTextColor = colors.gray, - backgroundColor = colors.black, -- colors.lightGray, - backgroundFocusColor = colors.black, --lightGray, - height = 1, - limit = 6, - pos = 0, - accelerators = { - [ 'control-c' ] = 'copy', - } -} -function UI.TextEntry:postInit() - self.value = tostring(self.value) -end - -function UI.TextEntry:setValue(value) - self.value = value -end - -function UI.TextEntry:setPosition(pos) - self.pos = pos -end - -function UI.TextEntry:updateScroll() - if not self.scroll then - self.scroll = 0 - end - - if not self.pos then - self.pos = #tostring(self.value) - self.scroll = 0 - elseif self.pos > #tostring(self.value) then - self.pos = #tostring(self.value) - self.scroll = 0 - end - - if self.pos - self.scroll > self.width - 2 then - self.scroll = self.pos - (self.width - 2) - elseif self.pos < self.scroll then - self.scroll = self.pos - end -end - -function UI.TextEntry:draw() - local bg = self.backgroundColor - local tc = self.textColor - if self.focused then - bg = self.backgroundFocusColor - end - - self:updateScroll() - local text = tostring(self.value) - if #text > 0 then - if self.scroll and self.scroll > 0 then - text = text:sub(1 + self.scroll) - end - if self.mask then - text = _rep('*', #text) - end - else - tc = self.shadowTextColor - text = self.shadowText - end - - self:write(1, 1, ' ' .. Util.widthify(text, self.width - 2) .. ' ', bg, tc) - if self.focused then - self:setCursorPos(self.pos-self.scroll+2, 1) - end -end - -function UI.TextEntry:reset() - self.pos = 0 - self.value = '' - self:draw() - self:updateCursor() -end - -function UI.TextEntry:updateCursor() - self:updateScroll() - self:setCursorPos(self.pos-self.scroll+2, 1) -end - -function UI.TextEntry:focus() - self:draw() - if self.focused then - self:setCursorBlink(true) - else - self:setCursorBlink(false) - end -end - ---[[ - A few lines below from theoriginalbit - http://www.computercraft.info/forums2/index.php?/topic/16070-read-and-limit-length-of-the-input-field/ ---]] -function UI.TextEntry:eventHandler(event) - if event.type == 'key' then - local ch = event.key - if ch == 'left' then - if self.pos > 0 then - self.pos = math.max(self.pos-1, 0) - self:draw() - end - elseif ch == 'right' then - local input = tostring(self.value) - if self.pos < #input then - self.pos = math.min(self.pos+1, #input) - self:draw() - end - elseif ch == 'home' then - self.pos = 0 - self:draw() - elseif ch == 'end' then - self.pos = #tostring(self.value) - self:draw() - elseif ch == 'backspace' then - if self.pos > 0 then - local input = tostring(self.value) - self.value = input:sub(1, self.pos-1) .. input:sub(self.pos+1) - self.pos = self.pos - 1 - self:draw() - self:emit({ type = 'text_change', text = self.value, element = self }) - end - elseif ch == 'delete' then - local input = tostring(self.value) - if self.pos < #input then - self.value = input:sub(1, self.pos) .. input:sub(self.pos+2) - self:draw() - self:emit({ type = 'text_change', text = self.value, element = self }) - end - elseif #ch == 1 then - local input = tostring(self.value) - if #input < self.limit then - self.value = input:sub(1, self.pos) .. ch .. input:sub(self.pos+1) - self.pos = self.pos + 1 - self:draw() - self:emit({ type = 'text_change', text = self.value, element = self }) - end - else - return false - end - return true - - elseif event.type == 'copy' then - os.queueEvent('clipboard_copy', self.value) - - elseif event.type == 'paste' then - local input = tostring(self.value) - local text = event.text - if #input + #text > self.limit then - text = text:sub(1, self.limit-#input) - end - self.value = input:sub(1, self.pos) .. text .. input:sub(self.pos+1) - self.pos = self.pos + #text - self:draw() - self:updateCursor() - self:emit({ type = 'text_change', text = self.value, element = self }) - return true - - elseif event.type == 'mouse_click' then - if self.focused and event.x > 1 then - self.pos = event.x + self.scroll - 2 - self:updateCursor() - return true - end - elseif event.type == 'mouse_rightclick' then - local input = tostring(self.value) - if #input > 0 then - self:reset() - self:emit({ type = 'text_change', text = self.value, element = self }) - end - end - - return false -end - ---[[-- Chooser --]]-- -UI.Chooser = class(UI.Window) -UI.Chooser.defaults = { - UIElement = 'Chooser', - choices = { }, - nochoice = 'Select', - backgroundFocusColor = colors.lightGray, - textInactiveColor = colors.gray, - leftIndicator = '<', - rightIndicator = '>', - height = 1, -} -function UI.Chooser:setParent() - if not self.width and not self.ex then - self.width = 1 - for _,v in pairs(self.choices) do - if #v.name > self.width then - self.width = #v.name - end - end - self.width = self.width + 4 - end - UI.Window.setParent(self) -end - -function UI.Chooser:draw() - local bg = self.backgroundColor - if self.focused then - bg = self.backgroundFocusColor - end - local fg = self.inactive and self.textInactiveColor or self.textColor - local choice = Util.find(self.choices, 'value', self.value) - local value = self.nochoice - if choice then - value = choice.name - end - self:write(1, 1, self.leftIndicator, self.backgroundColor, colors.black) - self:write(2, 1, ' ' .. Util.widthify(tostring(value), self.width-4) .. ' ', bg, fg) - self:write(self.width, 1, self.rightIndicator, self.backgroundColor, colors.black) -end - -function UI.Chooser:focus() - self:draw() -end - -function UI.Chooser:eventHandler(event) - if event.type == 'key' then - if event.key == 'right' or event.key == 'space' then - local _,k = Util.find(self.choices, 'value', self.value) - local choice - if not k then k = 1 end - if k and k < #self.choices then - choice = self.choices[k+1] - else - choice = self.choices[1] - end - self.value = choice.value - self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice }) - self:draw() - return true - elseif event.key == 'left' then - local _,k = Util.find(self.choices, 'value', self.value) - local choice - if k and k > 1 then - choice = self.choices[k-1] - else - choice = self.choices[#self.choices] - end - self.value = choice.value - self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice }) - self:draw() - return true - end - elseif event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then - if event.x == 1 then - self:emit({ type = 'key', key = 'left' }) - return true - elseif event.x == self.width then - self:emit({ type = 'key', key = 'right' }) - return true - end - end -end - ---[[-- Checkbox --]]-- -UI.Checkbox = class(UI.Window) -UI.Checkbox.defaults = { - UIElement = 'Checkbox', - nochoice = 'Select', - checkedIndicator = 'X', - leftMarker = '[', - rightMarker = ']', - value = false, - textColor = colors.white, - backgroundColor = colors.black, - backgroundFocusColor = colors.lightGray, - height = 1, - accelerators = { - space = 'checkbox_toggle', - mouse_click = 'checkbox_toggle', - } -} -function UI.Checkbox:setParent() - if not self.width and not self.ex then - self.width = (self.label and #self.label or 0) + 3 - else - self.widthh = 3 - end - UI.Window.setParent(self) -end - -function UI.Checkbox:draw() - local bg = self.backgroundColor - if self.focused then - bg = self.backgroundFocusColor - end - if type(self.value) == 'string' then - self.value = nil -- TODO: fix form - end - local text = string.format('[%s]', not self.value and ' ' or self.checkedIndicator) - local x = 1 - if self.label then - self:write(1, 1, self.label) - x = #self.label + 2 - end - self:write(x, 1, text, bg) - self:write(x, 1, self.leftMarker, self.backgroundColor, self.textColor) - self:write(x + 1, 1, not self.value and ' ' or self.checkedIndicator, bg) - self:write(x + 2, 1, self.rightMarker, self.backgroundColor, self.textColor) -end - -function UI.Checkbox:focus() - self:draw() -end - -function UI.Checkbox:reset() - self.value = false -end - -function UI.Checkbox:eventHandler(event) - if event.type == 'checkbox_toggle' then - self.value = not self.value - self:emit({ type = 'checkbox_change', checked = self.value, element = self }) - self:draw() - return true - end -end - ---[[-- Text --]]-- -UI.Text = class(UI.Window) -UI.Text.defaults = { - UIElement = 'Text', - value = '', - height = 1, -} -function UI.Text:setParent() - if not self.width and not self.ex then - self.width = #tostring(self.value) - end - UI.Window.setParent(self) -end - -function UI.Text:draw() - self:write(1, 1, Util.widthify(self.value or '', self.width), self.backgroundColor) -end - ---[[-- ScrollBar --]]-- -UI.ScrollBar = class(UI.Window) -UI.ScrollBar.defaults = { - UIElement = 'ScrollBar', - lineChar = '|', - sliderChar = '#', - upArrowChar = '^', - downArrowChar = 'v', - scrollbarColor = colors.lightGray, - value = '', - width = 1, - x = -1, - ey = -1, -} -function UI.ScrollBar:draw() - local parent = self.parent - local view = parent:getViewArea() - - if view.totalHeight > view.height then - local maxScroll = view.totalHeight - view.height - local percent = view.offsetY / maxScroll - local sliderSize = math.max(1, Util.round(view.height / view.totalHeight * (view.height - 2))) - local x = self.width - - local row = view.y - if not view.static then -- does the container scroll ? - self.y = row -- if so, move the scrollbar onscreen - row = 1 - end - - for i = 1, view.height - 2 do - self:write(x, row + i, self.lineChar, nil, self.scrollbarColor) - end - - local y = Util.round((view.height - 2 - sliderSize) * percent) - for i = 1, sliderSize do - self:write(x, row + y + i, self.sliderChar, nil, self.scrollbarColor) - end - - local color = self.scrollbarColor - if view.offsetY > 0 then - color = colors.white - end - self:write(x, row, self.upArrowChar, nil, color) - - color = self.scrollbarColor - if view.offsetY + view.height < view.totalHeight then - color = colors.white - end - self:write(x, row + view.height - 1, self.downArrowChar, nil, color) - end -end - -function UI.ScrollBar:eventHandler(event) - if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then - if event.x == 1 then - local view = self.parent:getViewArea() - if view.totalHeight > view.height then - if event.y == view.y then - self:emit({ type = 'scroll_up'}) - elseif event.y == self.height then - self:emit({ type = 'scroll_down'}) - -- else - -- ... percentage ... - end - end - return true - end - end -end - ---[[-- TextArea --]]-- -UI.TextArea = class(UI.Viewport) -UI.TextArea.defaults = { - UIElement = 'TextArea', - marginRight = 2, - value = '', -} -function UI.TextArea:postInit() - self.scrollBar = UI.ScrollBar() -end - -function UI.TextArea:setText(text) - self.offy = 0 - self.ymax = nil - self.value = text - self:draw() -end - -function UI.TextArea:focus() - -- allow keyboard scrolling -end - -function UI.TextArea:draw() - self:clear() --- self:setCursorPos(1, 1) - self.cursorX, self.cursorY = 1, 1 - self:print(self.value) - self.ymax = self.cursorY - - for _,child in pairs(self.children) do - if child.enabled then - child:draw() - end - end -end - ---[[-- Form --]]-- -UI.Form = class(UI.Window) -UI.Form.defaults = { - UIElement = 'Form', - values = { }, - margin = 2, - event = 'form_complete', - cancelEvent = 'form_cancel', -} -function UI.Form:postInit() - self:createForm() -end - -function UI.Form:reset() - for _,child in pairs(self.children) do - if child.reset then - child:reset() - end - end -end - -function UI.Form:setValues(values) - self:reset() - self.values = values - for _,child in pairs(self.children) do - if child.formKey then - -- this should be child:setValue(self.values[child.formKey]) - -- so chooser can set default choice if null - -- null should be valid as well - child.value = self.values[child.formKey] or '' - end - end -end - -function UI.Form:createForm() - self.children = self.children or { } - - if not self.labelWidth then - self.labelWidth = 1 - for _, child in pairs(self) do - if type(child) == 'table' and child.UIElement then - if child.formLabel then - self.labelWidth = math.max(self.labelWidth, #child.formLabel + 2) - end - end - end - end - - local y = self.margin - for _, child in pairs(self) do - if type(child) == 'table' and child.UIElement then - if child.formKey then - child.value = self.values[child.formKey] or '' - end - if child.formLabel then - child.x = self.labelWidth + self.margin - 1 - child.y = y - if not child.width and not child.ex then - child.ex = -self.margin - end - - table.insert(self.children, UI.Text { - x = self.margin, - y = y, - textColor = colors.black, - width = #child.formLabel, - value = child.formLabel, - }) - end - if child.formKey or child.formLabel then - y = y + 1 - end - end - end - - if not self.manualControls then - table.insert(self.children, UI.Button { - y = -self.margin, x = -12 - self.margin, - text = 'Ok', - event = 'form_ok', - }) - table.insert(self.children, UI.Button { - y = -self.margin, x = -7 - self.margin, - text = 'Cancel', - event = self.cancelEvent, - }) - end -end - -function UI.Form:validateField(field) - if field.required then - if not field.value or #tostring(field.value) == 0 then - return false, 'Field is required' - end - end - if field.validate == 'numeric' then - if #tostring(field.value) > 0 then - if not tonumber(field.value) then - return false, 'Invalid number' - end - end - end - return true -end - -function UI.Form:save() - for _,child in pairs(self.children) do - if child.formKey then - local s, m = self:validateField(child) - if not s then - self:setFocus(child) - Sound.play('entity.villager.no', .5) - self:emit({ type = 'form_invalid', message = m, field = child }) - return false - end - end - end - for _,child in pairs(self.children) do - if child.formKey then - if (child.pruneEmpty and type(child.value) == 'string' and #child.value == 0) or - (child.pruneEmpty and type(child.value) == 'boolean' and not child.value) then - self.values[child.formKey] = nil - elseif child.validate == 'numeric' then - self.values[child.formKey] = tonumber(child.value) - else - self.values[child.formKey] = child.value - end - end - end - - return true -end - -function UI.Form:eventHandler(event) - if event.type == 'form_ok' then - if not self:save() then - return false - end - self:emit({ type = self.event, UIElement = self, values = self.values }) - else - return UI.Window.eventHandler(self, event) - end - return true -end - ---[[-- Dialog --]]-- -UI.Dialog = class(UI.SlideOut) -UI.Dialog.defaults = { - UIElement = 'Dialog', - height = 7, - textColor = colors.black, - backgroundColor = colors.white, - okEvent ='dialog_ok', - cancelEvent = 'dialog_cancel', -} -function UI.Dialog:postInit() - self.y = -self.height - self.titleBar = UI.TitleBar({ event = self.cancelEvent, title = self.title }) -end - -function UI.Dialog:show(...) - local canvas = self.parent:getCanvas() - self.oldPalette = canvas.palette - canvas:applyPalette(Canvas.darkPalette) - UI.SlideOut.show(self, ...) -end - -function UI.Dialog:hide(...) - self.parent:getCanvas().palette = self.oldPalette - UI.SlideOut.hide(self, ...) - self.parent:draw() -end - -function UI.Dialog:eventHandler(event) - if event.type == 'dialog_cancel' then - self:hide() - end - return UI.SlideOut.eventHandler(self, event) -end - ---[[-- Image --]]-- -UI.Image = class(UI.Window) -UI.Image.defaults = { - UIElement = 'Image', - event = 'button_press', -} -function UI.Image:setParent() - if self.image then - self.height = #self.image - end - if self.image and not self.width then - self.width = #self.image[1] - end - UI.Window.setParent(self) -end - -function UI.Image:draw() - self:clear() - if self.image then - for y = 1, #self.image do - local line = self.image[y] - for x = 1, #line do - local ch = line[x] - if type(ch) == 'number' then - if ch > 0 then - self:write(x, y, ' ', ch) - end - else - self:write(x, y, ch) - end - end - end - end -end - -function UI.Image:setImage(image) - self.image = image -end - ---[[-- NftImage --]]-- -UI.NftImage = class(UI.Window) -UI.NftImage.defaults = { - UIElement = 'NftImage', - event = 'button_press', -} -function UI.NftImage:setParent() - if self.image then - self.height = self.image.height - end - if self.image and not self.width then - self.width = self.image.width - end - UI.Window.setParent(self) -end - -function UI.NftImage:draw() - if self.image then - for y = 1, self.image.height do - for x = 1, #self.image.text[y] do - self:write(x, y, self.image.text[y][x], self.image.bg[y][x], self.image.fg[y][x]) - end - end - else - self:clear() - end -end - -function UI.NftImage:setImage(image) - self.image = image -end +loadComponents() UI:loadTheme('usr/config/ui.theme') if Util.getVersion() >= 1.76 then diff --git a/sys/apis/ui/canvas.lua b/sys/apis/ui/canvas.lua index 5e0ed5b..931e9bd 100644 --- a/sys/apis/ui/canvas.lua +++ b/sys/apis/ui/canvas.lua @@ -2,9 +2,9 @@ local class = require('class') local Region = require('ui.region') local Util = require('util') -local _rep = string.rep -local _sub = string.sub -local _gsub = string.gsub +local _rep = string.rep +local _sub = string.sub +local _gsub = string.gsub local colors = _G.colors local Canvas = class() @@ -19,6 +19,10 @@ for n = 1, 16 do Canvas.darkPalette[2 ^ (n - 1)] = _sub("8777777f77fff77f", n, n) end +--[[ + A canvas can have more lines than canvas.height in order to scroll +]] + function Canvas:init(args) self.x = 1 self.y = 1 @@ -50,7 +54,7 @@ function Canvas:move(x, y) end function Canvas:resize(w, h) - for i = self.height, h do + for i = #self.lines, h do self.lines[i] = { } end @@ -78,7 +82,7 @@ function Canvas:copy() height = self.height, isColor = self.isColor, }) - for i = 1, self.height do + for i = 1, #self.lines do b.lines[i].text = self.lines[i].text b.lines[i].fg = self.lines[i].fg b.lines[i].bg = self.lines[i].bg @@ -117,6 +121,19 @@ function Canvas:setVisible(visible) end end +-- Push a layer to the top +function Canvas:raise() + if self.parent then + local layers = self.parent.layers or { } + for k, v in pairs(layers) do + if v == self then + table.insert(layers, table.remove(layers, k)) + break + end + end + end +end + function Canvas:write(x, y, text, bg, fg) if bg then bg = _rep(self.palette[bg], #text) @@ -124,10 +141,10 @@ function Canvas:write(x, y, text, bg, fg) if fg then fg = _rep(self.palette[fg], #text) end - self:writeBlit(x, y, text, bg, fg) + self:blit(x, y, text, bg, fg) end -function Canvas:writeBlit(x, y, text, bg, fg) +function Canvas:blit(x, y, text, bg, fg) if y > 0 and y <= #self.lines and x <= self.width then local width = #text @@ -157,7 +174,7 @@ function Canvas:writeBlit(x, y, text, bg, fg) if width > 0 then - local function replace(sstr, pos, rstr, width) + local function replace(sstr, pos, rstr) if pos == 1 and width == self.width then return rstr elseif pos == 1 then @@ -188,39 +205,48 @@ function Canvas:writeLine(y, text, fg, bg) self.lines[y].bg = bg end -function Canvas:reset() - self.regions = nil +function Canvas:clearLine(y, bg, fg) + fg = _rep(self.palette[fg or colors.white], self.width) + bg = _rep(self.palette[bg or colors.black], self.width) + self:writeLine(y, _rep(' ', self.width), fg, bg) end function Canvas:clear(bg, fg) local text = _rep(' ', self.width) fg = _rep(self.palette[fg or colors.white], self.width) bg = _rep(self.palette[bg or colors.black], self.width) - for i = 1, self.height do + for i = 1, #self.lines do self:writeLine(i, text, fg, bg) end end function Canvas:punch(rect) - if not self.regions then - self.regions = Region.new(self.x, self.y, self.ex, self.ey) - end - self.regions:subRect(rect.x, rect.y, rect.ex, rect.ey) + local offset = { x = 0, y = 0 } + + + self.regions:subRect(rect.x + offset.x, rect.y + offset.y, rect.ex + offset.x, rect.ey + offset.y) end -function Canvas:blitClipped(device) +function Canvas:blitClipped(device, offset) + offset = { x = self.x, y = self.y } + local parent = self.parent + while parent do + offset.x = offset.x + parent.x - 1 + offset.y = offset.y + parent.y - 1 + parent = parent.parent + end for _,region in ipairs(self.regions.region) do - self:blit(device, - { x = region[1] - self.x + 1, - y = region[2] - self.y + 1, - ex = region[3]- self.x + 1, - ey = region[4] - self.y + 1 }, - { x = region[1], y = region[2] }) + self:blitRect(device, + { x = region[1], + y = region[2], + ex = region[3], + ey = region[4] }, + { x = region[1] + offset.x - 1, y = region[2] + offset.y - 1 }) end end function Canvas:redraw(device) - self:reset() +--[[ if #self.layers > 0 then for _,layer in pairs(self.layers) do self:punch(layer) @@ -230,19 +256,20 @@ function Canvas:redraw(device) self:blit(device) end self:clean() +]] end function Canvas:isDirty() - for _, line in pairs(self.lines) do - if line.dirty then + for i = 1, #self.lines do + if self.lines[i].dirty then return true end end end function Canvas:dirty() - for _, line in pairs(self.lines) do - line.dirty = true + for i = 1, #self.lines do + self.lines[i].dirty = true end if self.layers then for _, canvas in pairs(self.layers) do @@ -252,37 +279,81 @@ function Canvas:dirty() end function Canvas:clean() - for _, line in pairs(self.lines) do - line.dirty = false + for i = 1, #self.lines do + self.lines[i].dirty = nil end end -function Canvas:render(device, layers) --- redrawAll ? - layers = layers or self.layers - if #layers > 0 then - self.regions = Region.new(self.x, self.y, self.ex, self.ey) - local l = Util.shallowCopy(layers) - for _, canvas in ipairs(layers) do - table.remove(l, 1) +function Canvas:renderLayers(device, offset) + if not offset then + offset = { x = self.x, y = self.y } + end + if #self.layers > 0 then + self.regions = Region.new(1, 1, self.ex, self.ey) + + for i = 1, #self.layers do + local canvas = self.layers[i] if canvas.visible then + + -- punch out this area from the parent's canvas self:punch(canvas) - canvas:render(device, l) + + -- get the area to render for this layer + canvas.regions = Region.new(canvas.x, canvas.y, canvas.ex, canvas.ey) + + -- determine if we should render this layer by punching + -- out any layers that overlap this one + for j = i + 1, #self.layers do + if self.layers[j].visible then + canvas:punch(self.layers[j]) + end + end end end - self:blitClipped(device) - self:reset() + + for _, canvas in ipairs(self.layers) do + if canvas.visible and #canvas.regions.region > 0 then + canvas:renderLayers(device, { + x = canvas.x, --offset.x + self.x - 1, + y = canvas.y, + }) + end + end + + self:blitClipped(device, offset) + + --elseif #self.regions.region > 0 then + -- self:blitClipped(device, offset) + else - self:blit(device) + offset = { x = self.x, y = self.y } + local parent = self.parent + while parent do + offset.x = offset.x + parent.x - 1 + offset.y = offset.y + parent.y - 1 + parent = parent.parent + end + self:blitRect(device, nil, offset) end self:clean() end -function Canvas:blit(device, src, tgt) +function Canvas:render(device) + --_G._p = self + if #self.layers > 0 then + self:renderLayers(device) + else + self:blitRect(device) + self:clean() + end +end + +function Canvas:blitRect(device, src, tgt) src = src or { x = 1, y = 1, ex = self.ex - self.x + 1, ey = self.ey - self.y + 1 } tgt = tgt or self for i = 0, src.ey - src.y do - local line = self.lines[src.y + i] + local line = self.lines[src.y + i + (self.offy or 0)] if line and line.dirty then local t, fg, bg = line.text, line.fg, line.bg if src.x > 1 or src.ex < self.ex then @@ -290,13 +361,11 @@ function Canvas:blit(device, src, tgt) fg = _sub(fg, src.x, src.ex) bg = _sub(bg, src.x, src.ex) end - --if tgt.y + i > self.ey then -- wrong place to do clipping ?? - -- break - --end device.setCursorPos(tgt.x, tgt.y + i) device.blit(t, fg, bg) end end + --os.sleep(.1) end function Canvas:applyPalette(palette) @@ -351,7 +420,7 @@ function Canvas.convertWindow(win, parent, wx, wy) function win.blit(text, fg, bg) local x, y = win.getCursorPos() - win.canvas:writeBlit(x, y, text, bg, fg) + win.canvas:blit(x, y, text, bg, fg) end function win.redraw() @@ -372,132 +441,4 @@ function Canvas.convertWindow(win, parent, wx, wy) win.clear() end -function Canvas.scrollingWindow(win, wx, wy) - local w, h = win.getSize() - local scrollPos = 0 - local maxScroll = h - - -- canvas lines are are a sliding window within the local lines table - local lines = { } - - local parent - local canvas = Canvas({ - x = wx, - y = wy, - width = w, - height = h, - isColor = win.isColor(), - }) - win.canvas = canvas - - local function scrollTo(p, forceRedraw) - local ms = #lines - canvas.height -- max scroll - p = math.min(math.max(p, 0), ms) -- normalize - - if p ~= scrollPos or forceRedraw then - scrollPos = p - for i = 1, canvas.height do - canvas.lines[i] = lines[i + scrollPos] - end - canvas:dirty() - end - end - - function win.blit(text, fg, bg) - local x, y = win.getCursorPos() - win.canvas:writeBlit(x, y, text, bg, fg) - win.redraw() - end - - function win.clear() - lines = { } - for i = 1, canvas.height do - lines[i] = canvas.lines[i] - end - scrollPos = 0 - canvas:clear(win.getBackgroundColor(), win.getTextColor()) - win.redraw() - end - - function win.clearLine() - local _, y = win.getCursorPos() - - scrollTo(#lines - canvas.height) - win.canvas:write(1, - y, - _rep(' ', win.canvas.width), - win.getBackgroundColor(), - win.getTextColor()) - win.redraw() - end - - function win.redraw() - if parent and canvas.visible then - local x, y = win.getCursorPos() - for i = 1, canvas.height do - local line = canvas.lines[i] - if line and line.dirty then - parent.setCursorPos(canvas.x, canvas.y + i - 1) - parent.blit(line.text, line.fg, line.bg) - line.dirty = false - end - end - win.setCursorPos(x, y) - end - end - - -- doesn't support negative scrolling... - function win.scroll(n) - for _ = 1, n do - lines[#lines + 1] = { - text = _rep(' ', canvas.width), - fg = _rep(canvas.palette[win.getTextColor()], canvas.width), - bg = _rep(canvas.palette[win.getBackgroundColor()], canvas.width), - } - end - - while #lines > maxScroll do - table.remove(lines, 1) - end - - scrollTo(maxScroll, true) - win.redraw() - end - - function win.scrollDown() - scrollTo(scrollPos + 1) - win.redraw() - end - - function win.scrollUp() - scrollTo(scrollPos - 1) - win.redraw() - end - - function win.setMaxScroll(ms) - maxScroll = ms - end - - function win.setParent(p) - parent = p - end - - function win.write(str) - str = tostring(str) or '' - - local x, y = win.getCursorPos() - scrollTo(#lines - canvas.height) - win.blit(str, - _rep(canvas.palette[win.getTextColor()], #str), - _rep(canvas.palette[win.getBackgroundColor()], #str)) - win.setCursorPos(x + #str, y) - end - - function win.reposition(x, y, width, height) - win.canvas.x, win.canvas.y = x, y - win.canvas:resize(width or win.canvas.width, height or win.canvas.height) - end - - win.clear() -end return Canvas diff --git a/sys/apis/ui/components/ActiveLayer.lua b/sys/apis/ui/components/ActiveLayer.lua new file mode 100644 index 0000000..8f84a4e --- /dev/null +++ b/sys/apis/ui/components/ActiveLayer.lua @@ -0,0 +1,30 @@ +local class = require('class') +local UI = require('ui') + +UI.ActiveLayer = class(UI.Window) +UI.ActiveLayer.defaults = { + UIElement = 'ActiveLayer', +} +function UI.ActiveLayer:setParent() + self:layout(self) + self.canvas = self:addLayer() + + UI.Window.setParent(self) +end + +function UI.ActiveLayer:enable(...) + self.canvas:raise() + self.canvas:setVisible(true) + UI.Window.enable(self, ...) + if self.parent.transitionHint then + self:addTransition(self.parent.transitionHint) + end + self:focusFirst() +end + +function UI.ActiveLayer:disable() + if self.canvas then + self.canvas:setVisible(false) + end + UI.Window.disable(self) +end diff --git a/sys/apis/ui/components/Button.lua b/sys/apis/ui/components/Button.lua new file mode 100644 index 0000000..be31cad --- /dev/null +++ b/sys/apis/ui/components/Button.lua @@ -0,0 +1,66 @@ +local class = require('class') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors + +UI.Button = class(UI.Window) +UI.Button.defaults = { + UIElement = 'Button', + text = 'button', + backgroundColor = colors.lightGray, + backgroundFocusColor = colors.gray, + textFocusColor = colors.white, + textInactiveColor = colors.gray, + textColor = colors.black, + centered = true, + height = 1, + focusIndicator = ' ', + event = 'button_press', + accelerators = { + space = 'button_activate', + enter = 'button_activate', + mouse_click = 'button_activate', + } +} +function UI.Button:setParent() + if not self.width and not self.ex then + self.width = #self.text + 2 + end + UI.Window.setParent(self) +end + +function UI.Button:draw() + local fg = self.textColor + local bg = self.backgroundColor + local ind = ' ' + if self.focused then + bg = self.backgroundFocusColor + fg = self.textFocusColor + ind = self.focusIndicator + elseif self.inactive then + fg = self.textInactiveColor + end + local text = ind .. self.text .. ' ' + if self.centered then + self:clear(bg) + self:centeredWrite(1 + math.floor(self.height / 2), text, bg, fg) + else + self:write(1, 1, Util.widthify(text, self.width), bg, fg) + end +end + +function UI.Button:focus() + if self.focused then + self:scrollIntoView() + end + self:draw() +end + +function UI.Button:eventHandler(event) + if event.type == 'button_activate' then + self:emit({ type = self.event, button = self }) + return true + end + return false +end diff --git a/sys/apis/ui/components/Checkbox.lua b/sys/apis/ui/components/Checkbox.lua new file mode 100644 index 0000000..ac3ec4c --- /dev/null +++ b/sys/apis/ui/components/Checkbox.lua @@ -0,0 +1,67 @@ +local class = require('class') +local UI = require('ui') + +local colors = _G.colors + +UI.Checkbox = class(UI.Window) +UI.Checkbox.defaults = { + UIElement = 'Checkbox', + nochoice = 'Select', + checkedIndicator = 'X', + leftMarker = '[', + rightMarker = ']', + value = false, + textColor = colors.white, + backgroundColor = colors.black, + backgroundFocusColor = colors.lightGray, + height = 1, + accelerators = { + space = 'checkbox_toggle', + mouse_click = 'checkbox_toggle', + } +} +function UI.Checkbox:setParent() + if not self.width and not self.ex then + self.width = (self.label and #self.label or 0) + 3 + else + self.widthh = 3 + end + UI.Window.setParent(self) +end + +function UI.Checkbox:draw() + local bg = self.backgroundColor + if self.focused then + bg = self.backgroundFocusColor + end + if type(self.value) == 'string' then + self.value = nil -- TODO: fix form + end + local text = string.format('[%s]', not self.value and ' ' or self.checkedIndicator) + local x = 1 + if self.label then + self:write(1, 1, self.label) + x = #self.label + 2 + end + self:write(x, 1, text, bg) + self:write(x, 1, self.leftMarker, self.backgroundColor, self.textColor) + self:write(x + 1, 1, not self.value and ' ' or self.checkedIndicator, bg) + self:write(x + 2, 1, self.rightMarker, self.backgroundColor, self.textColor) +end + +function UI.Checkbox:focus() + self:draw() +end + +function UI.Checkbox:reset() + self.value = false +end + +function UI.Checkbox:eventHandler(event) + if event.type == 'checkbox_toggle' then + self.value = not self.value + self:emit({ type = 'checkbox_change', checked = self.value, element = self }) + self:draw() + return true + end +end diff --git a/sys/apis/ui/components/Chooser.lua b/sys/apis/ui/components/Chooser.lua new file mode 100644 index 0000000..f6e632d --- /dev/null +++ b/sys/apis/ui/components/Chooser.lua @@ -0,0 +1,88 @@ +local class = require('class') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors + +UI.Chooser = class(UI.Window) +UI.Chooser.defaults = { + UIElement = 'Chooser', + choices = { }, + nochoice = 'Select', + backgroundFocusColor = colors.lightGray, + textInactiveColor = colors.gray, + leftIndicator = '<', + rightIndicator = '>', + height = 1, +} +function UI.Chooser:setParent() + if not self.width and not self.ex then + self.width = 1 + for _,v in pairs(self.choices) do + if #v.name > self.width then + self.width = #v.name + end + end + self.width = self.width + 4 + end + UI.Window.setParent(self) +end + +function UI.Chooser:draw() + local bg = self.backgroundColor + if self.focused then + bg = self.backgroundFocusColor + end + local fg = self.inactive and self.textInactiveColor or self.textColor + local choice = Util.find(self.choices, 'value', self.value) + local value = self.nochoice + if choice then + value = choice.name + end + self:write(1, 1, self.leftIndicator, self.backgroundColor, colors.black) + self:write(2, 1, ' ' .. Util.widthify(tostring(value), self.width-4) .. ' ', bg, fg) + self:write(self.width, 1, self.rightIndicator, self.backgroundColor, colors.black) +end + +function UI.Chooser:focus() + self:draw() +end + +function UI.Chooser:eventHandler(event) + if event.type == 'key' then + if event.key == 'right' or event.key == 'space' then + local _,k = Util.find(self.choices, 'value', self.value) + local choice + if not k then k = 1 end + if k and k < #self.choices then + choice = self.choices[k+1] + else + choice = self.choices[1] + end + self.value = choice.value + self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice }) + self:draw() + return true + elseif event.key == 'left' then + local _,k = Util.find(self.choices, 'value', self.value) + local choice + if k and k > 1 then + choice = self.choices[k-1] + else + choice = self.choices[#self.choices] + end + self.value = choice.value + self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice }) + self:draw() + return true + end + elseif event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then + if event.x == 1 then + self:emit({ type = 'key', key = 'left' }) + return true + elseif event.x == self.width then + self:emit({ type = 'key', key = 'right' }) + return true + end + end +end diff --git a/sys/apis/ui/components/Dialog.lua b/sys/apis/ui/components/Dialog.lua new file mode 100644 index 0000000..a293052 --- /dev/null +++ b/sys/apis/ui/components/Dialog.lua @@ -0,0 +1,39 @@ +local Canvas = require('ui.canvas') +local class = require('class') +local UI = require('ui') + +local colors = _G.colors + +UI.Dialog = class(UI.SlideOut) +UI.Dialog.defaults = { + UIElement = 'Dialog', + height = 7, + textColor = colors.black, + backgroundColor = colors.white, + okEvent ='dialog_ok', + cancelEvent = 'dialog_cancel', +} +function UI.Dialog:postInit() + self.y = -self.height + self.titleBar = UI.TitleBar({ event = self.cancelEvent, title = self.title }) +end + +function UI.Dialog:show(...) + local canvas = self.parent:getCanvas() + self.oldPalette = canvas.palette + canvas:applyPalette(Canvas.darkPalette) + UI.SlideOut.show(self, ...) +end + +function UI.Dialog:hide(...) + self.parent:getCanvas().palette = self.oldPalette + UI.SlideOut.hide(self, ...) + self.parent:draw() +end + +function UI.Dialog:eventHandler(event) + if event.type == 'dialog_cancel' then + self:hide() + end + return UI.SlideOut.eventHandler(self, event) +end \ No newline at end of file diff --git a/sys/apis/ui/components/DropMenu.lua b/sys/apis/ui/components/DropMenu.lua new file mode 100644 index 0000000..98f92be --- /dev/null +++ b/sys/apis/ui/components/DropMenu.lua @@ -0,0 +1,71 @@ +local class = require('class') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors + +UI.DropMenu = class(UI.MenuBar) +UI.DropMenu.defaults = { + UIElement = 'DropMenu', + backgroundColor = colors.white, + buttonClass = 'DropMenuItem', +} +function UI.DropMenu:setParent() + UI.MenuBar.setParent(self) + + local maxWidth = 1 + for y,child in ipairs(self.children) do + child.x = 1 + child.y = y + if #(child.text or '') > maxWidth then + maxWidth = #child.text + end + end + for _,child in ipairs(self.children) do + child.width = maxWidth + 2 + if child.spacer then + child.text = string.rep('-', child.width - 2) + end + end + + self.height = #self.children + 1 + self.width = maxWidth + 2 + self.ow = self.width + + self.canvas = self:addLayer() +end + +function UI.DropMenu:enable() +end + +function UI.DropMenu:show(x, y) + self.x, self.y = x, y + self.canvas:move(x, y) + self.canvas:setVisible(true) + + UI.Window.enable(self) + + self:draw() + self:capture(self) + self:focusFirst() +end + +function UI.DropMenu:hide() + self:disable() + self.canvas:setVisible(false) + self:release(self) +end + +function UI.DropMenu:eventHandler(event) + if event.type == 'focus_lost' and self.enabled then + if not Util.contains(self.children, event.focused) then + self:hide() + end + elseif event.type == 'mouse_out' and self.enabled then + self:hide() + self:refocus() + else + return UI.MenuBar.eventHandler(self, event) + end + return true +end diff --git a/sys/apis/ui/components/DropMenuItem.lua b/sys/apis/ui/components/DropMenuItem.lua new file mode 100644 index 0000000..a08f505 --- /dev/null +++ b/sys/apis/ui/components/DropMenuItem.lua @@ -0,0 +1,21 @@ +local class = require('class') +local UI = require('ui') + +local colors = _G.colors + +--[[-- DropMenuItem --]]-- +UI.DropMenuItem = class(UI.Button) +UI.DropMenuItem.defaults = { + UIElement = 'DropMenuItem', + textColor = colors.black, + backgroundColor = colors.white, + textFocusColor = colors.white, + textInactiveColor = colors.lightGray, + backgroundFocusColor = colors.lightGray, +} +function UI.DropMenuItem:eventHandler(event) + if event.type == 'button_activate' then + self.parent:hide() + end + return UI.Button.eventHandler(self, event) +end diff --git a/sys/apis/ui/components/Embedded.lua b/sys/apis/ui/components/Embedded.lua new file mode 100644 index 0000000..626516f --- /dev/null +++ b/sys/apis/ui/components/Embedded.lua @@ -0,0 +1,69 @@ +local class = require('class') +local Terminal = require('terminal') +local UI = require('ui') + +local colors = _G.colors + +UI.Embedded = class(UI.Window) +UI.Embedded.defaults = { + UIElement = 'Embedded', + backgroundColor = colors.black, + textColor = colors.white, + maxScroll = 100, + accelerators = { + up = 'scroll_up', + down = 'scroll_down', + } +} +function UI.Embedded:setParent() + UI.Window.setParent(self) + + self.win = Terminal.window(UI.term.device, self.x, self.y, self.width, self.height, false) + self.win.setMaxScroll(self.maxScroll) + + local canvas = self:getCanvas() + self.win.getCanvas().parent = canvas + table.insert(canvas.layers, self.win.getCanvas()) + self.canvas = self.win.getCanvas() + + self.win.setCursorPos(1, 1) + self.win.setBackgroundColor(self.backgroundColor) + self.win.setTextColor(self.textColor) + self.win.clear() +end + +function UI.Embedded:draw() + self.canvas:dirty() +end + +function UI.Embedded:enable() + self.canvas:setVisible(true) + self.canvas:raise() + if self.visible then + -- the window will automatically update on changes + -- the canvas does not need to be rendereed + self.win.setVisible(true) + end + UI.Window.enable(self) + self.canvas:dirty() +end + +function UI.Embedded:disable() + self.canvas:setVisible(false) + self.win.setVisible(false) + UI.Window.disable(self) +end + +function UI.Embedded:eventHandler(event) + if event.type == 'scroll_up' then + self.win.scrollUp() + return true + elseif event.type == 'scroll_down' then + self.win.scrollDown() + return true + end +end + +function UI.Embedded:focus() + -- allow scrolling +end diff --git a/sys/apis/ui/components/Form.lua b/sys/apis/ui/components/Form.lua new file mode 100644 index 0000000..43b3557 --- /dev/null +++ b/sys/apis/ui/components/Form.lua @@ -0,0 +1,149 @@ +local class = require('class') +local Sound = require('sound') +local UI = require('ui') + +local colors = _G.colors + +UI.Form = class(UI.Window) +UI.Form.defaults = { + UIElement = 'Form', + values = { }, + margin = 2, + event = 'form_complete', + cancelEvent = 'form_cancel', +} +function UI.Form:postInit() + self:createForm() +end + +function UI.Form:reset() + for _,child in pairs(self.children) do + if child.reset then + child:reset() + end + end +end + +function UI.Form:setValues(values) + self:reset() + self.values = values + for _,child in pairs(self.children) do + if child.formKey then + -- this should be child:setValue(self.values[child.formKey]) + -- so chooser can set default choice if null + -- null should be valid as well + child.value = self.values[child.formKey] or '' + end + end +end + +function UI.Form:createForm() + self.children = self.children or { } + + if not self.labelWidth then + self.labelWidth = 1 + for _, child in pairs(self) do + if type(child) == 'table' and child.UIElement then + if child.formLabel then + self.labelWidth = math.max(self.labelWidth, #child.formLabel + 2) + end + end + end + end + + local y = self.margin + for _, child in pairs(self) do + if type(child) == 'table' and child.UIElement then + if child.formKey then + child.value = self.values[child.formKey] or '' + end + if child.formLabel then + child.x = self.labelWidth + self.margin - 1 + child.y = y + if not child.width and not child.ex then + child.ex = -self.margin + end + + table.insert(self.children, UI.Text { + x = self.margin, + y = y, + textColor = colors.black, + width = #child.formLabel, + value = child.formLabel, + }) + end + if child.formKey or child.formLabel then + y = y + 1 + end + end + end + + if not self.manualControls then + table.insert(self.children, UI.Button { + y = -self.margin, x = -12 - self.margin, + text = 'Ok', + event = 'form_ok', + }) + table.insert(self.children, UI.Button { + y = -self.margin, x = -7 - self.margin, + text = 'Cancel', + event = self.cancelEvent, + }) + end +end + +function UI.Form:validateField(field) + if field.required then + if not field.value or #tostring(field.value) == 0 then + return false, 'Field is required' + end + end + if field.validate == 'numeric' then + if #tostring(field.value) > 0 then + if not tonumber(field.value) then + return false, 'Invalid number' + end + end + end + return true +end + +function UI.Form:save() + for _,child in pairs(self.children) do + if child.formKey then + local s, m = self:validateField(child) + if not s then + self:setFocus(child) + Sound.play('entity.villager.no', .5) + self:emit({ type = 'form_invalid', message = m, field = child }) + return false + end + end + end + for _,child in pairs(self.children) do + if child.formKey then + if (child.pruneEmpty and type(child.value) == 'string' and #child.value == 0) or + (child.pruneEmpty and type(child.value) == 'boolean' and not child.value) then + self.values[child.formKey] = nil + elseif child.validate == 'numeric' then + self.values[child.formKey] = tonumber(child.value) + else + self.values[child.formKey] = child.value + end + end + end + + return true +end + +function UI.Form:eventHandler(event) + if event.type == 'form_ok' then + if not self:save() then + return false + end + self:emit({ type = self.event, UIElement = self, values = self.values }) + else + return UI.Window.eventHandler(self, event) + end + return true +end diff --git a/sys/apis/ui/components/Grid.lua b/sys/apis/ui/components/Grid.lua new file mode 100644 index 0000000..c5ce6db --- /dev/null +++ b/sys/apis/ui/components/Grid.lua @@ -0,0 +1,492 @@ +local class = require('class') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors +local os = _G.os +local _rep = string.rep +local _sub = string.sub + +local function safeValue(v) + local t = type(v) + if t == 'string' or t == 'number' then + return v + end + return tostring(v) +end + +local Writer = class() +function Writer:init(element, y) + self.element = element + self.y = y + self.x = 1 +end + +function Writer:write(s, width, justify, bg, fg) + local len = #tostring(s or '') + if len > width then + s = _sub(s, 1, width) + end + local padding = len < width and _rep(' ', width - len) + if padding then + if justify == 'right' then + s = padding .. s + else + s = s .. padding + end + end + self.element:write(self.x, self.y, s, bg, fg) + self.x = self.x + width +end + +function Writer:finish(bg) + if self.x <= self.element.width then + self.element:write(self.x, self.y, _rep(' ', self.element.width - self.x + 1), bg) + end + self.x = 1 + self.y = self.y + 1 +end + +--[[-- Grid --]]-- +UI.Grid = class(UI.Window) +UI.Grid.defaults = { + UIElement = 'Grid', + index = 1, + inverseSort = false, + disableHeader = false, + headerHeight = 1, + marginRight = 0, + textColor = colors.white, + textSelectedColor = colors.white, + backgroundColor = colors.black, + backgroundSelectedColor = colors.gray, + headerBackgroundColor = colors.cyan, + headerTextColor = colors.white, + headerSortColor = colors.yellow, + unfocusedTextSelectedColor = colors.white, + unfocusedBackgroundSelectedColor = colors.gray, + focusIndicator = '>', + sortIndicator = ' ', + inverseSortIndicator = '^', + values = { }, + columns = { }, + accelerators = { + enter = 'key_enter', + [ 'control-c' ] = 'copy', + down = 'scroll_down', + up = 'scroll_up', + home = 'scroll_top', + [ 'end' ] = 'scroll_bottom', + pageUp = 'scroll_pageUp', + [ 'control-b' ] = 'scroll_pageUp', + pageDown = 'scroll_pageDown', + [ 'control-f' ] = 'scroll_pageDown', + }, +} +function UI.Grid:setParent() + UI.Window.setParent(self) + + for _,c in pairs(self.columns) do + c.cw = c.width + if not c.heading then + c.heading = '' + end + end + + self:update() + + if not self.pageSize then + if self.disableHeader then + self.pageSize = self.height + else + self.pageSize = self.height - self.headerHeight + end + end +end + +function UI.Grid:resize() + UI.Window.resize(self) + + if self.disableHeader then + self.pageSize = self.height + else + self.pageSize = self.height - self.headerHeight + end + self:adjustWidth() +end + +function UI.Grid:adjustWidth() + local t = { } -- cols without width + local w = self.width - #self.columns - 1 - self.marginRight -- width remaining + + for _,c in pairs(self.columns) do + if c.width then + c.cw = c.width + w = w - c.cw + else + table.insert(t, c) + end + end + + if #t == 0 then + return + end + + if #t == 1 then + t[1].cw = #(t[1].heading or '') + t[1].cw = math.max(t[1].cw, w) + return + end + + if not self.autospace then + for k,c in ipairs(t) do + c.cw = math.floor(w / (#t - k + 1)) + w = w - c.cw + end + + else + for _,c in ipairs(t) do + c.cw = #(c.heading or '') + w = w - c.cw + end + -- adjust the size to the length of the value + for key,row in pairs(self.values) do + if w <= 0 then + break + end + row = self:getDisplayValues(row, key) + for _,col in pairs(t) do + local value = row[col.key] + if value then + value = tostring(value) + if #value > col.cw then + w = w + col.cw + col.cw = math.min(#value, w) + w = w - col.cw + if w <= 0 then + break + end + end + end + end + end + + -- last column does not get padding (right alignment) + if not self.columns[#self.columns].width then + Util.removeByValue(t, self.columns[#self.columns]) + end + + -- got some extra room - add some padding + if w > 0 then + for k,c in ipairs(t) do + local padding = math.floor(w / (#t - k + 1)) + c.cw = c.cw + padding + w = w - padding + end + end + end +end + +function UI.Grid:setPageSize(pageSize) + self.pageSize = pageSize +end + +function UI.Grid:getValues() + return self.values +end + +function UI.Grid:setValues(t) + self.values = t + self:update() +end + +function UI.Grid:setInverseSort(inverseSort) + self.inverseSort = inverseSort + self:update() + self:setIndex(self.index) +end + +function UI.Grid:setSortColumn(column) + self.sortColumn = column +end + +function UI.Grid:getDisplayValues(row, key) + return row +end + +function UI.Grid:getSelected() + if self.sorted then + return self.values[self.sorted[self.index]], self.sorted[self.index] + end +end + +function UI.Grid:setSelected(name, value) + if self.sorted then + for k,v in pairs(self.sorted) do + if self.values[v][name] == value then + self:setIndex(k) + return + end + end + end + self:setIndex(1) +end + +function UI.Grid:focus() + self:drawRows() +end + +function UI.Grid:draw() + if not self.disableHeader then + self:drawHeadings() + end + + if self.index <= 0 then + self:setIndex(1) + elseif self.index > #self.sorted then + self:setIndex(#self.sorted) + end + self:drawRows() +end + +-- Something about the displayed table has changed +-- resort the table +function UI.Grid:update() + local function sort(a, b) + if not a[self.sortColumn] then + return false + elseif not b[self.sortColumn] then + return true + end + return self:sortCompare(a, b) + end + + local function inverseSort(a, b) + return not sort(a, b) + end + + local order + if self.sortColumn then + order = sort + if self.inverseSort then + order = inverseSort + end + end + + self.sorted = Util.keys(self.values) + if order then + table.sort(self.sorted, function(a,b) + return order(self.values[a], self.values[b]) + end) + end + + self:adjustWidth() +end + +function UI.Grid:drawHeadings() + if self.headerHeight > 1 then + self:clear(self.headerBackgroundColor) + end + local sb = Writer(self, math.ceil(self.headerHeight / 2)) + for _,col in ipairs(self.columns) do + local ind = ' ' + local color = self.headerTextColor + if col.key == self.sortColumn then + if self.inverseSort then + ind = self.inverseSortIndicator + else + ind = self.sortIndicator + end + color = self.headerSortColor + end + sb:write(ind .. col.heading, + col.cw + 1, + col.justify, + self.headerBackgroundColor, + color) + end + sb:finish(self.headerBackgroundColor) +end + +function UI.Grid:sortCompare(a, b) + a = safeValue(a[self.sortColumn]) + b = safeValue(b[self.sortColumn]) + if type(a) == type(b) then + return a < b + end + return tostring(a) < tostring(b) +end + +function UI.Grid:drawRows() + local startRow = math.max(1, self:getStartRow()) + + local sb = Writer(self, self.disableHeader and 1 or self.headerHeight + 1) + + local lastRow = math.min(startRow + self.pageSize - 1, #self.sorted) + for index = startRow, lastRow do + + local key = self.sorted[index] + local rawRow = self.values[key] + local row = self:getDisplayValues(rawRow, key) + + local ind = ' ' + if self.focused and index == self.index and not self.inactive then + ind = self.focusIndicator + end + + local selected = index == self.index and not self.inactive + local bg = self:getRowBackgroundColor(rawRow, selected) + local fg = self:getRowTextColor(rawRow, selected) + + for _,col in pairs(self.columns) do + sb:write(ind .. safeValue(row[col.key] or ''), + col.cw + 1, + col.justify, + bg, + fg) + ind = ' ' + end + sb:finish(bg) + end + + if sb.y <= self.height then + self:clearArea(1, sb.y, self.width, self.height - sb.y + 1) + end +end + +function UI.Grid:getRowTextColor(row, selected) + if selected then + if self.focused then + return self.textSelectedColor + end + return self.unfocusedTextSelectedColor + end + return self.textColor +end + +function UI.Grid:getRowBackgroundColor(row, selected) + if selected then + if self.focused then + return self.backgroundSelectedColor + end + return self.unfocusedBackgroundSelectedColor + end + return self.backgroundColor +end + +function UI.Grid:getIndex() + return self.index +end + +function UI.Grid:setIndex(index) + index = math.max(1, index) + self.index = math.min(index, #self.sorted) + + local selected = self:getSelected() + if selected ~= self.selected then + self:drawRows() + self.selected = selected + if selected then + self:emit({ type = 'grid_focus_row', selected = selected, element = self }) + end + end +end + +function UI.Grid:getStartRow() + return math.floor((self.index - 1) / self.pageSize) * self.pageSize + 1 +end + +function UI.Grid:getPage() + return math.floor(self.index / self.pageSize) + 1 +end + +function UI.Grid:getPageCount() + local tableSize = Util.size(self.values) + local pc = math.floor(tableSize / self.pageSize) + if tableSize % self.pageSize > 0 then + pc = pc + 1 + end + return pc +end + +function UI.Grid:nextPage() + self:setPage(self:getPage() + 1) +end + +function UI.Grid:previousPage() + self:setPage(self:getPage() - 1) +end + +function UI.Grid:setPage(pageNo) + -- 1 based paging + self:setIndex((pageNo-1) * self.pageSize + 1) +end + +function UI.Grid:eventHandler(event) + if event.type == 'mouse_click' or + event.type == 'mouse_rightclick' or + event.type == 'mouse_doubleclick' then + if not self.disableHeader then + if event.y <= self.headerHeight then + local col = 2 + for _,c in ipairs(self.columns) do + if event.x < col + c.cw then + self:emit({ + type = 'grid_sort', + sortColumn = c.key, + inverseSort = self.sortColumn == c.key and not self.inverseSort, + element = self, + }) + break + end + col = col + c.cw + 1 + end + return true + end + end + local row = self:getStartRow() + event.y - 1 + if not self.disableHeader then + row = row - self.headerHeight + end + if row > 0 and row <= Util.size(self.values) then + self:setIndex(row) + if event.type == 'mouse_doubleclick' then + self:emit({ type = 'key_enter' }) + elseif event.type == 'mouse_rightclick' then + self:emit({ type = 'grid_select_right', selected = self.selected, element = self }) + end + return true + end + return false + + elseif event.type == 'grid_sort' then + self.sortColumn = event.sortColumn + self:setInverseSort(event.inverseSort) + self:draw() + elseif event.type == 'scroll_down' then + self:setIndex(self.index + 1) + elseif event.type == 'scroll_up' then + self:setIndex(self.index - 1) + elseif event.type == 'scroll_top' then + self:setIndex(1) + elseif event.type == 'scroll_bottom' then + self:setIndex(Util.size(self.values)) + elseif event.type == 'scroll_pageUp' then + self:setIndex(self.index - self.pageSize) + elseif event.type == 'scroll_pageDown' then + self:setIndex(self.index + self.pageSize) + elseif event.type == 'scroll_to' then + self:setIndex(event.offset) + elseif event.type == 'key_enter' then + if self.selected then + self:emit({ type = 'grid_select', selected = self.selected, element = self }) + end + elseif event.type == 'copy' then + if self.selected then + os.queueEvent('clipboard_copy', self.selected) + end + else + return false + end + return true +end diff --git a/sys/apis/ui/components/Image.lua b/sys/apis/ui/components/Image.lua new file mode 100644 index 0000000..630cf3c --- /dev/null +++ b/sys/apis/ui/components/Image.lua @@ -0,0 +1,40 @@ +local class = require('class') +local UI = require('ui') + +UI.Image = class(UI.Window) +UI.Image.defaults = { + UIElement = 'Image', + event = 'button_press', +} +function UI.Image:setParent() + if self.image then + self.height = #self.image + end + if self.image and not self.width then + self.width = #self.image[1] + end + UI.Window.setParent(self) +end + +function UI.Image:draw() + self:clear() + if self.image then + for y = 1, #self.image do + local line = self.image[y] + for x = 1, #line do + local ch = line[x] + if type(ch) == 'number' then + if ch > 0 then + self:write(x, y, ' ', ch) + end + else + self:write(x, y, ch) + end + end + end + end +end + +function UI.Image:setImage(image) + self.image = image +end diff --git a/sys/apis/ui/components/Menu.lua b/sys/apis/ui/components/Menu.lua new file mode 100644 index 0000000..3377a0e --- /dev/null +++ b/sys/apis/ui/components/Menu.lua @@ -0,0 +1,60 @@ +local class = require('class') +local UI = require('ui') + +--[[-- Menu --]]-- +UI.Menu = class(UI.Grid) +UI.Menu.defaults = { + UIElement = 'Menu', + disableHeader = true, + columns = { { heading = 'Prompt', key = 'prompt', width = 20 } }, +} +function UI.Menu:postInit() + self.values = self.menuItems + self.pageSize = #self.menuItems +end + +function UI.Menu:setParent() + UI.Grid.setParent(self) + self.itemWidth = 1 + for _,v in pairs(self.values) do + if #v.prompt > self.itemWidth then + self.itemWidth = #v.prompt + end + end + self.columns[1].width = self.itemWidth + + if self.centered then + self:center() + else + self.width = self.itemWidth + 2 + end +end + +function UI.Menu:center() + self.x = (self.width - self.itemWidth + 2) / 2 + self.width = self.itemWidth + 2 +end + +function UI.Menu:eventHandler(event) + if event.type == 'key' then + if event.key == 'enter' then + local selected = self.menuItems[self.index] + self:emit({ + type = selected.event or 'menu_select', + selected = selected + }) + return true + end + elseif event.type == 'mouse_click' then + if event.y <= #self.menuItems then + UI.Grid.setIndex(self, event.y) + local selected = self.menuItems[self.index] + self:emit({ + type = selected.event or 'menu_select', + selected = selected + }) + return true + end + end + return UI.Grid.eventHandler(self, event) +end diff --git a/sys/apis/ui/components/MenuBar.lua b/sys/apis/ui/components/MenuBar.lua new file mode 100644 index 0000000..34ee7b9 --- /dev/null +++ b/sys/apis/ui/components/MenuBar.lua @@ -0,0 +1,92 @@ +local class = require('class') +local UI = require('ui') + +local colors = _G.colors + +local function getPosition(element) + local x, y = 1, 1 + repeat + x = element.x + x - 1 + y = element.y + y - 1 + element = element.parent + until not element + return x, y +end + +UI.MenuBar = class(UI.Window) +UI.MenuBar.defaults = { + UIElement = 'MenuBar', + buttons = { }, + height = 1, + backgroundColor = colors.lightGray, + textColor = colors.black, + spacing = 2, + lastx = 1, + showBackButton = false, + buttonClass = 'MenuItem', +} +UI.MenuBar.spacer = { spacer = true, text = 'spacer', inactive = true } + +function UI.MenuBar:postInit() + self:addButtons(self.buttons) +end + +function UI.MenuBar:addButtons(buttons) + if not self.children then + self.children = { } + end + + for _,button in pairs(buttons) do + if button.UIElement then + table.insert(self.children, button) + else + local buttonProperties = { + x = self.lastx, + width = #button.text + self.spacing, + centered = false, + } + self.lastx = self.lastx + buttonProperties.width + UI:mergeProperties(buttonProperties, button) + + button = UI[self.buttonClass](buttonProperties) + if button.name then + self[button.name] = button + else + table.insert(self.children, button) + end + + if button.dropdown then + button.dropmenu = UI.DropMenu { buttons = button.dropdown } + end + end + end + if self.parent then + self:initChildren() + end +end + +function UI.MenuBar:getActive(menuItem) + return not menuItem.inactive +end + +function UI.MenuBar:eventHandler(event) + if event.type == 'button_press' and event.button.dropmenu then + if event.button.dropmenu.enabled then + event.button.dropmenu:hide() + self:refocus() + return true + else + local x, y = getPosition(event.button) + if x + event.button.dropmenu.width > self.width then + x = self.width - event.button.dropmenu.width + 1 + end + for _,c in pairs(event.button.dropmenu.children) do + if not c.spacer then + c.inactive = not self:getActive(c) + end + end + event.button.dropmenu:show(x, y + 1) + end + return true + end +end diff --git a/sys/apis/ui/components/MenuItem.lua b/sys/apis/ui/components/MenuItem.lua new file mode 100644 index 0000000..c4dbc7a --- /dev/null +++ b/sys/apis/ui/components/MenuItem.lua @@ -0,0 +1,14 @@ +local class = require('class') +local UI = require('ui') + +local colors = _G.colors + +--[[-- MenuItem --]]-- +UI.MenuItem = class(UI.Button) +UI.MenuItem.defaults = { + UIElement = 'MenuItem', + textColor = colors.black, + backgroundColor = colors.lightGray, + textFocusColor = colors.white, + backgroundFocusColor = colors.lightGray, +} diff --git a/sys/apis/ui/components/NftImage.lua b/sys/apis/ui/components/NftImage.lua new file mode 100644 index 0000000..37740c5 --- /dev/null +++ b/sys/apis/ui/components/NftImage.lua @@ -0,0 +1,33 @@ +local class = require('class') +local UI = require('ui') + +UI.NftImage = class(UI.Window) +UI.NftImage.defaults = { + UIElement = 'NftImage', + event = 'button_press', +} +function UI.NftImage:setParent() + if self.image then + self.height = self.image.height + end + if self.image and not self.width then + self.width = self.image.width + end + UI.Window.setParent(self) +end + +function UI.NftImage:draw() + if self.image then + for y = 1, self.image.height do + for x = 1, #self.image.text[y] do + self:write(x, y, self.image.text[y][x], self.image.bg[y][x], self.image.fg[y][x]) + end + end + else + self:clear() + end +end + +function UI.NftImage:setImage(image) + self.image = image +end diff --git a/sys/apis/ui/components/Notification.lua b/sys/apis/ui/components/Notification.lua new file mode 100644 index 0000000..f71408b --- /dev/null +++ b/sys/apis/ui/components/Notification.lua @@ -0,0 +1,67 @@ +local class = require('class') +local Event = require('event') +local Sound = require('sound') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors + +UI.Notification = class(UI.Window) +UI.Notification.defaults = { + UIElement = 'Notification', + backgroundColor = colors.gray, + height = 3, +} +function UI.Notification:draw() +end + +function UI.Notification:enable() +end + +function UI.Notification:error(value, timeout) + self.backgroundColor = colors.red + Sound.play('entity.villager.no', .5) + self:display(value, timeout) +end + +function UI.Notification:info(value, timeout) + self.backgroundColor = colors.gray + self:display(value, timeout) +end + +function UI.Notification:success(value, timeout) + self.backgroundColor = colors.green + self:display(value, timeout) +end + +function UI.Notification:cancel() + if self.canvas then + Event.cancelNamedTimer('notificationTimer') + self.enabled = false + self.canvas:removeLayer() + self.canvas = nil + end +end + +function UI.Notification:display(value, timeout) + self.enabled = true + local lines = Util.wordWrap(value, self.width - 2) + self.height = #lines + 1 + self.y = self.parent.height - self.height + 1 + if self.canvas then + self.canvas:removeLayer() + end + + self.canvas = self:addLayer(self.backgroundColor, self.textColor) + self:addTransition('expandUp', { ticks = self.height }) + self.canvas:setVisible(true) + self:clear() + for k,v in pairs(lines) do + self:write(2, k, v) + end + + Event.addNamedTimer('notificationTimer', timeout or 3, false, function() + self:cancel() + self:sync() + end) +end diff --git a/sys/apis/ui/components/ProgressBar.lua b/sys/apis/ui/components/ProgressBar.lua new file mode 100644 index 0000000..2a78c5f --- /dev/null +++ b/sys/apis/ui/components/ProgressBar.lua @@ -0,0 +1,18 @@ +local class = require('class') +local UI = require('ui') + +local colors = _G.colors + +UI.ProgressBar = class(UI.Window) +UI.ProgressBar.defaults = { + UIElement = 'ProgressBar', + progressColor = colors.lime, + backgroundColor = colors.gray, + height = 1, + value = 0, +} +function UI.ProgressBar:draw() + self:clear() + local width = math.ceil(self.value / 100 * self.width) + self:clearArea(1, 1, width, self.height, self.progressColor) +end diff --git a/sys/apis/ui/components/ScrollBar.lua b/sys/apis/ui/components/ScrollBar.lua new file mode 100644 index 0000000..8be6de9 --- /dev/null +++ b/sys/apis/ui/components/ScrollBar.lua @@ -0,0 +1,74 @@ +local class = require('class') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors + +UI.ScrollBar = class(UI.Window) +UI.ScrollBar.defaults = { + UIElement = 'ScrollBar', + lineChar = '|', + sliderChar = '#', + upArrowChar = '^', + downArrowChar = 'v', + scrollbarColor = colors.lightGray, + width = 1, + x = -1, + ey = -1, +} +function UI.ScrollBar:draw() + local view = self.parent:getViewArea() + + if view.totalHeight > view.height then + local maxScroll = view.totalHeight - view.height + local percent = view.offsetY / maxScroll + local sliderSize = math.max(1, Util.round(view.height / view.totalHeight * (view.height - 2))) + local x = self.width + + local row = view.y + if not view.static then -- does the container scroll ? + self.height = view.totalHeight + end + + for i = 1, view.height - 2 do + self:write(x, row + i, self.lineChar, nil, self.scrollbarColor) + end + + local y = Util.round((view.height - 2 - sliderSize) * percent) + for i = 1, sliderSize do + self:write(x, row + y + i, self.sliderChar, nil, self.scrollbarColor) + end + + local color = self.scrollbarColor + if view.offsetY > 0 then + color = colors.white + end + self:write(x, row, self.upArrowChar, nil, color) + + color = self.scrollbarColor + if view.offsetY + view.height < view.totalHeight then + color = colors.white + end + self:write(x, row + view.height - 1, self.downArrowChar, nil, color) + end +end + +function UI.ScrollBar:eventHandler(event) + if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then + if event.x == 1 then + local view = self.parent:getViewArea() + if view.totalHeight > view.height then + if event.y == view.y then + self:emit({ type = 'scroll_up'}) + elseif event.y == view.y + view.height - 1 then + self:emit({ type = 'scroll_down'}) + else + local percent = (event.y - view.y) / (view.height - 2) + local y = math.floor((view.totalHeight - view.height) * percent) + self:emit({ type = 'scroll_to', offset = y }) + end + end + return true + end + end +end diff --git a/sys/apis/ui/components/ScrollingGrid.lua b/sys/apis/ui/components/ScrollingGrid.lua new file mode 100644 index 0000000..c29fe74 --- /dev/null +++ b/sys/apis/ui/components/ScrollingGrid.lua @@ -0,0 +1,60 @@ +local class = require('class') +local UI = require('ui') +local Util = require('util') + +--[[-- ScrollingGrid --]]-- +UI.ScrollingGrid = class(UI.Grid) +UI.ScrollingGrid.defaults = { + UIElement = 'ScrollingGrid', + scrollOffset = 0, + marginRight = 1, +} +function UI.ScrollingGrid:postInit() + self.scrollBar = UI.ScrollBar() +end + +function UI.ScrollingGrid:drawRows() + UI.Grid.drawRows(self) + self.scrollBar:draw() +end + +function UI.ScrollingGrid:getViewArea() + local y = 1 + if not self.disableHeader then + y = y + self.headerHeight + end + + return { + static = true, -- the container doesn't scroll + y = y, -- scrollbar Y + height = self.pageSize, -- viewable height + totalHeight = Util.size(self.values), -- total height + offsetY = self.scrollOffset, -- scroll offset + } +end + +function UI.ScrollingGrid:getStartRow() + local ts = Util.size(self.values) + if ts < self.pageSize then + self.scrollOffset = 0 + end + return self.scrollOffset + 1 +end + +function UI.ScrollingGrid:setIndex(index) + if index < self.scrollOffset + 1 then + self.scrollOffset = index - 1 + elseif index - self.scrollOffset > self.pageSize then + self.scrollOffset = index - self.pageSize + end + + if self.scrollOffset < 0 then + self.scrollOffset = 0 + else + local ts = Util.size(self.values) + if self.pageSize + self.scrollOffset + 1 > ts then + self.scrollOffset = math.max(0, ts - self.pageSize) + end + end + UI.Grid.setIndex(self, index) +end diff --git a/sys/apis/ui/components/SlideOut.lua b/sys/apis/ui/components/SlideOut.lua new file mode 100644 index 0000000..5994c69 --- /dev/null +++ b/sys/apis/ui/components/SlideOut.lua @@ -0,0 +1,51 @@ +local class = require('class') +local UI = require('ui') + +--[[-- SlideOut --]]-- +UI.SlideOut = class(UI.Window) +UI.SlideOut.defaults = { + UIElement = 'SlideOut', + pageType = 'modal', +} +function UI.SlideOut:setParent() + -- TODO: size should be set at this point + self:layout() + self.canvas = self:addLayer() + + UI.Window.setParent(self) +end + +function UI.SlideOut:enable() +end + +function UI.SlideOut:show(...) + self:addTransition('expandUp') + self.canvas:raise() + self.canvas:setVisible(true) + UI.Window.enable(self, ...) + self:draw() + self:capture(self) + self:focusFirst() +end + +function UI.SlideOut:disable() + self.canvas:setVisible(false) + UI.Window.disable(self) +end + +function UI.SlideOut:hide() + self:disable() + self:release(self) + self:refocus() +end + +function UI.SlideOut:eventHandler(event) + if event.type == 'slide_show' then + self:show() + return true + + elseif event.type == 'slide_hide' then + self:hide() + return true + end +end diff --git a/sys/apis/ui/components/StatusBar.lua b/sys/apis/ui/components/StatusBar.lua new file mode 100644 index 0000000..183014a --- /dev/null +++ b/sys/apis/ui/components/StatusBar.lua @@ -0,0 +1,99 @@ +local class = require('class') +local Event = require('event') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors + +UI.StatusBar = class(UI.Window) +UI.StatusBar.defaults = { + UIElement = 'StatusBar', + backgroundColor = colors.lightGray, + textColor = colors.gray, + height = 1, + ey = -1, +} +function UI.StatusBar:adjustWidth() + -- Can only have 1 adjustable width + if self.columns then + local w = self.width - #self.columns - 1 + for _,c in pairs(self.columns) do + if c.width then + c.cw = c.width -- computed width + w = w - c.width + end + end + for _,c in pairs(self.columns) do + if not c.width then + c.cw = w + end + end + end +end + +function UI.StatusBar:resize() + UI.Window.resize(self) + self:adjustWidth() +end + +function UI.StatusBar:setParent() + UI.Window.setParent(self) + self:adjustWidth() +end + +function UI.StatusBar:setStatus(status) + if self.values ~= status then + self.values = status + self:draw() + end +end + +function UI.StatusBar:setValue(name, value) + if not self.values then + self.values = { } + end + self.values[name] = value +end + +function UI.StatusBar:getValue(name) + if self.values then + return self.values[name] + end +end + +function UI.StatusBar:timedStatus(status, timeout) + timeout = timeout or 3 + self:write(2, 1, Util.widthify(status, self.width-2), self.backgroundColor) + Event.addNamedTimer('statusTimer', timeout, false, function() + if self.parent.enabled then + self:draw() + self:sync() + end + end) +end + +function UI.StatusBar:getColumnWidth(name) + local c = Util.find(self.columns, 'key', name) + return c and c.cw +end + +function UI.StatusBar:setColumnWidth(name, width) + local c = Util.find(self.columns, 'key', name) + if c then + c.cw = width + end +end + +function UI.StatusBar:draw() + if not self.values then + self:clear() + elseif type(self.values) == 'string' then + self:write(1, 1, Util.widthify(' ' .. self.values, self.width)) + else + local s = '' + for _,c in ipairs(self.columns) do + s = s .. ' ' .. Util.widthify(tostring(self.values[c.key] or ''), c.cw) + end + self:write(1, 1, Util.widthify(s, self.width)) + end +end diff --git a/sys/apis/ui/components/Tab.lua b/sys/apis/ui/components/Tab.lua new file mode 100644 index 0000000..d564c86 --- /dev/null +++ b/sys/apis/ui/components/Tab.lua @@ -0,0 +1,12 @@ +local class = require('class') +local UI = require('ui') + +local colors = _G.colors + +UI.Tab = class(UI.ActiveLayer) +UI.Tab.defaults = { + UIElement = 'Tab', + tabTitle = 'tab', + backgroundColor = colors.cyan, + y = 2, +} diff --git a/sys/apis/ui/components/TabBar.lua b/sys/apis/ui/components/TabBar.lua new file mode 100644 index 0000000..1431314 --- /dev/null +++ b/sys/apis/ui/components/TabBar.lua @@ -0,0 +1,45 @@ +local class = require('class') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors + +UI.TabBar = class(UI.MenuBar) +UI.TabBar.defaults = { + UIElement = 'TabBar', + buttonClass = 'TabBarMenuItem', + selectedBackgroundColor = colors.cyan, +} +function UI.TabBar:enable() + UI.MenuBar.enable(self) + if not Util.find(self.children, 'selected', true) then + local menuItem = self:getFocusables()[1] + if menuItem then + menuItem.selected = true + end + end +end + +function UI.TabBar:eventHandler(event) + if event.type == 'tab_select' then + local selected, si = Util.find(self.children, 'uid', event.button.uid) + local previous, pi = Util.find(self.children, 'selected', true) + + if si ~= pi then + selected.selected = true + if previous then + previous.selected = false + self:emit({ type = 'tab_change', current = si, last = pi, tab = selected }) + end + end + UI.MenuBar.draw(self) + end + return UI.MenuBar.eventHandler(self, event) +end + +function UI.TabBar:selectTab(text) + local menuItem = Util.find(self.children, 'text', text) + if menuItem then + menuItem.selected = true + end +end diff --git a/sys/apis/ui/components/TabBarMenuItem.lua b/sys/apis/ui/components/TabBarMenuItem.lua new file mode 100644 index 0000000..28e585b --- /dev/null +++ b/sys/apis/ui/components/TabBarMenuItem.lua @@ -0,0 +1,25 @@ +local class = require('class') +local UI = require('ui') + +local colors = _G.colors + +--[[-- TabBarMenuItem --]]-- +UI.TabBarMenuItem = class(UI.Button) +UI.TabBarMenuItem.defaults = { + UIElement = 'TabBarMenuItem', + event = 'tab_select', + textColor = colors.black, + selectedBackgroundColor = colors.cyan, + unselectedBackgroundColor = colors.lightGray, + backgroundColor = colors.lightGray, +} +function UI.TabBarMenuItem:draw() + if self.selected then + self.backgroundColor = self.selectedBackgroundColor + self.backgroundFocusColor = self.selectedBackgroundColor + else + self.backgroundColor = self.unselectedBackgroundColor + self.backgroundFocusColor = self.unselectedBackgroundColor + end + UI.Button.draw(self) +end diff --git a/sys/apis/ui/components/Tabs.lua b/sys/apis/ui/components/Tabs.lua new file mode 100644 index 0000000..7c2d967 --- /dev/null +++ b/sys/apis/ui/components/Tabs.lua @@ -0,0 +1,89 @@ +local class = require('class') +local UI = require('ui') +local Util = require('util') + +UI.Tabs = class(UI.Window) +UI.Tabs.defaults = { + UIElement = 'Tabs', +} +function UI.Tabs:postInit() + self:add(self) +end + +function UI.Tabs:add(children) + local buttons = { } + for _,child in pairs(children) do + if type(child) == 'table' and child.UIElement and child.tabTitle then + child.y = 2 + table.insert(buttons, { + text = child.tabTitle, + event = 'tab_select', + tabUid = child.uid, + }) + end + end + + if not self.tabBar then + self.tabBar = UI.TabBar({ + buttons = buttons, + }) + else + self.tabBar:addButtons(buttons) + end + + if self.parent then + return UI.Window.add(self, children) + end +end + +function UI.Tabs:selectTab(tab) + local menuItem = Util.find(self.tabBar.children, 'tabUid', tab.uid) + if menuItem then + self.tabBar:emit({ type = 'tab_select', button = { uid = menuItem.uid } }) + end +end + +function UI.Tabs:setActive(tab, active) + local menuItem = Util.find(self.tabBar.children, 'tabUid', tab.uid) + if menuItem then + menuItem.inactive = not active + end +end + +function UI.Tabs:enable() + self.enabled = true + self.transitionHint = nil + self.tabBar:enable() + + local menuItem = Util.find(self.tabBar.children, 'selected', true) + + for _,child in pairs(self.children) do + if child.uid == menuItem.tabUid then + child:enable() + self:emit({ type = 'tab_activate', activated = child }) + elseif child.tabTitle then + child:disable() + end + end +end + +function UI.Tabs:eventHandler(event) + if event.type == 'tab_change' then + local tab = self:find(event.tab.tabUid) + if event.current > event.last then + self.transitionHint = 'slideLeft' + else + self.transitionHint = 'slideRight' + end + + for _,child in pairs(self.children) do + if child.uid == event.tab.tabUid then + child:enable() + elseif child.tabTitle then + child:disable() + end + end + self:emit({ type = 'tab_activate', activated = tab }) + tab:draw() + end +end diff --git a/sys/apis/ui/components/Text.lua b/sys/apis/ui/components/Text.lua new file mode 100644 index 0000000..96310ec --- /dev/null +++ b/sys/apis/ui/components/Text.lua @@ -0,0 +1,20 @@ +local class = require('class') +local UI = require('ui') +local Util = require('util') + +UI.Text = class(UI.Window) +UI.Text.defaults = { + UIElement = 'Text', + value = '', + height = 1, +} +function UI.Text:setParent() + if not self.width and not self.ex then + self.width = #tostring(self.value) + end + UI.Window.setParent(self) +end + +function UI.Text:draw() + self:write(1, 1, Util.widthify(self.value or '', self.width), self.backgroundColor) +end diff --git a/sys/apis/ui/components/TextArea.lua b/sys/apis/ui/components/TextArea.lua new file mode 100644 index 0000000..8602866 --- /dev/null +++ b/sys/apis/ui/components/TextArea.lua @@ -0,0 +1,36 @@ +local class = require('class') +local UI = require('ui') + +--[[-- TextArea --]]-- +UI.TextArea = class(UI.Viewport) +UI.TextArea.defaults = { + UIElement = 'TextArea', + marginRight = 2, + value = '', +} +function UI.TextArea:postInit() + self.scrollBar = UI.ScrollBar() +end + +function UI.TextArea:setText(text) + self:reset() + self.value = text + self:draw() +end + +function UI.TextArea:focus() + -- allow keyboard scrolling +end + +function UI.TextArea:draw() + self:clear() +-- self:setCursorPos(1, 1) + self.cursorX, self.cursorY = 1, 1 + self:print(self.value) + + for _,child in pairs(self.children) do + if child.enabled then + child:draw() + end + end +end diff --git a/sys/apis/ui/components/TextEntry.lua b/sys/apis/ui/components/TextEntry.lua new file mode 100644 index 0000000..384c098 --- /dev/null +++ b/sys/apis/ui/components/TextEntry.lua @@ -0,0 +1,189 @@ +local class = require('class') +local UI = require('ui') +local Util = require('util') + +local colors = _G.colors +local os = _G.os +local _rep = string.rep + +UI.TextEntry = class(UI.Window) +UI.TextEntry.defaults = { + UIElement = 'TextEntry', + value = '', + shadowText = '', + focused = false, + textColor = colors.white, + shadowTextColor = colors.gray, + backgroundColor = colors.black, -- colors.lightGray, + backgroundFocusColor = colors.black, --lightGray, + height = 1, + limit = 6, + pos = 0, + accelerators = { + [ 'control-c' ] = 'copy', + } +} +function UI.TextEntry:postInit() + self.value = tostring(self.value) +end + +function UI.TextEntry:setValue(value) + self.value = value +end + +function UI.TextEntry:setPosition(pos) + self.pos = pos +end + +function UI.TextEntry:updateScroll() + if not self.scroll then + self.scroll = 0 + end + + if not self.pos then + self.pos = #tostring(self.value) + self.scroll = 0 + elseif self.pos > #tostring(self.value) then + self.pos = #tostring(self.value) + self.scroll = 0 + end + + if self.pos - self.scroll > self.width - 2 then + self.scroll = self.pos - (self.width - 2) + elseif self.pos < self.scroll then + self.scroll = self.pos + end +end + +function UI.TextEntry:draw() + local bg = self.backgroundColor + local tc = self.textColor + if self.focused then + bg = self.backgroundFocusColor + end + + self:updateScroll() + local text = tostring(self.value) + if #text > 0 then + if self.scroll and self.scroll > 0 then + text = text:sub(1 + self.scroll) + end + if self.mask then + text = _rep('*', #text) + end + else + tc = self.shadowTextColor + text = self.shadowText + end + + self:write(1, 1, ' ' .. Util.widthify(text, self.width - 2) .. ' ', bg, tc) + if self.focused then + self:setCursorPos(self.pos-self.scroll+2, 1) + end +end + +function UI.TextEntry:reset() + self.pos = 0 + self.value = '' + self:draw() + self:updateCursor() +end + +function UI.TextEntry:updateCursor() + self:updateScroll() + self:setCursorPos(self.pos-self.scroll+2, 1) +end + +function UI.TextEntry:focus() + self:draw() + if self.focused then + self:setCursorBlink(true) + else + self:setCursorBlink(false) + end +end + +--[[ + A few lines below from theoriginalbit + http://www.computercraft.info/forums2/index.php?/topic/16070-read-and-limit-length-of-the-input-field/ +--]] +function UI.TextEntry:eventHandler(event) + if event.type == 'key' then + local ch = event.key + if ch == 'left' then + if self.pos > 0 then + self.pos = math.max(self.pos-1, 0) + self:draw() + end + elseif ch == 'right' then + local input = tostring(self.value) + if self.pos < #input then + self.pos = math.min(self.pos+1, #input) + self:draw() + end + elseif ch == 'home' then + self.pos = 0 + self:draw() + elseif ch == 'end' then + self.pos = #tostring(self.value) + self:draw() + elseif ch == 'backspace' then + if self.pos > 0 then + local input = tostring(self.value) + self.value = input:sub(1, self.pos-1) .. input:sub(self.pos+1) + self.pos = self.pos - 1 + self:draw() + self:emit({ type = 'text_change', text = self.value, element = self }) + end + elseif ch == 'delete' then + local input = tostring(self.value) + if self.pos < #input then + self.value = input:sub(1, self.pos) .. input:sub(self.pos+2) + self:draw() + self:emit({ type = 'text_change', text = self.value, element = self }) + end + elseif #ch == 1 then + local input = tostring(self.value) + if #input < self.limit then + self.value = input:sub(1, self.pos) .. ch .. input:sub(self.pos+1) + self.pos = self.pos + 1 + self:draw() + self:emit({ type = 'text_change', text = self.value, element = self }) + end + else + return false + end + return true + + elseif event.type == 'copy' then + os.queueEvent('clipboard_copy', self.value) + + elseif event.type == 'paste' then + local input = tostring(self.value) + local text = event.text + if #input + #text > self.limit then + text = text:sub(1, self.limit-#input) + end + self.value = input:sub(1, self.pos) .. text .. input:sub(self.pos+1) + self.pos = self.pos + #text + self:draw() + self:updateCursor() + self:emit({ type = 'text_change', text = self.value, element = self }) + return true + + elseif event.type == 'mouse_click' then + if self.focused and event.x > 1 then + self.pos = event.x + self.scroll - 2 + self:updateCursor() + return true + end + elseif event.type == 'mouse_rightclick' then + local input = tostring(self.value) + if #input > 0 then + self:reset() + self:emit({ type = 'text_change', text = self.value, element = self }) + end + end + + return false +end diff --git a/sys/apis/ui/components/Throttle.lua b/sys/apis/ui/components/Throttle.lua new file mode 100644 index 0000000..1e2dc02 --- /dev/null +++ b/sys/apis/ui/components/Throttle.lua @@ -0,0 +1,65 @@ +local class = require('class') +local UI = require('ui') + +local colors = _G.colors +local os = _G.os + +UI.Throttle = class(UI.Window) +UI.Throttle.defaults = { + UIElement = 'Throttle', + backgroundColor = colors.gray, + bordercolor = colors.cyan, + height = 4, + width = 10, + timeout = .075, + ctr = 0, + image = { + ' //) (O )~@ &~&-( ?Q ', + ' //) (O )- @ \\-( ?) && ', + ' //) (O ), @ \\-(?) && ', + ' //) (O ). @ \\-d ) (@ ' + } +} +function UI.Throttle:setParent() + self.x = math.ceil((self.parent.width - self.width) / 2) + self.y = math.ceil((self.parent.height - self.height) / 2) + UI.Window.setParent(self) +end + +function UI.Throttle:enable() + self.c = os.clock() + self.enabled = false +end + +function UI.Throttle:disable() + if self.canvas then + self.enabled = false + self.canvas:removeLayer() + self.canvas = nil + self.ctr = 0 + end +end + +function UI.Throttle:update() + local cc = os.clock() + if cc > self.c + self.timeout then + os.sleep(0) + self.c = os.clock() + self.enabled = true + if not self.canvas then + self.canvas = self:addLayer(self.backgroundColor, self.borderColor) + self.canvas:setVisible(true) + self:clear(self.borderColor) + end + local image = self.image[self.ctr + 1] + local width = self.width - 2 + for i = 0, #self.image do + self:write(2, i + 1, image:sub(width * i + 1, width * i + width), + self.backgroundColor, self.textColor) + end + + self.ctr = (self.ctr + 1) % #self.image + + self:sync() + end +end diff --git a/sys/apis/ui/components/TitleBar.lua b/sys/apis/ui/components/TitleBar.lua new file mode 100644 index 0000000..0a464ab --- /dev/null +++ b/sys/apis/ui/components/TitleBar.lua @@ -0,0 +1,73 @@ +local class = require('class') +local UI = require('ui') + +local colors = _G.colors +local _rep = string.rep +local _sub = string.sub + +-- For manipulating text in a fixed width string +local SB = class() +function SB:init(width) + self.width = width + self.buf = _rep(' ', width) +end +function SB:insert(x, str, width) + if x < 1 then + x = self.width + x + 1 + end + width = width or #str + if x + width - 1 > self.width then + width = self.width - x + end + if width > 0 then + self.buf = _sub(self.buf, 1, x - 1) .. _sub(str, 1, width) .. _sub(self.buf, x + width) + end +end +function SB:fill(x, ch, width) + width = width or self.width - x + 1 + self:insert(x, _rep(ch, width)) +end +function SB:center(str) + self:insert(math.max(1, math.ceil((self.width - #str + 1) / 2)), str) +end +function SB:get() + return self.buf +end + +UI.TitleBar = class(UI.Window) +UI.TitleBar.defaults = { + UIElement = 'TitleBar', + height = 1, + textColor = colors.white, + backgroundColor = colors.cyan, + title = '', + frameChar = '-', + closeInd = '*', +} +function UI.TitleBar:draw() + local sb = SB(self.width) + sb:fill(2, self.frameChar, sb.width - 3) + sb:center(string.format(' %s ', self.title)) + if self.previousPage or self.event then + sb:insert(-1, self.closeInd) + else + sb:insert(-2, self.frameChar) + end + self:write(1, 1, sb:get()) +end + +function UI.TitleBar:eventHandler(event) + if event.type == 'mouse_click' then + if (self.previousPage or self.event) and event.x == self.width then + if self.event then + self:emit({ type = self.event, element = self }) + elseif type(self.previousPage) == 'string' or + type(self.previousPage) == 'table' then + UI:setPage(self.previousPage) + else + UI:setPreviousPage() + end + return true + end + end +end diff --git a/sys/apis/ui/components/VerticalMeter.lua b/sys/apis/ui/components/VerticalMeter.lua new file mode 100644 index 0000000..e4f1e7b --- /dev/null +++ b/sys/apis/ui/components/VerticalMeter.lua @@ -0,0 +1,18 @@ +local class = require('class') +local UI = require('ui') + +local colors = _G.colors + +UI.VerticalMeter = class(UI.Window) +UI.VerticalMeter.defaults = { + UIElement = 'VerticalMeter', + backgroundColor = colors.gray, + meterColor = colors.lime, + width = 1, + value = 0, +} +function UI.VerticalMeter:draw() + local height = self.height - math.ceil(self.value / 100 * self.height) + self:clear() + self:clearArea(1, height + 1, self.width, self.height, self.meterColor) +end diff --git a/sys/apis/ui/components/Viewport.lua b/sys/apis/ui/components/Viewport.lua new file mode 100644 index 0000000..e8289bd --- /dev/null +++ b/sys/apis/ui/components/Viewport.lua @@ -0,0 +1,96 @@ +local class = require('class') +local UI = require('ui') + +local colors = _G.colors + +--[[-- Viewport --]]-- +UI.Viewport = class(UI.Window) +UI.Viewport.defaults = { + UIElement = 'Viewport', + backgroundColor = colors.cyan, + accelerators = { + down = 'scroll_down', + up = 'scroll_up', + home = 'scroll_top', + [ 'end' ] = 'scroll_bottom', + pageUp = 'scroll_pageUp', + [ 'control-b' ] = 'scroll_pageUp', + pageDown = 'scroll_pageDown', + [ 'control-f' ] = 'scroll_pageDown', + }, +} +function UI.Viewport:setParent() + UI.Window.setParent(self) + self.canvas = self:addLayer() +end + +function UI.Viewport:enable() + UI.Window.enable(self) + self.canvas:setVisible(true) +end + +function UI.Viewport:disable() + UI.Window.disable(self) + self.canvas:setVisible(false) +end + +function UI.Viewport:setScrollPosition(offset) + local oldOffset = self.offy + self.offy = math.max(offset, 0) + self.offy = math.min(self.offy, math.max(#self.canvas.lines, self.height) - self.height) + if self.offy ~= oldOffset then + if self.scrollBar then + self.scrollBar:draw() + end + self.canvas.offy = offset + self.canvas:dirty() + end +end + +function UI.Viewport:write(x, y, text, bg, tc) + if y > #self.canvas.lines then + for i = #self.canvas.lines, y do + self.canvas.lines[i + 1] = { } + self.canvas:clearLine(i + 1, self.backgroundColor, self.textColor) + end + end + return UI.Window.write(self, x, y, text, bg, tc) +end + +function UI.Viewport:reset() + self.offy = 0 + self.canvas.offy = 0 + for i = self.height + 1, #self.canvas.lines do + self.canvas.lines[i] = nil + end +end + +function UI.Viewport:getViewArea() + return { + y = (self.offy or 0) + 1, + height = self.height, + totalHeight = #self.canvas.lines, + offsetY = self.offy or 0, + } +end + +function UI.Viewport:eventHandler(event) + if event.type == 'scroll_down' then + self:setScrollPosition(self.offy + 1) + elseif event.type == 'scroll_up' then + self:setScrollPosition(self.offy - 1) + elseif event.type == 'scroll_top' then + self:setScrollPosition(0) + elseif event.type == 'scroll_bottom' then + self:setScrollPosition(10000000) + elseif event.type == 'scroll_pageUp' then + self:setScrollPosition(self.offy - self.height) + elseif event.type == 'scroll_pageDown' then + self:setScrollPosition(self.offy + self.height) + elseif event.type == 'scroll_to' then + self:setScrollPosition(event.offset) + else + return false + end + return true +end diff --git a/sys/apis/ui/components/Wizard.lua b/sys/apis/ui/components/Wizard.lua new file mode 100644 index 0000000..67dcc2b --- /dev/null +++ b/sys/apis/ui/components/Wizard.lua @@ -0,0 +1,124 @@ +local class = require('class') +local UI = require('ui') +local Util = require('util') + +UI.Wizard = class(UI.Window) +UI.Wizard.defaults = { + UIElement = 'Wizard', + pages = { }, +} +function UI.Wizard:postInit() + self.cancelButton = UI.Button { + x = 2, y = -1, + text = 'Cancel', + event = 'cancel', + } + self.previousButton = UI.Button { + x = -18, y = -1, + text = '< Back', + event = 'previousView', + } + self.nextButton = UI.Button { + x = -9, y = -1, + text = 'Next >', + event = 'nextView', + } + + Util.merge(self, self.pages) + --for _, child in pairs(self.pages) do + -- child.ey = -2 + --end +end + +function UI.Wizard:add(pages) + Util.merge(self.pages, pages) + Util.merge(self, pages) + + for _, child in pairs(self.pages) do + child.ey = child.ey or -2 + end + + if self.parent then + self:initChildren() + end +end + +function UI.Wizard:getPage(index) + return Util.find(self.pages, 'index', index) +end + +function UI.Wizard:enable(...) + self.enabled = true + self.index = 1 + self.transitionHint = nil + local initial = self:getPage(1) + for _,child in pairs(self.children) do + if child == initial or not child.index then + child:enable(...) + else + child:disable() + end + end + self:emit({ type = 'enable_view', next = initial }) +end + +function UI.Wizard:isViewValid() + local currentView = self:getPage(self.index) + return not currentView.validate and true or currentView:validate() +end + +function UI.Wizard:eventHandler(event) + if event.type == 'nextView' then + local currentView = self:getPage(self.index) + if self:isViewValid() then + self.index = self.index + 1 + local nextView = self:getPage(self.index) + currentView:emit({ type = 'enable_view', next = nextView, current = currentView }) + end + + elseif event.type == 'previousView' then + local currentView = self:getPage(self.index) + local nextView = self:getPage(self.index - 1) + if nextView then + self.index = self.index - 1 + currentView:emit({ type = 'enable_view', prev = nextView, current = currentView }) + end + return true + + elseif event.type == 'wizard_complete' then + if self:isViewValid() then + self:emit({ type = 'accept' }) + end + + elseif event.type == 'enable_view' then + local current = event.next or event.prev + if not current then error('property "index" is required on wizard pages') end + + if event.current then + if event.next then + self.transitionHint = 'slideLeft' + elseif event.prev then + self.transitionHint = 'slideRight' + end + event.current:disable() + end + + if self:getPage(self.index - 1) then + self.previousButton:enable() + else + self.previousButton:disable() + end + + if self:getPage(self.index + 1) then + self.nextButton.text = 'Next >' + self.nextButton.event = 'nextView' + else + self.nextButton.text = 'Accept' + self.nextButton.event = 'wizard_complete' + end + -- a new current view + current:enable() + current:emit({ type = 'view_enabled', view = current }) + self:draw() + end +end diff --git a/sys/apis/ui/components/WizardPage.lua b/sys/apis/ui/components/WizardPage.lua new file mode 100644 index 0000000..dae4e9a --- /dev/null +++ b/sys/apis/ui/components/WizardPage.lua @@ -0,0 +1,11 @@ +local class = require('class') +local UI = require('ui') + +local colors = _G.colors + +UI.WizardPage = class(UI.ActiveLayer) +UI.WizardPage.defaults = { + UIElement = 'WizardPage', + backgroundColor = colors.cyan, + ey = -2, +} diff --git a/sys/apis/ui/transition.lua b/sys/apis/ui/transition.lua index f6c7af3..90db9ee 100644 --- a/sys/apis/ui/transition.lua +++ b/sys/apis/ui/transition.lua @@ -3,35 +3,33 @@ local Tween = require('ui.tween') local Transition = { } function Transition.slideLeft(args) - local ticks = args.ticks or 6 + local ticks = args.ticks or 10 local easing = args.easing or 'outQuint' local pos = { x = args.ex } local tween = Tween.new(ticks, pos, { x = args.x }, easing) args.canvas:move(pos.x, args.canvas.y) - return function(device) + return function() local finished = tween:update(1) args.canvas:move(math.floor(pos.x), args.canvas.y) args.canvas:dirty() - args.canvas:render(device) return not finished end end function Transition.slideRight(args) - local ticks = args.ticks or 6 + local ticks = args.ticks or 10 local easing = args.easing or'outQuint' local pos = { x = -args.canvas.width } local tween = Tween.new(ticks, pos, { x = 1 }, easing) args.canvas:move(pos.x, args.canvas.y) - return function(device) + return function() local finished = tween:update(1) args.canvas:move(math.floor(pos.x), args.canvas.y) args.canvas:dirty() - args.canvas:render(device) return not finished end end @@ -44,11 +42,10 @@ function Transition.expandUp(args) args.canvas:move(args.x, pos.y) - return function(device) + return function() local finished = tween:update(1) args.canvas:move(args.x, math.floor(pos.y)) args.canvas:dirty() - args.canvas:render(device) return not finished end end diff --git a/sys/apps/Installer.lua b/sys/apps/Installer.lua deleted file mode 100644 index da576a5..0000000 --- a/sys/apps/Installer.lua +++ /dev/null @@ -1,457 +0,0 @@ -local colors = _G.colors -local fs = _G.fs -local http = _G.http -local install = _ENV.install -local os = _G.os - -local injector -if not install.testing then - _G.OPUS_BRANCH = 'master-1.8' - local url ='https://raw.githubusercontent.com/kepler155c/opus/master-1.8/sys/apis/injector.lua' - injector = load(http.get(url).readAll(), 'injector.lua', nil, _ENV)() -else - injector = _G.requireInjector -end - -injector(_ENV) - -if not install.testing then - if package then - for _ = 1, 4 do - table.remove(package.loaders, 1) - end - end -end - -local Git = require('git') -local UI = require('ui') -local Util = require('util') - -local currentFile = '' -local currentProgress = 0 -local cancelEvent - -local args = { ... } -local steps = install.steps[args[1] or 'install'] - -if not steps then - error('Invalid install type') -end - -local mode = steps[#steps] - -if UI.term.width < 32 then - cancelEvent = 'quit' -end - -local page = UI.Page { - backgroundColor = colors.cyan, - titleBar = UI.TitleBar { - event = cancelEvent, - }, - wizard = UI.Wizard { - y = 2, ey = -2, - }, - notification = UI.Notification(), - accelerators = { - q = 'quit', - }, -} - -local pages = { - splash = UI.Viewport { }, - review = UI.Viewport { }, - license = UI.Viewport { - backgroundColor = colors.black, - }, - branch = UI.Window { - grid = UI.ScrollingGrid { - ey = -3, - columns = { - { heading = 'Branch', key = 'branch' }, - { heading = 'Description', key = 'description' }, - }, - values = install.branches, - autospace = true, - }, - }, - files = UI.Window { - grid = UI.ScrollingGrid { - ey = -3, - columns = { - { heading = 'Files', key = 'file' }, - }, - sortColumn = 'file', - }, - }, - install = UI.Window { - progressBar = UI.ProgressBar { - y = -1, - }, - }, - uninstall = UI.Window { - progressBar = UI.ProgressBar { - y = -1, - }, - }, -} - -local function getFileList() - if install.gitRepo then - local gitFiles = Git.list(string.format('%s/%s', install.gitRepo, install.gitBranch or 'master')) - install.files = { } - install.diskspace = 0 - for path, entry in pairs(gitFiles) do - install.files[path] = entry.url - install.diskspace = install.diskspace + entry.size - end - end - - if not install.files or Util.empty(install.files) then - error('File list is missing or empty') - end -end - ---[[ Splash ]]-- -function pages.splash:enable() - page.titleBar.title = 'Installer v1.0' - UI.Viewport.enable(self) -end - -function pages.splash:draw() - self:clear() - self:setCursorPos(1, 1) - self:print( - string.format('%s v%s\n', install.title, install.version), nil, colors.yellow) - self:print( - string.format('By: %s\n\n%s\n', install.author, install.description)) - - self.ymax = self.cursorY -end - ---[[ License ]]-- -function pages.license:enable() - page.titleBar.title = 'License Review' - page.wizard.nextButton.text = 'Accept' - UI.Viewport.enable(self) -end - -function pages.license:draw() - self:clear() - self:setCursorPos(1, 1) - self:print( - string.format('Copyright (c) %s %s\n\n', install.copyrightYear, - install.copyrightHolders), - nil, colors.yellow) - self:print(install.license) - - self.ymax = self.cursorY + 1 -end - ---[[ Review ]]-- -function pages.review:enable() - if mode == 'uninstall' then - page.nextButton.text = 'Remove' - page.titleBar.title = 'Remove Installed Files' - else - page.wizard.nextButton.text = 'Begin' - page.titleBar.title = 'Download and Install' - end - UI.Viewport.enable(self) -end - -function pages.review:draw() - self:clear() - self:setCursorPos(1, 1) - - local text = 'Ready to begin installation.\n\nProceeding will download and install the files to the hard drive.' - if mode == 'uninstall' then - text = 'Ready to begin.\n\nProceeding will remove the files previously installed.' - end - self:print(text) - - self.ymax = self.cursorY + 1 -end - ---[[ Files ]]-- -function pages.files:enable() - page.titleBar.title = 'Review Files' - self.grid.values = { } - for k,v in pairs(install.files) do - table.insert(self.grid.values, { file = k, code = v }) - end - self.grid:update() - UI.Window.enable(self) -end - -function pages.files:draw() - self:clear() - - local function formatSize(size) - if size >= 1000000 then - return string.format('%dM', math.floor(size/1000000, 2)) - elseif size >= 1000 then - return string.format('%dK', math.floor(size/1000, 2)) - end - return size - end - - if install.diskspace then - - local bg = self.backgroundColor - - local diskFree = fs.getFreeSpace('/') - if install.diskspace > diskFree then - bg = colors.red - end - - local text = string.format('Space Required: %s, Free: %s', - formatSize(install.diskspace), formatSize(diskFree)) - - if #text > self.width then - text = string.format('Space: %s Free: %s', - formatSize(install.diskspace), formatSize(diskFree)) - end - - self:write(1, self.height, Util.widthify(text, self.width), bg) - end - self.grid:draw() -end - ---[[ -function pages.files:view(url) - local s, m = pcall(function() - page.notification:info('Downloading') - page:sync() - Util.download(url, '/.source') - end) - page.notification:disable() - if s then - shell.run('edit /.source') - fs.delete('/.source') - page:draw() - page.notification:cancel() - else - page.notification:error(m:gsub('.*: (.*)', '%1')) - end -end - -function pages.files:eventHandler(event) - if event.type == 'grid_select' then - self:view(event.selected.code) - return true - end -end ---]] - -local function drawCommon(self) - if currentFile then - self:write(1, 3, 'File:') - self:write(1, 4, Util.widthify(currentFile, self.width)) - else - self:write(1, 3, 'Finished') - end - if self.failed then - self:write(1, 5, Util.widthify(self.failed, self.width), colors.red) - end - self:write(1, self.height - 1, 'Progress') - - self.progressBar.value = currentProgress - self.progressBar:draw() - self:sync() -end - ---[[ Branch ]]-- -function pages.branch:enable() - page.titleBar.title = 'Select Branch' - UI.Window.enable(self) -end - -function pages.branch:eventHandler(event) - -- user is navigating to next view (not previous) - if event.type == 'enable_view' and event.next then - install.gitBranch = self.grid:getSelected().branch - getFileList() - end -end - ---[[ Install ]]-- -function pages.install:enable() - page.wizard.cancelButton:disable() - page.wizard.previousButton:disable() - page.wizard.nextButton:disable() - - page.titleBar.title = 'Installing...' - page.titleBar.event = nil - - UI.Window.enable(self) - - page:draw() - page:sync() - - local i = 0 - local numFiles = Util.size(install.files) - for filename,url in pairs(install.files) do - currentFile = filename - currentProgress = i / numFiles * 100 - self:draw(self) - self:sync() - local s, m = pcall(function() - Util.download(url, fs.combine(install.directory or '', filename)) - end) - if not s then - self.failed = m:gsub('.*: (.*)', '%1') - break - end - i = i + 1 - end - - if not self.failed then - currentProgress = 100 - currentFile = nil - - if install.postInstall then - local s, m = pcall(function() install.postInstall(page, UI) end) - if not s then - self.failed = m:gsub('.*: (.*)', '%1') - end - end - end - - page.wizard.nextButton.text = 'Exit' - page.wizard.nextButton.event = 'quit' - if not self.failed and install.rebootAfter then - page.wizard.nextButton.text = 'Reboot' - page.wizard.nextButton.event = 'reboot' - end - - page.wizard.nextButton:enable() - page:draw() - page:sync() - - if not self.failed and Util.key(args, 'automatic') then - if install.rebootAfter then - os.reboot() - else - UI:exitPullEvents() - end - end -end - -function pages.install:draw() - self:clear() - local text = 'The files are being installed' - if #text > self.width then - text = 'Installing files' - end - self:write(1, 1, text, nil, colors.yellow) - - drawCommon(self) -end - ---[[ Uninstall ]]-- -function pages.uninstall:enable() - page.wizard.cancelButton:disable() - page.wizard.previousButton:disable() - page.wizard.nextButton:disable() - - page.titleBar.title = 'Uninstalling...' - page.titleBar.event = nil - - page:draw() - page:sync() - - UI.Window.enable(self) - - local function pruneDir(dir) - if #dir > 0 then - if fs.exists(dir) then - local files = fs.list(dir) - if #files == 0 then - fs.delete(dir) - pruneDir(fs.getDir(dir)) - end - end - end - end - - local i = 0 - local numFiles = Util.size(install.files) - for k in pairs(install.files) do - currentFile = k - currentProgress = i / numFiles * 100 - self:draw() - self:sync() - fs.delete(k) - pruneDir(fs.getDir(k)) - i = i + 1 - end - - currentProgress = 100 - currentFile = nil - - page.wizard.nextButton.text = 'Exit' - page.wizard.nextButton.event = 'quit' - page.wizard.nextButton:enable() - - page:draw() - page:sync() -end - -function pages.uninstall:draw() - self:clear() - self:write(1, 1, 'Uninstalling files', nil, colors.yellow) - drawCommon(self) -end - -function page:eventHandler(event) - if event.type == 'cancel' then - UI:exitPullEvents() - - elseif event.type == 'reboot' then - os.reboot() - - elseif event.type == 'quit' then - UI:exitPullEvents() - - else - return UI.Page.eventHandler(self, event) - end - return true -end - -function page:enable() - UI.Page.enable(self) - self:setFocus(page.wizard.nextButton) - if UI.term.width < 32 then - page.wizard.cancelButton:disable() - page.wizard.previousButton.x = 2 - end -end - -getFileList() - -local wizardPages = { } -for k,v in ipairs(steps) do - if not pages[v] then - error('Invalid step: ' .. v) - end - wizardPages[k] = pages[v] - wizardPages[k].index = k - wizardPages[k].x = 2 - wizardPages[k].y = 2 - wizardPages[k].ey = -3 - wizardPages[k].ex = -2 -end -page.wizard:add(wizardPages) - -if Util.key(steps, 'install') and install.preInstall then - install.preInstall(page, UI) -end - -UI:setPage(page) -local s, m = pcall(function() UI:pullEvents() end) -if not s then - UI.term:reset() - _G.printError(m) -end diff --git a/sys/apps/Lua.lua b/sys/apps/Lua.lua index f4653a2..78e3a13 100644 --- a/sys/apps/Lua.lua +++ b/sys/apps/Lua.lua @@ -58,6 +58,7 @@ local page = UI.Page { }, output = UI.Embedded { y = -6, + visible = true, backgroundColor = colors.gray, }, } @@ -328,6 +329,7 @@ function page:executeStatement(statement) local s, m local oterm = term.redirect(self.output.win) + self.output.win.scrollBottom() pcall(function() s, m = self:rawExecute(command) end) diff --git a/sys/apps/Overview.lua b/sys/apps/Overview.lua index 2fb9871..797a1a7 100644 --- a/sys/apps/Overview.lua +++ b/sys/apps/Overview.lua @@ -252,7 +252,7 @@ end function page.container:setCategory(categoryName, animate) -- reset the viewport window self.children = { } - self.offy = 0 + self:reset() local function filter(it, f) local ot = { } @@ -334,10 +334,10 @@ function page.container:setCategory(categoryName, animate) for k,child in ipairs(self.children) do if r == 1 then child.x = math.random(1, self.width) - child.y = math.random(1, self.height) + child.y = math.random(1, self.height - 3) elseif r == 2 then child.x = self.width - child.y = self.height + child.y = self.height - 3 elseif r == 3 then child.x = math.floor(self.width / 2) child.y = math.floor(self.height / 2) @@ -349,7 +349,7 @@ function page.container:setCategory(categoryName, animate) child.y = row if k == #self.children then child.x = self.width - child.y = self.height + child.y = self.height - 3 end end child.tween = Tween.new(6, child, { x = col, y = row }, 'linear') @@ -369,10 +369,10 @@ function page.container:setCategory(categoryName, animate) end self:initChildren() - if animate then -- need to fix transitions under layers - local function transition(args) + if animate then + local function transition() local i = 1 - return function(device) + return function() self:clear() for _,child in pairs(self.children) do child.tween:update(1) @@ -380,7 +380,6 @@ function page.container:setCategory(categoryName, animate) child.y = math.floor(child.y) child:draw() end - args.canvas:blit(device, args, args) i = i + 1 return i < 7 end diff --git a/sys/apps/PackageManager.lua b/sys/apps/PackageManager.lua index a52964f..ea4bab9 100644 --- a/sys/apps/PackageManager.lua +++ b/sys/apps/PackageManager.lua @@ -45,8 +45,10 @@ local page = UI.Page { help = 'Download the latest package list', }, action = UI.SlideOut { - backgroundColor = colors.cyan, + backgroundColor = colors.brown, + y = 3, titleBar = UI.TitleBar { + backgroundColor = colors.brown, event = 'hide-action', }, button = UI.Button { @@ -56,9 +58,6 @@ local page = UI.Page { output = UI.Embedded { y = 5, ey = -2, x = 2, ex = -2, }, - statusBar = UI.StatusBar { - backgroundColor = colors.cyan, - }, }, statusBar = UI.StatusBar { }, } @@ -101,9 +100,10 @@ function page.grid:getRowTextColor(row, selected) end function page.action:show() + self.output.win:clear() UI.SlideOut.show(self) - self.output:draw() - self.output.win.redraw() + --self.output:draw() + --self.output.win.redraw() end function page:run(operation, name) @@ -116,6 +116,7 @@ function page:run(operation, name) print(cmd .. '\n') term.setTextColor(colors.white) local s, m = Util.run(_ENV, '/sys/apps/package.lua', operation, name) + if not s and m then _G.printError(m) end diff --git a/sys/apps/Welcome.lua b/sys/apps/Welcome.lua index 737a999..2648cd6 100644 --- a/sys/apps/Welcome.lua +++ b/sys/apps/Welcome.lua @@ -24,7 +24,7 @@ local page = UI.Page { wizard = UI.Wizard { ey = -2, pages = { - splash = UI.Window { + splash = UI.WizardPage { index = 1, intro = UI.TextArea { textColor = colors.yellow, @@ -33,7 +33,7 @@ local page = UI.Page { value = string.format(splashIntro, Ansi.white), }, }, - label = UI.Window { + label = UI.WizardPage { index = 2, labelText = UI.Text { x = 3, y = 2, @@ -51,7 +51,7 @@ local page = UI.Page { value = string.format(labelIntro, Ansi.white), }, }, - password = UI.Window { + password = UI.WizardPage { index = 3, labelText = UI.Text { x = 3, y = 2, @@ -73,7 +73,7 @@ local page = UI.Page { value = string.format(passwordIntro, Ansi.white), }, }, - packages = UI.Window { + packages = UI.WizardPage { index = 4, button = UI.Button { x = 3, y = -3, diff --git a/sys/apps/shell b/sys/apps/shell index 24805dd..3877073 100644 --- a/sys/apps/shell +++ b/sys/apps/shell @@ -361,16 +361,21 @@ local Config = require('config') local Entry = require('entry') local History = require('history') local Input = require('input') -local Terminal = require('terminal') local colors = _G.colors local os = _G.os local term = _G.term local textutils = _G.textutils +local oldTerm local terminal = term.current() ---Terminal.scrollable(terminal, 100) -terminal.noAutoScroll = true + +if not terminal.scrollUp then + local Terminal = require('terminal') + terminal = Terminal.window(term.current()) + terminal.setMaxScroll(200) + oldTerm = term.redirect(terminal) +end local config = { standard = { @@ -555,6 +560,9 @@ local function shellRead(history) term.setCursorBlink(true) local function redraw() + if terminal.scrollBottom then + terminal.scrollBottom() + end local _,cy = term.getCursorPos() term.setCursorPos(3, cy) local filler = #entry.value < lastLen @@ -571,11 +579,11 @@ local function shellRead(history) local ie = Input:translate(event, p1, p2, p3) if ie then - if ie.code == 'scroll_up' then - --terminal.scrollUp() + if ie.code == 'scroll_up' and terminal.scrollUp then + terminal.scrollUp() - elseif ie.code == 'scroll_down' then - --terminal.scrollDown() + elseif ie.code == 'scroll_down' and terminal.scrollDown then + terminal.scrollDown() elseif ie.code == 'terminate' then bExit = true @@ -652,3 +660,7 @@ while not bExit do end end end + +if oldTerm then + term.redirect(oldTerm) +end diff --git a/sys/autorun/log.lua b/sys/autorun/log.lua index 224d6e9..fe8de05 100644 --- a/sys/autorun/log.lua +++ b/sys/autorun/log.lua @@ -25,11 +25,13 @@ local function systemLog() if y > 1 then local currentTab = kernel.getFocused() - if currentTab.terminal.scrollUp and not currentTab.terminal.noAutoScroll then - if dir == -1 then - currentTab.terminal.scrollUp() - else - currentTab.terminal.scrollDown() + if currentTab == routine then + if currentTab.terminal.scrollUp and not currentTab.terminal.noAutoScroll then + if dir == -1 then + currentTab.terminal.scrollUp() + else + currentTab.terminal.scrollDown() + end end end end diff --git a/sys/kernel.lua b/sys/kernel.lua index cbd6806..407f217 100644 --- a/sys/kernel.lua +++ b/sys/kernel.lua @@ -14,13 +14,12 @@ local kernel = _G.kernel local os = _G.os local shell = _ENV.shell local term = _G.term -local window = _G.window local w, h = term.getSize() kernel.terminal = term.current() -kernel.window = window.create(kernel.terminal, 1, 1, w, h, false) -Terminal.scrollable(kernel.window) +kernel.window = Terminal.window(kernel.terminal, 1, 1, w, h, false) +kernel.window.setMaxScroll(100) local focusedRoutineEvents = Util.transpose { 'char', 'key', 'key_up', @@ -30,6 +29,7 @@ local focusedRoutineEvents = Util.transpose { _G._debug = function(pattern, ...) local oldTerm = term.redirect(kernel.window) + kernel.window.scrollBottom() Util.print(pattern, ...) term.redirect(oldTerm) end