Guides
Custom widgets
The widgets without a look that every widget is built on, and widgets of your own.
Every widget is built on a base, the same widget without a look: the base handles the pointer, the keyboard and the focus, and tells assistive technology what it is, and leaves every color, size and shape to the elements it returns, which you style as any other. Build a design of your own on them, as headless component libraries do on the web.
| Base | What it does |
|---|---|
ButtonBase |
a row that takes the focus, and reports Clicked for the pointer, Enter and Space |
CheckboxBase, SwitchBase, ToggleBase |
a row that toggles a *bool |
CheckboxStateBase |
a check box that can be mixed, setting a *ui.CheckState (CheckOff, CheckOn, CheckMixed), which a press checks, or unchecks once checked (Checkbox) |
RadioBase |
a row that selects its value into a *T |
SliderBase |
sets a *float64 from where the pointer is across its content box, inside its padding, or up it (Vertical), and with the arrows, Page Up and Down, Home and End, by its Step |
TabsBase |
the tab List, whose Tabs choose a *int, with the arrows moving the choice and the focus past those disabled |
SegmentedBase |
a Track of Segments, a radio group choosing a *int |
CollapsibleBase |
a Trigger that shows and hides what Panel builds, growing and shrinking it with Progress, which goes from 0 closed to 1 open |
AccordionBase |
a Root of sections, each Item a CollapsibleBase whose triggers the arrows, Home and End move the focus between (Accordion) |
TreeBase |
a Root of Items, each a Row and a Group of the items inside it while its *bool is true, with the arrows, Home, End and typing moving the focus and opening and closing them (Tree) |
SelectBase |
a Trigger opening a Popup of Items choosing a *T, which the arrows highlight (Highlighted) and Enter chooses; SetOpen opens and closes it, and SelectBaseOpen takes the open flag from you |
ComboboxBase |
an Input whose text filters the Items of a Popup below it, which the arrows highlight and Enter or a click chooses (Chosen), or shows a row of yours when none match (Combobox) |
PopoverBase, DialogBase |
a panel beside an anchor, or over a backdrop covering the window, that a press outside or Escape closes |
MenuBase, ContextMenuBase |
a menu button's Trigger, or an element's context menu, opening a Popup of Items, CheckItems, RadioItems and SubmenuItems with their Submenus, which the pointer and the keys highlight (Highlighted) and choose (Clicked), closing the menu unless KeepOpen, and Groups of items the keys take as one stop (Menu button) |
TooltipBase |
a tip beside an anchor while the pointer rests on it or the keyboard focus is on it, which a press or Escape hides (Tooltip) |
ToastViewportBase, ToastBase |
the window's toasts in a viewport of your own, each a status with an ActionButton and a CloseButton, their time stopping while the pointer or the focus is on them (Toast) |
TextInputBase, TextAreaBase |
text inputs without padding, background, border or corners, ReadOnly or not |
Charts have bases of their own: package ui/chart has scales,
axes, marks and a selection without a look.
Base constructors return value parts with the same build lifetime as other
elements. Give stateful bases a key with Context.Key before construction.
A segmented control on SegmentedBase, and a select on SelectBase:
t := c.Theme()
view := ui.SegmentedBase(c.Key("period"), &app.view, 3)
view.Track.Padding(3).Radius(999).Background(t.Surface).Children(func() {
for i, name := range []string{"Day", "Week", "Month"} {
seg := view.Segment(i).Padding(5, 14).Radius(999)
if i == app.view {
seg.Background(t.Background).Shadow(0, 1, 2, 0, ui.RGBA(0, 0, 0, 0.15))
}
seg.Children(func() { ui.Text(c, name) })
}
})
sel := ui.SelectBase(c.Key("size"), &app.size)
sel.Trigger.Gap(6).Padding(6, 10).Radius(8).Border(1, t.Border).Children(func() {
ui.Text(c, app.size)
ui.Icon(c, chevron)
})
sel.Popup(func(panel ui.Element) {
panel.Margin(4, 0, 0, 0).Padding(4).Radius(10).Background(t.Background).Border(1, t.Border)
for _, size := range sizes {
item := sel.Item(size).Padding(6, 10).Radius(6)
if item.Highlighted() {
item.Background(t.Accent).TextColor(t.AccentText)
}
item.Children(func() { ui.Text(c, size) })
}
})Bases ring the element with the keyboard focus, as every element taking
the focus is, with the theme's ring: FocusRingWidth wide,
FocusRingOffset outside the element's edge, in Focus (see
the focus ring). FocusRingOffset(d) moves
an element's ring, inside it for a negative d, as for a header in a box
that clips what goes past it, and FocusRingColor colors it.
FocusRing(false) turns the ring off for a widget that draws its own,
with Painter.FocusRing while FocusVisible.
A disabled element shows at half its opacity, with what is inside it,
bases too. DimDisabled(false) leaves that to you, for a widget that
shows being disabled in a look of its own: a card that stays readable
with the reason it is unavailable, or a select's options grayed your way.
Style it while IsDisabled:
card := ui.RadioBase(c, &app.plan, p.ID).Disabled(!p.Available).DimDisabled(false)
card.Column().Gap(4).Padding(12).Radius(8).Border(1, t.Border).Children(func() {
name := ui.Text(c, p.Name)
if card.IsDisabled() {
name.TextColor(t.TextMuted)
ui.Text(c, p.DisabledReason).FontSize(12)
}
})A widget that must stay focusable while it is unavailable, for screen
readers to reach it, tells them with
AccessDisabled instead
of Disabled, and styles itself while it is.
Widgets of your own
For text controls with their own buffer or selection model, use
HandleTextInput and retained text geometry.
Build your own widgets from elements. An element keeps state of its own
from frame to frame with ui.Local:
// Spoiler hides text until clicked.
func Spoiler(c *ui.Context, text string) {
t := c.Theme()
box := ui.Box(c).Padding(2, 6).Radius(4).Focusable()
shown := ui.Local(box, "shown", func() bool { return false })
if box.Clicked() {
*shown = !*shown
}
if *shown {
box.Background(t.Surface)
} else {
box.Background(t.Text) // the color of the text, hiding it
}
box.Children(func() { ui.Text(c, text) })
}Stopping what a widget started
What Local's init makes is the element's. When a frame no longer
builds the element, its state goes, and a value that is an io.Closer
(T or *T has Close() error) is closed, as the web aborts an
AbortController when an element is removed. A widget that starts work
in the background, a streaming request, a ticker or a subscription,
stops it in Close:
type stream struct{ cancel context.CancelFunc }
func (s *stream) Close() error { s.cancel(); return nil }
// Feed shows the lines read from url while it is in the view.
func Feed(c *ui.Context, win *mygo.Window, app *App, url string) {
e := ui.Column(c)
ui.Local(e, "stream", func() *stream {
ctx, cancel := context.WithCancel(context.Background())
go read(ctx, url, func(line string) {
win.Update(func() { app.lines = append(app.lines, line) })
})
return &stream{cancel}
})
e.Children(func() {
for _, line := range app.lines {
ui.Text(c, line)
}
})
}Closeruns once, on the main thread, after the frame that no longer built the element; an element's lastLocalmade closes first, as deferred calls run. The window waits for it: cancel the work and return, rather than wait for the work to stop.Closemay change the app's state, on the main thread; ask for the frame that shows it withWindow.Invalidate, orInvalidateof thec.Services()the widget kept.- An element leaving with an exit
transition closes its values, and its
children's, once the exit ended, as its copy is gone: the copy paints
what the element had until then. Built again before that, the element
starts anew, with
init, and the values of the copy close. - A list builds only the rows in view: a row scrolled out of
view closes its values, and makes them anew with
initas it comes back. Keep what outlives a row in the app's state, by its item. - The pages a router keeps in its history keep their elements' values, which close as it lets the pages go.
- Every element's values close as its window closes, and so as the app quits.
- A
Closethat panics panics out of the frame, as the view would; the values left close after the next frame. - A nil value is not closed, as a
*streamthatinitleft nil to start later, andClose's error is ignored. - A value the element must not close, as one the app shares, belongs in
the app's state rather than in a
Local. - A widget that only needs to run a function as its element goes, as
cancel, gives it to the element'sOnUnmount, which runs it asClosewould, without a type of its own (state of an element).
A widget whose input changes what it edits reports it with SetChanged
on its element, as it builds: Changed then returns true for it, as for
the widgets' own. The rich text editor does so.
Give a widget a role, a name and its
states for assistive technology, take the keys
it needs with Shortcut on its element, or every key as it comes with
HandleInput, and paint what elements
do not with Draw. Overlays of your own, as drawers, hover
cards and menus, are placed with AttachTo and close with
PressedOutside and OverlayShortcut; Modal keeps the focus in one:
see overlays.