Panels, labels, buttons, sliders and lists drawn inside your game's window.
Import blitz.gameui As G gives your program these, with G. in front. Each name leads to its part of the reference.
CreateUI:UI(width:Float = 1280, height:Float = 720)AddPanel:Control(ui:UI, parent:Control, layoutKind:Int = Layout.Column)AddLabel:Control(ui:UI, text:Str, parent:Control)AddButton:Control(ui:UI, id:Str, text:Str, parent:Control)AddSlider:Control(ui:UI, id:Str, minimum:Float, maximum:Float, value:Float, parent:Control)AddList:Control(ui:UI, id:Str, items:Str List, parent:Control)SetLayout(control:Control, options:LayoutOptions)ControlText(control:Control, text:Str)ControlValue(control:Control, value:Float)Value:Float(control:Control)ControlEnabled(control:Control, enabled:Int = True)ControlVisible(control:Control, visible:Int = True)Reparent(control:Control, parent:Control)BringToFront(control:Control)FindControl:Control(ui:UI, id:Str)FocusControl:Int(ui:UI, id:Str)FocusID:Str(ui:UI)RefreshLayout(ui:UI)SetTheme(ui:UI, theme:Theme)NextEvent:UIEvent(ui:UI)LayoutUI(ui:UI, viewport:Viewport)ControlBounds(control:Control, output:Bounds)UpdateUI:InputUse(ui:UI, input:UIInput, seconds:Double, viewport:Viewport)DrawUI(ui:UI, viewport:Viewport)LayoutOptionswidthPolicy:Int = SizePolicy.Fill, heightPolicy:Int = SizePolicy.Auto, width:Float = 160, height:Float = 32, padding:Float = 8, gap:Float = 8, alignment:Int = Alignment.StretchViewportx:Float, y:Float, width:Float = 1280, height:Float = 720Boundsx:Float, y:Float, width:Float, height:Float, clipX:Float, clipY:Float, clipWidth:Float, clipHeight:FloatUIInputpointerX:Float, pointerY:Float, pointerDown:Int, pointerPressed:Int, pointerReleased:Int, wheel:Int, navigation:Int = Navigation.None, accept:Int, cancel:IntThemepanel:Int = $172235, surface:Int = $293A54, text:Int = $F3F6FF, accent:Int = $65D4C6, disabled:Int = $687589, focus:Int = $FFD166InputUsePointerUsed:Int(), NavigationUsed:Int(), AcceptUsed:Int(), CancelUsed:Int()UIEventKind:Int(), ID:Str(), Value:Float()ControlUILayoutColumn, Row, OverlaySizePolicyAuto, Fixed, FillAlignmentStart, Centre, Finish, StretchNavigationNone, Left, Right, Up, DownEventKindClicked, ValueChanged, SelectionChanged, CancelAn optional part. Import blitz.gameui.inputactions as well to use these.
CreateInputAdapter:InputAdapter(controls:I.InputMap, prefix:Str = "ui.")FillInput(adapter:InputAdapter, frame:I.InputFrame, output:G.UIInput)An optional part. Import blitz.gameui.localisation as well to use these.
ControlText:Int(control:G.Control, languages:L.Languages, key:Str, arguments:L.Arguments)ControlCount:Int(control:G.Control, languages:L.Languages, key:Str, count:Long, arguments:L.Arguments)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.

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.
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
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.
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"
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.
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.
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.
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
pointerReleasedandpointerDownas well aspointerPressed, asReadUIInputabove does, or buttons will never click.
UpdateUI and DrawUI the same viewport, so that clicks land where
the player sees the controls.UpdateUI time them.Six more complete programs come with the module:
The reference below has every function, option and rule.
blitz.gameui.inputactions, which needs blitz.inputactionsblitz.gameui.localisation, which needs blitz.localisationPanels, 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.
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/value | Owner 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, Theme | Caller-owned value records, constructed with New. Options/theme copied; input/viewport borrowed only during call. |
UpdateUI(ui,input,seconds,viewport):InputUse | Borrowed view, valid until next update or UI deletion. Never Delete it. Read PointerUsed(), NavigationUsed(), AcceptUsed(), CancelUsed(). |
NextEvent(ui):UIEvent | Transfers 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.
| Operation | Contract / 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):Int | Focus 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.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.
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.
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.
Import blitz.gameui.inputactions As GI provides:
CreateInputAdapter(controls:I.InputMap, prefix:Str="ui."):InputAdapter:
caller-owned adapter, borrowing the map and owning a reusable pointer copy.
Delete the adapter before the borrowed map. It never owns the frame or UIInput.FillInput(adapter, frame:I.InputFrame, output:G.UIInput): call after the game's
one I.ReadInput and before its one G.UpdateUI. Pass the current captured
frame to all maps. This resolves the adapter's map with the idempotent public
UpdateInput, copies its pointer position and overwrites every UIInput field.
It does not poll or read a clock. Never mix an older frame with a newer map.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.