Files
slate32/test/ui_theme.lua
T
evan 0a27062a70 feat(ui): themes derived from three seed colors
Every app re-declared the same black/white/accent triple, so a palette now lives in
/lib/theme.lua and ui.lua derives muted, contrast, gradients and radius from it.
Deriving rather than listing means a new component costs no theme keys, and a theme
cannot pick text that is unreadable on its own accent.

Components take the palette by inheritance, so the three apps name no colors at all;
ui.theme stays for drawing outside the tree. Settings cycles the installed themes and
stores only the name.
2026-08-01 10:04:52 -04:00

80 lines
3.2 KiB
Lua

-- Run: lua test/ui_theme.lua
-- Asserts the palette ui.lua derives from a theme's three seed colors.
package.path = "sdcard/lib/?.lua;" .. package.path
local themeName = "light"
-- Packing the channels keeps them recoverable, so contrast is checkable.
gui = {
color = function(r, g, b) return r * 65536 + g * 256 + b end,
width = function() return 320 end,
height = function() return 480 end,
clear = function() end,
fillRect = function() end,
drawText = function() end,
fillRoundRect = function() end,
drawRoundRect = function() end,
fillRectGradient = function() end,
fontHeight = function() return 8 end,
textWidth = function(text) return #text * 6 end,
}
sys = {millis = function() return 0 end, getTheme = function() return themeName end}
local ui = require("ui")
local BLACK, WHITE = gui.color(0, 0, 0), gui.color(255, 255, 255)
local function reload(name)
themeName = name
return ui.reloadTheme()
end
-- Seeds pass through untouched.
local light = reload("light")
assert(light.bg == WHITE, "light bg")
assert(light.fg == BLACK, "light fg")
assert(light.accent == gui.color(0, 120, 255), "light accent")
-- Muted sits between the text and the surface, so it reads as secondary on both.
assert(light.muted > BLACK and light.muted < WHITE, "muted between fg and bg")
-- Contrast is derived, never configured: text on a dark accent must be light.
assert(light.accent_fg == WHITE, "white text on the blue accent")
assert(reload("mono").accent_fg == WHITE, "white text on the black accent")
-- A pale accent has to flip the label to black; no theme file can get this wrong.
local pale = ui.themeNames() -- force the module to be loaded before patching it
assert(#pale >= 3, "expected the canned themes")
package.loaded["theme"].pale = {bg = {255, 255, 255}, fg = {0, 0, 0}, accent = {255, 235, 120}}
assert(reload("pale").accent_fg == BLACK, "black text on a pale accent")
-- Dark inverts the surface without any per-component configuration.
local dark = reload("dark")
assert(dark.bg < dark.fg, "dark theme keeps light text on a dark surface")
assert(#dark.face == 2 and #dark.face_pressed == 2, "button gradients are pairs")
-- Pinned roles win over derived ones, which is the deviation hatch.
assert(reload("mono").radius == 0, "mono pins radius")
assert(light.radius == 6, "light keeps the derived radius")
-- An unknown name must not leave an app without colors.
local missing = reload("does-not-exist")
assert(missing.bg == WHITE and missing.fg == BLACK, "unknown theme falls back to light")
-- The root seeds the tree, so an app naming no colors still gets themed.
reload("dark")
local button = ui.button{label = "go", on_press = function() end}
local screen = ui.screen(ui.box{pad = 12, button})
assert(screen.root.bg == ui.theme.bg, "root takes the theme surface")
assert(screen.root.color == ui.theme.fg, "root takes the theme text")
screen:draw()
assert(button.color == ui.theme.fg, "the button inherited the theme")
assert(button.press_bg == ui.theme.accent, "the button inherited the accent")
-- An explicit value still wins over the theme.
local custom = ui.button{label = "delete", press_bg = 123, on_press = function() end}
ui.screen(ui.box{pad = 12, custom}):draw()
assert(custom.press_bg == 123, "explicit styling overrides the theme")
print("ok")