Package github.com/matjam/bunyip/ui
ui
Package ui is an immediate-mode interface toolkit drawn with gfx. To build the interface, call widget methods inside Begin's closure every frame, nesting containers (Panel, Window, Row, Columns, ScrollArea, Tabs, Table, Tree, MenuBar, Modal) the same way. Widgets return what happened (a click, a changed value). Values live in the game's variables and are passed by pointer; Context retains interaction state such as focus, caret, selection, undo history and scrolling. Games using engine.Context.NewUI get a default theme and shared font plus clipboard and input-method placement wiring. Use New directly when supplying those dependencies yourself.
The widgets are Label and RichLabel, Button and IconButton, Checkbox, Radio and RadioGroup, Slider, IntSlider and Spinner, Progress, Dropdown, ListBox and ReorderableList, TextField and TextArea with selection, clipboard and undo, ColorPicker, Image, Tooltip and Separator. Anchored, Stretched and Split place rectangles from the view's size. DragSource and DropTarget carry a payload from one widget to another and draw a ghost under the pointer.
A widget's identity comes from its label and its enclosing containers. Widgets with the same label in one container are told apart by the order they are called in, so a list of identical buttons works as long as the order is stable. Focus moves with Tab and the d-pad and activates with Enter, Space or a gamepad's A. A list, a row of tabs, a table's rows, a tree, a radio group and an open dropdown are each one Tab stop, and the arrows, Home, End, PageUp and PageDown move between the items inside. Sliders and spinners step with the left and right arrows. WantsMouse reports panel hover and dragging; WantsKeyboard reports text focus. Neither consumes input or gates the game's own controls. Accessible lists the last frame's widgets with roles and values for screen readers and tests.
Colours, spacing and the font live in a Theme. To restyle the toolkit, swap that one value. The built-in themes come from NamedTheme. A Skin of nine-slice textures inside the theme replaces the flat rectangles with drawn art.
Index
- Variables
func Move[T any](s []T, from, to int)func ThemeNames() []string- type AccessibleNode
- type Anchor
- type Clipboard
- type Context
func New(g *gfx.Graphics, theme Theme) *Contextfunc (c *Context) Accessible() []AccessibleNodefunc (c *Context) Begin(in *input.State, body func())func (c *Context) Button(label string) boolfunc (c *Context) Cell(text string)func (c *Context) Checkbox(label string, value *bool) boolfunc (c *Context) ColorPicker(label string, col *gfx.Color) boolfunc (c *Context) Columns(weights []float32, body func())func (c *Context) CurveEditor(label string, points *[]lin.Vec2, lo, hi, height float32) boolfunc (c *Context) DragSource(label string, payload any, body func()) boolfunc (c *Context) Dragging() (payload any, ok bool)func (c *Context) DropTarget(label string, accept func(payload any) bool) (payload any, dropped bool)func (c *Context) DropTargetRect(label string, r Rect, accept func(payload any) bool) (payload any, dropped bool)func (c *Context) Dropdown(label string, selected *int, options []string) boolfunc (c *Context) IconButton(icon gfx.Region, label string) boolfunc (c *Context) Image(tex *gfx.Texture, w, h float32)func (c *Context) ImageRegion(reg gfx.Region, w, h float32)func (c *Context) IntSlider(label string, value *int, lo, hi int) boolfunc (c *Context) Label(text string)func (c *Context) ListBox(label string, height float32, items []string, selected *int) boolfunc (c *Context) Menu(label string, body func())func (c *Context) MenuBar(r Rect, body func())func (c *Context) MenuItem(label string) boolfunc (c *Context) Modal(title string, r Rect, open *bool, body func())func (c *Context) Panel(title string, r Rect, body func())func (c *Context) Progress(label string, t float32)func (c *Context) Radio(label string, value *int, option int) boolfunc (c *Context) RadioGroup(value *int, options []string) boolfunc (c *Context) ReorderableList(label string, items []string, height float32) (from, to int, moved bool)func (c *Context) RichLabel(markup string) stringfunc (c *Context) Row(n int, body func())func (c *Context) ScrollArea(label string, r Rect, contentHeight float32, contents func())func (c *Context) Separator()func (c *Context) Slider(label string, value *float32, lo, hi float32) boolfunc (c *Context) Space(h float32)func (c *Context) Spinner(label string, value *int, lo, hi, step int) boolfunc (c *Context) Table(columns []string, weights []float32, rows int, cell func(row, col int)) (clicked int)func (c *Context) Tabs(labels []string, selected *int) boolfunc (c *Context) TextArea(label string, value *string, height float32) boolfunc (c *Context) TextField(label string, value *string) boolfunc (c *Context) Tooltip(text string)func (c *Context) Tree(label string, body func())func (c *Context) TreeOpen(label string, body func())func (c *Context) WantsKeyboard() boolfunc (c *Context) WantsMouse() boolfunc (c *Context) Window(title string, r *Rect, body func())
- type Palette
- type Rect
- type Skin
- type Slice
- type Theme
Variables
var Palettes = map[string]Palette{ /* … */ }
Palettes are the built-in colour schemes by name; see ThemeNames.
Functions
Move source
func Move[T any](s []T, from, to int)
Move shifts the element at from so it ends up at index to, keeping the order of the rest: what a caller does with ReorderableList's result.
ThemeNames source
func ThemeNames() []string
ThemeNames returns the built-in theme names in menu order.
Types
type AccessibleNode source
type AccessibleNode struct {
// Role is one of button, checkbox, slider, textfield, textarea, tab,
// tree, menu, menuitem, radio, spinner, listbox, colorpicker, image,
// label, window, dropdown, progress, row (of a Table), list and
// listitem (a ReorderableList and its rows), draggable and droptarget.
Role string
Label string // human-readable widget label
Value string // the current value where there is one
Rect Rect // bounds in view units
State bool // checked, selected, open, dragged or ready for a drop, as the role implies
Focused bool // has keyboard focus
}
AccessibleNode describes one widget of the last frame for assistive technology: what it is, what it says, where it is and its state. A game or platform layer can read the list with Accessible and hand it to a screen reader, log it, or drive the interface from it.
type Clipboard source
type Clipboard interface {
Clipboard() (string, error)
SetClipboard(string) error
}
Clipboard is what text fields cut, copy and paste through; the engine's Context satisfies it, so a game sets ui.Clipboard = ctx.
type Context source
type Context struct {
Theme Theme // live widget styling; referenced resources are managed separately
// OnTextInputRect, when set, is told where the focused text field is
// so the platform can place input-method candidate windows. Wire it to
// engine.Context.SetTextInputRect.
OnTextInputRect func(x, y, w, h float32)
// DragGhost, when set, draws what follows the pointer during a drag
// in place of the source's label.
DragGhost func(label string, payload any, x, y float32)
// Clipboard, when set, is what text fields cut, copy and paste
// through; the engine's Context satisfies it.
Clipboard Clipboard
// contains filtered or unexported fields
}
Context drives one interface. Create one and reuse it every frame. Build it during the game's Draw on the rendering goroutine; it is not safe for concurrent use and does not own fonts or skin textures.
New source
func New(g *gfx.Graphics, theme Theme) *Context
New makes a context drawing with g under theme.
Accessible source
func (c *Context) Accessible() []AccessibleNode
Accessible returns the widgets of the last finished frame in reading order. The slice is borrowed and reused by later frames. Do not modify it; copy it if it must outlive the next Begin. No operating-system accessibility bridge is installed by this method.
Begin source
func (c *Context) Begin(in *input.State, body func())
Begin runs one frame of interface: body calls the widget methods, and the frame is finished (overlays drawn, state settled) when it returns.
ui.Begin(ctx.Input, func() {
ui.Panel("Menu", rect, func() { ... })
})
Example
package main
import (
"github.com/matjam/bunyip/gfx"
"github.com/matjam/bunyip/input"
"github.com/matjam/bunyip/ui"
)
// In a game these come from engine.Context: ctx.Gfx and ctx.Input. The
// font is created once in Init.
var (
g *gfx.Graphics
in *input.State
font *gfx.Font
)
// Options a menu edits.
var (
volume float32 = 0.8
fullscreen bool
name string
quality int
)
func main() {
u := ui.New(g, ui.DarkTheme(font))
// Every frame, inside Draw:
u.Begin(in, func() {
u.Panel("Options", ui.Rect{X: 20, Y: 20, W: 300, H: 260}, func() {
u.Slider("Volume", &volume, 0, 1)
u.Checkbox("Fullscreen", &fullscreen)
u.Dropdown("Quality", &quality, []string{"Low", "High"})
u.TextField("Player name", &name)
u.Row(2, func() {
if u.Button("Apply") {
// ...
}
u.Button("Cancel")
})
})
})
}
Button source
func (c *Context) Button(label string) bool
Button draws a push button and reports a click.
Checkbox source
func (c *Context) Checkbox(label string, value *bool) bool
Checkbox toggles *value on click and reports a change.
ColorPicker source
func (c *Context) ColorPicker(label string, col *gfx.Color) bool
ColorPicker edits a colour with a hue bar and a saturation-value square, showing a swatch and the hex value; it reports a change.
Columns source
func (c *Context) Columns(weights []float32, body func())
Columns lays the widgets body creates side by side, one per weight, with widths in proportion to the weights:
ui.Columns([]float32{2, 1}, func() { ui.Dropdown(...); ui.Checkbox(...) })
CurveEditor source
func (c *Context) CurveEditor(label string, points *[]lin.Vec2, lo, hi, height float32) bool
CurveEditor edits a curve of points drawn as a graph: drag a point to move it, click an empty part of the graph to add one, right-click a point to remove it. It reports whether the curve changed this frame.
The points are (x, y) pairs kept in increasing x. X runs from 0 to 1 across the graph and y from lo to hi up it, so a particle curve over a lifetime passes lo and hi as the range its values may take. The first and last points keep their x, so a curve always spans the whole range; the rest move freely between their neighbours. A curve of fewer than two points is drawn but only its points move.
height is the graph's height in view units; zero is 80.
DragSource source
func (c *Context) DragSource(label string, payload any, body func()) bool
DragSource makes the widgets body creates draggable: pressing on them and moving the pointer a few units starts a drag carrying payload, with a ghost of label (or what DragGhost draws) following the pointer until it is released on a DropTarget or Escape cancels it. It reports whether its payload is being dragged this frame.
Dragging source
func (c *Context) Dragging() (payload any, ok bool)
Dragging returns the payload of the drag in progress, so a target can show itself ready while something it accepts is over it.
DropTarget source
func (c *Context) DropTarget(label string, accept func(payload any) bool) (payload any, dropped bool)
DropTarget makes the previous widget a place a drag can end. While an accepted payload hovers it is outlined in the accent colour; on the frame the pointer is released there it reports the payload dropped. accept may be nil to take anything.
DropTargetRect source
func (c *Context) DropTargetRect(label string, r Rect, accept func(payload any) bool) (payload any, dropped bool)
DropTargetRect is DropTarget for an explicit rectangle, such as a cell of an inventory grid drawn without widgets.
Dropdown source
func (c *Context) Dropdown(label string, selected *int, options []string) bool
Dropdown shows the selected option and opens a list on click; it reports a change to *selected. While the list is open the arrows move through it, Enter chooses and Escape closes it.
IconButton source
func (c *Context) IconButton(icon gfx.Region, label string) bool
IconButton is a Button with an icon before its label.
Image source
func (c *Context) Image(tex *gfx.Texture, w, h float32)
Image shows a texture at a size; zero size means the texture's own, fitted to the width available.
ImageRegion source
func (c *Context) ImageRegion(reg gfx.Region, w, h float32)
ImageRegion shows a region of a texture at a size, keeping its aspect ratio when only one of w and h is given.
IntSlider source
func (c *Context) IntSlider(label string, value *int, lo, hi int) bool
IntSlider drags *value across [lo, hi] in whole steps; while focused the left and right arrows (or the d-pad) step it by one.
Label source
func (c *Context) Label(text string)
Label draws text, wrapped to the width available to it.
ListBox source
func (c *Context) ListBox(label string, height float32, items []string, selected *int) bool
ListBox shows items in a scrolling box of the given height with one selected; clicking selects and reports a change. selected may be -1. The list is one Tab stop: the arrows, Home, End, PageUp and PageDown move through it, Enter selects, and the focused row scrolls into view.
Menu source
func (c *Context) Menu(label string, body func())
Menu is one heading in a MenuBar; clicking it opens a list beneath in which body's MenuItem calls appear. The list closes when an item is chosen or the pointer clicks elsewhere.
MenuBar source
func (c *Context) MenuBar(r Rect, body func())
MenuBar lays a row of menus across r; body calls Menu for each.
MenuItem source
func (c *Context) MenuItem(label string) bool
MenuItem is a choice in an open Menu; it reports a click and closes the menu.
Modal source
func (c *Context) Modal(title string, r Rect, open *bool, body func())
Modal dims everything and draws a panel above it that alone takes input while open; body builds its contents. Pass the same open flag each frame and clear it to close. Its body runs later in Begin, after ordinary widgets. Once submitted, it blocks subsequent background widgets; on following frames it owns input from the first widget. Submit a newly opened modal before background controls to block them in that opening frame too. The game must separately suppress its own input while the modal is open.
Panel source
func (c *Context) Panel(title string, r Rect, body func())
Panel draws a titled box at r and lays out the widgets body creates inside it, top to bottom.
Progress source
func (c *Context) Progress(label string, t float32)
Progress draws a bar filled to t in [0,1].
Radio source
func (c *Context) Radio(label string, value *int, option int) bool
Radio is one button of a group: it sets *value to option when clicked and shows filled while they match. RadioGroup stacks several.
RadioGroup source
func (c *Context) RadioGroup(value *int, options []string) bool
RadioGroup stacks a radio button per option and reports a change. The group is one Tab stop; the arrows move between its buttons.
ReorderableList source
func (c *Context) ReorderableList(label string, items []string, height float32) (from, to int, moved bool)
ReorderableList shows items in a scrolling box of the given height where a row can be dragged to a new place, with a marker showing where it will land. It reports that the item at from should move to index to; the caller applies the move, for instance with Move. With a row focused, Ctrl or Cmd with Up or Down moves it a step.
RichLabel source
func (c *Context) RichLabel(markup string) string
RichLabel draws markup (see gfx.ParseRich) wrapped to the width available, in the theme's regular, bold and italic fonts, and returns the name of a link clicked this frame, or "". The markup is parsed and laid out once and kept for as long as the interface keeps drawing it, so the same label every frame costs a map lookup.
Row source
func (c *Context) Row(n int, body func())
Row lays the widgets body creates side by side, n of equal width.
ScrollArea source
func (c *Context) ScrollArea(label string, r Rect, contentHeight float32, contents func())
ScrollArea lays out its contents inside r with a vertical scrollbar, clipping what does not fit; contentHeight is the total height the contents need. The scroll position is kept per label across frames.
Slider source
func (c *Context) Slider(label string, value *float32, lo, hi float32) bool
Slider drags *value across [lo, hi] and reports a change. While focused, the left and right arrows (or the d-pad) step it by a twentieth of the range.
Spinner source
func (c *Context) Spinner(label string, value *int, lo, hi, step int) bool
Spinner shows *value between minus and plus buttons that step it within [lo, hi]; the label sits before it. The value is the Tab stop; while focused the left and right arrows (or the d-pad) step it.
Table source
func (c *Context) Table(columns []string, weights []float32, rows int, cell func(row, col int)) (clicked int)
Table lays out a header row and rows of cells in columns; weights give the columns' relative widths (nil for equal) and cell draws each cell with the usual widgets. Rows alternate in shade. The rows are one Tab stop that the arrows move through; it returns the row clicked or activated with Enter this frame, or -1. Widgets inside cells are their own Tab stops, and their identity is scoped to their row.
Rows outside the clip the table is drawn under, such as the rows of a long table scrolled out of a ScrollArea, keep their place, their Tab stop and their accessibility entry, but cell is not called for them, so a thousand-row table costs about what its visible rows do. A row holding the focused or held widget is always built, so scrolling it out of view keeps the focus. A skipped row takes the height it had when it was last built, or a row of the theme's height before that.
Tabs source
func (c *Context) Tabs(labels []string, selected *int) bool
Tabs draws a row of tabs and keeps *selected on the clicked one, reporting a change; the widgets that follow belong to that tab. The row is one Tab stop: the left and right arrows move along it and Enter selects.
TextArea source
func (c *Context) TextArea(label string, value *string, height float32) bool
TextArea edits *value over several wrapped lines in a box of the given height, with the same keys as TextField plus Up, Down and Enter for a new line; it scrolls to keep the caret in view. An open modal releases focus from areas outside it. Tab and gamepad navigation transfer text focus while preserving each area's caret and selection.
TextField source
func (c *Context) TextField(label string, value *string) bool
TextField edits *value on one line while focused and reports a change. It has a caret, a selection (Shift with the arrows, or a drag), Home and End, word jumps with Ctrl or Cmd and the arrows, select all, cut, copy and paste through the Clipboard, and undo and redo (Ctrl or Cmd with Z, Shift+Z or Y). Enter and Escape drop focus. An open modal releases focus from fields outside it. Tab and gamepad navigation transfer text focus while preserving each field's caret and selection.
Tooltip source
func (c *Context) Tooltip(text string)
Tooltip shows text near the pointer when it has rested on the previous widget for a moment.
Tree source
func (c *Context) Tree(label string, body func())
Tree draws a collapsible node: a triangle and a label that toggle on click, with body's widgets indented beneath while open. Nodes start closed; TreeOpen starts them open.
TreeOpen source
func (c *Context) TreeOpen(label string, body func())
TreeOpen is Tree starting open.
WantsKeyboard source
func (c *Context) WantsKeyboard() bool
WantsKeyboard reports whether a text editor has focus after the most recent Begin. It does not report navigation focus or a modal without a focused editor; gate gameplay separately while a modal is open.
WantsMouse source
func (c *Context) WantsMouse() bool
WantsMouse reports whether the pointer is over a panel or a drag is in progress, so the game can ignore clicks the interface consumed.
type Palette source
type Palette struct {
Background gfx.Color // panels
Surface gfx.Color // buttons and fields sit on this
Border gfx.Color // panel and control outlines
Text gfx.Color // ordinary text
TextDim gfx.Color // secondary text and placeholders
Accent gfx.Color // sliders, focus, checks
Title gfx.Color // panel titles
}
Palette is the handful of colours a theme is derived from. Build one with a game's art in mind and pass it to FromPalette.
type Rect source
type Rect = lin.Rect
Rect is an axis-aligned box in view units: lin.Rect under a short name.
Anchored source
func Anchored(area Rect, a Anchor, w, h, margin float32) Rect
Anchored places a w by h rectangle at an anchor of area, margin units in from the edges: a panel that stays in the corner whatever the window's size.
type Skin source
type Skin struct {
Panel *Slice // panel background and border
Button *Slice // idle button
ButtonHover *Slice // hovered button; falls back to Button
ButtonActive *Slice // held button; falls back to ButtonHover, then Button
Field *Slice // text fields and drop-down heads
FieldFocus *Slice // focused editor; falls back to Field
Check *Slice // checkbox box, unticked
CheckOn *Slice // checkbox box, ticked
Track *Slice // slider, progress and scrollbar tracks
Fill *Slice // the filled part of sliders and progress bars
Knob *Slice // slider handle
Thumb *Slice // scrollbar handle
}
Skin holds the art for each widget part. Any nil slice falls back to the theme's flat colours, so a skin can start with one button.
type Slice source
type Slice = gfx.NineSlice
Slice is a nine-slice texture. The corners keep their size, the edges stretch along one axis and the centre fills the rest, so one small image skins boxes of any size. It is gfx.NineSlice, so the same value draws outside the interface too.
type Theme source
type Theme struct {
Font *gfx.Font // required default font; this Theme does not manage its lifetime
// BoldFont, ItalicFont and BoldItalicFont serve RichLabel; nil falls
// back to Font.
BoldFont, ItalicFont, BoldItalicFont *gfx.Font
Text gfx.Color // ordinary labels and widget values
TextDim gfx.Color // secondary labels and placeholders
Panel gfx.Color // panel background
PanelBorder gfx.Color // panel outline and separators
Title gfx.Color // panel titles
Button gfx.Color // idle button fill
ButtonHover gfx.Color // hovered button fill
ButtonActive gfx.Color // pressed button fill
Accent gfx.Color // selections, focus rings and filled controls
Field gfx.Color // editor background
FieldBorder gfx.Color // unfocused editor outline
Track gfx.Color // slider, progress and scrollbar background
Padding float32 // inside panels and buttons
Spacing float32 // between stacked widgets
BorderWidth float32 // outline thickness in view units
RowHeight float32 // minimum widget height
FocusWidth float32 // the keyboard focus ring; zero means 2
// Skin, when set, draws widgets from textures; colours above still
// tint text and fill in for any slice the skin leaves nil.
Skin *Skin
}
Theme is every colour and measure the widgets use. Measures are in view units. Start with a built-in theme or FromPalette; the zero Theme has no font and is not usable for text widgets through New; engine.Context.NewUI expands it into the built-in dark theme.
DarkTheme source
func DarkTheme(font *gfx.Font) Theme
DarkTheme is the dark default; pass the font the interface should use.
FromPalette source
func FromPalette(font *gfx.Font, p Palette) Theme
FromPalette derives a full theme: buttons brighten as they are hovered and pressed, fields sit slightly below the background, tracks between.
NamedTheme source
func NamedTheme(name string, font *gfx.Font) (theme Theme, ok bool)
NamedTheme builds a built-in theme by name; ok is false for an unknown
name.
Example
package main
import (
"github.com/matjam/bunyip/gfx"
"github.com/matjam/bunyip/input"
"github.com/matjam/bunyip/ui"
)
// In a game these come from engine.Context: ctx.Gfx and ctx.Input. The
// font is created once in Init.
var (
g *gfx.Graphics
font *gfx.Font
)
func main() {
theme, _ := ui.NamedTheme("nord", font)
theme.RowHeight = 32
u := ui.New(g, theme)
_ = u
}
Source files
access.go bench_test.go caret_test.go context.go curve.go curve_test.go dragdrop.go example_test.go frame_bench_test.go hook_test.go layout.go modal_text_test.go nav.go panel.go review_test.go skin.go tab_focus_test.go table_test.go textedit.go theme.go ui2_test.go ui3_test.go ui_test.go widgets.go widgets2.go widgets3.go