Skip to content

Latest commit

 

History

History
595 lines (477 loc) · 16 KB

File metadata and controls

595 lines (477 loc) · 16 KB
icon lucide/boxes

Components

Animated GIF showing Glyph text, image, button, input, meter, tabs, and panel components.

Tip

See the minimal text/button loop in examples/basic, then the broader input, meter, semantic-mode, and scroll-view composition in examples/workbench. examples/themes shows ui.tabs in a conventional horizontal content region.

Glyph components return virtual nodes. Components are plain Lua functions; there is no class system.

Component constructors shallow-copy the props table before applying defaults. Reusing or later changing a caller-owned props table therefore does not change an existing node's top-level props. Nested values such as style tables are shared rather than deep-copied; replace or copy those explicitly when they need independent mutation.

Text

ui.text("Hello", {
  wrap = true,
  width = 240,
  textStyle = "paragraph",
  style = { color = { 1, 1, 1, 1 } },
})

Use wrap = true with a known width for text that may overflow. Use textStyle to select a theme typography preset such as h1, h2, paragraph, caption, or code. Convenience helpers set that prop for you:

ui.h1("Mission Briefing")
ui.h2("Objectives")
ui.p("Hold the zone until extraction.")
ui.caption("Autosaved 4 seconds ago")

Rich/game text is opt-in with ui.richText, which uses the configured SYSL-Text backend when available:

ui.richText("Status: [color=#7cffae]online[/color] [font=mono]stable[/font]", {
  wrap = true,
  width = 320,
  height = 64,
  textVerticalAlign = "center",
})

ui.richText(value, props) is sugar for ui.text(value, { format = "sysl" }). On a plain ui.text, rich = true is the same shorthand (format = "sysl"); an explicit format always wins.

textVerticalAlign = "top" | "center" | "bottom" offsets plain or SYSL-backed text inside an explicit text node height. It is visual-only and does not change layout, hit testing, or focus geometry.

Configure the backend with an app-provided SYSL module:

ui.richTextBackend.configure({
  sysl = require("slog-text"),
  defaults = { font = love.graphics.getFont() },
  configure = function(Text)
    Text.configure.font_table({ mono = monoFont })
  end,
})

Glyph disables SYSL function commands after configuration by default. Apps that intentionally want scripting tags should own that risk in app code. The typography example uses a development copy from dev/vendor; app code should provide or install its own SYSL module.

For localized text, use ui.textKey after configuring ui.i18n:

ui.textKey("hud.ready", {
  textFallback = "Ready",
})

Rich localized text uses ui.richTextKey.

Box

ui.box(props, children) is a visual/container primitive. It does not lay out children unless you provide a layout mode with display, or use ui.row, ui.column, or ui.stack.

ui.box({
  width = 200,
  height = 80,
  style = { background = { 0.1, 0.1, 0.12, 1 } },
})

Image

ui.image(props) draws a Love2D image/canvas-like object that your app has already loaded. Glyph owns layout, fit, tint, opacity, clipping, and stencil integration; asset loading stays in your game.

local portrait = love.graphics.newImage("assets/portrait.png")

ui.image({
  source = portrait,
  width = 120,
  height = 80,
  fit = "cover", -- "contain" | "cover" | "stretch" | "none"
  align = "center",
  valign = "center",
  tint = { 1, 1, 1, 1 },
  opacity = 1,
  filter = "nearest",
  clip = { kind = "circle" },
  interactive = false,
})

filter may be "linear" or "nearest" for both min/mag filtering, or a table such as { min = "nearest", mag = "linear", anisotropy = 2 }. Glyph applies image filters only for the draw and restores the image's previous filter afterward.

Use quad for atlas cells:

ui.image({
  source = atlas,
  quad = itemQuad,
  width = 32,
  height = 32,
  fit = "contain",
})

Use ui.spriteSheet when the atlas is a uniform grid:

local sheet = ui.spriteSheet(atlas, {
  frameWidth = 16,
  frameHeight = 24,
})

