1
0
mirror of https://github.com/kepler155c/opus synced 2024-11-15 05:04:50 +00:00
opus/sys/apis/ui.lua

1209 lines
26 KiB
Lua
Raw Normal View History

local Canvas = require('ui.canvas')
local class = require('class')
local Event = require('event')
2017-10-15 23:55:05 +00:00
local Input = require('input')
2018-01-06 11:07:49 +00:00
local Peripheral = require('peripheral')
local Transition = require('ui.transition')
local Util = require('util')
2016-12-11 19:24:52 +00:00
2017-10-11 15:37:52 +00:00
local _rep = string.rep
local _sub = string.sub
local colors = _G.colors
local device = _G.device
local fs = _G.fs
local os = _G.os
local term = _G.term
2016-12-11 19:24:52 +00:00
2017-10-09 04:26:19 +00:00
--[[
2018-01-24 22:39:38 +00:00
Using the shorthand window definition, elements are created from
the bottom up. Once reaching the top, setParent is called top down.
2017-10-09 04:26:19 +00:00
2018-01-24 22:39:38 +00:00
On :init(), elements do not know the parent or can calculate sizing.
2017-10-09 04:26:19 +00:00
]]
2016-12-11 19:24:52 +00:00
-- need to add offsets to this test
local function getPosition(element)
2018-01-24 22:39:38 +00:00
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
2016-12-11 19:24:52 +00:00
end
--[[-- Top Level Manager --]]--
local Manager = class()
function Manager:init()
2019-01-02 15:33:47 +00:00
self.devices = { }
2019-01-03 04:56:01 +00:00
2018-01-24 22:39:38 +00:00
local function keyFunction(event, code, held)
local ie = Input:translate(event, code, held)
2019-01-02 15:33:47 +00:00
local currentPage = self:getActivePage()
if ie and currentPage then
local target = currentPage.focused or currentPage
2018-01-24 22:39:38 +00:00
self:inputEvent(target,
{ type = 'key', key = ie.code == 'char' and ie.ch or ie.code, element = target })
2019-01-02 15:33:47 +00:00
currentPage:sync()
end
end
local function resize(_, side)
local dev = self.devices[side or 'terminal']
if dev and dev.currentPage then
-- the parent doesn't have any children set...
-- that's why we have to resize both the parent and the current page
-- kinda makes sense
dev.currentPage.parent:resize()
dev.currentPage:resize()
dev.currentPage:draw()
dev.currentPage:sync()
2018-01-24 22:39:38 +00:00
end
end
local handlers = {
char = keyFunction,
key_up = keyFunction,
key = keyFunction,
2019-01-02 15:33:47 +00:00
term_resize = resize,
monitor_resize = resize,
2018-01-24 22:39:38 +00:00
mouse_scroll = function(_, direction, x, y)
2019-01-02 15:33:47 +00:00
local currentPage = self:getActivePage()
if currentPage then
local event = currentPage:pointToChild(x, y)
2018-01-24 22:39:38 +00:00
local directions = {
[ -1 ] = 'up',
[ 1 ] = 'down'
}
-- revisit - should send out scroll_up and scroll_down events
-- let the element convert them to up / down
self:inputEvent(event.element,
{ type = 'key', key = directions[direction] })
2019-01-02 15:33:47 +00:00
currentPage:sync()
2018-01-24 22:39:38 +00:00
end
end,
monitor_touch = function(_, side, x, y)
2019-01-02 15:33:47 +00:00
local dev = self.devices[side]
if dev and dev.currentPage then
2019-01-09 13:50:42 +00:00
Input:translate('mouse_click', 1, x, y)
local ie = Input:translate('mouse_up', 1, x, y)
2019-01-02 15:33:47 +00:00
self:click(dev.currentPage, ie.code, 1, x, y)
2018-01-24 22:39:38 +00:00
end
end,
mouse_click = function(_, button, x, y)
Input:translate('mouse_click', button, x, y)
2019-01-02 15:33:47 +00:00
local currentPage = self:getActivePage()
if currentPage then
if not currentPage.parent.device.side then
local event = currentPage:pointToChild(x, y)
2018-01-24 22:39:38 +00:00
if event.element.focus and not event.element.inactive then
2019-01-02 15:33:47 +00:00
currentPage:setFocus(event.element)
currentPage:sync()
2018-01-24 22:39:38 +00:00
end
end
end
end,
mouse_up = function(_, button, x, y)
local ie = Input:translate('mouse_up', button, x, y)
2019-01-02 15:33:47 +00:00
local currentPage = self:getActivePage()
2018-01-24 22:39:38 +00:00
if ie.code == 'control-shift-mouse_click' then -- hack
2019-01-02 15:33:47 +00:00
local event = currentPage:pointToChild(x, y)
2018-01-24 22:39:38 +00:00
_ENV.multishell.openTab({
path = 'sys/apps/Lua.lua',
args = { event.element },
focused = true })
2019-01-02 15:33:47 +00:00
elseif ie and currentPage then
2018-01-24 22:39:38 +00:00
--if not self.currentPage.parent.device.side then
2019-01-02 15:33:47 +00:00
self:click(currentPage, ie.code, button, x, y)
2018-01-24 22:39:38 +00:00
--end
end
end,
mouse_drag = function(_, button, x, y)
local ie = Input:translate('mouse_drag', button, x, y)
2019-01-02 15:33:47 +00:00
local currentPage = self:getActivePage()
if ie and currentPage then
local event = currentPage:pointToChild(x, y)
2018-01-24 22:39:38 +00:00
event.type = ie.code
self:inputEvent(event.element, event)
2019-01-02 15:33:47 +00:00
currentPage:sync()
2018-01-24 22:39:38 +00:00
end
end,
paste = function(_, text)
Input:translate('paste')
self:emitEvent({ type = 'paste', text = text })
2019-01-02 15:33:47 +00:00
self:getActivePage():sync()
2018-01-24 22:39:38 +00:00
end,
}
-- use 1 handler to single thread all events
Event.on({
2019-01-02 15:33:47 +00:00
'char', 'key_up', 'key', 'term_resize', 'monitor_resize',
2018-01-24 22:39:38 +00:00
'mouse_scroll', 'monitor_touch', 'mouse_click',
'mouse_up', 'mouse_drag', 'paste' },
function(event, ...)
handlers[event](event, ...)
end)
2016-12-11 19:24:52 +00:00
end
function Manager:configure(appName, ...)
2018-01-24 22:39:38 +00:00
local options = {
device = { arg = 'd', type = 'string',
desc = 'Device type' },
textScale = { arg = 't', type = 'number',
desc = 'Text scale' },
}
local defaults = Util.loadTable('usr/config/' .. appName) or { }
if not defaults.device then
defaults.device = { }
end
Util.getOptions(options, { ... }, true)
local optionValues = {
name = options.device.value,
textScale = options.textScale.value,
}
Util.merge(defaults.device, optionValues)
if defaults.device.name then
local dev
if defaults.device.name == 'terminal' then
dev = term.current()
else
dev = Peripheral.lookup(defaults.device.name) --- device[defaults.device.name]
end
if not dev then
error('Invalid display device')
end
self:setDefaultDevice(self.Device({
device = dev,
textScale = defaults.device.textScale,
}))
end
if defaults.theme then
for k,v in pairs(defaults.theme) do
if self[k] and self[k].defaults then
Util.merge(self[k].defaults, v)
end
end
end
2016-12-11 19:24:52 +00:00
end
2016-12-15 14:45:27 +00:00
function Manager:disableEffects()
2018-01-24 22:39:38 +00:00
self.defaultDevice.effectsEnabled = false
2016-12-15 14:45:27 +00:00
end
2016-12-11 19:24:52 +00:00
function Manager:loadTheme(filename)
2018-01-24 22:39:38 +00:00
if fs.exists(filename) then
local theme, err = Util.loadTable(filename)
if not theme then
error(err)
end
2019-02-06 04:03:57 +00:00
self.theme = theme
2018-01-24 22:39:38 +00:00
end
2016-12-11 19:24:52 +00:00
end
function Manager:emitEvent(event)
2019-01-02 15:33:47 +00:00
local currentPage = self:getActivePage()
if currentPage and currentPage.focused then
return currentPage.focused:emit(event)
2018-01-24 22:39:38 +00:00
end
2016-12-11 19:24:52 +00:00
end
function Manager:inputEvent(parent, event)
2018-01-24 22:39:38 +00:00
while parent do
if parent.accelerators then
local acc = parent.accelerators[event.key]
if acc then
if parent:emit({ type = acc, element = parent }) then
return true
end
end
end
if parent.eventHandler then
if parent:eventHandler(event) then
return true
end
end
parent = parent.parent
end
2016-12-11 19:24:52 +00:00
end
2019-01-02 15:33:47 +00:00
function Manager:click(target, code, button, x, y)
local clickEvent = target:pointToChild(x, y)
2018-01-24 22:39:38 +00:00
2019-01-02 15:33:47 +00:00
if code == 'mouse_doubleclick' then
if self.doubleClickElement ~= clickEvent.element then
return
2018-01-24 22:39:38 +00:00
end
2019-01-02 15:33:47 +00:00
else
self.doubleClickElement = clickEvent.element
end
2018-01-24 22:39:38 +00:00
2019-01-02 15:33:47 +00:00
clickEvent.button = button
clickEvent.type = code
clickEvent.key = code
2018-01-24 22:39:38 +00:00
2019-01-02 15:33:47 +00:00
if clickEvent.element.focus then
target:setFocus(clickEvent.element)
2018-01-24 22:39:38 +00:00
end
2019-01-02 15:33:47 +00:00
self:inputEvent(clickEvent.element, clickEvent)
target:sync()
2016-12-11 19:24:52 +00:00
end
2017-10-11 15:37:52 +00:00
function Manager:setDefaultDevice(dev)
2018-01-24 22:39:38 +00:00
self.defaultDevice = dev
self.term = dev
2016-12-11 19:24:52 +00:00
end
function Manager:addPage(name, page)
2018-10-21 22:48:08 +00:00
if not self.pages then
self.pages = { }
end
2018-01-24 22:39:38 +00:00
self.pages[name] = page
2016-12-11 19:24:52 +00:00
end
function Manager:setPages(pages)
2018-01-24 22:39:38 +00:00
self.pages = pages
2016-12-11 19:24:52 +00:00
end
function Manager:getPage(pageName)
2018-01-24 22:39:38 +00:00
local page = self.pages[pageName]
2016-12-11 19:24:52 +00:00
2018-01-24 22:39:38 +00:00
if not page then
error('UI:getPage: Invalid page: ' .. tostring(pageName), 2)
end
2016-12-11 19:24:52 +00:00
2018-01-24 22:39:38 +00:00
return page
2016-12-11 19:24:52 +00:00
end
2019-01-02 15:33:47 +00:00
function Manager:getActivePage(page)
if page then
return page.parent.currentPage
end
return self.defaultDevice.currentPage
end
function Manager:setActivePage(page)
page.parent.currentPage = page
2019-01-30 20:11:41 +00:00
page.parent.canvas = page.canvas
2019-01-02 15:33:47 +00:00
end
2016-12-11 19:24:52 +00:00
function Manager:setPage(pageOrName, ...)
2018-01-24 22:39:38 +00:00
local page = pageOrName
if type(pageOrName) == 'string' then
page = self.pages[pageOrName] or error('Invalid page: ' .. pageOrName)
end
2019-01-02 15:33:47 +00:00
local currentPage = self:getActivePage(page)
if page == currentPage then
2018-01-24 22:39:38 +00:00
page:draw()
else
2019-01-02 15:33:47 +00:00
if currentPage then
if currentPage.focused then
currentPage.focused.focused = false
currentPage.focused:focus()
2018-01-24 22:39:38 +00:00
end
2019-01-02 15:33:47 +00:00
currentPage:disable()
page.previousPage = currentPage
2018-01-24 22:39:38 +00:00
end
2019-01-02 15:33:47 +00:00
self:setActivePage(page)
2019-01-30 20:11:41 +00:00
--page:clear(page.backgroundColor)
2018-01-24 22:39:38 +00:00
page:enable(...)
page:draw()
2019-01-02 15:33:47 +00:00
if page.focused then
page.focused.focused = true
page.focused:focus()
2018-01-24 22:39:38 +00:00
end
2019-01-30 20:11:41 +00:00
page:sync()
2018-01-24 22:39:38 +00:00
end
2016-12-11 19:24:52 +00:00
end
function Manager:getCurrentPage()
2019-01-02 15:33:47 +00:00
return self.defaultDevice.currentPage
2016-12-11 19:24:52 +00:00
end
function Manager:setPreviousPage()
2019-01-02 15:33:47 +00:00
if self.defaultDevice.currentPage.previousPage then
local previousPage = self.defaultDevice.currentPage.previousPage.previousPage
self:setPage(self.defaultDevice.currentPage.previousPage)
self.defaultDevice.currentPage.previousPage = previousPage
2018-01-24 22:39:38 +00:00
end
2016-12-11 19:24:52 +00:00
end
function Manager:getDefaults(element, args)
2018-01-24 22:39:38 +00:00
local defaults = Util.deepCopy(element.defaults)
if args then
Manager:mergeProperties(defaults, args)
end
return defaults
2016-12-11 19:24:52 +00:00
end
2017-10-12 21:58:35 +00:00
function Manager:mergeProperties(obj, args)
2018-01-24 22:39:38 +00:00
if args then
for k,v in pairs(args) do
if k == 'accelerators' then
if obj.accelerators then
Util.merge(obj.accelerators, args.accelerators)
else
obj[k] = v
end
else
obj[k] = v
end
end
end
2016-12-11 19:24:52 +00:00
end
2017-10-12 21:58:35 +00:00
function Manager:pullEvents(...)
2018-01-24 22:39:38 +00:00
Event.pullEvents(...)
self.term:reset()
2017-10-12 21:58:35 +00:00
end
function Manager:exitPullEvents()
2018-01-24 22:39:38 +00:00
Event.exitPullEvents()
2017-10-12 21:58:35 +00:00
end
2016-12-11 19:24:52 +00:00
local UI = Manager()
--[[-- Basic drawable area --]]--
UI.Window = class()
2017-10-11 15:37:52 +00:00
UI.Window.uid = 1
2016-12-11 19:24:52 +00:00
UI.Window.defaults = {
2018-01-24 22:39:38 +00:00
UIElement = 'Window',
x = 1,
y = 1,
-- z = 0, -- eventually...
offx = 0,
offy = 0,
cursorX = 1,
cursorY = 1,
2016-12-11 19:24:52 +00:00
}
function UI.Window:init(args)
2018-01-24 22:39:38 +00:00
-- merge defaults for all subclasses
local defaults = args
local m = getmetatable(self) -- get the class for this instance
repeat
defaults = UI:getDefaults(m, defaults)
m = m._base
until not m
UI:mergeProperties(self, defaults)
-- each element has a unique ID
self.uid = UI.Window.uid
UI.Window.uid = UI.Window.uid + 1
-- at this time, the object has all the properties set
-- postInit is a special constructor. the element does not need to implement
-- the method. But we need to guarantee that each subclass which has this
-- method is called.
m = self
local lpi
repeat
if m.postInit and m.postInit ~= lpi then
m.postInit(self)
lpi = m.postInit
end
m = m._base
until not m
2017-10-11 15:37:52 +00:00
end
function UI.Window:postInit()
2018-01-24 22:39:38 +00:00
if self.parent then
-- this will cascade down the whole tree of elements starting at the
-- top level window (which has a device as a parent)
self:setParent()
end
2016-12-11 19:24:52 +00:00
end
function UI.Window:initChildren()
2018-01-24 22:39:38 +00:00
local children = self.children
-- insert any UI elements created using the shorthand
-- window definition into the children array
for k,child in pairs(self) do
if k ~= 'parent' then -- reserved
if type(child) == 'table' and child.UIElement and not child.parent then
if not children then
children = { }
end
table.insert(children, child)
end
end
end
if children then
for _,child in pairs(children) do
if not child.parent then
child.parent = self
child:setParent()
-- child:reposition() -- maybe
if self.enabled then
child:enable()
end
end
end
self.children = children
end
2016-12-11 19:24:52 +00:00
end
2019-02-06 04:03:57 +00:00
function UI.Window:layout()
2018-01-24 22:39:38 +00:00
if self.x < 0 then
self.x = self.parent.width + self.x + 1
end
if self.y < 0 then
self.y = self.parent.height + self.y + 1
end
if self.ex then
local ex = self.ex
if self.ex <= 1 then
ex = self.parent.width + self.ex + 1
end
if self.width then
self.x = ex - self.width + 1
else
self.width = ex - self.x + 1
end
end
if self.ey then
local ey = self.ey
if self.ey <= 1 then
ey = self.parent.height + self.ey + 1
end
if self.height then
self.y = ey - self.height + 1
else
self.height = ey - self.y + 1
end
end
if not self.width then
self.width = self.parent.width - self.x + 1
end
if not self.height then
self.height = self.parent.height - self.y + 1
end
2017-09-30 02:30:01 +00:00
end
2019-02-09 00:21:20 +00:00
-- Called when the window's parent has be assigned
2017-09-30 02:30:01 +00:00
function UI.Window:setParent()
2018-01-24 22:39:38 +00:00
self.oh, self.ow = self.height, self.width
self.ox, self.oy = self.x, self.y
2017-09-30 02:30:01 +00:00
2019-02-06 04:03:57 +00:00
self:layout()
2016-12-11 19:24:52 +00:00
2018-01-24 22:39:38 +00:00
self:initChildren()
2016-12-11 19:24:52 +00:00
end
2017-10-01 00:35:36 +00:00
function UI.Window:resize()
2018-01-24 22:39:38 +00:00
self.height, self.width = self.oh, self.ow
self.x, self.y = self.ox, self.oy
2017-10-01 00:35:36 +00:00
2019-02-06 04:03:57 +00:00
self:layout()
2017-10-01 00:35:36 +00:00
2018-01-24 22:39:38 +00:00
if self.children then
for _,child in ipairs(self.children) do
child:resize()
end
end
2017-10-01 00:35:36 +00:00
end
2016-12-11 19:24:52 +00:00
function UI.Window:add(children)
2018-01-24 22:39:38 +00:00
UI:mergeProperties(self, children)
self:initChildren()
2016-12-11 19:24:52 +00:00
end
function UI.Window:getCursorPos()
2018-01-24 22:39:38 +00:00
return self.cursorX, self.cursorY
2016-12-11 19:24:52 +00:00
end
function UI.Window:setCursorPos(x, y)
2018-01-24 22:39:38 +00:00
self.cursorX = x
self.cursorY = y
self.parent:setCursorPos(self.x + x - 1, self.y + y - 1)
2016-12-11 19:24:52 +00:00
end
function UI.Window:setCursorBlink(blink)
2018-01-24 22:39:38 +00:00
self.parent:setCursorBlink(blink)
2016-12-11 19:24:52 +00:00
end
function UI.Window:draw()
2018-01-24 22:39:38 +00:00
self:clear(self.backgroundColor)
if self.children then
for _,child in pairs(self.children) do
if child.enabled then
child:draw()
end
end
end
2016-12-11 19:24:52 +00:00
end
function UI.Window:sync()
2018-01-24 22:39:38 +00:00
if self.parent then
self.parent:sync()
end
2016-12-11 19:24:52 +00:00
end
2019-01-28 22:54:00 +00:00
function UI.Window:enable(...)
2018-01-24 22:39:38 +00:00
self.enabled = true
if self.children then
for _,child in pairs(self.children) do
2019-01-28 22:54:00 +00:00
child:enable(...)
2018-01-24 22:39:38 +00:00
end
end
2016-12-11 19:24:52 +00:00
end
function UI.Window:disable()
2018-01-24 22:39:38 +00:00
self.enabled = false
if self.children then
for _,child in pairs(self.children) do
child:disable()
end
end
2016-12-11 19:24:52 +00:00
end
function UI.Window:setTextScale(textScale)
2018-01-24 22:39:38 +00:00
self.textScale = textScale
self.parent:setTextScale(textScale)
2016-12-11 19:24:52 +00:00
end
function UI.Window:clear(bg, fg)
2019-01-30 20:11:41 +00:00
if self.canvas then
self.canvas:clear(bg or self.backgroundColor, fg or self.textColor)
else
self:clearArea(1 + self.offx, 1 + self.offy, self.width, self.height, bg)
2018-01-24 22:39:38 +00:00
end
2016-12-11 19:24:52 +00:00
end
function UI.Window:clearLine(y, bg)
2018-01-24 22:39:38 +00:00
self:write(1, y, _rep(' ', self.width), bg)
2016-12-11 19:24:52 +00:00
end
function UI.Window:clearArea(x, y, width, height, bg)
2018-01-24 22:39:38 +00:00
if width > 0 then
local filler = _rep(' ', width)
for i = 0, height - 1 do
self:write(x, y + i, filler, bg)
end
end
2016-12-11 19:24:52 +00:00
end
function UI.Window:write(x, y, text, bg, tc)
2019-01-30 20:11:41 +00:00
bg = bg or self.backgroundColor
tc = tc or self.textColor
2019-02-06 04:03:57 +00:00
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
2019-01-30 20:11:41 +00:00
self.parent:write(
self.x + x - 1, self.y + y - 1, tostring(text), bg, tc)
2018-01-24 22:39:38 +00:00
end
end
2016-12-11 19:24:52 +00:00
end
function UI.Window:centeredWrite(y, text, bg, fg)
2018-01-24 22:39:38 +00:00
if #text >= self.width then
self:write(1, y, text, bg, fg)
else
local space = math.floor((self.width-#text) / 2)
local filler = _rep(' ', space + 1)
local str = _sub(filler, 1, space) .. text
str = str .. _sub(filler, self.width - #str + 1)
self:write(1, y, str, bg, fg)
end
2016-12-11 19:24:52 +00:00
end
2017-10-08 03:03:18 +00:00
function UI.Window:print(text, bg, fg)
2018-01-24 22:39:38 +00:00
local marginLeft = self.marginLeft or 0
local marginRight = self.marginRight or 0
local width = self.width - marginLeft - marginRight
local function nextWord(line, cx)
local result = { line:find("(%w+)", cx) }
if #result > 1 and result[2] > cx then
return _sub(line, cx, result[2] + 1)
elseif #result > 0 and result[1] == cx then
result = { line:find("(%w+)", result[2]) }
if #result > 0 then
return _sub(line, cx, result[1] + 1)
end
end
if cx <= #line then
return _sub(line, cx, #line)
end
end
local function pieces(f, bg, fg)
local pos = 1
local t = { }
while true do
local s = string.find(f, '\027', pos, true)
if not s then
break
end
if pos < s then
table.insert(t, _sub(f, pos, s - 1))
end
local seq = _sub(f, s)
seq = seq:match("\027%[([%d;]+)m")
local e = { }
for color in string.gmatch(seq, "%d+") do
color = tonumber(color)
if color == 0 then
e.fg = fg
e.bg = bg
elseif color > 20 then
e.bg = 2 ^ (color - 21)
else
e.fg = 2 ^ (color - 1)
end
end
table.insert(t, e)
pos = s + #seq + 3
end
if pos <= #f then
table.insert(t, _sub(f, pos))
end
return t
end
local lines = Util.split(text)
for k,line in pairs(lines) do
local fragments = pieces(line, bg, fg)
for _, fragment in ipairs(fragments) do
local lx = 1
if type(fragment) == 'table' then -- ansi sequence
fg = fragment.fg
bg = fragment.bg
else
while true do
local word = nextWord(fragment, lx)
if not word then
break
end
local w = word
if self.cursorX + #word > width then
self.cursorX = marginLeft + 1
self.cursorY = self.cursorY + 1
w = word:gsub('^ ', '')
end
self:write(self.cursorX, self.cursorY, w, bg, fg)
self.cursorX = self.cursorX + #w
lx = lx + #word
end
end
end
if lines[k + 1] then
self.cursorX = marginLeft + 1
self.cursorY = self.cursorY + 1
end
end
return self.cursorX, self.cursorY
2016-12-11 19:24:52 +00:00
end
function UI.Window:setFocus(focus)
2018-01-24 22:39:38 +00:00
if self.parent then
self.parent:setFocus(focus)
end
2016-12-11 19:24:52 +00:00
end
2017-12-13 06:37:31 +00:00
function UI.Window:capture(child)
2018-01-24 22:39:38 +00:00
if self.parent then
self.parent:capture(child)
end
2017-12-13 06:37:31 +00:00
end
function UI.Window:release(child)
2018-01-24 22:39:38 +00:00
if self.parent then
self.parent:release(child)
end
2017-12-13 06:37:31 +00:00
end
function UI.Window:pointToChild(x, y)
2019-02-06 04:03:57 +00:00
-- TODO: get rid of this offx/y mess and scroll canvas instead
2018-01-24 22:39:38 +00:00
x = x + self.offx - self.x + 1
y = y + self.offy - self.y + 1
if self.children then
for _,child in pairs(self.children) do
if child.enabled and not child.inactive and
x >= child.x and x < child.x + child.width and
y >= child.y and y < child.y + child.height then
local c = child:pointToChild(x, y)
if c then
return c
end
end
end
end
return {
element = self,
x = x,
y = y
}
2017-12-13 06:37:31 +00:00
end
2016-12-11 19:24:52 +00:00
function UI.Window:getFocusables()
2018-01-24 22:39:38 +00:00
local focusable = { }
2016-12-11 19:24:52 +00:00
2018-01-24 22:39:38 +00:00
local function focusSort(a, b)
if a.y == b.y then
return a.x < b.x
end
return a.y < b.y
end
2016-12-11 19:24:52 +00:00
2019-02-06 04:03:57 +00:00
local function getFocusable(parent)
2018-01-24 22:39:38 +00:00
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
2019-02-06 04:03:57 +00:00
getFocusable(child)
2018-01-24 22:39:38 +00:00
end
end
end
2016-12-11 19:24:52 +00:00
2018-01-24 22:39:38 +00:00
if self.children then
getFocusable(self, self.x, self.y)
end
2016-12-11 19:24:52 +00:00
2018-01-24 22:39:38 +00:00
return focusable
2016-12-11 19:24:52 +00:00
end
function UI.Window:focusFirst()
2018-01-24 22:39:38 +00:00
local focusables = self:getFocusables()
local focused = focusables[1]
if focused then
self:setFocus(focused)
end
2016-12-11 19:24:52 +00:00
end
2017-10-18 23:51:55 +00:00
function UI.Window:refocus()
2018-01-24 22:39:38 +00:00
local el = self
while el do
local focusables = el:getFocusables()
if focusables[1] then
self:setFocus(focusables[1])
break
end
el = el.parent
end
2017-10-18 23:51:55 +00:00
end
2016-12-11 19:24:52 +00:00
function UI.Window:scrollIntoView()
2018-01-24 22:39:38 +00:00
local parent = self.parent
2016-12-11 19:24:52 +00:00
2018-01-24 22:39:38 +00:00
if self.x <= parent.offx then
parent.offx = math.max(0, self.x - 1)
parent:draw()
elseif self.x + self.width > parent.width + parent.offx then
parent.offx = self.x + self.width - parent.width - 1
parent:draw()
end
2016-12-11 19:24:52 +00:00
2019-02-10 02:41:51 +00:00
-- TODO: fix
local function setOffset(y)
parent.offy = y
if parent.canvas then
parent.canvas.offy = parent.offy
end
2018-01-24 22:39:38 +00:00
parent:draw()
2019-02-10 02:41:51 +00:00
end
if self.y <= parent.offy then
setOffset(math.max(0, self.y - 1))
2018-01-24 22:39:38 +00:00
elseif self.y + self.height > parent.height + parent.offy then
2019-02-10 02:41:51 +00:00
setOffset(self.y + self.height - parent.height - 1)
2018-01-24 22:39:38 +00:00
end
2016-12-11 19:24:52 +00:00
end
2017-10-06 17:39:47 +00:00
function UI.Window:getCanvas()
2018-01-24 22:39:38 +00:00
local el = self
repeat
if el.canvas then
return el.canvas
end
el = el.parent
until not el
2017-10-06 17:39:47 +00:00
end
function UI.Window:addLayer(bg, fg)
2018-01-24 22:39:38 +00:00
local canvas = self:getCanvas()
2019-01-30 21:27:09 +00:00
local x, y = self.x, self.y
local parent = self.parent
while parent and not parent.canvas do
x = x + parent.x - 1
y = y + parent.y - 1
parent = parent.parent
end
canvas = canvas:addLayer({
x = x, y = y, height = self.height, width = self.width
}, bg, fg)
2018-01-24 22:39:38 +00:00
canvas:clear(bg or self.backgroundColor, fg or self.textColor)
return canvas
2017-10-06 17:39:47 +00:00
end
2017-10-03 04:50:54 +00:00
function UI.Window:addTransition(effect, args)
2018-01-24 22:39:38 +00:00
if self.parent then
args = args or { }
if not args.x then -- not good
args.x, args.y = getPosition(self)
args.width = self.width
args.height = self.height
end
2017-10-03 04:50:54 +00:00
2018-01-24 22:39:38 +00:00
args.canvas = args.canvas or self.canvas
self.parent:addTransition(effect, args)
end
2016-12-14 17:36:28 +00:00
end
2016-12-11 19:24:52 +00:00
function UI.Window:emit(event)
2018-01-24 22:39:38 +00:00
local parent = self
while parent do
if parent.eventHandler then
if parent:eventHandler(event) then
return true
end
end
parent = parent.parent
end
2016-12-11 19:24:52 +00:00
end
2017-10-18 23:51:55 +00:00
function UI.Window:find(uid)
2018-01-24 22:39:38 +00:00
if self.children then
return Util.find(self.children, 'uid', uid)
end
2017-10-18 23:51:55 +00:00
end
2019-02-06 04:03:57 +00:00
function UI.Window:eventHandler()
2018-01-24 22:39:38 +00:00
return false
2016-12-11 19:24:52 +00:00
end
--[[-- Terminal for computer / advanced computer / monitor --]]--
UI.Device = class(UI.Window)
UI.Device.defaults = {
2018-01-24 22:39:38 +00:00
UIElement = 'Device',
backgroundColor = colors.black,
textColor = colors.white,
textScale = 1,
effectsEnabled = true,
2016-12-11 19:24:52 +00:00
}
2017-10-11 15:37:52 +00:00
function UI.Device:postInit()
2018-01-24 22:39:38 +00:00
self.device = self.device or term.current()
2016-12-11 19:24:52 +00:00
2018-01-24 22:39:38 +00:00
if self.deviceType then
self.device = device[self.deviceType]
end
2016-12-11 19:24:52 +00:00
2018-01-24 22:39:38 +00:00
if not self.device.setTextScale then
self.device.setTextScale = function() end
end
2016-12-11 19:24:52 +00:00
2018-01-24 22:39:38 +00:00
self.device.setTextScale(self.textScale)
self.width, self.height = self.device.getSize()
2016-12-11 19:24:52 +00:00
2018-01-24 22:39:38 +00:00
self.isColor = self.device.isColor()
2016-12-23 04:22:04 +00:00
2019-01-02 15:33:47 +00:00
UI.devices[self.device.side or 'terminal'] = self
2016-12-11 19:24:52 +00:00
end
function UI.Device:resize()
2018-12-05 01:55:08 +00:00
self.device.setTextScale(self.textScale)
2018-01-24 22:39:38 +00:00
self.width, self.height = self.device.getSize()
self.lines = { }
2019-01-30 20:11:41 +00:00
-- TODO: resize all pages added to this device
2018-01-24 22:39:38 +00:00
self.canvas:resize(self.width, self.height)
self.canvas:clear(self.backgroundColor, self.textColor)
2016-12-11 19:24:52 +00:00
end
function UI.Device:setCursorPos(x, y)
2018-01-24 22:39:38 +00:00
self.cursorX = x
self.cursorY = y
2016-12-11 19:24:52 +00:00
end
function UI.Device:getCursorBlink()
2018-01-24 22:39:38 +00:00
return self.cursorBlink
2016-12-11 19:24:52 +00:00
end
function UI.Device:setCursorBlink(blink)
2018-01-24 22:39:38 +00:00
self.cursorBlink = blink
self.device.setCursorBlink(blink)
2016-12-11 19:24:52 +00:00
end
function UI.Device:setTextScale(textScale)
2018-01-24 22:39:38 +00:00
self.textScale = textScale
self.device.setTextScale(self.textScale)
2016-12-11 19:24:52 +00:00
end
function UI.Device:reset()
2018-01-24 22:39:38 +00:00
self.device.setBackgroundColor(colors.black)
self.device.setTextColor(colors.white)
self.device.clear()
self.device.setCursorPos(1, 1)
2016-12-11 19:24:52 +00:00
end
2017-10-03 04:50:54 +00:00
function UI.Device:addTransition(effect, args)
2018-01-24 22:39:38 +00:00
if not self.transitions then
self.transitions = { }
end
2016-12-18 19:38:48 +00:00
2018-01-24 22:39:38 +00:00
args = args or { }
args.ex = args.x + args.width - 1
args.ey = args.y + args.height - 1
args.canvas = args.canvas or self.canvas
2017-10-03 04:50:54 +00:00
2018-01-24 22:39:38 +00:00
if type(effect) == 'string' then
effect = Transition[effect]
if not effect then
error('Invalid transition')
end
end
2017-10-03 04:50:54 +00:00
2018-01-24 22:39:38 +00:00
table.insert(self.transitions, { update = effect(args), args = args })
2016-12-14 17:36:28 +00:00
end
2016-12-23 04:22:04 +00:00
function UI.Device:runTransitions(transitions, canvas)
2018-01-24 22:39:38 +00:00
while true do
for _,k in ipairs(Util.keys(transitions)) do
local transition = transitions[k]
2019-02-06 04:03:57 +00:00
if not transition.update() then
2018-01-24 22:39:38 +00:00
transitions[k] = nil
end
end
2019-02-06 04:03:57 +00:00
canvas:render(self.device)
2018-01-24 22:39:38 +00:00
if Util.empty(transitions) then
break
end
os.sleep(0)
end
2016-12-14 17:36:28 +00:00
end
2016-12-11 19:24:52 +00:00
function UI.Device:sync()
2019-02-06 04:03:57 +00:00
local transitions = self.effectsEnabled and self.transitions
self.transitions = nil
2016-12-14 17:36:28 +00:00
2018-01-24 22:39:38 +00:00
if self:getCursorBlink() then
self.device.setCursorBlink(false)
end
2016-12-14 17:36:28 +00:00
2019-01-30 20:11:41 +00:00
self.canvas:render(self.device)
2018-01-24 22:39:38 +00:00
if transitions then
self:runTransitions(transitions, self.canvas)
end
2016-12-14 17:36:28 +00:00
2018-01-24 22:39:38 +00:00
if self:getCursorBlink() then
self.device.setCursorPos(self.cursorX, self.cursorY)
self.device.setCursorBlink(true)
end
2016-12-11 19:24:52 +00:00
end
--[[-- Page (focus manager) --]]--
UI.Page = class(UI.Window)
UI.Page.defaults = {
2018-01-24 22:39:38 +00:00
UIElement = 'Page',
accelerators = {
down = 'focus_next',
enter = 'focus_next',
tab = 'focus_next',
['shift-tab' ] = 'focus_prev',
up = 'focus_prev',
},
backgroundColor = colors.cyan,
textColor = colors.white,
2016-12-11 19:24:52 +00:00
}
2017-10-11 15:37:52 +00:00
function UI.Page:postInit()
2018-01-24 22:39:38 +00:00
self.parent = self.parent or UI.defaultDevice
self.__target = self
2019-01-30 20:11:41 +00:00
self.canvas = Canvas({
x = 1, y = 1, width = self.parent.width, height = self.parent.height,
isColor = self.parent.isColor,
})
self.canvas:clear(self.backgroundColor, self.textColor)
2017-10-11 15:37:52 +00:00
end
2016-12-23 04:22:04 +00:00
2016-12-11 19:24:52 +00:00
function UI.Page:enable()
2018-01-24 22:39:38 +00:00
self.canvas.visible = true
UI.Window.enable(self)
2016-12-11 19:24:52 +00:00
2018-01-24 22:39:38 +00:00
if not self.focused or not self.focused.enabled then
self:focusFirst()
end
2016-12-11 19:24:52 +00:00
end
2016-12-23 04:22:04 +00:00
function UI.Page:disable()
2019-01-30 20:53:09 +00:00
self.canvas.visible = false
2019-01-01 12:29:44 +00:00
UI.Window.disable(self)
2016-12-23 04:22:04 +00:00
end
2019-01-30 20:11:41 +00:00
function UI.Page:sync()
if self.enabled then
self.parent:sync()
end
end
2017-12-13 06:37:31 +00:00
function UI.Page:capture(child)
2018-01-24 22:39:38 +00:00
self.__target = child
2017-12-13 06:37:31 +00:00
end
function UI.Page:release(child)
2018-01-24 22:39:38 +00:00
if self.__target == child then
self.__target = self
end
2017-12-13 06:37:31 +00:00
end
function UI.Page:pointToChild(x, y)
2018-01-24 22:39:38 +00:00
if self.__target == self then
return UI.Window.pointToChild(self, x, y)
end
x = x + self.offx - self.x + 1
y = y + self.offy - self.y + 1
return self.__target:pointToChild(x, y)
2017-12-13 06:37:31 +00:00
end
function UI.Page:getFocusables()
2018-01-24 22:39:38 +00:00
if self.__target == self or self.__target.pageType ~= 'modal' then
return UI.Window.getFocusables(self)
end
return self.__target:getFocusables()
2017-12-13 06:37:31 +00:00
end
2016-12-11 19:24:52 +00:00
function UI.Page:getFocused()
2018-01-24 22:39:38 +00:00
return self.focused
2016-12-11 19:24:52 +00:00
end
function UI.Page:focusPrevious()
2018-01-24 22:39:38 +00:00
local function getPreviousFocus(focused)
local focusables = self:getFocusables()
local k = Util.contains(focusables, focused)
if k then
if k > 1 then
return focusables[k - 1]
end
return focusables[#focusables]
end
end
local focused = getPreviousFocus(self.focused)
if focused then
self:setFocus(focused)
end
2016-12-11 19:24:52 +00:00
end
function UI.Page:focusNext()
2018-01-24 22:39:38 +00:00
local function getNextFocus(focused)
local focusables = self:getFocusables()
local k = Util.contains(focusables, focused)
if k then
if k < #focusables then
return focusables[k + 1]
end
return focusables[1]
end
end
local focused = getNextFocus(self.focused)
if focused then
self:setFocus(focused)
end
2016-12-11 19:24:52 +00:00
end
function UI.Page:setFocus(child)
2018-10-28 01:34:34 +00:00
if not child or not child.focus then
2018-01-24 22:39:38 +00:00
return
end
2016-12-11 19:24:52 +00:00
2018-01-24 22:39:38 +00:00
if self.focused and self.focused ~= child then
self.focused.focused = false
self.focused:focus()
self.focused:emit({ type = 'focus_lost', focused = child })
end
2016-12-11 19:24:52 +00:00
2018-01-24 22:39:38 +00:00
self.focused = child
if not child.focused then
child.focused = true
2018-10-24 02:33:40 +00:00
child:emit({ type = 'focus_change', focused = child })
--self:emit({ type = 'focus_change', focused = child })
2018-01-24 22:39:38 +00:00
end
2016-12-11 19:24:52 +00:00
2018-01-24 22:39:38 +00:00
child:focus()
2016-12-11 19:24:52 +00:00
end
function UI.Page:eventHandler(event)
2018-01-24 22:39:38 +00:00
if self.focused then
if event.type == 'focus_next' then
self:focusNext()
return true
elseif event.type == 'focus_prev' then
self:focusPrevious()
return true
end
end
2016-12-11 19:24:52 +00:00
end
2019-02-06 04:03:57 +00:00
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)
2018-01-24 22:39:38 +00:00
end
2019-02-06 04:03:57 +00:00
if UI[name]._preload then
error('Error loading UI.' .. name)
2018-01-24 22:39:38 +00:00
end
2019-02-06 04:03:57 +00:00
if UI.theme[name] and UI[name].defaults then
Util.merge(UI[name].defaults, UI.theme[name])
2018-01-24 22:39:38 +00:00
end
2019-02-06 04:03:57 +00:00
return UI[name]
2018-01-24 22:39:38 +00:00
end
2016-12-11 19:24:52 +00:00
2019-02-06 04:03:57 +00:00
local components = fs.list('sys/apis/ui/components')
for _, f in pairs(components) do
local name = f:match('(.+)%.')
2016-12-11 19:24:52 +00:00
2019-02-06 04:03:57 +00:00
UI[name] = setmetatable({ }, {
__call = function(self, ...)
load(name)
setmetatable(self, getmetatable(UI[name]))
return self(...)
2018-01-24 22:39:38 +00:00
end
2019-02-06 04:03:57 +00:00
})
UI[name]._preload = function(self)
return load(name)
2018-01-24 22:39:38 +00:00
end
end
2016-12-11 19:24:52 +00:00
end
2019-02-06 04:03:57 +00:00
loadComponents()
2016-12-11 19:24:52 +00:00
2017-05-20 22:27:26 +00:00
UI:loadTheme('usr/config/ui.theme')
2017-10-12 21:58:35 +00:00
if Util.getVersion() >= 1.76 then
2018-01-24 22:39:38 +00:00
UI:loadTheme('sys/etc/ext.theme')
2016-12-11 19:24:52 +00:00
end
2016-12-15 14:45:27 +00:00
UI:setDefaultDevice(UI.Device({ device = term.current() }))
2016-12-11 19:24:52 +00:00
return UI