build: add a format target and one shared editor config
The Lua modules were split between tabs and spaces because nothing pinned a style; .editorconfig is what lua-language-server reads on save, so the editor and the tree now agree. clang-format already had a config and left native/ unchanged. Embedded modules regenerate from the reformatted ui.lua.
This commit is contained in:
+30
-24
@@ -7,39 +7,45 @@ local hints = {}
|
||||
local ORDER = { "back", "left", "up", "down", "right", "confirm" }
|
||||
|
||||
local function available()
|
||||
local roles = {}
|
||||
if input and input.getButtons then
|
||||
for _, role in ipairs(input.getButtons()) do roles[role] = true end
|
||||
end
|
||||
return roles
|
||||
local roles = {}
|
||||
if input and input.getButtons then
|
||||
for _, role in ipairs(input.getButtons()) do
|
||||
roles[role] = true
|
||||
end
|
||||
end
|
||||
return roles
|
||||
end
|
||||
|
||||
---Paints the labels this device can actually act on, and returns the height it used.
|
||||
---@param actions table<string, string> Label per Button role; roles the device lacks are skipped.
|
||||
---@param options table|nil `y`, `font`, `color`, and `background` overrides.
|
||||
function hints.draw(actions, options)
|
||||
options = options or {}
|
||||
local font = options.font or gui.FONT_SMALL
|
||||
local color = options.color or gui.color(0, 0, 0)
|
||||
local background = options.background or gui.color(255, 255, 255)
|
||||
local height = gui.getFontHeight(font) + 6
|
||||
local y = options.y or (gui.getHeight() - height)
|
||||
options = options or {}
|
||||
local font = options.font or gui.FONT_SMALL
|
||||
local color = options.color or gui.color(0, 0, 0)
|
||||
local background = options.background or gui.color(255, 255, 255)
|
||||
local height = gui.getFontHeight(font) + 6
|
||||
local y = options.y or (gui.getHeight() - height)
|
||||
|
||||
local roles = available()
|
||||
local labels = {}
|
||||
for _, role in ipairs(ORDER) do
|
||||
if roles[role] and actions[role] then labels[#labels + 1] = actions[role] end
|
||||
end
|
||||
local roles = available()
|
||||
local labels = {}
|
||||
for _, role in ipairs(ORDER) do
|
||||
if roles[role] and actions[role] then
|
||||
labels[#labels + 1] = actions[role]
|
||||
end
|
||||
end
|
||||
|
||||
gui.fillRect(0, y, gui.getWidth(), height, background)
|
||||
if #labels == 0 then return height end
|
||||
gui.fillRect(0, y, gui.getWidth(), height, background)
|
||||
if #labels == 0 then
|
||||
return height
|
||||
end
|
||||
|
||||
local slot = gui.getWidth() // #labels
|
||||
for index, label in ipairs(labels) do
|
||||
local left = slot * (index - 1) + (slot - gui.getTextWidth(font, label)) // 2
|
||||
gui.drawText(font, left, y + 3, label, color, gui.STYLE_NORMAL, background)
|
||||
end
|
||||
return height
|
||||
local slot = gui.getWidth() // #labels
|
||||
for index, label in ipairs(labels) do
|
||||
local left = slot * (index - 1) + (slot - gui.getTextWidth(font, label)) // 2
|
||||
gui.drawText(font, left, y + 3, label, color, gui.STYLE_NORMAL, background)
|
||||
end
|
||||
return height
|
||||
end
|
||||
|
||||
return hints
|
||||
|
||||
+312
-233
@@ -44,9 +44,9 @@ local ui = {}
|
||||
|
||||
local THEME_PATH = "/.lua/theme"
|
||||
local THEMES = {
|
||||
light = { background = { 255, 255, 255 }, color = { 0, 0, 0 }, accent = { 0, 120, 255 }, radius = 6 },
|
||||
dark = { background = { 18, 18, 20 }, color = { 235, 235, 235 }, accent = { 166, 118, 255 }, radius = 6 },
|
||||
mono = { background = { 255, 255, 255 }, color = { 0, 0, 0 }, accent = { 0, 0, 0 }, radius = 0 },
|
||||
light = { background = { 255, 255, 255 }, color = { 0, 0, 0 }, accent = { 0, 120, 255 }, radius = 6 },
|
||||
dark = { background = { 18, 18, 20 }, color = { 235, 235, 235 }, accent = { 166, 118, 255 }, radius = 6 },
|
||||
mono = { background = { 255, 255, 255 }, color = { 0, 0, 0 }, accent = { 0, 0, 0 }, radius = 0 },
|
||||
}
|
||||
|
||||
local enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
||||
@@ -59,124 +59,148 @@ local activeScreen
|
||||
local applyPalette
|
||||
|
||||
local function mix(a, b, amount)
|
||||
local out = {}
|
||||
for i = 1, 3 do out[i] = math.floor(a[i] + (b[i] - a[i]) * amount + 0.5) end
|
||||
return out
|
||||
local out = {}
|
||||
for i = 1, 3 do
|
||||
out[i] = math.floor(a[i] + (b[i] - a[i]) * amount + 0.5)
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function color(rgb)
|
||||
return gui.color(rgb[1], rgb[2], rgb[3])
|
||||
return gui.color(rgb[1], rgb[2], rgb[3])
|
||||
end
|
||||
|
||||
local function palette(seed)
|
||||
local background, foreground, accent = seed.background, seed.color, seed.accent
|
||||
return {
|
||||
background = color(background),
|
||||
color = color(foreground),
|
||||
muted = color(mix(foreground, background, 0.45)),
|
||||
accent = color(accent),
|
||||
disabled = color(mix(background, foreground, 0.25)),
|
||||
face = color(mix(background, foreground, 0.08)),
|
||||
pressedFace = color(accent),
|
||||
pressedColor = color(background),
|
||||
focusColor = color(accent),
|
||||
radius = seed.radius,
|
||||
}
|
||||
local background, foreground, accent = seed.background, seed.color, seed.accent
|
||||
return {
|
||||
background = color(background),
|
||||
color = color(foreground),
|
||||
muted = color(mix(foreground, background, 0.45)),
|
||||
accent = color(accent),
|
||||
disabled = color(mix(background, foreground, 0.25)),
|
||||
face = color(mix(background, foreground, 0.08)),
|
||||
pressedFace = color(accent),
|
||||
pressedColor = color(background),
|
||||
focusColor = color(accent),
|
||||
radius = seed.radius,
|
||||
}
|
||||
end
|
||||
|
||||
local function loadTheme(name)
|
||||
themeName = THEMES[name] and name or "light"
|
||||
ui.theme = palette(THEMES[themeName])
|
||||
themeName = THEMES[name] and name or "light"
|
||||
ui.theme = palette(THEMES[themeName])
|
||||
end
|
||||
|
||||
---@return string
|
||||
function ui.getTheme()
|
||||
return themeName
|
||||
return themeName
|
||||
end
|
||||
|
||||
---@return string[]
|
||||
function ui.themeNames()
|
||||
local names = {}
|
||||
for name in pairs(THEMES) do names[#names + 1] = name end
|
||||
table.sort(names)
|
||||
return names
|
||||
local names = {}
|
||||
for name in pairs(THEMES) do
|
||||
names[#names + 1] = name
|
||||
end
|
||||
table.sort(names)
|
||||
return names
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function ui.setTheme(name)
|
||||
if not THEMES[name] then return nil, "Unknown theme" end
|
||||
local ok, err = fs.writeFile(THEME_PATH, name)
|
||||
if not ok then return nil, err end
|
||||
loadTheme(name)
|
||||
if activeScreen then
|
||||
applyPalette(activeScreen.root)
|
||||
gui.clear(ui.theme.background)
|
||||
node.invalidate(activeScreen.root)
|
||||
end
|
||||
return true
|
||||
if not THEMES[name] then
|
||||
return nil, "Unknown theme"
|
||||
end
|
||||
local ok, err = fs.writeFile(THEME_PATH, name)
|
||||
if not ok then
|
||||
return nil, err
|
||||
end
|
||||
loadTheme(name)
|
||||
if activeScreen then
|
||||
applyPalette(activeScreen.root)
|
||||
gui.clear(ui.theme.background)
|
||||
node.invalidate(activeScreen.root)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local savedTheme = fs.readFile(THEME_PATH, 32)
|
||||
loadTheme(savedTheme and savedTheme:match("^%s*(.-)%s*$") or "light")
|
||||
loadTheme(savedTheme and savedTheme:match "^%s*(.-)%s*$" or "light")
|
||||
|
||||
local function clearState()
|
||||
enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
||||
pressStyles = {}
|
||||
enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
||||
pressStyles = {}
|
||||
end
|
||||
|
||||
node.setPainter(function(id, x, y, w, h)
|
||||
local painter = painters[id]
|
||||
if painter then painter(id, x, y, w, h) end
|
||||
local painter = painters[id]
|
||||
if painter then
|
||||
painter(id, x, y, w, h)
|
||||
end
|
||||
end)
|
||||
|
||||
local STYLE_KEYS = {
|
||||
"color", "fill", "border", "face", "pressedFace", "pressedColor",
|
||||
"focusColor", "radius", "font", "textStyle",
|
||||
"color",
|
||||
"fill",
|
||||
"border",
|
||||
"face",
|
||||
"pressedFace",
|
||||
"pressedColor",
|
||||
"focusColor",
|
||||
"radius",
|
||||
"font",
|
||||
"textStyle",
|
||||
}
|
||||
|
||||
local function applyStyle(id, spec)
|
||||
local style, hasStyle = {}, false
|
||||
for _, key in ipairs(STYLE_KEYS) do
|
||||
if spec[key] ~= nil then
|
||||
style[key], hasStyle = spec[key], true
|
||||
end
|
||||
end
|
||||
if spec.background ~= nil then
|
||||
style.background, style.fill, hasStyle = spec.background, spec.background, true
|
||||
end
|
||||
if hasStyle then node.setStyle(id, style) end
|
||||
local style, hasStyle = {}, false
|
||||
for _, key in ipairs(STYLE_KEYS) do
|
||||
if spec[key] ~= nil then
|
||||
style[key], hasStyle = spec[key], true
|
||||
end
|
||||
end
|
||||
if spec.background ~= nil then
|
||||
style.background, style.fill, hasStyle = spec.background, spec.background, true
|
||||
end
|
||||
if hasStyle then
|
||||
node.setStyle(id, style)
|
||||
end
|
||||
end
|
||||
|
||||
local function build(spec, kind)
|
||||
spec = spec or {}
|
||||
if laidOut then ui.reset() end
|
||||
spec = spec or {}
|
||||
if laidOut then
|
||||
ui.reset()
|
||||
end
|
||||
|
||||
local children = {}
|
||||
for index, child in ipairs(spec) do
|
||||
children[index] = child
|
||||
spec[index] = nil
|
||||
end
|
||||
local children = {}
|
||||
for index, child in ipairs(spec) do
|
||||
children[index] = child
|
||||
spec[index] = nil
|
||||
end
|
||||
|
||||
spec.type = kind
|
||||
spec.interactive = spec.on_enter ~= nil or spec.on_exit ~= nil or spec.on_click ~= nil
|
||||
local id = node.create(nil, spec)
|
||||
for _, child in ipairs(children) do node.attach(id, child) end
|
||||
spec.type = kind
|
||||
spec.interactive = spec.on_enter ~= nil or spec.on_exit ~= nil or spec.on_click ~= nil
|
||||
local id = node.create(nil, spec)
|
||||
for _, child in ipairs(children) do
|
||||
node.attach(id, child)
|
||||
end
|
||||
|
||||
applyStyle(id, spec)
|
||||
enterHandlers[id] = spec.on_enter
|
||||
exitHandlers[id] = spec.on_exit
|
||||
clickHandlers[id] = spec.on_click
|
||||
painters[id] = spec.paint
|
||||
pressStyles[id] = spec.press_style ~= false
|
||||
return id
|
||||
applyStyle(id, spec)
|
||||
enterHandlers[id] = spec.on_enter
|
||||
exitHandlers[id] = spec.on_exit
|
||||
clickHandlers[id] = spec.on_click
|
||||
painters[id] = spec.paint
|
||||
pressStyles[id] = spec.press_style ~= false
|
||||
return id
|
||||
end
|
||||
|
||||
---@param spec UiSpec
|
||||
---@return NodeId
|
||||
function ui.box(spec)
|
||||
return build(spec, "box")
|
||||
return build(spec, "box")
|
||||
end
|
||||
|
||||
---Side length for a grid of square cards that fits the frame, and the column count that
|
||||
@@ -188,112 +212,123 @@ end
|
||||
---@return integer side
|
||||
---@return integer columns
|
||||
function ui.cardSide(count, pad, gap, reserve)
|
||||
local columns = gui.getWidth() >= gui.getHeight() and 3 or 2
|
||||
local rows = math.ceil(count / columns)
|
||||
local byWidth = (gui.getWidth() - 2 * pad - (columns - 1) * gap) // columns
|
||||
local byHeight = (gui.getHeight() - 2 * pad - (reserve or 0) - (rows - 1) * gap) // rows
|
||||
return math.min(byWidth, byHeight), columns
|
||||
local columns = gui.getWidth() >= gui.getHeight() and 3 or 2
|
||||
local rows = math.ceil(count / columns)
|
||||
local byWidth = (gui.getWidth() - 2 * pad - (columns - 1) * gap) // columns
|
||||
local byHeight = (gui.getHeight() - 2 * pad - (reserve or 0) - (rows - 1) * gap) // rows
|
||||
return math.min(byWidth, byHeight), columns
|
||||
end
|
||||
|
||||
---@param spec UiSpec
|
||||
---@return NodeId
|
||||
function ui.spacer(spec)
|
||||
spec = spec or {}
|
||||
return build({ w = spec.w, h = spec.h }, "box")
|
||||
spec = spec or {}
|
||||
return build({ w = spec.w, h = spec.h }, "box")
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@param spec? UiSpec
|
||||
---@return NodeId
|
||||
function ui.text(text, spec)
|
||||
spec = spec or {}
|
||||
spec.label = text
|
||||
spec.textStyle, spec.style = spec.style, nil
|
||||
return build(spec, "text")
|
||||
spec = spec or {}
|
||||
spec.label = text
|
||||
spec.textStyle, spec.style = spec.style, nil
|
||||
return build(spec, "text")
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@param spec? UiSpec
|
||||
---@return NodeId
|
||||
function ui.label(text, spec)
|
||||
spec = spec or {}
|
||||
local font, style = spec.font or gui.FONT_UI, spec.style or gui.STYLE_NORMAL
|
||||
if spec.fit and gui.getTextWidth(font, text, style) > spec.fit then
|
||||
while #text > 1 and gui.getTextWidth(font, text .. "~", style) > spec.fit do
|
||||
text = text:sub(1, -2)
|
||||
end
|
||||
text = text .. "~"
|
||||
end
|
||||
spec.w = gui.getTextWidth(font, text, style)
|
||||
spec.h = gui.getFontHeight(font, style)
|
||||
spec.font, spec.fit = font, nil
|
||||
return ui.text(text, spec)
|
||||
spec = spec or {}
|
||||
local font, style = spec.font or gui.FONT_UI, spec.style or gui.STYLE_NORMAL
|
||||
if spec.fit and gui.getTextWidth(font, text, style) > spec.fit then
|
||||
while #text > 1 and gui.getTextWidth(font, text .. "~", style) > spec.fit do
|
||||
text = text:sub(1, -2)
|
||||
end
|
||||
text = text .. "~"
|
||||
end
|
||||
spec.w = gui.getTextWidth(font, text, style)
|
||||
spec.h = gui.getFontHeight(font, style)
|
||||
spec.font, spec.fit = font, nil
|
||||
return ui.text(text, spec)
|
||||
end
|
||||
|
||||
---@param spec UiSpec
|
||||
---@return NodeId
|
||||
function ui.button(spec)
|
||||
spec = spec or {}
|
||||
spec.pad = spec.pad or 8
|
||||
spec.align = spec.align or "center"
|
||||
local label, font = spec.label, spec.font
|
||||
spec.label = nil
|
||||
local id = build(spec, "button")
|
||||
if label then node.create(id, { type = "text", label = label, font = font or gui.FONT_UI }) end
|
||||
return id
|
||||
spec = spec or {}
|
||||
spec.pad = spec.pad or 8
|
||||
spec.align = spec.align or "center"
|
||||
local label, font = spec.label, spec.font
|
||||
spec.label = nil
|
||||
local id = build(spec, "button")
|
||||
if label then
|
||||
node.create(id, { type = "text", label = label, font = font or gui.FONT_UI })
|
||||
end
|
||||
return id
|
||||
end
|
||||
|
||||
---@param spec UiSpec
|
||||
---@return NodeId
|
||||
function ui.custom(spec)
|
||||
return build(spec, "custom")
|
||||
return build(spec, "custom")
|
||||
end
|
||||
|
||||
---@param id NodeId
|
||||
---@param text string
|
||||
function ui.setText(id, text)
|
||||
if node.getLabel(id) == text then return end
|
||||
node.setLabel(id, text)
|
||||
node.invalidate(id)
|
||||
if node.getLabel(id) == text then
|
||||
return
|
||||
end
|
||||
node.setLabel(id, text)
|
||||
node.invalidate(id)
|
||||
end
|
||||
|
||||
---@param id NodeId
|
||||
function ui.invalidate(id)
|
||||
node.invalidate(id)
|
||||
node.invalidate(id)
|
||||
end
|
||||
|
||||
---@param spec UiConfirmSpec
|
||||
---@return NodeId
|
||||
function ui.confirm(spec)
|
||||
local card = {
|
||||
w = spec.w or 0.85,
|
||||
pad = 16,
|
||||
gap = 12,
|
||||
background = spec.background or ui.theme.background,
|
||||
border = spec.border or ui.theme.muted,
|
||||
ui.text(spec.title),
|
||||
}
|
||||
if spec.message then card[#card + 1] = ui.text(spec.message, { color = ui.theme.muted }) end
|
||||
local card = {
|
||||
w = spec.w or 0.85,
|
||||
pad = 16,
|
||||
gap = 12,
|
||||
background = spec.background or ui.theme.background,
|
||||
border = spec.border or ui.theme.muted,
|
||||
ui.text(spec.title),
|
||||
}
|
||||
if spec.message then
|
||||
card[#card + 1] = ui.text(spec.message, { color = ui.theme.muted })
|
||||
end
|
||||
|
||||
local buttons = { row = true, gap = 8, justify = "end" }
|
||||
if spec.cancel ~= false then
|
||||
buttons[#buttons + 1] = ui.button { label = spec.cancel or "cancel", on_click = spec.on_cancel }
|
||||
end
|
||||
buttons[#buttons + 1] = ui.button { label = spec.ok or "ok", on_click = spec.on_ok }
|
||||
card[#card + 1] = ui.box(buttons)
|
||||
local buttons = { row = true, gap = 8, justify = "end" }
|
||||
if spec.cancel ~= false then
|
||||
buttons[#buttons + 1] = ui.button { label = spec.cancel or "cancel", on_click = spec.on_cancel }
|
||||
end
|
||||
buttons[#buttons + 1] = ui.button { label = spec.ok or "ok", on_click = spec.on_ok }
|
||||
card[#card + 1] = ui.box(buttons)
|
||||
|
||||
return ui.box {
|
||||
at = { x = 0, y = 0 }, w = "fill", h = "fill", capture = true,
|
||||
align = "center", justify = "center", on_click = spec.on_outside,
|
||||
ui.box(card),
|
||||
}
|
||||
return ui.box {
|
||||
at = { x = 0, y = 0 },
|
||||
w = "fill",
|
||||
h = "fill",
|
||||
capture = true,
|
||||
align = "center",
|
||||
justify = "center",
|
||||
on_click = spec.on_outside,
|
||||
ui.box(card),
|
||||
}
|
||||
end
|
||||
|
||||
function ui.reset()
|
||||
node.reset()
|
||||
clearState()
|
||||
laidOut = false
|
||||
activeScreen = nil
|
||||
node.reset()
|
||||
clearState()
|
||||
laidOut = false
|
||||
activeScreen = nil
|
||||
end
|
||||
|
||||
---@class UiScreen
|
||||
@@ -301,159 +336,203 @@ local Screen = {}
|
||||
Screen.__index = Screen
|
||||
|
||||
applyPalette = function(root)
|
||||
-- The root is the panel background, not a card: no border, so it takes the fast fillRect
|
||||
-- path rather than the per-pixel roundRect one. Radius stays so cards inherit it.
|
||||
node.setStyle(root, {
|
||||
color = ui.theme.color,
|
||||
background = ui.theme.background,
|
||||
face = ui.theme.face,
|
||||
pressedFace = ui.theme.pressedFace,
|
||||
pressedColor = ui.theme.pressedColor,
|
||||
focusColor = ui.theme.focusColor,
|
||||
radius = ui.theme.radius,
|
||||
font = gui.FONT_UI,
|
||||
})
|
||||
-- The root is the panel background, not a card: no border, so it takes the fast fillRect
|
||||
-- path rather than the per-pixel roundRect one. Radius stays so cards inherit it.
|
||||
node.setStyle(root, {
|
||||
color = ui.theme.color,
|
||||
background = ui.theme.background,
|
||||
face = ui.theme.face,
|
||||
pressedFace = ui.theme.pressedFace,
|
||||
pressedColor = ui.theme.pressedColor,
|
||||
focusColor = ui.theme.focusColor,
|
||||
radius = ui.theme.radius,
|
||||
font = gui.FONT_UI,
|
||||
})
|
||||
end
|
||||
|
||||
---@param root NodeId
|
||||
---@param style? NodeStyle
|
||||
---@return UiScreen
|
||||
function ui.screen(root, style)
|
||||
node.setSize(root, "fill", "fill")
|
||||
applyPalette(root)
|
||||
if style then node.setStyle(root, style) end
|
||||
local screen = setmetatable({ root = root }, Screen)
|
||||
activeScreen = screen
|
||||
screen:relayout()
|
||||
return screen
|
||||
node.setSize(root, "fill", "fill")
|
||||
applyPalette(root)
|
||||
if style then
|
||||
node.setStyle(root, style)
|
||||
end
|
||||
local screen = setmetatable({ root = root }, Screen)
|
||||
activeScreen = screen
|
||||
screen:relayout()
|
||||
return screen
|
||||
end
|
||||
|
||||
function Screen:relayout()
|
||||
local ok, err = node.layout(self.root, 0, 0, gui.getWidth(), gui.getHeight())
|
||||
if not ok then error(err, 2) end
|
||||
node.dropScratch()
|
||||
laidOut = true
|
||||
gui.clear(ui.theme.background)
|
||||
local ok, err = node.layout(self.root, 0, 0, gui.getWidth(), gui.getHeight())
|
||||
if not ok then
|
||||
error(err, 2)
|
||||
end
|
||||
node.dropScratch()
|
||||
laidOut = true
|
||||
gui.clear(ui.theme.background)
|
||||
end
|
||||
|
||||
function Screen:draw()
|
||||
node.draw(self.root)
|
||||
node.draw(self.root)
|
||||
end
|
||||
|
||||
local function inside(id, x, y)
|
||||
local rx, ry, rw, rh = node.getRect(id)
|
||||
return x >= rx and x < rx + rw and y >= ry and y < ry + rh
|
||||
local rx, ry, rw, rh = node.getRect(id)
|
||||
return x >= rx and x < rx + rw and y >= ry and y < ry + rh
|
||||
end
|
||||
|
||||
local function enter(id, x, y)
|
||||
if pressStyles[id] then node.setPressed(id, true) end
|
||||
local handler = enterHandlers[id]
|
||||
if handler then handler(id, x, y) end
|
||||
if pressStyles[id] then
|
||||
node.setPressed(id, true)
|
||||
end
|
||||
local handler = enterHandlers[id]
|
||||
if handler then
|
||||
handler(id, x, y)
|
||||
end
|
||||
end
|
||||
|
||||
local function exit(id, x, y)
|
||||
if pressStyles[id] then node.setPressed(id, false) end
|
||||
local handler = exitHandlers[id]
|
||||
if handler then handler(id, x, y) end
|
||||
if pressStyles[id] then
|
||||
node.setPressed(id, false)
|
||||
end
|
||||
local handler = exitHandlers[id]
|
||||
if handler then
|
||||
handler(id, x, y)
|
||||
end
|
||||
end
|
||||
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return boolean handled
|
||||
function Screen:down(x, y)
|
||||
local focused = node.getFocus()
|
||||
if focused then
|
||||
node.setFocus(nil)
|
||||
local handler = exitHandlers[focused]
|
||||
if handler then handler(focused) end
|
||||
end
|
||||
local focused = node.getFocus()
|
||||
if focused then
|
||||
node.setFocus(nil)
|
||||
local handler = exitHandlers[focused]
|
||||
if handler then
|
||||
handler(focused)
|
||||
end
|
||||
end
|
||||
|
||||
local target = node.hit(self.root, x, y)
|
||||
if not target then return false end
|
||||
self.captured, self.inside = target, true
|
||||
enter(target, x, y)
|
||||
return true
|
||||
local target = node.hit(self.root, x, y)
|
||||
if not target then
|
||||
return false
|
||||
end
|
||||
self.captured, self.inside = target, true
|
||||
enter(target, x, y)
|
||||
return true
|
||||
end
|
||||
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return boolean handled
|
||||
function Screen:move(x, y)
|
||||
local target = self.captured
|
||||
if not target then return false end
|
||||
local isInside = inside(target, x, y)
|
||||
if isInside ~= self.inside then
|
||||
self.inside = isInside
|
||||
if isInside then enter(target, x, y) else exit(target, x, y) end
|
||||
end
|
||||
return true
|
||||
local target = self.captured
|
||||
if not target then
|
||||
return false
|
||||
end
|
||||
local isInside = inside(target, x, y)
|
||||
if isInside ~= self.inside then
|
||||
self.inside = isInside
|
||||
if isInside then
|
||||
enter(target, x, y)
|
||||
else
|
||||
exit(target, x, y)
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return boolean handled
|
||||
function Screen:up(x, y)
|
||||
local target = self.captured
|
||||
if not target then return false end
|
||||
local wasActive = self.inside
|
||||
local releasedInside = inside(target, x, y)
|
||||
local handler = releasedInside and clickHandlers[target] or nil
|
||||
self.captured, self.inside = nil, nil
|
||||
if wasActive then exit(target, x, y) end
|
||||
if handler then handler(target, x, y) end
|
||||
return true
|
||||
local target = self.captured
|
||||
if not target then
|
||||
return false
|
||||
end
|
||||
local wasActive = self.inside
|
||||
local releasedInside = inside(target, x, y)
|
||||
local handler = releasedInside and clickHandlers[target] or nil
|
||||
self.captured, self.inside = nil, nil
|
||||
if wasActive then
|
||||
exit(target, x, y)
|
||||
end
|
||||
if handler then
|
||||
handler(target, x, y)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local DIRECTIONS = { up = true, down = true, left = true, right = true }
|
||||
|
||||
local function focusFirst(screen)
|
||||
local focused = node.focusFirst(screen.root)
|
||||
if focused then
|
||||
local handler = enterHandlers[focused]
|
||||
if handler then handler(focused) end
|
||||
end
|
||||
return focused
|
||||
local focused = node.focusFirst(screen.root)
|
||||
if focused then
|
||||
local handler = enterHandlers[focused]
|
||||
if handler then
|
||||
handler(focused)
|
||||
end
|
||||
end
|
||||
return focused
|
||||
end
|
||||
|
||||
---@param name string Button name; directions and confirm are handled.
|
||||
---@param pressed boolean
|
||||
---@return boolean handled
|
||||
function Screen:button(name, pressed)
|
||||
if type(pressed) ~= "boolean" then error("button state must be boolean", 2) end
|
||||
if type(pressed) ~= "boolean" then
|
||||
error("button state must be boolean", 2)
|
||||
end
|
||||
|
||||
if DIRECTIONS[name] then
|
||||
if not pressed then return true end
|
||||
local previous = node.getFocus()
|
||||
if not previous then
|
||||
focusFirst(self)
|
||||
return true
|
||||
end
|
||||
local focused = node.moveFocus(self.root, name)
|
||||
if focused ~= previous then
|
||||
local leave = exitHandlers[previous]
|
||||
if leave then leave(previous) end
|
||||
local arrive = enterHandlers[focused]
|
||||
if arrive then arrive(focused) end
|
||||
end
|
||||
return true
|
||||
end
|
||||
if DIRECTIONS[name] then
|
||||
if not pressed then
|
||||
return true
|
||||
end
|
||||
local previous = node.getFocus()
|
||||
if not previous then
|
||||
focusFirst(self)
|
||||
return true
|
||||
end
|
||||
local focused = node.moveFocus(self.root, name)
|
||||
if focused ~= previous then
|
||||
local leave = exitHandlers[previous]
|
||||
if leave then
|
||||
leave(previous)
|
||||
end
|
||||
local arrive = enterHandlers[focused]
|
||||
if arrive then
|
||||
arrive(focused)
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if name ~= "confirm" then return false end
|
||||
local focused = node.getFocus() or focusFirst(self)
|
||||
if not focused then return false end
|
||||
if pressed then
|
||||
node.setPressed(focused, true)
|
||||
self.confirming = focused
|
||||
else
|
||||
local target = self.confirming
|
||||
self.confirming = nil
|
||||
if target then
|
||||
node.setPressed(target, false)
|
||||
local handler = clickHandlers[target]
|
||||
if handler then handler(target) end
|
||||
end
|
||||
end
|
||||
return true
|
||||
if name ~= "confirm" then
|
||||
return false
|
||||
end
|
||||
local focused = node.getFocus() or focusFirst(self)
|
||||
if not focused then
|
||||
return false
|
||||
end
|
||||
if pressed then
|
||||
node.setPressed(focused, true)
|
||||
self.confirming = focused
|
||||
else
|
||||
local target = self.confirming
|
||||
self.confirming = nil
|
||||
if target then
|
||||
node.setPressed(target, false)
|
||||
local handler = clickHandlers[target]
|
||||
if handler then
|
||||
handler(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return ui
|
||||
|
||||
+31
-13
@@ -1,25 +1,43 @@
|
||||
package.path = "./lua/lib/?.lua;" .. package.path
|
||||
|
||||
local drawn = {}
|
||||
local roles = {"confirm", "back", "right"}
|
||||
local roles = { "confirm", "back", "right" }
|
||||
|
||||
gui = {
|
||||
FONT_SMALL = 0,
|
||||
STYLE_NORMAL = 0,
|
||||
color = function(r, g, b) return r * 65536 + g * 256 + b end,
|
||||
getWidth = function() return 300 end,
|
||||
getHeight = function() return 240 end,
|
||||
getFontHeight = function() return 8 end,
|
||||
getTextWidth = function(_, text) return #text * 6 end,
|
||||
fillRect = function(x, y, w, h) drawn.strip = {x, y, w, h} end,
|
||||
drawText = function(_, x, _, text) drawn[#drawn + 1] = {text = text, x = x} end,
|
||||
color = function(r, g, b)
|
||||
return r * 65536 + g * 256 + b
|
||||
end,
|
||||
getWidth = function()
|
||||
return 300
|
||||
end,
|
||||
getHeight = function()
|
||||
return 240
|
||||
end,
|
||||
getFontHeight = function()
|
||||
return 8
|
||||
end,
|
||||
getTextWidth = function(_, text)
|
||||
return #text * 6
|
||||
end,
|
||||
fillRect = function(x, y, w, h)
|
||||
drawn.strip = { x, y, w, h }
|
||||
end,
|
||||
drawText = function(_, x, _, text)
|
||||
drawn[#drawn + 1] = { text = text, x = x }
|
||||
end,
|
||||
}
|
||||
|
||||
input = {getButtons = function() return roles end}
|
||||
input = {
|
||||
getButtons = function()
|
||||
return roles
|
||||
end,
|
||||
}
|
||||
|
||||
local hints = require("hints")
|
||||
local hints = require "hints"
|
||||
|
||||
local height = hints.draw({back = "Close", confirm = "Select", up = "Scroll"})
|
||||
local height = hints.draw { back = "Close", confirm = "Select", up = "Scroll" }
|
||||
assert(height == 14, "height covers the font plus padding")
|
||||
assert(drawn.strip[2] == 226, "the strip sits at the bottom by default")
|
||||
|
||||
@@ -30,7 +48,7 @@ assert(drawn[1].x == 60 and drawn[2].x == 207, "each label centres in its slot")
|
||||
|
||||
drawn = {}
|
||||
roles = {}
|
||||
assert(hints.draw({confirm = "Select"}) == 14, "a device with no buttons still clears the strip")
|
||||
assert(hints.draw { confirm = "Select" } == 14, "a device with no buttons still clears the strip")
|
||||
assert(#drawn == 0 and drawn.strip, "nothing is labelled, but the strip is cleared")
|
||||
|
||||
print("ok")
|
||||
print "ok"
|
||||
|
||||
+100
-49
@@ -6,7 +6,9 @@ local cleared, invalidated = nil, {}
|
||||
fs = {
|
||||
readFile = function(path, maxBytes)
|
||||
local value = files[path]
|
||||
if value and #value > maxBytes then return nil, "too large" end
|
||||
if value and #value > maxBytes then
|
||||
return nil, "too large"
|
||||
end
|
||||
return value, value and nil or "missing"
|
||||
end,
|
||||
writeFile = function(path, value)
|
||||
@@ -24,12 +26,24 @@ gui = {
|
||||
FONT_LARGE = 3,
|
||||
STYLE_NORMAL = 0,
|
||||
STYLE_BOLD = 1,
|
||||
color = function(r, g, b) return r * 65536 + g * 256 + b end,
|
||||
getWidth = function() return frameWidth end,
|
||||
getHeight = function() return frameHeight end,
|
||||
getTextWidth = function(_, text) return #text * 6 end,
|
||||
getFontHeight = function() return 8 end,
|
||||
clear = function(color) cleared = color end,
|
||||
color = function(r, g, b)
|
||||
return r * 65536 + g * 256 + b
|
||||
end,
|
||||
getWidth = function()
|
||||
return frameWidth
|
||||
end,
|
||||
getHeight = function()
|
||||
return frameHeight
|
||||
end,
|
||||
getTextWidth = function(_, text)
|
||||
return #text * 6
|
||||
end,
|
||||
getFontHeight = function()
|
||||
return 8
|
||||
end,
|
||||
clear = function(color)
|
||||
cleared = color
|
||||
end,
|
||||
}
|
||||
|
||||
local nodes, focus, painter = {}, nil, nil
|
||||
@@ -38,7 +52,9 @@ local buttonCount = 0
|
||||
local function interactiveNodes()
|
||||
local result = {}
|
||||
for id, entry in ipairs(nodes) do
|
||||
if entry.interactive then result[#result + 1] = id end
|
||||
if entry.interactive then
|
||||
result[#result + 1] = id
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
@@ -59,18 +75,24 @@ node = {
|
||||
type = spec.type,
|
||||
label = spec.label,
|
||||
interactive = spec.interactive,
|
||||
rect = {x, 0, 90, 50},
|
||||
rect = { x, 0, 90, 50 },
|
||||
pressed = false,
|
||||
style = {},
|
||||
}
|
||||
return id
|
||||
end,
|
||||
attach = function(parent, child) nodes[child].parent = parent end,
|
||||
attach = function(parent, child)
|
||||
nodes[child].parent = parent
|
||||
end,
|
||||
setSize = function() end,
|
||||
setStyle = function(id, style)
|
||||
for key, value in pairs(style) do nodes[id].style[key] = value end
|
||||
for key, value in pairs(style) do
|
||||
nodes[id].style[key] = value
|
||||
end
|
||||
end,
|
||||
layout = function()
|
||||
return true
|
||||
end,
|
||||
layout = function() return true end,
|
||||
dropScratch = function() end,
|
||||
hit = function(_, x, y)
|
||||
for _, id in ipairs(interactiveNodes()) do
|
||||
@@ -80,17 +102,33 @@ node = {
|
||||
end
|
||||
end
|
||||
end,
|
||||
getRect = function(id) return table.unpack(nodes[id].rect) end,
|
||||
setLabel = function(id, text) nodes[id].label = text end,
|
||||
getLabel = function(id) return nodes[id].label end,
|
||||
invalidate = function(id) invalidated[#invalidated + 1] = id end,
|
||||
setPressed = function(id, pressed) nodes[id].pressed = pressed end,
|
||||
isPressed = function(id) return nodes[id].pressed end,
|
||||
setPainter = function(callback) painter = callback end,
|
||||
getRect = function(id)
|
||||
return table.unpack(nodes[id].rect)
|
||||
end,
|
||||
setLabel = function(id, text)
|
||||
nodes[id].label = text
|
||||
end,
|
||||
getLabel = function(id)
|
||||
return nodes[id].label
|
||||
end,
|
||||
invalidate = function(id)
|
||||
invalidated[#invalidated + 1] = id
|
||||
end,
|
||||
setPressed = function(id, pressed)
|
||||
nodes[id].pressed = pressed
|
||||
end,
|
||||
isPressed = function(id)
|
||||
return nodes[id].pressed
|
||||
end,
|
||||
setPainter = function(callback)
|
||||
painter = callback
|
||||
end,
|
||||
draw = function()
|
||||
if painter then
|
||||
for id, entry in ipairs(nodes) do
|
||||
if entry.type == "custom" then painter(id, table.unpack(entry.rect)) end
|
||||
if entry.type == "custom" then
|
||||
painter(id, table.unpack(entry.rect))
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
@@ -98,49 +136,58 @@ node = {
|
||||
focus = interactiveNodes()[1]
|
||||
return focus
|
||||
end,
|
||||
setFocus = function(id) focus = id end,
|
||||
getFocus = function() return focus end,
|
||||
setFocus = function(id)
|
||||
focus = id
|
||||
end,
|
||||
getFocus = function()
|
||||
return focus
|
||||
end,
|
||||
moveFocus = function(_, direction)
|
||||
local items, current = interactiveNodes(), nil
|
||||
for index, id in ipairs(items) do
|
||||
if id == focus then current = index break end
|
||||
if id == focus then
|
||||
current = index
|
||||
break
|
||||
end
|
||||
end
|
||||
local step = (direction == "right" or direction == "down") and 1 or -1
|
||||
local nextIndex = current and current + step or 1
|
||||
if items[nextIndex] then focus = items[nextIndex] end
|
||||
if items[nextIndex] then
|
||||
focus = items[nextIndex]
|
||||
end
|
||||
return focus
|
||||
end,
|
||||
}
|
||||
|
||||
local ui = require("ui")
|
||||
local ui = require "ui"
|
||||
|
||||
assert(ui.getTheme() == "light")
|
||||
assert(table.concat(ui.themeNames(), ",") == "dark,light,mono")
|
||||
local ok, err = ui.setTheme("missing")
|
||||
local ok, err = ui.setTheme "missing"
|
||||
assert(ok == nil and err == "Unknown theme")
|
||||
assert(ui.setTheme("dark") == true)
|
||||
assert(ui.setTheme "dark" == true)
|
||||
assert(files["/.lua/theme"] == "dark" and ui.getTheme() == "dark")
|
||||
|
||||
local events = {}
|
||||
local function handler(name)
|
||||
return function(id, x, y)
|
||||
events[#events + 1] = {name, id, x, y}
|
||||
events[#events + 1] = { name, id, x, y }
|
||||
end
|
||||
end
|
||||
|
||||
local first = ui.button{
|
||||
local first = ui.button {
|
||||
label = "one",
|
||||
on_enter = handler("enter"),
|
||||
on_exit = handler("exit"),
|
||||
on_click = handler("click"),
|
||||
on_enter = handler "enter",
|
||||
on_exit = handler "exit",
|
||||
on_click = handler "click",
|
||||
}
|
||||
local second = ui.button{
|
||||
local second = ui.button {
|
||||
label = "two",
|
||||
on_enter = handler("enter"),
|
||||
on_exit = handler("exit"),
|
||||
on_click = handler("click"),
|
||||
on_enter = handler "enter",
|
||||
on_exit = handler "exit",
|
||||
on_click = handler "click",
|
||||
}
|
||||
local screen = ui.screen(ui.box{row = true, first, second})
|
||||
local screen = ui.screen(ui.box { row = true, first, second })
|
||||
|
||||
assert(screen:down(10, 10))
|
||||
assert(node.isPressed(first))
|
||||
@@ -151,7 +198,7 @@ assert(node.isPressed(first))
|
||||
assert(screen:up(10, 10))
|
||||
assert(not node.isPressed(first))
|
||||
|
||||
local expectedTouch = {"enter", "exit", "enter", "exit", "click"}
|
||||
local expectedTouch = { "enter", "exit", "enter", "exit", "click" }
|
||||
for index, name in ipairs(expectedTouch) do
|
||||
local event = events[index]
|
||||
assert(event and event[1] == name and event[2] == first)
|
||||
@@ -172,10 +219,10 @@ assert(not node.isPressed(second))
|
||||
assert(screen:button("back", true) == false)
|
||||
|
||||
local expectedButtons = {
|
||||
{"enter", first},
|
||||
{"exit", first},
|
||||
{"enter", second},
|
||||
{"click", second},
|
||||
{ "enter", first },
|
||||
{ "exit", first },
|
||||
{ "enter", second },
|
||||
{ "click", second },
|
||||
}
|
||||
for index, expected in ipairs(expectedButtons) do
|
||||
local event = events[index]
|
||||
@@ -184,7 +231,7 @@ for index, expected in ipairs(expectedButtons) do
|
||||
end
|
||||
|
||||
local before = #invalidated
|
||||
assert(ui.setTheme("mono") == true)
|
||||
assert(ui.setTheme "mono" == true)
|
||||
assert(ui.getTheme() == "mono" and #invalidated == before + 1)
|
||||
assert(cleared == ui.theme.background)
|
||||
|
||||
@@ -204,12 +251,16 @@ frameWidth, frameHeight = 320, 480
|
||||
-- still is. Which node was hit is geometry, so the test names the target directly.
|
||||
ui.reset()
|
||||
local pressedCalls = {}
|
||||
node.setPressed = function(_, on) pressedCalls[#pressedCalls + 1] = on end
|
||||
local own = ui.custom{h = 20, press_style = false, on_click = function() end}
|
||||
local styled = ui.button{h = 20, label = "ok", on_click = function() end}
|
||||
local board = ui.screen(ui.box{own, styled})
|
||||
node.setPressed = function(_, on)
|
||||
pressedCalls[#pressedCalls + 1] = on
|
||||
end
|
||||
local own = ui.custom { h = 20, press_style = false, on_click = function() end }
|
||||
local styled = ui.button { h = 20, label = "ok", on_click = function() end }
|
||||
local board = ui.screen(ui.box { own, styled })
|
||||
local target
|
||||
node.hit = function() return target end
|
||||
node.hit = function()
|
||||
return target
|
||||
end
|
||||
|
||||
target = own
|
||||
board:down(0, 0)
|
||||
@@ -220,4 +271,4 @@ target = styled
|
||||
board:down(0, 0)
|
||||
assert(pressedCalls[1] == true, "an ordinary widget still gets its pressed style")
|
||||
|
||||
print("ok")
|
||||
print "ok"
|
||||
|
||||
Reference in New Issue
Block a user