opus/sys/modules/opus/ui.lua

1132 lines
26 KiB
Lua

local Array = require('opus.array')
local Blit = require('opus.ui.blit')
local Canvas = require('opus.ui.canvas')
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')
local _rep = string.rep
local colors = _G.colors
local device = _G.device
local fs = _G.fs
local os = _G.os
local term = _G.term
local textutils = _G.textutils
--[[
Using the shorthand window definition, elements are created from
the bottom up. Once reaching the top, setParent is called top down.
On :init(), elements do not know the parent or can calculate sizing.
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
]]
--[[-- Top Level Manager --]]--
local UI = { }
function UI:init()
self.devices = { }
self.theme = {
colors = {
primary = colors.green,
secondary = colors.lightGray,
tertiary = colors.gray,
}
}
self.extChars = Util.getVersion() >= 1.76
local function keyFunction(event, code, held)
local ie = Input:translate(event, code, held)
local currentPage = self:getActivePage()
if ie and currentPage then
local target = currentPage.focused or currentPage
target:emit({ type = 'key', key = ie.code == 'char' and ie.ch or ie.code, element = target, ie = ie })
currentPage:sync()
end
end
local function resize(_, side)
local dev = self.devices[side or 'terminal']
if dev and dev.currentPage then
dev:resize()
dev.currentPage:resize()
dev.currentPage:draw()
dev.currentPage:sync()
end
end
local handlers = {
char = keyFunction,
key_up = keyFunction,
key = keyFunction,
term_resize = resize,
monitor_resize = resize,
mouse_scroll = function(_, direction, x, y, side)
local ie = Input:translate('mouse_scroll', direction, x, y)
local currentPage = self:getActivePage()
if currentPage and currentPage.parent.device.side == side then
local event = currentPage:pointToChild(x, y)
event.type = ie.code
event.ie = { code = ie.code, x = event.x, y = event.y }
event.element:emit(event)
currentPage:sync()
end
end,
monitor_touch = function(_, side, x, y)
local dev = self.devices[side]
if dev and dev.currentPage then
Input:translate('mouse_click', 1, x, y)
local ie = Input:translate('mouse_up', 1, x, y)
self:click(dev.currentPage, ie)
end
end,
mouse_click = function(_, button, x, y, side)
local ie = Input:translate('mouse_click', button, x, y)
local currentPage = self:getActivePage()
if currentPage and currentPage.parent.device.side == side then
local event = currentPage:pointToChild(x, y)
if event.element.focus and not event.element.inactive then
currentPage:setFocus(event.element)
currentPage:sync()
end
self:click(currentPage, ie)
end
end,
mouse_up = function(_, button, x, y, side)
local ie = Input:translate('mouse_up', button, x, y)
local currentPage = self:getActivePage()
if ie.code == 'control-shift-mouse_click' then -- hack
local event = currentPage:pointToChild(x, y)
_ENV.multishell.openTab(_ENV, {
path = 'sys/apps/Lua.lua',
args = { event.element, self, _ENV },
focused = true })
elseif ie and currentPage and currentPage.parent.device.side == side then
self:click(currentPage, ie)
end
end,
mouse_drag = function(_, button, x, y, side)
local ie = Input:translate('mouse_drag', button, x, y)
local currentPage = self:getActivePage()
if ie and currentPage and currentPage.parent.device.side == side then
self:click(currentPage, ie)
end
end,
paste = function(_, text)
local ie = Input:translate('paste', text)
self:emitEvent({ type = 'paste', text = text, ie = ie })
self:getActivePage():sync()
end,
}
-- use 1 handler to single thread all events
Event.on({
'char', 'key_up', 'key', 'term_resize', 'monitor_resize',
'mouse_scroll', 'monitor_touch', 'mouse_click',
'mouse_up', 'mouse_drag', 'paste' },
function(event, ...)
handlers[event](event, ...)
end)
end
function UI:configure(appName, ...)
local defaults = Util.loadTable('usr/config/' .. appName) or { }
if not defaults.device then
defaults.device = { }
end
-- starting a program: gpsServer --display=monitor_3148 --scale=.5 gps
local _, options = Util.parse(...)
local optionValues = {
name = options.display,
textScale = tonumber(options.scale),
}
Util.merge(defaults.device, optionValues)
if defaults.device.name then
local dev
if defaults.device.name == 'terminal' then
dev = term.current()
else
dev = 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
Util.deepMerge(self.theme, defaults.theme)
end
end
function UI:disableEffects()
self.term.effectsEnabled = false
end
function UI:loadTheme(filename)
if fs.exists(filename) then
local theme, err = Util.loadTable(filename)
if not theme then
error(err)
end
Util.deepMerge(self.theme, theme)
end
for k,v in pairs(self.theme.colors) do
Canvas.colorPalette[k] = Canvas.colorPalette[v]
Canvas.grayscalePalette[k] = Canvas.grayscalePalette[v]
end
end
function UI:generateTheme(filename)
local t = { }
local function getName(d)
if type(d) == 'string' then
return string.format("'%s'", d)
end
for c, n in pairs(colors) do
if n == d then
return 'colors.' .. c
end
end
end
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
t[k][p] = getName(d)
end
end
end
end
end
t.colors = {
primary = getName(self.colors.primary),
secondary = getName(self.colors.secondary),
tertiary = getName(self.colors.tertiary),
}
Util.writeFile(filename, textutils.serialize(t):gsub('(")', ''))
end
function UI:emitEvent(event)
local currentPage = self:getActivePage()
if currentPage and currentPage.focused then
return currentPage.focused:emit(event)
end
end
function UI:click(target, ie)
local clickEvent
if ie.code == 'mouse_drag' then
local function getPosition(element, x, y)
repeat
x = x - element.x + 1
y = y - element.y + 1
element = element.parent
until not element
return x, y
end
local x, y = getPosition(self.lastClicked, ie.x, ie.y)
clickEvent = {
element = self.lastClicked,
x = x,
y = y,
dx = ie.dx,
dy = ie.dy,
}
else
clickEvent = target:pointToChild(ie.x, ie.y)
end
-- hack for dropdown menus
if ie.code == 'mouse_click' and not clickEvent.element.focus then
self:emitEvent({ type = 'mouse_out' })
end
if ie.code == 'mouse_doubleclick' then
if self.lastClicked ~= clickEvent.element then
return
end
else
self.lastClicked = clickEvent.element
end
clickEvent.button = ie.button
clickEvent.type = ie.code
clickEvent.key = ie.code
clickEvent.ie = { code = ie.code, x = clickEvent.x, y = clickEvent.y }
clickEvent.raw = ie
if clickEvent.element.focus then
target:setFocus(clickEvent.element)
end
clickEvent.element:emit(clickEvent)
target:sync()
end
function UI:setDefaultDevice(dev)
self.term = dev
end
function UI:addPage(name, page)
if not self.pages then
self.pages = { }
end
self.pages[name] = page
end
function UI:setPages(pages)
self.pages = pages
end
function UI:getPage(pageName)
local page = self.pages[pageName]
if not page then
error('UI:getPage: Invalid page: ' .. tostring(pageName), 2)
end
return page
end
function UI:getActivePage(page)
if page then
return page.parent.currentPage
end
return self.term.currentPage
end
function UI:setActivePage(page)
page.parent.currentPage = page
end
function UI:setPage(pageOrName, ...)
local page = pageOrName
if type(pageOrName) == 'string' then
page = self.pages[pageOrName] or error('Invalid page: ' .. pageOrName)
end
local currentPage = self:getActivePage(page)
if page == currentPage then
page:draw()
else
if currentPage then
if currentPage.focused then
currentPage.focused.focused = false
currentPage.focused:focus()
end
currentPage:disable()
page.previousPage = currentPage
end
self:setActivePage(page)
page:enable(...)
page:draw()
if page.focused then
page.focused.focused = true
page.focused:focus()
end
page:sync()
end
end
function UI:getCurrentPage()
return self.term.currentPage
end
function UI:setPreviousPage()
if self.term.currentPage.previousPage then
local previousPage = self.term.currentPage.previousPage.previousPage
self:setPage(self.term.currentPage.previousPage)
self.term.currentPage.previousPage = previousPage
end
end
function UI:getDefaults(element, args)
local defaults = Util.deepCopy(element.defaults)
if args then
UI:mergeProperties(defaults, args)
end
return defaults
end
function UI:mergeProperties(obj, args)
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
end
function UI:pullEvents(...)
local s, m = pcall(Event.pullEvents, ...)
self.term:reset()
if not s and m then
error(m, -1)
end
end
UI.exitPullEvents = Event.exitPullEvents
UI.quit = Event.exitPullEvents
UI.start = UI.pullEvents
UI:init()
--[[-- Basic drawable area --]]--
UI.Window = class(Canvas)
UI.Window.uid = 1
UI.Window.docs = { }
UI.Window.defaults = {
UIElement = 'Window',
x = 1,
y = 1,
offx = 0,
offy = 0,
cursorX = 1,
cursorY = 1,
}
function UI.Window:init(args)
-- merge defaults for all subclasses
local defaults = args
local m = getmetatable(self) -- get the class for this instance
repeat
if m ~= Canvas then
defaults = UI:getDefaults(m, defaults)
end
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
end
UI.Window.docs.postInit = [[postInit(VOID)
Called once the window has all the properties set.
Override to calculate properties or to dynamically add children]]
function UI.Window:postInit()
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
end
function UI.Window:initChildren()
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
end
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
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
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
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
self.width = math.max(self.width, 1)
self.height = math.max(self.height, 1)
self:reposition(self.x, self.y, self.width, self.height)
end
-- Called when the window's parent has be assigned
function UI.Window:setParent()
self.oh, self.ow = self.height, self.width
self.ox, self.oy = self.x, self.y
self.oex, self.oey = self.ex, self.ey
self:layout()
self:initChildren()
end
function UI.Window:resize()
self.height, self.width = self.oh, self.ow
self.x, self.y = self.ox, self.oy
self.ex, self.ey = self.oex, self.oey
self:layout()
if self.children then
for child in self:eachChild() do
child:resize()
end
end
end
function UI.Window:reposition(x, y, w, h)
if not self.lines then
Canvas.init(self, {
x = x,
y = y,
width = w,
height = h,
isColor = self.parent.isColor,
})
else
self:move(x, y)
Canvas.resize(self, w, h)
end
end
UI.Window.docs.raise = [[raise(VOID)
Raise this window to the top]]
function UI.Window:raise()
Array.removeByValue(self.parent.children, self)
table.insert(self.parent.children, self)
self:dirty(true)
end
UI.Window.docs.add = [[add(TABLE)
Add element(s) to a window. Example:
page:add({
text = UI.Text {
x=5,value='help'
}
})]]
function UI.Window:add(children)
UI:mergeProperties(self, children)
self:initChildren()
end
function UI.Window:eachChild()
local c = self.children and Util.shallowCopy(self.children)
local i = 0
return function()
i = i + 1
return c and c[i]
end
end
function UI.Window:remove()
Array.removeByValue(self.parent.children, self)
self.parent:dirty(true)
end
function UI.Window:getCursorPos()
return self.cursorX, self.cursorY
end
function UI.Window:setCursorPos(x, y)
self.cursorX = x
self.cursorY = y
self.parent:setCursorPos(self.x + x - 1, self.y + y - 1)
end
function UI.Window:setCursorBlink(blink)
self.cursorBlink = blink
end
UI.Window.docs.draw = [[draw(VOID)
Redraws the window in the internal buffer.]]
function UI.Window:draw()
self:clear()
self:drawChildren()
end
function UI.Window:drawChildren()
for child in self:eachChild() do
if child.enabled then
child:draw()
end
end
end
UI.Window.docs.getDoc = [[getDoc(STRING method)
Get 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.]]
function UI.Window:sync()
if self.parent then
self.parent:sync()
end
end
function UI.Window:enable(...)
if not self.enabled then
self.enabled = true
if self.transitionHint then
self:addTransition(self.transitionHint)
end
if self.modal then
self:raise()
self:capture(self)
end
for child in self:eachChild() do
if not child.enabled then
child:enable(...)
end
end
end
end
function UI.Window:disable()
if self.enabled then
self.enabled = false
self.parent:dirty(true)
if self.modal then
self:release(self)
end
for child in self:eachChild() do
if child.enabled then
child:disable()
end
end
end
end
function UI.Window:setTextScale(textScale)
self.textScale = textScale
self.parent:setTextScale(textScale)
end
UI.Window.docs.clear = [[clear(opt COLOR bg, opt COLOR fg)
Clears the window using either the passed values or the defaults for that window.]]
function UI.Window:clear(bg, fg)
Canvas.clear(self, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor'))
end
UI.Window.docs.clearLine = [[clearLine(NUMBER y, opt COLOR bg)
Clears the specified line.]]
function UI.Window:clearLine(y, bg)
self:write(1, y, _rep(' ', self.width), bg)
end
function UI.Window:clearArea(x, y, width, height, bg)
self:fillArea(x, y, width, height, ' ', bg)
end
function UI.Window:fillArea(x, y, width, height, fillChar, bg, fg)
if width > 0 then
local filler = _rep(fillChar, width)
for i = 0, height - 1 do
self:write(x, y + i, filler, bg, fg)
end
end
end
UI.Window.docs.write = [[write(NUMBER x, NUMBER y, STRING text, opt COLOR bg, opt COLOR fg)
Write text to the canvas.
If colors are not specified, the colors from the base class will be used.
If the base class does not have colors defined, colors will be inherited from the parent container.]]
function UI.Window:write(x, y, text, bg, fg)
Canvas.write(self, x, y, text, bg or self:getProperty('backgroundColor'), fg or self:getProperty('textColor'))
end
function UI.Window:centeredWrite(y, text, bg, fg)
if #text >= self.width then
self:write(1, y, text, bg, fg)
else
local x = math.floor((self.width-#text) / 2) + 1
self:write(x, y, text, bg, fg)
end
end
function UI.Window:print(text, bg, fg)
local marginLeft = self.marginLeft or 0
local marginRight = self.marginRight or 0
local width = self.width - marginLeft - marginRight
local cs = {
bg = bg or self:getProperty('backgroundColor'),
fg = fg or self:getProperty('textColor'),
palette = self.palette,
}
local y = (self.marginTop or 0) + 1
for _,line in pairs(Util.split(text)) do
for _, ln in ipairs(Blit(line, cs):wrap(width)) do
self:blit(marginLeft + 1, y, ln.text, ln.bg, ln.fg)
y = y + 1
end
end
end
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.]]
function UI.Window:setFocus(focus)
if self.parent then
self.parent:setFocus(focus)
end
end
UI.Window.docs.capture = [[capture(ELEMENT el)
Restricts input to the passed element's tree.]]
function UI.Window:capture(child)
if self.parent then
self.parent:capture(child)
end
end
function UI.Window:release(child)
if self.parent then
self.parent:release(child)
end
end
function UI.Window:pointToChild(x, y)
x = x + self.offx - self.x + 1
y = y + self.offy - self.y + 1
if self.children then
for i = #self.children, 1, -1 do
local child = self.children[i]
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
}
end
UI.Window.docs.getFocusables = [[getFocusables(VOID)
Returns a list of children that can accept focus.]]
function UI.Window:getFocusables()
local focusable = { }
local function focusSort(a, b)
if a.y == b.y then
return a.x < b.x
end
return a.y < b.y
end
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)
end
end
end
if self.children then
getFocusable(self)
end
return focusable
end
function UI.Window:focusFirst()
local focusables = self:getFocusables()
local focused = focusables[1]
if focused then
self:setFocus(focused)
end
end
function UI.Window:scrollIntoView()
local parent = self.parent
local offx, offy = parent.offx, parent.offy
if self.x <= parent.offx then
parent.offx = math.max(0, self.x - 1)
if offx ~= parent.offx then
parent:draw()
end
elseif self.x + self.width > parent.width + parent.offx then
parent.offx = self.x + self.width - parent.width - 1
if offx ~= parent.offx then
parent:draw()
end
end
-- TODO: fix
local function setOffset(y)
parent.offy = y
if offy ~= parent.offy then
parent:draw()
end
end
if self.y <= parent.offy then
setOffset(math.max(0, self.y - 1))
elseif self.y + self.height > parent.height + parent.offy then
setOffset(self.y + self.height - parent.height - 1)
end
end
function UI.Window:addTransition(effect, args, canvas)
self.parent:addTransition(effect, args, canvas or self)
end
UI.Window.docs.emit = [[emit(TABLE event)
Send an event to the element. The event handler for the element is called.
If the event handler returns true, then no further processing is done.
If the event handler does not return true, then the event is sent to the parent element
and continues up the element tree.
If an accelerator is defined, the accelerated event is processed in the same manner.
Accelerators are useful for making events unique.]]
function UI.Window:emit(event)
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
end
end
if parent.eventHandler then
if parent:eventHandler(event) then
return true
end
end
parent = parent.parent
end
end
function UI.Window:getProperty(property)
return self[property] or self.parent and self.parent:getProperty(property)
end
function UI.Window:find(uid)
local el = self.children and Util.find(self.children, 'uid', uid)
if not el then
for child in self:eachChild() do
el = child:find(uid)
if el then
break
end
end
end
return el
end
function UI.Window:eventHandler()
return false
end
--[[-- Terminal for computer / advanced computer / monitor --]]--
UI.Device = class(UI.Window)
UI.Device.defaults = {
UIElement = 'Device',
backgroundColor = colors.black,
textColor = colors.white,
textScale = 1,
effectsEnabled = true,
}
function UI.Device:postInit()
self.device = self.device or term.current()
if not self.device.setTextScale then
self.device.setTextScale = function() end
end
self.device.setTextScale(self.textScale)
self.width, self.height = self.device.getSize()
self.isColor = self.device.isColor()
Canvas.init(self, { isColor = self.isColor })
UI.devices[self.device.side or 'terminal'] = self
end
function UI.Device:resize()
self.device.setTextScale(self.textScale)
self.width, self.height = self.device.getSize()
self.lines = { }
-- TODO: resize all pages added to this device
Canvas.resize(self, self.width, self.height)
Canvas.clear(self, self.backgroundColor, self.textColor)
end
function UI.Device:setCursorPos(x, y)
self.cursorX = x
self.cursorY = y
end
function UI.Device:getCursorBlink()
return self.cursorBlink
end
function UI.Device:setCursorBlink(blink)
self.cursorBlink = blink
end
function UI.Device:setTextScale(textScale)
self.textScale = textScale
self.device.setTextScale(self.textScale)
end
function UI.Device:reset()
self.device.setBackgroundColor(colors.black)
self.device.setTextColor(colors.white)
self.device.clear()
self.device.setCursorPos(1, 1)
end
function UI.Device:addTransition(effect, args, canvas)
if not self.transitions then
self.transitions = { }
end
if type(effect) == 'string' then
effect = Transition[effect] or error('Invalid transition')
end
-- there can be only one
for k,v in pairs(self.transitions) do
if v.canvas == canvas then
table.remove(self.transitions, k)
break
end
end
table.insert(self.transitions, { effect = effect, args = args or { }, canvas = canvas })
end
function UI.Device:runTransitions(transitions)
for _,k in pairs(transitions) do
k.update = k.effect(k.canvas, k.args)
end
while true do
for _,k in ipairs(Util.keys(transitions)) do
local transition = transitions[k]
if not transition.update() then
transitions[k] = nil
end
end
self.currentPage:render(self, true)
if Util.empty(transitions) then
break
end
os.sleep(0)
end
end
function UI.Device:sync()
local transitions = self.effectsEnabled and self.transitions
self.transitions = nil
self.device.setCursorBlink(false)
if transitions then
self:runTransitions(transitions)
else
self.currentPage:render(self, true)
end
if self:getCursorBlink() then
self.device.setCursorPos(self.cursorX, self.cursorY)
if self.isColor then
self.device.setTextColor(colors.orange)
end
self.device.setCursorBlink(true)
end
end
-- lazy load components
local function loadComponents()
local function load(name)
local s, m = Util.run(_ENV, 'sys/modules/opus/ui/components/' .. name .. '.lua')
if not s then
error(m)
end
if UI[name]._preload then
error('Error loading UI.' .. name)
end
if UI.theme[name] and UI[name].defaults then
Util.merge(UI[name].defaults, UI.theme[name])
end
return UI[name]
end
local components = fs.list('sys/modules/opus/ui/components')
for _, f in pairs(components) do
local name = f:match('(.+)%.')
UI[name] = setmetatable({ }, {
__call = function(self, ...)
load(name)
setmetatable(self, getmetatable(UI[name]))
return self(...)
end
})
UI[name]._preload = function()
return load(name)
end
end
end
loadComponents()
UI:loadTheme('usr/config/ui.theme')
UI:setDefaultDevice(UI.Device())
return UI