ui.image({
  source = atlas,
  quad = sheet:quad(12),
  width = 32,
  height = 48,
  fit = "contain",
})

For animated sprite-backed UI, pass anim8 explicitly per sheet or configure it once through ui.spriteSheetBackend:

local sheet = ui.spriteSheet(atlas, {
  frameWidth = 16,
  frameHeight = 24,
  anim8 = anim8,
})
local glow = sheet:animation({ "1-4", 1 }, 0.12)

glow:update(dt)

ui.image({
  source = atlas,
  quad = sheet:currentQuad(glow),
  width = 32,
  height = 48,
})

If no explicit size is provided, image nodes measure from the quad viewport or from source:getWidth() / source:getHeight(). Missing sources draw nothing and measure as explicit size or 0x0.

Vector Path

ui.path(props) draws a Glyph-native vector path. It accepts either normalized Lua commands or SVG path d data for common path commands. This is SVG path data only, not a full SVG document renderer.

ui.path({
  d = "M10 70 C40 10 90 110 130 35 Q160 10 180 70",
  width = 220,
  height = 120,
  fit = "contain",
  stroke = { 0.1, 0.9, 0.75, 1 },
  strokeWidth = 4,
  progress = charge, -- 0..1 stroke reveal
})

Lua path commands use normalized command arrays:

local badge = {
  { "M", 8, 40 },
  { "L", 52, 8 },
  { "L", 96, 40 },
  { "L", 76, 92 },
  { "L", 28, 92 },
  { "Z" },
}

ui.path({
  path = badge,
  width = 120,
  height = 120,
  mode = "both",
  fill = { 0.2, 0.5, 1, 0.18 },
  stroke = { 0.55, 0.8, 1, 1 },
  strokeWidth = 3,
})

Supported SVG commands are M/m, L/l, H/h, V/v, C/c, Q/q, and Z/z. Arcs, gradients, CSS styling, masks, transforms, holes, and winding rules are out of scope for v1.

Filled paths target simple closed single-contour shapes. Concave contours are triangulated with Love2D's math backend when available, so stars and chevrons fill correctly; self-intersecting outlines and holes are still app-owned.

Paths can morph between compatible command sequences, or resample both outlines when the shapes differ:

ui.path({
  d = "M10 10 L90 10 L90 90 L10 90 Z",
  morphTo = "M50 4 L96 50 L50 96 L4 50 Z",
  morph = pulse,
  morphMode = "resample",
  mode = "both",
  fill = { 1, 0.7, 0.18, 0.2 },
  stroke = { 1, 0.7, 0.18, 1 },
})

ui.path.parse(d), ui.path.bounds(path), ui.path.flatten(path, opts), and ui.path.length(path, opts) expose the same parser and geometry helpers for app code.

Row And Column

Use ui.row and ui.column for normal flex-style flow.

ui.row({ gap = 8, width = "100%" }, {
  ui.input({ flex = 1, value = filter, onChange = setFilter }),
  ui.button({ label = "Clear", onClick = clearFilter }),
})

Grid

Use ui.grid for uniform repeated cells such as inventory slots, cards, menu buttons, and skill nodes.

ui.grid({ columns = 4, cellWidth = 72, cellHeight = 72, gap = 8 }, slotNodes)

Responsive grids use minCellWidth and optional maxColumns:

ui.grid({ width = "100%", minCellWidth = 140, maxColumns = 5, gap = 10 }, cards)

Use ui.grid.pointToCell(bounds, gridProps, x, y) with onLayout bounds when drag/drop or pointer selection needs a row-major cell index.

Stack

Use ui.stack for layered UI.

ui.stack({ width = "100%", height = "100%" }, {
  ui.box({ position = "absolute", inset = 0, interactive = false, draw = drawBackground }),
  ui.column({ position = "absolute", top = 24, left = 24 }, {
    ui.text("HUD"),
  }),
})

Later children draw above earlier children unless zIndex changes the order.

Portal

Use ui.portal for floating overlays that should escape later sibling branches inside the current render root.

