jeopardy/gui/docs/gui-library-docs.md
2026-05-06 19:39:08 -07:00

26 KiB
Raw Blame History

GUI Library Documentation

A component-based UI framework for LÖVE2D (Love2D) built on top of the multi concurrency library. The library provides a scene graph with dual-dimension layout, event-driven input handling, and a rich set of built-in element types.


Table of Contents

  1. Setup & Initialization
  2. Core Concepts
  3. Creating Elements
  4. Layout & Positioning
  5. Events & Connections
  6. Element Methods
  7. Text Elements
  8. Image Elements
  9. Clipping & Scissor
  10. Roundness & Shape
  11. Aspect Ratio & Resize Handling
  12. The apply Helper
  13. Tagging System
  14. Cloning Elements
  15. Processors & Threading
  16. Drawing Internals
  17. Virtual GUI

Setup & Initialization

local gui = require("path.to.gui")

The library self-initializes on require. It hooks into LÖVE's callback system automatically (quit, resize, mouse, keyboard, touch, gamepad, etc.) and starts its internal update and draw processors.

In your love.update and love.draw:

function love.update(dt)
    gui.update(dt)
end

function love.draw()
    gui.draw()
end

Note: The library hooks LÖVE callbacks via a Hook function that wraps any pre-existing handler you define. Define your own love.* callbacks before require-ing the library, or they will be chained automatically.


Core Concepts

The Scene Graph

The library maintains two root nodes:

Root Description
gui The main scene root. All elements created with gui:newXxx() are parented here by default.
gui.virtual A secondary root for off-screen or hidden elements. Children here are not drawn but still have their absolute positions updated.

Elements form a tree. Every element has a parent, a children table, and inherits methods from gui via __index.

Dual-Dimension Layout (DualDim)

Every element stores its position and size as a dual dimension: a combination of a scale component (relative to the parent) and an offset component (absolute pixels).

actualX = parent.w * scale.pos.x  + offset.pos.x + parent.x
actualY = parent.h * scale.pos.y  + offset.pos.y + parent.y
actualW = parent.w * scale.size.x + offset.size.x
actualH = parent.h * scale.size.y + offset.size.y

Constructor signature for newDualDim / all newXxx creation functions:

x, y, w, h          -- pixel offset for position and size
sx, sy, sw, sh      -- scale (01) for position and size

Examples:

-- 200×100 box at pixel position (50, 50):
gui:newFrame(50, 50, 200, 100)

-- Full-screen frame (uses scale only):
local f = gui:newFrame()
f:fullFrame()           -- sets scale size to (1,1) and offset to (0,0,0,0)

-- Half-width, 40px tall, starting at 25% from left:
gui:newFrame(0, 100, 0, 40, 0.25, 0, 0.5, 0)

Retrieve the computed screen-space rectangle at any time:

local x, y, w, h = element:getAbsolutes()

Element Types (Bitmask)

Types are stored as a bitmask so an element can have multiple roles:

Constant Value Meaning
gui.TYPE_FRAME 0 Basic container
gui.TYPE_IMAGE 1 Renders an image
gui.TYPE_TEXT 2 Renders text
gui.TYPE_BOX 4 Text input cursor/selection overlay
gui.TYPE_VIDEO 8 Renders a video
gui.TYPE_BUTTON 16 Interactive button (sets hand cursor)
gui.TYPE_ANIM 32 Animation / spritesheet

Test membership:

if element:hasType(gui.TYPE_TEXT) then ... end
if element:hasType(gui.TYPE_TEXT + gui.TYPE_BOX) then ... end  -- is a text box

Form Factors

Controls the shape used for both fills and hit-testing:

Constant Shape
gui.FORM_RECTANGLE Rounded or plain rectangle (default)
gui.FORM_CIRCLE Circle; w and h are set to 2*r
gui.FORM_ARC Arc segment

Creating Elements

All creation functions are called on a parent element (or on gui itself for top-level elements). The new element is automatically inserted into the parent's children table.

Frames

A plain container with a background fill and optional border.

local frame = parent:newFrame(x, y, w, h, sx, sy, sw, sh)

A virtual frame is parented to gui.virtual regardless of the caller:

local vframe = parent:newVirtualFrame(x, y, w, h, sx, sy, sw, sh)

A visual frame is a regular frame tagged "visual". Mouse events on it and its descendants are suppressed (useful for purely decorative overlays):

local overlay = parent:newVisualFrame(x, y, w, h, sx, sy, sw, sh)

Text Labels

A non-interactive text element.

local label = parent:newTextLabel("Hello world", x, y, w, h, sx, sy, sw, sh)

