Game UI

Panels, labels, buttons, sliders and lists drawn inside your game's window.

Commands

Import blitz.gameui As G gives your program these, with G. in front. Each name leads to its part of the reference.

Functions

Types

Enums

blitz.gameui.inputactions

An optional part. Import blitz.gameui.inputactions as well to use these.

Functions

Types

blitz.gameui.localisation

An optional part. Import blitz.gameui.localisation as well to use these.

Functions

Guide

Game UI gives your game menus, settings screens, shops and HUDs, drawn inside the game's own window: panels, labels, buttons, sliders and lists. You add the controls once, and the module lays them out, draws them, and works out what the player clicked, dragged or chose, with the mouse or with the keys. Your game decides what each button does.

A settings panel with a speed slider, a size list showing Large, and a Pause button, beside a yellow ball

Try it

Open settings.bb and run it. Drag the slider to change the ball's speed, pick a size from the list, and click Pause. Then try it without the mouse: the arrows move a yellow outline from control to control, and Enter presses the button. On the slider, Left and Right change the speed. On the list, Up and Down change the size, and Left or Right moves on.

; A ball rolls beside a panel of settings. Use the mouse, or the arrows and
; Enter, to change its speed and size or pause it. Escape quits.
Dialect "modern"
Import blitz.gameui As G

; Fills in the UI's input from the mouse and keyboard. Call it once a frame.
Function ReadUIInput(input:G.UIInput)
    Local down:Int = MouseDown(1)
    input.pointerX = MouseX()
    input.pointerY = MouseY()
    input.pointerPressed = MouseHit(1) > 0
    ; Down last frame, or pressed since, and up now: the button was let go.
    input.pointerReleased = (input.pointerDown Or input.pointerPressed) And down = False
    input.pointerDown = down
    input.navigation = G.Navigation.None
    If KeyDown(203) Then input.navigation = G.Navigation.Left
    If KeyDown(205) Then input.navigation = G.Navigation.Right
    If KeyDown(200) Then input.navigation = G.Navigation.Up
    If KeyDown(208) Then input.navigation = G.Navigation.Down
    input.accept = KeyHit(28) ; Enter
    input.cancel = KeyHit(1)  ; Escape
End Function

Graphics 640, 360, 0, 2
SetBuffer BackBuffer()
ClsColor 18, 52, 79

; The UI owns every control you add. Null puts a control in the UI's own
; column, and naming a panel puts it inside that panel.
Local ui:G.UI = G.CreateUI(640, 360)
Local panel:G.Control = G.AddPanel(ui, Null)
Local options:G.LayoutOptions = New G.LayoutOptions
options.widthPolicy = G.SizePolicy.Fixed ; 200 wide, not the whole width.
options.width = 200
G.SetLayout panel, options
Delete options ; The panel copied them.
G.AddLabel ui, "Speed", panel
Local speed:G.Control = G.AddSlider(ui, "speed", 0, 400, 200, panel)
G.AddLabel ui, "Size", panel
Local sizes:Str List = New List
sizes.Add("Small")
sizes.Add("Medium")
sizes.Add("Large")
Local size:G.Control = G.AddList(ui, "size", sizes, panel)
Delete sizes ; The list copied them.
Local pause:G.Control = G.AddButton(ui, "pause", "Pause", panel)

; The game fills in the input, and says where on the screen the UI goes.
Local input:G.UIInput = New G.UIInput
Local view:G.Viewport = New G.Viewport
view.width = 640
view.height = 360

Local x:Float = 240
Local paused:Int = False
Local running:Int = True
Local previous:Int = MilliSecs()
While running
    Local now:Int = MilliSecs()
    Local seconds:Double = Double(now - previous) / 1000D
    previous = now
    ReadUIInput input
    G.UpdateUI ui, input, seconds, view

    ; The UI tells the game what happened with events, oldest first.
    Local event:G.UIEvent = G.NextEvent(ui)
    While event <> Null
        If event.Kind() = G.EventKind.Clicked And event.ID() = "pause"
            paused = Not paused
            If paused Then G.ControlText pause, "Carry on" Else G.ControlText pause, "Pause"
        EndIf
        If event.Kind() = G.EventKind.Cancel Then running = False ; Escape
        Delete event ; Each event is yours to delete.
        event = G.NextEvent(ui)
    Wend
    ; Read a slider's value, or a list's choice, whenever you need it.
    If Not paused Then x = x + G.Value(speed) * Float(seconds)
    If x > 660 Then x = 220
    Local radius:Int = 15 + 10 * Int(G.Value(size)) ; 0 is Small, 2 Large.

    Cls
    Color 255, 220, 0
    Oval Int(x) - radius, 240 - radius, radius * 2, radius * 2, True
    G.DrawUI ui, view ; The UI draws over the game.
    Flip