ui.portal({
  left = pointerX - 32,
  top = pointerY - 32,
  width = 64,
  height = 64,
  zIndex = 500,
  interactive = false,
}, {
  ui.image({ source = atlas, quad = potionQuad, fit = "contain" }),
})

ui.portal defaults to position = "absolute", zScope = "root", and stack layout. It is useful for drag previews, tooltips, menus, and HUD callouts, but it does not create a scene or modal layer.

Button

ui.button({
  label = "Run",
  onClick = run,
  style = {
    background = { 0.1, 0.5, 0.9, 1 },
    color = { 1, 1, 1, 1 },
    hover = { background = { 0.15, 0.6, 1, 1 } },
  },
})

Buttons are focusable by default.

Note

The default theme gives focused buttons a two-pixel contrasting border. Primary buttons use a dark focus border against the amber surface. The primary palette keeps normal-size text at or above a 4.5:1 contrast ratio in its idle, hover, pressed, and focused states.

Buttons can resolve labels from i18n keys:

ui.button({ labelKey = "actions.confirm", onClick = confirm })

Buttons and other nodes can run triggerable feedback sequences:

ui.button({
  label = "Launch",
  feedback = {
    press = "button.squash",
    release = "button.release",
    activate = "button.pop",
  },
})

See Feedback for sequence steps and app-owned FX events.

Input

ui.input({
  value = query,
  placeholder = "Filter logs...",
  onChange = setQuery,
  flex = 1,
})

Inputs are controlled: keep the value in state and update it through onChange. Use placeholderKey for localized placeholder text.

Pointer presses place the cursor at the nearest glyph boundary. Left, Right, Backspace, and Delete preserve UTF-8 codepoint boundaries, and consecutive events are applied to the pending controlled value even when they arrive before the next render. If an app replaces the value with shorter text, Glyph clamps the cursor to the new value. Set padding to adjust both text and cursor insets.

Meter

ui.meter(props) draws generic value displays such as health, mana, cooldown, durability, progress, or debug telemetry. It is intentionally not a game-specific healthBar component.

ui.meter({
  value = hp,
  min = 0,
  max = maxHp,
  animate = { duration = 0.2, ease = "quadout" },
  width = 180,
  height = 14,
  shape = { kind = "skew", skew = 12 },
  trackStyle = { background = { 0, 0, 0, 0.35 } },
  fillStyle = { background = { 0.1, 0.9, 0.55, 1 } },
})

Meters support:

  • kind = "linear" | "radial" | "arc"
  • direction = "right" | "left" | "up" | "down" for linear meters
  • shape for rectangular, skewed, polygon, circle, ellipse, or blob fills
  • segments, gap, thickness, startAngle, and endAngle
  • style.background and style.borderColor for the track, with clipped fills that stay inside the track shape
  • radial and arc meters draw open arcs and honor fillStyle.background or fillStyle.color
  • label, children overlays, trackStyle, fillStyle, overfillStyle, and backgroundStyle
  • labelKey, labelParams, and labelCacheKey for localized labels
  • animate = true or an animation table for opt-in, interruptible value interpolation. Tables support duration, ease, initial, initialValue, and initialDuration. The default duration is 0.2 seconds with ease = "quadout"; initial values do not animate unless initial = true.

Meter animation changes only the drawn fill and function-based label. Layout, hit testing, and accessibility continue to use the current value. Bind animate to the app's motion preference when players can reduce motion.

Custom meter drawing reads the same interpolated number from ctx.visualValue:

ui.meter({
  value = charge,
  max = 100,
  animate = { duration = 0.24, ease = "expoout", initial = true },
  draw = function(_, x, y, width, height, _, _, ctx)
    local ratio = ctx.visualValue / ctx.props.max
    ctx:rect("fill", x, y, width * ratio, height)
  end,
})

Radial and arc meters use the same value and animation options:

ui.meter({
  kind = "arc",
  value = charge,
  max = 100,
  width = 72,
  height = 72,
  thickness = 8,
  startAngle = math.rad(135),
  endAngle = math.rad(405),
  fillStyle = { color = { 1, 0.8, 0.2, 1 } },
})