Text Buttons

A text element that fires pointer events and shows a hand cursor on hover.

local btn = parent:newTextButton("Click me", x, y, w, h, sx, sy, sw, sh)
btn.OnPressed(function(self, x, y) print("pressed!") end)

Text Boxes (Input)

A single-line text input field.

local box = parent:newTextBox("default text", x, y, w, h, sx, sy, sw, sh)
box.OnReturn(function(self, text) print("Submitted:", text) end)

Keyboard navigation, backspace/delete, selection (click-drag or Ctrl+A), copy/paste/cut, and undo/redo are all handled automatically when the box has focus.

Image Labels

A non-interactive image element.

local img = parent:newImageLabel("path/to/image.png", x, y, w, h, sx, sy, sw, sh)

GIF files are detected automatically by the .gif extension and animated.

Image Buttons

An image element that fires pointer events and shows a hand cursor on hover.

local ibtn = parent:newImageButton("icon.png", x, y, w, h, sx, sy, sw, sh)
ibtn.OnPressed(function(self, x, y) print("image clicked") end)

Videos

Wraps a LÖVE Video object.

local vid = parent:newVideo("clip.ogv", x, y, w, h, sx, sy, sw, sh)
vid:play()
vid.OnVideoFinished(function(self) print("done") end)

Video methods:

Method Description
vid:setVideo(path_or_video) Load or swap the video source
vid:play() Start playback
vid:pause() Pause without rewinding
vid:stop() Pause and rewind
vid:rewind() Seek to start
vid:seek(seconds) Jump to position
vid:tell() Return current playback position (seconds)
vid:getDuration() Return total duration (seconds)
vid:setVolume(vol) Set audio volume (01)
vid:getVideo() Return the underlying LÖVE Video object

Layout & Positioning

Setting the Dual Dimension

-- Fires OnSizeChanged
element:setDualDim(x, y, w, h, sx, sy, sw, sh)

-- Silent version (no event)
element:rawSetDualDim(x, y, w, h, sx, sy, sw, sh)

-- Read back
local x, y, w, h, sx, sy, sw, sh = element:getDualDim()

Pass nil for any argument to keep the current value.

Moving and Resizing

-- Delta move (fires OnPositionChanged)
element:move(dx, dy)

-- Delta resize (fires OnSizeChanged)
element:size(dw, dh)

-- Move but clamp to parent bounds
element:moveInBounds(dx, dy)

Centering

element:centerX(true)   -- horizontally center within parent
element:centerY(true)   -- vertically center within parent

These attach internal loops that continuously recompute the offset whenever the element's size or position changes.

Convenience

element:fullFrame()     -- scale size (1,1), offset (0,0,0,0) — fills parent

Dragging

element:enableDragging(button)   -- button = love mouse button number (1=left, 2=right, …)
element:enableDragging(nil)      -- disable dragging

While dragging, OnDragging, OnDragStart, and OnDragEnd are fired.

Z-Order

element:topStack()      -- move to end of parent.children (drawn last = on top)
element:bottomStack()   -- move to front of parent.children (drawn first = behind)

Events & Connections

Events use the multi connection system. Connect a handler by calling the connection as a function:

element.OnPressed(function(self, x, y, button, istouch, presses)
    -- ...
end)

Connections support composition:

-- OR: fires when either fires
(connA + connB)(handler)

-- AND: fires only when both conditions are met
(connA * connB)(handler)

Global GUI Events

These fire for the entire application window regardless of which element is focused.

Event LÖVE callback Arguments
gui.Events.OnQuit love.quit
gui.Events.OnDirectoryDropped love.directorydropped dir
gui.Events.OnDisplayRotated love.displayrotated index, orient
gui.Events.OnFilesDropped love.filedropped file
gui.Events.OnFocus love.focus focused
gui.Events.OnMouseFocus love.mousefocus focused
gui.Events.OnResized love.resize w, h
gui.Events.OnVisible love.visible visible
gui.Events.OnKeyPressed love.keypressed key, scancode, isrepeat
gui.Events.OnKeyReleased love.keyreleased key, scancode
gui.Events.OnTextEdited love.textedited text, start, length
gui.Events.OnTextInputed love.textinput text
gui.Events.OnMouseMoved love.mousemoved x, y, dx, dy, istouch
gui.Events.OnMousePressed love.mousepressed x, y, button, istouch, presses
gui.Events.OnMouseReleased love.mousereleased x, y, button, istouch, presses
gui.Events.OnWheelMoved love.wheelmoved x, y
gui.Events.OnTouchMoved love.touchmoved id, x, y, dx, dy, pressure
gui.Events.OnTouchPressed love.touchpressed id, x, y, dx, dy, pressure
gui.Events.OnTouchReleased love.touchreleased id, x, y, dx, dy, pressure
gui.Events.OnGamepadPressed love.gamepadpressed joystick, button
gui.Events.OnGamepadReleased love.gamepadreleased joystick, button
gui.Events.OnGamepadAxis love.gamepadaxis joystick, axis, value
gui.Events.OnJoystickAdded love.joystickadded joystick
gui.Events.OnJoystickRemoved love.joystickremoved joystick
gui.Events.OnJoystickHat love.joystickhat joystick, hat, dir
gui.Events.OnJoystickPressed love.joystickpressed joystick, button
gui.Events.OnJoystickReleased love.joystickreleased joystick, button
gui.Events.OnCreated internal element — fires when any element is created
gui.Events.OnObjectFocusChanged internal old, new — fires when click focus changes

