opus/sys/modules/opus/ui.lua

1158 lines
26 KiB
Lua
Raw Normal View History

local class = require('opus.class')
local Event = require('opus.event')
local Input = require('opus.input')
local Transition = require('opus.ui.transition')
local Util = require('opus.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
2019-07-21 16:16:47 +00:00
local device = _G.device
2017-10-11 15:37:52 +00:00
local fs = _G.fs
local os = _G.os
local term = _G.term
local textutils = _G.textutils
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
2019-11-18 21:32:10 +00:00
Calling order:
window:postInit()
at this point, the window has all default values set
window:setParent()
parent has been assigned
following are called:
window:layout()
sizing / positioning is performed
window:initChildren()
each child of window will get initialized
]]
2016-12-11 19:24:52 +00:00
--[[-- Top Level Manager --]]--
local Manager = class()
function Manager:init()
2019-01-02 15:33:47 +00:00
self.devices = { }
self.theme = { }
self.extChars = Util.getVersion() >= 1.76
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,
2019-03-29 13:56:56 +00:00
{ type = 'key', key = ie.code == 'char' and ie.ch or ie.code, element = target, ie = ie })
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)
2019-03-29 13:56:56 +00:00
local ie = Input:translate('mouse_click', button, x, y)
2018-01-24 22:39:38 +00:00
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
2019-03-29 13:56:56 +00:00
self:click(currentPage, ie.code, button, x, y)
2018-01-24 22:39:38 +00:00
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
2019-07-21 01:29:44 +00:00
if not currentPage.parent.device.side then
2019-01-02 15:33:47 +00:00
self:click(currentPage, ie.code, button, x, y)
2019-07-21 01:29:44 +00:00
end
2018-01-24 22:39:38 +00:00
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()
2019-05-04 10:11:22 +00:00
2019-01-02 15:33:47 +00:00
if ie and currentPage then
2019-05-04 10:11:22 +00:00
self:click(currentPage, ie.code, button, x, y)
2018-01-24 22:39:38 +00:00
end
end,
paste = function(_, text)
2019-03-29 13:56:56 +00:00
local ie = Input:translate('paste', text)
self:emitEvent({ type = 'paste', text = text, ie = ie })
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 defaults = Util.loadTable('usr/config/' .. appName) or { }
if not defaults.device then
defaults.device = { }
end
2019-07-21 01:29:44 +00:00
-- starting a program: gpsServer --display=monitor_3148 --scale=.5 gps
local _, options = Util.parse(...)
2018-01-24 22:39:38 +00:00
local optionValues = {
2019-07-21 01:29:44 +00:00
name = options.display,
textScale = tonumber(options.scale),
2018-01-24 22:39:38 +00:00
}
Util.merge(defaults.device, optionValues)
if defaults.device.name then
local dev
if defaults.device.name == 'terminal' then
dev = term.current()
else
2019-07-21 16:16:47 +00:00
dev = device[defaults.device.name]
2018-01-24 22:39:38 +00:00
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-03-24 07:44:38 +00:00
Util.deepMerge(self.theme, theme)
2018-01-24 22:39:38 +00:00
end
2016-12-11 19:24:52 +00:00
end
function Manager:generateTheme(filename)
local t = { }
for k,v in pairs(self) do
if type(v) == 'table' then
if v._preload then
v._preload()
v = self[k]
end
if v.defaults and v.defaults.UIElement ~= 'Device' then
for p,d in pairs(v.defaults) do
if p:find('olor') then
if not t[k] then
t[k] = { }
end
for c, n in pairs(colors) do
if n == d then
t[k][p] = 'colors.' .. c
break
end
end
end
end
end
end
end
Util.writeFile(filename, textutils.serialize(t):gsub('(")', ''))
end
2016-12-11 19:24:52 +00:00
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
2019-11-13 22:17:23 +00:00
function Manager:inputEvent(parent, event) -- deprecate ?
return parent and parent:emit(event)
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
2019-03-29 19:47:01 +00:00
clickEvent.ie = { code = code, x = clickEvent.x, y = clickEvent.y }
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(...)
2019-03-26 06:10:10 +00:00
local s, m = pcall(Event.pullEvents, ...)
2018-01-24 22:39:38 +00:00
self.term:reset()
2019-03-26 06:10:10 +00:00
if not s and m then
error(m, -1)
end
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
2019-11-17 05:12:02 +00:00
UI.Window.docs = { }
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()
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()
local function calc(p, max)
p = tonumber(p:match('(%d+)%%'))
return p and math.floor(max * p / 100) or 1
end
if type(self.x) == 'string' then
2019-11-17 05:12:02 +00:00
self.x = calc(self.x, self.parent.width) + 1
-- +1 in order to allow both x and ex to use the same %
end
if type(self.ex) == 'string' then
self.ex = calc(self.ex, self.parent.width)
end
if type(self.y) == 'string' then
2019-11-17 05:12:02 +00:00
self.y = calc(self.y, self.parent.height) + 1
end
if type(self.ey) == 'string' then
self.ey = calc(self.ey, self.parent.height)
end
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
2019-11-17 05:12:02 +00:00
-- Experimental
-- Inherit properties from the parent container
-- does this need to be in reverse order ?
local m = getmetatable(self) -- get the class for this instance
repeat
if m.inherits then
for k, v in pairs(m.inherits) do
local value = self.parent:getProperty(v)
if value then
self[k] = value
end
end
end
m = m._base
until not m
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
2019-11-17 05:12:02 +00:00
UI.Window.docs.add = [[add(TABLE)
Add element(s) to a window. Example:
page:add({
text = UI.Text {
x=5,value='help'
}
})]]
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
2019-11-17 05:12:02 +00:00
UI.Window.docs.draw = [[draw(VOID)
Redraws the window in the internal buffer.]]
2016-12-11 19:24:52 +00:00
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
2019-11-17 05:12:02 +00:00
UI.Window.docs.getDoc = [[getDoc(STRING method)
Gets the documentation for a method.]]
function UI.Window:getDoc(method)
local m = getmetatable(self) -- get the class for this instance
repeat
if m.docs and m.docs[method] then
return m.docs[method]
end
m = m._base
until not m
end
UI.Window.docs.sync = [[sync(VOID)
Invoke a screen update. Automatically called at top level after an input event.
Call to force a screen update.]]
2016-12-11 19:24:52 +00:00
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
2019-11-17 05:12:02 +00:00
UI.Window.docs.clear = [[clear(opt COLOR bg, opt COLOR fg)
Clears the window using the either the passed values or the defaults for that window.]]
function UI.Window:clear(bg, fg)
2019-01-30 20:11:41 +00:00
if self.canvas then
2019-11-17 05:12:02 +00:00
self.canvas:clear(bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor'))
2019-01-30 20:11:41 +00:00
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
2019-11-17 05:12:02 +00:00
function UI.Window:write(x, y, text, bg, fg)
2019-01-30 20:11:41 +00:00
bg = bg or self.backgroundColor
2019-11-17 05:12:02 +00:00
fg = fg or self.textColor
2019-02-06 04:03:57 +00:00
if self.canvas then
2019-11-17 05:12:02 +00:00
self.canvas:write(x, y, text, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor'))
2019-02-06 04:03:57 +00:00
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(
2019-11-17 05:12:02 +00:00
self.x + x - 1, self.y + y - 1, tostring(text), bg, fg)
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
2019-11-17 05:12:02 +00:00
UI.Window.docs.focus = [[focus(VOID)
If the function is present on a class, it indicates
that this element can accept focus. Called when receiving focus.]]
UI.Window.docs.setFocus = [[setFocus(ELEMENT el)
Set the page's focus to the passed element.]]
2016-12-11 19:24:52 +00:00
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
2019-11-17 05:12:02 +00:00
UI.Window.docs.capture = [[capture(ELEMENT el)
Restricts input to the passed element's tree.]]
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
2019-11-17 05:12:02 +00:00
UI.Window.docs.getFocusables = [[getFocusables(VOID)
Returns a list of children that can accept focus.]]
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
2019-11-18 21:32:10 +00:00
args.x, args.y = self.x, self.y -- getPosition(self)
2018-01-24 22:39:38 +00:00
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.accelerators then
-- events types can be made unique via accelerators
local acc = parent.accelerators[event.key or event.type]
if acc and acc ~= event.type then -- don't get stuck in a loop
local event2 = Util.shallowCopy(event)
event2.type = acc
event2.key = nil
if parent:emit(event2) then
return true
end
2019-11-13 22:17:23 +00:00
end
end
2018-01-24 22:39:38 +00:00
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-07-11 01:02:46 +00:00
function UI.Window:getProperty(property)
return self[property] or self.parent and self.parent:getProperty(property)
end
2017-10-18 23:51:55 +00:00
function UI.Window:find(uid)
2019-07-11 01:02:46 +00:00
return self.children and Util.find(self.children, 'uid', uid)
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
2019-03-12 03:48:22 +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
2019-11-18 21:32:10 +00:00
-- lazy load components
2019-02-06 04:03:57 +00:00
local function loadComponents()
local function load(name)
local s, m = Util.run(_ENV, 'sys/modules/opus/ui/components/' .. name .. '.lua')
2019-02-06 04:03:57 +00:00
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
local components = fs.list('sys/modules/opus/ui/components')
2019-02-06 04:03:57 +00:00
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()
2019-03-24 07:44:38 +00:00
UI:loadTheme('usr/config/ui.theme')
2019-07-10 17:06:29 +00:00
Util.merge(UI.Window.defaults, UI.theme.Window)
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