Scroll View

ui.scrollView({ width = "100%", height = 300 }, rows)

Scroll views clamp scrolling to content bounds and support optional scroll indicators. When content overflows, the indicator track is clickable and the thumb is draggable; the thumb uses a small visual press/release animation without changing layout or hit-test geometry.

Wheel scrolling defaults to 24 pixels per wheel unit. Tune the feel per scroll view with scrollPixelsPerStep for the base distance and scrollSpeed for a multiplier:

ui.scrollView({
  width = "100%",
  height = 300,
  scrollPixelsPerStep = 16,
  scrollSpeed = 1.5,
}, rows)

scrollSensitivity is accepted as an alias for scrollSpeed when that term better matches the setting exposed by your game or tool.

Virtual List

Use virtualList when a list may contain many rows but each row has a fixed height. It renders only the visible range plus an overscan buffer, while spacer nodes preserve the full scroll height and scrollbar behavior.

Use a numeric height when possible. If the surrounding layout is flexible, provide visibleCount so Glyph can compute the mounted range before layout.

ui.virtualList({
  key = "combat-log",
  width = "100%",
  height = 320,
  itemCount = #events,
  itemHeight = 28,
  visibleCount = 12,
  overscan = 4,
  itemKey = function(index)
    return events[index].id
  end,
  renderItem = function(index)
    return EventRow(events[index])
  end,
  onRangeChange = function(first, last, info)
    mountedRows = info.mounted
  end,
})

Programmatic scrolling uses the same stable key:

ui.scrollTo("combat-log", 280)
ui.scrollToItem("combat-log", 42, 28, { align = "center" })
local offset = ui.getScrollOffset("combat-log")

Tip

Give virtual lists a stable key so the scroll offset can be reused across frames. Use itemKey when rows can be inserted, removed, or reordered.

Tabs

Uncontrolled tabs:

ui.tabs({ defaultActive = 1 }, {
  { label = "Logs", content = LogsPanel() },
  { label = "Stats", content = StatsPanel() },
})

Tabs also accept labelKey:

ui.tabs({}, {
  { labelKey = "tabs.logs", content = LogsPanel() },
  { labelKey = "tabs.stats", content = StatsPanel() },
})

Controlled tabs:

ui.tabs({
  active = activeTab,
  tabWidth = 92,
  tabPadding = { x = 10, y = 4 },
  onChange = setActiveTab,
}, tabs)

Use tabWidth, per-tab width, or tabPadding when tabs should read as a segmented control with stable button sizes.

In a height-constrained shell, use flex = 1 on ui.tabs and on each pane root to keep the tab strip and content viewport fixed while switching between panes with different intrinsic heights. The tab strip itself never shrinks below tabHeight. Put an oversized pane body in scrollView instead of allowing it to resize the surrounding shell.

The default theme keeps selection and focus separate: an active tab uses the accent fill, while an active-and-focused tab retains that fill and adds the two-pixel focus border.

Panel

ui.panel is a small convenience component for framed tool sections:

ui.panel({ title = "Logs", width = "100%", flex = 1 }, {
  ui.text("Ready"),
})

Panel titles use the h2 typography preset by default. Use titleTextStyle to select another preset, and titleKey for localized panel titles.

Panels accept the full shared component prop set. Stable key values, interactive, layout callbacks such as onLayout and onBounds, lifecycle animation through enter and exit, and feedback or audio metadata are forwarded to the panel node. Glyph shallow-copies the supplied props, then adds the panel defaults display = "column", gap = 8, and padding = 10 without mutating the caller's table; explicit spacing values, including 0, are kept.

Accessibility Props

All nodes accept semantic props such as role, accessibilityLabel, accessibilityDescription, accessibilityValueText, accessibilityHidden, and accessibilityLive. Buttons, inputs, meters, tabs, text, and panels have best-effort defaults, and semantic strings can use i18n key props. See Accessibility for adapter events and snapshot APIs.