Wend
Delete ui ; This deletes its controls too.
Delete input
Delete view
End

The program adds its controls once. Then, every frame, it fills in a UIInput with what the mouse and keys are doing, and UpdateUI works out what that did to the controls. The module never reads the mouse or keyboard itself, so the same UI works with a gamepad, a touch screen or Input Actions: whatever fills in the UIInput. The yellow outline shows the focus: the control that the keys work.

How do I...

Put controls side by side?

Add a panel that lays its controls out in a row. Controls fill the width they are given, so two buttons share the row equally.

Local buttons:G.Control = G.AddPanel(ui, Null, G.Layout.Row)
G.AddButton ui, "yes", "Yes", buttons
G.AddButton ui, "no", "No", buttons

Show or hide a whole menu?

Put the menu in a panel, and hide or show the panel. Hidden controls take up no space, aren't drawn and can't be used.

G.ControlVisible pauseMenu, False ; Hide it, and everything in it.
G.ControlVisible pauseMenu, True  ; Show it again.

Grey out a button the player can't use?

Disable it. It's drawn faded, and can't be clicked or reached with the keys. Change its text to say why.

G.ControlEnabled buy, coins >= price
If coins >= price Then G.ControlText buy, "Buy" Else G.ControlText buy, "Not enough coins"

Find out as soon as a slider or list changes?

Look for its event: ValueChanged for a slider, or SelectionChanged for a list. The event's Value() is the slider's new value, or the number of the list's new choice, counting from 0.

If event.Kind() = G.EventKind.ValueChanged And event.ID() = "volume"
    volume = event.Value()
EndIf

Changing a value yourself, with ControlValue, sends no event.

Stop a click on the UI from also reaching the game?

UpdateUI says which parts of the input it used. A click on a panel or control is the UI's, so the game should ignore it.

Local use:G.InputUse = G.UpdateUI(ui, input, seconds, view)
If input.pointerPressed And use.PointerUsed() = False Then Print "Clicked the game"

AcceptUsed() and CancelUsed() say whether it used the accept and cancel inputs, such as Enter and Escape. The InputUse is only lent to you, so don't delete it.

Change the colours?

Fill in a theme and give it to the UI. Colours are written as $RRGGBB.

Local theme:G.Theme = New G.Theme
theme.panel = $402020   ; Behind the panels,
theme.surface = $603030 ; behind buttons, sliders and lists,
theme.accent = $FFAA00  ; and the slider's handle and the list's choice.
G.SetTheme ui, theme
Delete theme ; The UI copied it.

Fit the UI to any size of window?

Make the UI at the size you design it at, and set the viewport to the part of the screen it should fill. The UI grows or shrinks to fit, keeping its shape, and sits in the middle.

view.width = GraphicsWidth()
view.height = GraphicsHeight()

Text stays the size of the current font, so choose a bigger font with SetFont for a much bigger window, then call G.RefreshLayout ui.

Blip's tip

A button clicks when the pointer is pressed and let go over it. Fill in pointerReleased and pointerDown as well as pointerPressed, as ReadUIInput above does, or buttons will never click.

Good to know

More examples

Six more complete programs come with the module:

The reference below has every function, option and rule.

Reference

Needs
No other modules
Optional parts
blitz.gameui.inputactions, which needs blitz.inputactions
blitz.gameui.localisation, which needs blitz.localisation
Examples
adapters.bb, dialogue_shop.bb, inventory_pack.bb, menus_hud.bb, quick_start.bb, shop.bb

Panels, labels, buttons, sliders and single-selection lists inside your game viewport. Importing allocates nothing, polls nothing and changes no runtime settings. Requires Extended with object-lifecycle-v1 (runtime 1114, module ABI 5); Original is unsupported. Core has no package dependencies. Optional blitz.gameui.inputactions and blitz.gameui.localisation import their corresponding accepted packages.

Blitz3D+ comes with this module, so any Extended program can Import blitz.gameui. Open examples/quick_start.bb and run it. The button changes the sky; Enter activates focus and Escape leaves.