Per-Element Events

These are attached to each element instance. All mouse/pointer events are automatically pre-filtered: they only fire when the element is active and (for most events) when the pointer is within the element's bounds.

Event Fires when…
OnLoad (manual) element is "loaded" — user-defined
OnPressed pointer pressed inside element
OnPressedOuter pointer pressed outside element
OnReleased pointer released inside element
OnReleasedOuter pointer released outside (but was pressed inside)
OnReleasedOther pointer released with no relevant press history
OnDragStart drag begins (element must have enableDragging set)
OnDragging pointer moves while dragging
OnDragEnd drag ends
OnEnter pointer enters the element bounds
OnExit pointer leaves the element bounds
OnMoved pointer moves while inside (or while dragging)
OnWheelMoved scroll wheel moves while pointer is inside element
OnSizeChanged setDualDim or size called
OnPositionChanged setDualDim or move called
OnDestroy element is about to be destroyed
OnCreated element was created (forwarded from gui.Events.OnCreated)
OnReturn (text boxes only) Enter/Return key pressed
OnFontUpdated (text elements only) font changed via setFont
OnVideoFinished (video elements only) video reaches its end
OnLeftStickUp/Down/Left/Right gamepad left-stick events
OnRightStickUp/Down/Left/Right gamepad right-stick events

Hierarchy Mode

By default events fire if another element is not on top. Call:

element:respectHierarchy(false) -- events will fire regardless

to make OnPressed, OnReleased, OnEnter, and OnMoved skip when the element is covered by a sibling.


Hot Keys

Register a keyboard shortcut that fires a connection:

local conn = element:setHotKey({"lctrl", "s"})   -- returns a connection
conn(function(ref) print("Ctrl+S on", ref) end)

You may pass an existing connection as the second argument to reuse it.

Built-in Hot Keys

Hot Key Trigger
gui.HotKeys.OnSelectAll Ctrl+A
gui.HotKeys.OnCopy Ctrl+C
gui.HotKeys.OnPaste Ctrl+V
gui.HotKeys.OnCut Ctrl+X
gui.HotKeys.OnUndo Ctrl+Z
gui.HotKeys.OnRedo Ctrl+Y / Ctrl+Shift+Z

These are already wired to the currently-focused text box for standard editing operations.


Element Methods

Positioning & Sizing

Method Description
el:getAbsolutes([transform]) Returns x, y, w, h in screen space. Optional transform function is applied to each value.
el:setDualDim(x,y,w,h,sx,sy,sw,sh) Set layout, fires OnSizeChanged.
el:rawSetDualDim(...) Set layout, no event.
el:getDualDim() Returns all 8 dual-dim components.
el:move(dx, dy) Translate by delta, fires OnPositionChanged.
el:size(dw, dh) Resize by delta, fires OnSizeChanged.
el:moveInBounds(dx, dy) Translate while keeping element inside parent.
el:fullFrame() Fill parent entirely.
el:centerX(bool) Auto-center horizontally.
el:centerY(bool) Auto-center vertically.
el:getLocalCords(mx, my) Convert screen coordinates to element-local coordinates.

Visual Properties

Property Type Default Description
color {r,g,b} {0.6, 0.6, 0.6} Background fill color
borderColor {r,g,b} black Border color
drawBorder boolean true Whether to draw the border
visibility number 1 Background alpha (01)
rotation number 0 Rotation in degrees
active boolean true When false, element and all descendants ignore input
visible boolean true Controls getAllChildren visibility filter
ignore boolean When true, element is skipped in coverage tests

Set color (also sets visibility if a 4th component is present):

element:setColor("color", {1, 0, 0, 0.8})
element:setColor("borderColor", {0, 0, 0})

