| 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, 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.useState(initial) stores state by component tree position:
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 })Hook identity is tree-position based. Keyed reconciliation is not part of v0.1.
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.
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.
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.