Dialect "modern"
Import blitz.gameui As G
Graphics 640, 360, 0, 2
SetBuffer BackBuffer()
Local ui:G.UI = G.CreateUI(640, 360)
Local button:G.Control = G.AddButton(ui, "start", "Start game", Null)
Local frame:G.UIInput = New G.UIInput
Local view:G.Viewport = New G.Viewport
view.width = 640
view.height = 360
; Fill frame once from your game's shared input snapshot.
G.UpdateUI ui, frame, 1D / 60D, view
G.DrawUI ui, view
Local event:G.UIEvent = G.NextEvent(ui)
While event <> Null
    If event.Kind() = G.EventKind.Clicked Then Print event.ID()
    Delete event
    event = G.NextEvent(ui)
Wend
Delete ui
Delete frame
Delete view
EndGraphics
End

The complete loop is in examples/quick_start.bb. menus_hud.bb implements working title/pause/settings/HUD screens and a moving player. Both sliders alter the simulation/rendering. shop.bb implements selection, scrolling, purchasing, insufficient-funds feedback, reset, viewport changes and tree mutations. Each example exits with assertions and a PASS line when run with --verify.

inventory_pack.bb adapts the accepted inventory pickup/container examples: use herbs, collect leftovers and transfer the stack both ways with actual public inventory operations. Its twelve-herb ledger is checked every frame; disabled buttons reflect actual availability/capacity. dialogue_shop.bb adapts the accepted branching shopkeeper: report the bridge, request a lantern, see an actual declined/accepted purchase, capture and resume the conversation without replaying its event. The game owns gold and lanterns; capture saves only dialogue in memory. Both use mouse or arrows/Enter, Escape to close, and the same support/frame.bb helper. These game-owned examples add no dependency to any core. Views are borrowed: reacquire a dialogue view after every dialogue mutation and do not delete it.

API and ownership

Operations are module-qualified functions. Resource constructors and internals are private. Public enums are Int-backed because that is Extended's enum model. All operations are main-thread only. No callbacks are registered or invoked; drain events after UpdateUI, then mutate the tree or call UpdateUI again.

Resource/valueOwner and lifetime
CreateUI(width=1280,height=720)Caller owns UI and deletes it when leaving screen. Positive finite logical size, at most 1048576 each.
AddPanel(ui,parent,layout=Layout.Column)UI owns returned Control. Null parent uses implicit root. Explicit parent must be same-UI panel.
AddLabel(ui,text,parent)Same ownership. Uses current runtime font.
AddButton(ui,id,text,parent)Same ownership; nonempty unique stable ID.
AddSlider(ui,id,min,max,value,parent)Finite increasing Float range, finite difference, value clamped.
AddList(ui,id,items,parent)Copies caller's Str List (at most 2^24 items, exact Float indices). Initial selection 0 or -1 if empty.
LayoutOptions, UIInput, Viewport, Bounds, ThemeCaller-owned value records, constructed with New. Options/theme copied; input/viewport borrowed only during call.
UpdateUI(ui,input,seconds,viewport):InputUseBorrowed view, valid until next update or UI deletion. Never Delete it. Read PointerUsed(), NavigationUsed(), AcceptUsed(), CancelUsed().
NextEvent(ui):UIEventTransfers oldest event to caller, or Null. Read Kind(), ID(), Value(); Delete each returned event.

Delete a Control to remove its subtree. It detaches first, clears affected focus/capture, deletes descendants iteratively and removes their queued events. Events already transferred to the caller remain valid ID/value snapshots. Reusing an ID does not resurrect a removed event. Delete UI drains everything remaining, including its borrowed InputUse view. Fonts, input records and caller Lists are never deleted. Use returned controls/FindControl, not global Type enumeration or Delete Each: the implicit root belongs exclusively to UI. Deleting a root discovered by enumeration is a terminating programming error (Delete UI instead). Destruction never draws, clicks, cancels, saves or commits a game transaction. EventKind.Cancel is a user action; the game decides whether that means leave, revert settings or close a modal. There are no disposal synonyms.