Apply a LÖVE shader:

element.shader = love.graphics.newShader(...)

Apply an effect wrapper (called around the draw call):

element.effect = function(drawFunc)
    love.graphics.push()
    -- setup
    drawFunc()
    love.graphics.pop()
end

Apply a post-draw hook:

element.post = function(self)
    -- called after drawing, inside the same scissor/shader state
end

Hierarchy & Parenting

Method Description
el:setParent(newParent) Re-parent element. Pass nil to detach.
el:getChildren() Returns direct children table.
el:getAllChildren([includeHidden]) Returns all visible descendants recursively.
el:isDescendantOf(obj) Returns true if obj is an ancestor of el.
el:topStack() Draw on top of siblings.
el:bottomStack() Draw behind siblings.
el:destroy() Destroy element, its children, and all connections.
el:removeChildren() Destroy all children but leave element itself.
el:isActive() true if active and not parented under gui.virtual.
el:isOffScreen() true if element rect is entirely outside screen bounds.

Utilities

Method Description
el:hasType(t) Bitmask type test.
el:canPress(mx, my) true if point is inside element (respects clip area).
el:isBeingCovered(mx, my) true if a sibling is in front of this element at the given point.
el:intersecpt(x, y, w, h) Returns intersection rect with a given AABB.
el:newThread(func) Spawn a coroutine-style thread scoped to this element.
el:getObjectFocus() Returns the currently focused element.
el:getProcessor() Returns the internal updater processor.

Text Elements

All text elements (newTextLabel, newTextButton, newTextBox) inherit from newTextBase.

Properties

Property Type Default Description
text string Displayed string
textColor {r,g,b} black Text color
font Font 12px default LÖVE Font object
align constant ALIGN_LEFT gui.ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT
textOffsetX/Y number 0 Additional pixel offset for text drawing
textScaleX/Y number 1 Scale applied to text rendering
textShearingFactorX/Y number 0 Shearing factor for text transform
textVisibility number 1 Text alpha (01)

Font Management

-- By size (default font)
element:setFont(14)

-- By path and size
element:setFont("fonts/myfont.ttf", 18)

-- By LÖVE font object
element:setFont(love.graphics.newFont("fonts/myfont.ttf", 18))

Automatically resize font to fill element bounds:

-- Binary-search fit between min and max size
element:fitFont(minSize, maxSize, {scale = 1})
-- Returns bestFont, bestSize

Center text vertically inside the element:

element:centerFont(y_offset)

Calculate where the top and bottom of rendered text actually are (pixel offsets within element):

local top, bottom = element:calculateFontOffset(font, adjust)

Text Box Internals

Property Description
cur_pos Integer cursor position (0 = before first character)
selection {start, stop} character indices (may be reversed)
bar_show true when the cursor bar should be visible (blinks via internal thread)
doSelection true while a drag-selection is in progress

Methods:

box:HasSelection()          -- returns true/false
box:GetSelection()          -- returns start, stop (always start ≤ stop)
box:GetSelectedText()       -- returns selected substring
box:ClearSelection()        -- clear selection state

Image Elements

All image elements (newImageLabel, newImageButton) inherit from newImageBase.

setImage

-- From a file path (PNG, JPG, etc.)
element:setImage("path/to/image.png")

-- GIF animation (auto-detected by extension)
element:setImage("path/to/anim.gif")

-- From a LÖVE Image object
element:setImage(loveImageObject)

Properties

Property Description
imageColor Tint color applied when drawing
imageVisibility Image alpha (01)
scaleX / scaleY Flip/scale. Negative values flip the axis.
quad LÖVE Quad used for rendering (sub-region)

Flipping

element:flip(false)   -- flip horizontally
element:flip(true)    -- flip vertically

Gradient

Apply a gradient as the image of any element:

element:applyGradient("horizontal", {r,g,b,a}, {r,g,b,a}, ...)
element:applyGradient("vertical",   {r,g,b,a}, {r,g,b,a}, ...)

Image Caching

-- Pre-load a single image into the cache
gui.cacheImage(gui, "path/to/img.png")

-- Pre-load multiple images; reports progress via OnStatus
gui.cacheImage(gui, {"img1.png", "img2.png"})

-- Tile helper: returns imagedata and quad
local imgdata, quad = gui:getTile("sheet.png", tileX, tileY, tileW, tileH)

Clipping & Scissor

Clipping is set on a parent and affects all descendants:

parent.clipDescendants = true

During each draw pass, the parent propagates its screen-space rectangle to each child's __variables.clip. Children then apply LÖVE's scissor test to avoid drawing outside the parent.


Roundness & Shape

