Files
noctalia-ostt/panel.luau
T

103 lines
3.7 KiB
Luau
Raw Normal View History

--!nonstrict
-- Reference [[panel]] entry — a settings-style panel of interactive controls.
--
-- Panels describe their UI declaratively, exactly like desktop widgets: build a
-- tree with the ui.* constructors and hand it to panel.render(). The host diffs
-- the tree against the previous one and updates retained native controls, so
-- rendering an unchanged tree is free.
--
-- panel.render(tree) replace the panel's UI tree
-- panel.close() ask the host to close this panel
-- panel.getConfig(key) settings (seeded from the manifest)
-- button/toggle/slider/... onClick / onChange call the plugin global of
-- that name; values arrive as string args
--
-- Layout notes:
-- * a Flex (column/row/scroll) centers its children on the cross axis by
-- default — pass align = "stretch" so controls fill the panel width.
-- * panel size is host-owned: declare width/height on the [[panel]] manifest
-- entry, so the surface is the same size on every open.
--
-- Stateful controls follow a controlled/uncontrolled split:
-- * toggle / slider / select are value-driven — pass the current value each
-- render; the callback reports the user's new value.
-- * input is uncontrolled — `value` seeds the field once; the host owns the
-- text buffer afterward, so re-rendering never clobbers what the user typed.
-- Observe edits via onChange (live) / onSubmit (Enter).
local enabled = true
local volume = 50
local greeting = ""
local themeIndex = 0
local themes = { "Dark", "Light", "System" }
-- A labelled section: a heading above a control. ui.column stretches children to
-- the panel width by default, so the control fills the row.
local function section(title, control)
return ui.column({ gap = 8 }, {
ui.label({ text = title, fontWeight = "medium", color = "on_surface_variant" }),
control,
})
end
local function render()
-- The panel surface already insets its content (Style::panelPadding), so this
-- adds gaps between rows, not its own outer padding.
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
-- Header: title on the left, close button pinned to the top right —
-- matching the built-in panels (no divider rule).
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
ui.label({ text = "Example Panel", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
ui.button({ glyph = "close", onClick = "onCloseClicked" }),
}),
-- Body: a scrollable stack of settings.
ui.scroll({ flexGrow = 1, gap = 16 }, {
section("Notifications", ui.row({ gap = 12, align = "center" }, {
ui.toggle({ checked = enabled, onChange = "onToggle" }),
ui.label({ text = enabled and "Enabled" or "Disabled", flexGrow = 1 }),
})),
section(`Volume — {volume}%`, ui.slider({
min = 0, max = 100, step = 1, value = volume, onChange = "onVolume",
})),
section("Your name (press Enter to greet)", ui.column({ gap = 8 }, {
ui.input({ key = "name", placeholder = "Type a name…", onSubmit = "onNameSubmit" }),
ui.label({ text = greeting, color = "primary" }),
})),
section("Theme", ui.select({ options = themes, selectedIndex = themeIndex, onChange = "onTheme" })),
}),
}))
end
function onOpen(_context)
render()
end
function onToggle(value)
enabled = value == "true"
render()
end
function onVolume(value)
volume = math.floor(tonumber(value) or 0)
render()
end
function onNameSubmit(value)
greeting = value ~= "" and `Hello, {value}!` or ""
noctalia.notify("Example Panel", greeting)
render()
end
function onTheme(index, _text)
themeIndex = tonumber(index) or 0
render()
end
function onCloseClicked()
panel.close()
end