OperationContract / example
SetLayout(control,options)Copy all options after validation; G.SetLayout panel, options.
ControlText(control,text)Labels/buttons only; G.ControlText title, "Paused".
ControlValue(control,value)Slider clamp or exact list index; -1 clears list selection. Emits no event.
Value(control)Read slider Float or list index-as-Float. Unsupported widgets fail.
ControlEnabled(control,enabled=True)Applies through ancestor chain; disabled controls draw muted, block pointer, never activate.
ControlVisible(control,visible=True)Hidden subtrees are excluded from layout, drawing and input. Both operations clear affected capture/focus.
Reparent(control,parent)Move subtree to final sibling position; Null means root. Cycles/foreign parents fail before mutation. Clears affected focus/capture.
BringToFront(control)Move to final sibling position; changes painting/picking and navigation ties.
FindControl(ui,id)Borrowed control or Null; only interactive widgets have IDs.
FocusControl(ui,id):IntFocus visible enabled control; returns False when unavailable. Empty ID clears.
FocusID(ui)Current ID or empty string.
LayoutUI(ui,viewport)Explicit measure/arrange without input/drawing, before querying bounds.
ControlBounds(control,output)Copy last logical x/y/width/height and clipX/clipY/clipWidth/clipHeight into caller's Bounds.
RefreshLayout(ui)After changing runtime font, including same-height fonts; marks all measurements dirty.
SetTheme(ui,theme)Copy RGB values panel/surface/text/accent/disabled/focus; each 0..$FFFFFF.
DrawUI(ui,viewport)Layout and draw into current buffer using the same destination as UpdateUI.

Programming errors raise RuntimeError with blitz.gameui: context. Null/deleted resources, invalid enums, NaN/infinity, wrong owners, unsupported properties, invalid selection and ID collisions are rejected. Expected missing focus/lookup or an empty event queue use False/Null; no recoverable I/O operation needs LastError.

Layout, font and rendering contract

Layout.Column, Row and Overlay use iterative measure then arrange passes. The implicit root is a Column with 12 logical pixels of padding and 8 of gap. A LayoutOptions defaults to Fill width, Auto height, 160x32 Fixed dimensions, 8 padding, 8 gap and Stretch alignment. Set widthPolicy/heightPolicy to SizePolicy.Auto, Fixed or Fill; width/height apply only to Fixed. Length values are finite 0..1048576.

Column shares unused height between Fill children; Row shares unused width. Fixed/Auto children keep their measured size if they overflow, and clipping contains them. Remaining Fill space bottoms at zero. Alignment.Start, Centre, Finish, Stretch control cross-axis positioning; Stretch stretches Auto cross sizes, not Fixed ones. Overlay uses both axes and Fill policies; alignment positions Auto/Fixed children. A zero-size control cannot be hit or focused by navigation. Hidden controls occupy no gap. Later siblings paint/pick above earlier ones; disabled controls and visible panels intercept pointer input.

Changed measurements invalidate the node and ancestors; clean subtree intrinsic sizes are reused. Arrangement runs once when layout/visibility/enablement changes. A cached preorder plus reverse preorder keeps layout, picking and destruction safe for deep hierarchies. Full arrangement and picking are O(n); a clean layout is O(1) apart from font queries. Building an extremely deep tree is O(n²) in ancestor invalidation.

Viewport x/y/width/height are pixels in the caller's current buffer. The logical canvas uses min(viewWidth/uiWidth,viewHeight/uiHeight) uniform scale and centred letterboxing. Scale must be 0.000001..1000000; viewport and pointer coordinates are bounded to +/-1048576 pixels. UpdateUI requires the viewport explicitly, so resizing during a drag immediately uses the new map. DrawUI must receive the same viewport. Rectangle edges are clipped against every ancestor; inward-rounded runtime Viewport rectangles prevent fractional edge leakage. Lists additionally clip inside their padding and draw only visible rows, with a partially visible last row permitted. Selection changes scroll the chosen item into view; wheel input scrolls independently without changing selection, and later relayouts keep the wheel position. A row covers its full height, so any click inside it selects it.

Text is measured by StringWidth/FontHeight using the current runtime font. Text stays at that font's device-pixel size; measured sizes convert back to logical units. Geometry scales uniformly. Select an appropriate runtime font size when zooming and call RefreshLayout; this avoids allocating images, switching buffers or silently owning/replacing fonts. UTF-8 strings are passed through unchanged. Actual glyph coverage, fallback and shaping belong to the runtime/font/backend; this package neither shapes complex scripts nor promises complete glyph coverage.

DrawUI restores ColorRed/Green/Blue and preserves buffer/font/clear colour. There are no public getters for the prior Origin/Viewport. Its explicit postconditions are Origin 0,0 and Viewport equal to the supplied destination (integer pixels). Restore your own origin/clip afterwards if needed. It never calls SetBuffer, SetFont, Cls, Flip, UpdateWorld or device polling functions.

Input, focus, repeats and events