-- Rounded corners
element:setRoundness(rx, ry, segments, side)
-- rx, ry: x/y radius (default 5)
-- segments: arc segments (default 30)
-- side: "top", "bottom", or true (all corners)

-- Directional override
element:setRoundnessDirection(horizontal, vertical)

Circle and arc shapes are set at creation time:

-- Circle
element:makeCircle(x, y, radius, sx, sy, sr, segments)

-- Arc
element:makeArc(arcType, x, y, radius, sx, sy, sr, startAngle, endAngle, segments)
-- arcType: "open", "closed", or "pie" (passed to love.graphics.arc)
-- Angles in radians

Aspect Ratio & Resize Handling

Lock the root GUI to a design resolution:

gui:setAspectSize(1920, 1080)    -- set design resolution
gui.aspect_ratio = true          -- enable aspect-ratio mode

When the window resizes, the library calculates letterbox/pillarbox offsets and adjusts gui.x, gui.y, gui.w, gui.h (and the same on gui.virtual) so all elements remain proportional.

Disable it:

gui:setAspectSize(nil, nil)
gui.aspect_ratio = false

Utility to compute the scaled size manually:

local nw, nh, offsetX, offsetY = gui:GetSizeAdjustedToAspectRatio(windowW, windowH)

The apply Helper

gui.apply is a batch property setter that inspects each field name for a prefix:

Prefix Meaning
C_ Connect to the named connection (value = handler function)
I_ Invoke the named method with args from a table
(none) Direct assignment or smart detection (connection vs function vs value)
gui.apply({
    color         = {1, 0, 0},
    C_OnPressed   = function(self) print("pressed") end,
    I_setFont     = {"fonts/bold.ttf", 16},
}, buttonA, buttonB, buttonC)

Tagging System

Arbitrary string tags can be attached to any element:

element:setTag("draggable")
element:setTag("ui-panel")

element:hasTag("draggable")        -- true / false (direct tag)
element:parentHasTag("ui-panel")   -- true if any ancestor has the tag

The built-in "visual" tag suppresses all mouse event connections:

local deco = parent:newVisualFrame(...)   -- automatically gets "visual" tag

Cloning Elements

Deep-copy an element and optionally its connection handlers:

local copy = element:clone({
    copyTo      = targetParent,   -- parent for the clone (default: gui.virtual)
    connections = true,           -- also copy connection handlers
})

clone recurses through all children. Connection handlers from the original are bound (not moved) to the clone's connections, so both elements remain independently connected.


Processors & Threading

The library uses two internal processors from the multi library:

Processor Purpose
updater Input hooks, hot keys, text-box blink, video completion, image loading
drawer Per-frame draw loop, virtual element position pass

Create a new processor that participates in gui.update:

local proc = gui:newProcessor("MyProcessor")
-- proc is a multi Processor; attach tasks/loops to it normally

Spawn a coroutine thread scoped to an element:

element:newThread(function(self, thread)
    while true do
        thread.sleep(1)
        print("tick", self.text)
    end
end)

Attach a per-frame update callback (called every update loop):

gui:OnUpdate(function(self, dt)
    -- called every frame
end)

element:OnUpdate(function(self, dt)
    -- called every frame with element as self
end)

Create a one-shot or reusable function that runs asynchronously:

local fn = gui.newFunction(function(arg1, arg2)
    -- runs in updater context
end)
fn(arg1, arg2)

Drawing Internals

The draw loop iterates gui:getAllChildren() each frame and calls draw_handler on each element in order (back-to-front).

draw_handler does, in order:

  1. Compute and cache child.x/y/w/h via getAbsolutes.
  2. Propagate clip rects to descendants if clipDescendants is set.
  3. Activate shader if present.
  4. Apply LÖVE scissor (clip or roundness-based).
  5. Fill background with child.color and child.visibility.
  6. Draw border with child.borderColor.
  7. Handle special roundness sides ("top"/"bottom").
  8. Dispatch to type-specific draw functions (video → image → text → box cursor/selection).
  9. Call child:post() if defined.
  10. Remove scissor and shader.

gui.draw_handler is exposed publicly so custom renderers can call it directly.


Virtual GUI

gui.virtual is a root node whose children are never rendered on screen but still participate in the layout pass (absolute positions are computed). Use it to keep pre-built off-screen components ready to be re-parented:

-- Create off-screen
local popup = gui.virtual:newFrame(0, 0, 400, 300)

-- Show it by re-parenting
popup:setParent(gui)

-- Hide it again
popup:setParent(gui.virtual)

gui.virtual shares the same screen dimensions as gui, so positions remain correct when an element moves between them.