| icon | lucide/cpu |
|---|
Tip
See it in action: examples/basic is a full app with state,
input, focus, and the update/render loop.
Glyph has one shared runtime exposed through ui.update, ui.render, input forwarding functions, hooks, and helper APIs.
Manual wiring:
function love.update(dt)
ui.update(dt)
end
function love.draw()
ui.render(App)
endAutomatic wiring:
ui.load({
window = { width = 900, height = 600, resizable = true },
app = App,
})ui.update(dt) advances Glyph's runtime clock, visual animations, animated
meter values, style transitions, and ui.feedback sequences, including
Feel-backed spring steps. Raw ui.spring(...) values are intentionally
app-driven; call spring:update(dt) from your own update loop when you use one
outside a feedback sequence.
ui.render(App) always draws the mounted tree. It rebuilds and lays out that
tree only after invalidation or a viewport-size change; otherwise it reuses the
completed geometry and skips layout-callback traversal. State setters and Glyph
input helpers invalidate automatically. If app-owned state outside hooks changes
layout or component props, call ui.runtime:markDirty() after the mutation.
ui.useState(initial) stores a hook slot by call order in the current render
scope (the root app, a scene layer, or an offscreen surface):
local count, setCount = ui.useState(0)ui.useEffect(fn, deps) runs effects when dependencies change:
ui.useEffect(function()
print("mounted or changed")
return function()
print("cleanup")
end
end, { id })An effect cleanup runs before that effect is replaced after a dependency change,
and exactly once when its owning hook scope is discarded. Scene layers keep their
effects through exit animation; cleanup runs after the transition completes and
after onClose. Immediate stack replacement with ui.scene.set, or replacement
of a duplicate layer ID with ui.scene.push, disposes the replaced scopes during
that call. An offscreen surface disposes its root hook scope during
surface:destroy().
Call hooks unconditionally and in the same order on every render. A node's
key does not create a hook scope or make a useState / useEffect slot follow
that node; keyed hook-state reconciliation is not part of v0.1.
Node identity is path-based. Unkeyed siblings use their position in the parent;
a stable key replaces that positional path segment. Use keys for dynamic
siblings so path-based runtime state such as focus, an input's cursor, style
transitions, and enter/exit animation stays attached when siblings reorder:
local children = {}
for _, row in ipairs(rows) do
children[#children + 1] = ui.input({
key = row.id,
value = row.name,
onChange = row.rename,
})
end
return ui.column({}, children)The key stabilizes each input node's runtime path. It does not affect the order of any hooks called while building the rows.
Manual input forwarding:
function love.mousemoved(x, y, dx, dy)
ui.mousemoved(x, y, dx, dy)
end
function love.mousepressed(x, y, button)
ui.mousepressed(x, y, button)
end
function love.mousereleased(x, y, button)
ui.mousereleased(x, y, button)
end
function love.wheelmoved(dx, dy)
ui.wheelmoved(dx, dy)
end
function love.textinput(text)
ui.textinput(text)
end
function love.keypressed(key)
ui.keypressed(key)
end
function love.keyreleased(key)
ui.keyreleased(key)
end
function love.gamepadpressed(joystick, button)
ui.gamepadpressed(joystick, button)
end
function love.gamepadreleased(joystick, button)
ui.gamepadreleased(joystick, button)
endui.install and ui.load can install common callbacks automatically.
Gamepad callbacks are installed only when install.gamepad is enabled.
Warning
Give each Love callback one owner. If an existing love.keypressed or
love.keyreleased callback already calls the matching ui.* forwarder,
disable Glyph's automatic copy with
install = { keypressed = false, keyreleased = false }. Otherwise one
physical key is delivered twice; this is especially visible in text inputs.
Note
If a fixed viewport backend is active, Glyph converts mouse and touch screen coordinates into virtual viewport coordinates before hover, focus, click, and scroll routing. Pointer events outside the virtual viewport do not hit UI.
Buttons and focusable nodes with role = "button" plus onClick use the same
press lifecycle for pointer and keyboard activation: mouse/touch down and
Return/Space down enter the pressed state, and release activates the node when
focus is still on the same node. This keeps pressed styles, feedback, audio
cues, and accessibility activation events consistent across mouse, keyboard,
and gamepad mappings that forward to ui.keypressed / ui.keyreleased.
Before keyboard, mapped gamepad, or text input is delivered, Glyph validates the focused node against the active scene stack. Controls below a blocking layer and nodes removed from a rebuilt or closed layer cannot receive input.
Focus suspension dispatches focusChanged(nil, previousNode) without a focus
audio cue. Restoring a still-reachable control is a normal focus acquisition: it
dispatches focusChanged(restoredNode, nil) and emits the control's resolved
focus audio, feedback, and accessibility events.
Touch callbacks are wired automatically by ui.install / ui.load. Gamepad
mapping is opt-in:
ui.load({
app = App,
install = {
gamepad = true,
},
})Use ui.on(name, fn, opts) to subscribe to runtime callbacks.
Supported names:
beforeUpdateafterUpdatebeforeRenderafterRenderlayoutaudioaccessibilityfeedbackfocusChangedhoverChangedevent
Unregister with the returned closure:
local off = ui.on("event", function(kind, ...)
print(kind)
end)
off()Use onBounds and onLayout when app code needs node geometry for drag/drop,
tooltips, popovers, minimap markers, overlays, or contextual menus.
onBounds(bounds, node) receives the node’s local parent-relative layout:
ui.box({
width = 64,
height = 64,
onBounds = function(bounds, node)
print(bounds.x, bounds.y, bounds.width, bounds.height)
end,
})onLayout(bounds, node) receives viewport-space bounds in the same coordinate
space as routed pointer input. It includes parent offsets, scene/modal layer
offsets, and scrollView visual scroll offsets.
Scroll offsets can also be controlled by stable key: use
ui.scrollTo(key, offset) for pixels, ui.scrollToItem(key, index, itemHeight)
for fixed-height lists, and ui.getScrollOffset(key) to read the current value.
ui.button({
label = "Drag",
onLayout = function(bounds)
dragTargets.primary = bounds
end,
})Both callbacks fire after layout publication and before drawing, only when the reported rectangle changes for that node path or when the callback function identity changes. Reported bounds are rectangular layout geometry; they do not include visual-only animation, feedback, shape, clip, stencil, or custom transition transforms.
Use ui.drag when app code needs a captured pointer lifecycle without wiring a
global ui.on("event") listener. Glyph owns the pointer start/move/drop/cancel
callbacks; your app still owns target lookup, validation, swapping, placement,
and previews.
local startDrag = ui.drag({
onStart = function(ctx)
dragging = ctx.data
end,
onMove = function(ctx)
pointer = { x = ctx.x, y = ctx.y }
end,
onDrop = function(ctx)
dropItem(ctx.data, ctx.x, ctx.y)
end,
onCancel = function(ctx)
dragging = nil
end,
})
ui.button({
label = "Potion",
onMousePressed = function(x, y, button, node)
if button == 1 then
startDrag(x, y, button, node, { itemId = "potion" })
end
end,
})ctx includes x, y, startX, startY, previousX, previousY, dx,
dy, totalDx, totalDy, button, sourceNode, sourcePath,
targetNode, targetPath, data, runtime, reason, and
cancel(reason).
Set minDistance to delay onStart until the pointer moves far enough.
Releasing before the threshold calls onCancel with reason = "threshold" and
preserves normal button activation. Once a drag has started, release calls
onDrop and suppresses the source button’s normal onClick. Active drags
cancel on Escape, viewport exit, focus loss, or when a new drag starts.
Use ui.surface.new when a Glyph tree should render into its own canvas and
runtime. Surfaces are useful for render-to-texture UI, minimap labels, and
Menori world billboards.
local surface = ui.surface.new({
width = 320,
height = 180,
component = function(surfaceUi)
return surfaceUi.button({ label = "World Button" })
end,
})
surface:update(dt)
surface:render()
surface:mousepressed(24, 32, 1)The surfaceUi argument is scoped to the surface runtime, so hooks, focus,
feedback, ui.drag, and pointer state do not leak into the main screen runtime.
Surfaces render with Love canvas stencil support enabled by default, so clipped
controls, meters, masks, and stencil-based custom draw can render safely
offscreen. Pass canvasOptions = { stencil = false } only when the surface does
not need stencil-backed UI drawing.
Glyph emits audio callbacks when configured cues resolve for interaction
events. It does not load or play sounds.
local sounds = {
hover = love.audio.newSource("hover.wav", "static"),
}
ui.on("audio", function(event)
local source = sounds[event.cue]
if source then
source:stop()
source:play()
end
end)The event includes cue, kind, node, type, path, variant,
styleType, and a best-effort label. Supported cue kinds are hover,
press, activate, and focus.
Glyph emits feedback callbacks from ui.feedback emit steps. These events
are for app-owned effects such as particles, camera shake, haptics, splats, or
custom shader systems.
ui.on("feedback", function(event)
if event.kind == "particles" then
spawnParticles(event.node, event.name)
end
end)Feedback events include kind, name, trigger, node, path, payload,
and the original step. See Feedback for sequence definitions.
Glyph exposes Love2D-friendly accessibility semantics through metadata and runtime events. It does not speak text or create native OS controls; apps own TTS, platform bridges, logs, or Love.js DOM adapters.
ui.on("accessibility", function(event)
print(event.kind, event.message)
end)Focus changes, button activation, manual announcements, and live-region updates
can emit events with kind, message, node, path, role, label,
description, valueText, and live. See Accessibility
for semantic props, snapshots, i18n keys, and adapter patterns.
Helpers:
ui.isHovered(node)ui.isPressed(node)ui.isFocused(node)ui.isActive(node)ui.isHot(node)
These are useful inside custom draw callbacks.
- Hit testing follows visual order.
- Higher
zIndexwins among siblings. ui.portalis promoted above normal content in the current render root and hit-tested before local content. The lower-level form isposition = "absolute"withzScope = "root".- Later stack children draw above earlier children and receive events first.
interactive = falselets decorative nodes pass events through.- Scene layers route input top-down.