Fill UIInput once per frame: pointerX/Y in buffer pixels, pointerDown held, pointerPressed/pointerReleased edges, wheel integer (positive scrolls up), one held Navigation.None/Left/Right/Up/Down, and accept/cancel edges. First-release core handles one primary pointer. Your touch/controller adapter chooses that pointer and navigation direction; other touches are consistently ignored by leaving them out of this snapshot. Examples map mouse/arrows/Enter/Escape. Controller hardware mapping remains with the application. The optional shared- frame adapter below uses the accepted input-actions map and no extra polling.

Order: capture input once; UpdateUI; read consumption; drain events and apply game mutations; simulate only gameplay-owned actions; draw game then DrawUI and Flip. A button activates on matching press/release inside the topmost same button. Slider capture keeps following outside bounds and clamps until release; hiding, disabling, deletion or cancel clears capture. A missing release (down goes false) cancels without clicking. A list selects only a matching pressed/released row.

Navigation uses arranged centres, preferring nearby controls on the same row or column; ties follow insertion/paint order. At an edge it wraps to the opposite edge with the same cross-axis preference. Slider Left/Right changes 1% of its range, clamped; list Up/Down changes selection, clamped; other directions move focus. Accept activates a focused button. Cancel clears pointer capture and queues Cancel with focused ID or empty ID. It does not itself change a screen.

New navigation direction acts immediately. Continued direction uses explicit nonnegative finite Double seconds: first repeat after 0.4 seconds, then every 0.08. At most eight repeats run per update; remaining time is kept, including across zero-delta calls. Direction change/release/cancel resets delay. No hidden clock. The game controls pause/time scale. InputUse reports pointer containment/capture, navigation with an available focus and accepted/cancel actions so the same input need not leak into gameplay. Hovering over a visible panel also consumes pointer.

Events are Clicked, ValueChanged, SelectionChanged and Cancel. Value/selection changes coalesce to the final value within one UpdateUI only. Programmatic ControlValue emits none. Polling is FIFO; no callbacks run during traversal. The queue holds at most 4096 entries and UpdateUI reserves twelve slots before any mutation, rejecting a backlog above 4084. Drain each frame; there is no silent drop policy. Deletion purges undelivered records for removed IDs.

Limitations and deferrals

Text entry/IME, rich text, arbitrary widget plugins and multiple simultaneous pointers remain explicitly deferred. The optional adapters use accepted public dependency APIs; the core remains shallow and acyclic. Menus illustrate game-owned transactions; UI deletion never saves settings, commits purchases or rolls back inventory implicitly.

Optional shared-frame and translation adapters

Import blitz.gameui.inputactions As GI provides:

Bind prefix + left/right/up/down/accept/cancel/pointer/wheel in your chosen context. Unknown names are inert. Directions act at a positive value >=0.5; use scale -1 for left/up stick alternatives. Simultaneous directions prefer Down, then Up, Right, Left, matching the ordinary sample. Accept/cancel/primary pointer are digital actions; fast pointer taps retain both press/release. Wheel is a PointerAxis.Wheel binding and resolves to -1/0/1 row per capture, respecting map context, suspension and rebinding gates. The core UI supplies held-direction repeat timing. Read InputUse and suppress consumed gameplay actions yourself. There is no hardcoded key/controller policy in the adapter; the example's support/action_bindings.bb supplies replaceable application defaults.

Import blitz.gameui.localisation As GL provides ControlText(control, languages, key, arguments):Int and ControlCount(control, languages, key, count:Long, arguments):Int. Both borrow arguments only for the call, resolve through public Localisation and copy text through public GameUI. Label/button measurement is invalidated when text changes. Return False reports resolver diagnostics in L.LastError; visible [key]/{argument} fallback markers are still displayed. An empty translation succeeds. Negative counts, unsupported widgets and invalid resources retain their owning core's programming-error contract. Custom plural callbacks must leave the target control alive until the call returns.

No translation binding, callback or resource is retained. Refresh explicitly when L.LanguageRevision changes or game arguments/counts change. No changes to text shape, font ownership or glyph support are implied. This forwarding submodule has no own lifecycle declaration; its imported cores require lifecycle support, which the compiler and registry resolve transitively.

examples/adapters.bb is a complete translated supply shop: real purchases, disabled unaffordable buying, English/French switching, exact plural counts, keyboard/mouse/controller bindings and world clicks that UI consumes correctly. It generates its text tables in a unique directory and removes only those files.