26 KiB
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
- Setup & Initialization
- Core Concepts
- Creating Elements
- Layout & Positioning
- Events & Connections
- Element Methods
- Text Elements
- Image Elements
- Clipping & Scissor
- Roundness & Shape
- Aspect Ratio & Resize Handling
- The
applyHelper - Tagging System
- Cloning Elements
- Processors & Threading
- Drawing Internals
- 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
Hookfunction that wraps any pre-existing handler you define. Define your ownlove.*callbacks beforerequire-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 (0–1) 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 (0–1) |
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 (0–1) |
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 (0–1) |
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 (0–1) |
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:
- Compute and cache
child.x/y/w/hviagetAbsolutes. - Propagate clip rects to descendants if
clipDescendantsis set. - Activate shader if present.
- Apply LÖVE scissor (clip or roundness-based).
- Fill background with
child.colorandchild.visibility. - Draw border with
child.borderColor. - Handle special roundness sides ("top"/"bottom").
- Dispatch to type-specific draw functions (video → image → text → box cursor/selection).
- Call
child:post()if defined. - 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.