toolkit

package module
v0.22.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 10, 2026 License: BSD-3-Clause Imports: 5 Imported by: 0

README

go-widgets/toolkit

CI release pkg.go.dev coverage go license

Pure-Go widget toolkit that renders into an RGBA byte buffer. Zero JS / DOM / canvas dependency — every widget composes pixels into a caller-supplied []byte, so the toolkit runs identically in GOOS=js GOARCH=wasm (browser SharedArrayBuffer clients), on any native target Go ships (native canvas backends, image files), or in headless tests (screenshot-hash regressions).

Goals

  • One toolkit per app: consumers stop reinventing buttons, scrollbars, text fields. They import github.com/go-widgets/toolkit and compose.
  • Coherent theming: a single Theme value cascades through every widget — change a colour at the top, see it everywhere. LoadGTKTheme(css) parses libadwaita / GTK3 @define-color declarations into a Theme, so any GTK-desktop palette (Adwaita, Juno, WhiteSur, Solarized …) drives the widget ink colours.
  • Pure Go + CGO=0: no C toolchain, no shared libraries. Builds for every Go target including GOOS=js GOARCH=wasm.
  • A11y bridge: core widgets implement an opt-in Accessible interface (A11y() A11yInfo) so a host can CollectA11y and publish them to screen readers without rewriting every widget.

Non-goals

  • Not a CodeMirror replacement. Complex web-grade editors are best embedded via an iframe overlay at the host level.
  • Not GTK. No CSS engine, no SVG renderer, no full BiDi/IME. A broad primitive set — buttons, inputs, containers, feedback, overlays, structural rows, semantic banners, data displays — around 8 kLoC of widget code with double that in tests.

Status

v0.9 — GTK 4 / DaisyUI parity pass. Three additive waves closed the coverage gap versus libadwaita and DaisyUI 4: v0.7 shipped 9 core widgets (Switch, Badge, Kbd, Alert, Card, Breadcrumbs, Steps, HeaderBar, Table), v0.8 shipped 12 overlays and structural rows (Avatar, Skeleton, Rating, Toast, Banner, Popover, ActionRow, ViewSwitcher, ChatBubble, SearchEntry, Diff, Pagination), v0.9 shipped 8 finishing widgets (SplitButton, IconButton, Stat, Timeline, DropZone, Chip, FormField, ProgressCircle).

62 widgets + 10 stock icons, ~8k LoC of widget code, 100% statement coverage. Pure Go, no CGO, stdlib only. Builds for GOOS=js GOARCH=wasm and every native target Go ships.

Family Widgets
Base Widget, Base, Rect, Event (+ IME composition), Theme, RGBA
Text bitmap 5x7 font (60+ glyphs), DrawText, TextWidth, Label
Action Button, ToggleButton, CheckButton, RadioButton + Group
Input Entry, TextView + Selection + IME preview, SpinButton, Scale, RangeSlider
Selection ListBox, TreeView, DropDown
Layout HBox, VBox, Grid, Frame, Stack, Overlay, Paned, Expander
Tabs Notebook
Scroll ScrollView
Feedback ProgressBar, LevelBar, Spinner, Image, Tooltip, Notification
Navigation Menu + MenuItem.Shortcut, MenuBar + Alt+letter, ContextMenu, Dialog, MessageDialog
Bars Toolbar, Statusbar, 10 stock DrawIcon* helpers
Composite FileChooser, ColorChooser, FontChooser, Calendar, DatePicker, MarkdownView
Charts LineChart, BarChart, PieChart
Theming LoadGTKTheme(css) (GTK3 + libadwaita @define-color → Theme)
v0.6 breaking change

Widget.Draw signature moved from

Draw(surface []byte, surfaceW int, theme *Theme)

to

Draw(p painter.Painter, theme *Theme)

where painter.Painter is a 5-primitive interface (FillRect, StrokeRect, PutPixel, Text, Size). Existing callers that had a []byte + width migrate one line:

// before v0.6:
wg.Draw(surface, w, theme)

// v0.6+:
p := painter.NewPixelPainter(surface, w, h)
wg.Draw(p, theme)

The []byte is still writable + owned by the caller — the PixelPainter just wraps it so the primitives translate to writes. All other widget APIs (Bounds, SetBounds, HitTest, OnEvent, NewButton, …) are unchanged.

toolkit.Rect + toolkit.RGBA became type aliases of painter.Rect + painter.RGBA, so cross-package assignments work transparently.

The 10 DrawIcon* helpers also lost their (surface, surfaceW) prefix; new signature is DrawIconX(p painter.Painter, r Rect, ink RGBA).

Earlier releases
  • v0.9 — 8 widgets: SplitButton (button + attached dropdown arrow), IconButton (toolbar-icon variant), Stat (KPI card with trend indicator), Timeline (vertical event log), DropZone (dashed file drop target), Chip (removable tag), FormField (Label + Child + Help/Error), ProgressCircle (approximated circular progress).
  • v0.8 — 12 widgets: Avatar, Skeleton (Text/Avatar/Block kinds), Rating, Toast (transient bottom-of-screen), Banner (persistent full-width), Popover (Visible container for a Child), ActionRow (libadwaita Title/Subtitle/Prefix/Suffix), ViewSwitcher (segmented tab picker), ChatBubble (User/Other bubble), SearchEntry (Entry with prefix + clear), Diff (Context/Added/Removed colored lines), Pagination (prev/numbers/next with disabled ink).
  • v0.7 — 9 widgets: Switch (iOS-style toggle distinct from ToggleButton), Badge (auto-sizing pill), Kbd (keyboard-shortcut chip), Alert (Info/Success/Warning/Error semantic banner), Card (Title/Body/Footer three-zone), Breadcrumbs (chevron path), Steps (numbered indicator with connector), HeaderBar (Start/Title/ Subtitle/End GTK CSD), Table (Columns/Rows/Selected data grid).
  • v0.6 — Painter back-end abstraction. Every widget's Draw now takes a painter.Painter instead of a fixed []byte + stride pair, so the same widget code renders into a pixel buffer (WUI browser canvas, GUI native window, image file), a terminal cell grid (TUI), or an SVG stream.
  • v0.5 — Toolbar / Statusbar / FileChooser / ColorChooser / Calendar, Selection on TextView, LoadGTKTheme(css), 10 stock icon helpers.
  • v0.4 — 34 widgets. Toolbar / Statusbar, FileChooser / ColorChooser / Calendar, Selection model on TextView, LoadGTKTheme(css).
  • v0.3 — 28 widgets. TextView (multi-line editor), Menu/MenuBar, Dialog/MessageDialog, Tooltip, DropDown, TreeView.
  • v0.2 — 22 widgets. Layout containers (HBox/VBox/Grid/Frame), scroll (ScrollView/ListBox), input (Entry/Check/Radio/Toggle), structural (Stack/Notebook/Paned/Expander), feedback (ProgressBar/LevelBar/Scale/SpinButton/Image/Spinner), bitmap font.
  • v0.1 — scaffolding. Widget interface, Theme value, Button + Label, primitive event dispatch.
Next (v1.0 sketch)

Widget coverage is now materially complete versus GTK 4 + DaisyUI 4. The remaining pre-1.0 work is around the edges of the widget model, not the widget catalogue:

  • Font family plumbingdone (v0.20): a Font interface (Advance, Height, Draw) + a scalable built-in bitmap font. SetFont(NewBitmapFont(2)) doubles all text ("retina"), and every widget re-lays-out because metrics are now read at draw time. Breaking: GlyphHeight / GlyphAdvance and the metric-derived dimensions (CardHeaderH, DatePickerFieldH, DiffLineH, FormFieldLabelH, TimelineEventH, CardFooterH) are now functions — append () at call sites when upgrading.
  • First-class drag-and-drop event kindsdone (v0.16): EventDragStart / EventDragMove / EventDragLeave / EventDrop, plus a DragSource / DropTarget interface pair and SplitDropPayload / JoinDropPayload for multi-item payloads. DropZone dropped its synthetic-EventChar seam and now drives its hover cue + OnDrop from the formal lifecycle as a DropTarget.
  • Context menu helperdone (v0.17): ContextMenu wraps a Menu as a right-click popup — Popup(x, y) shows it at the cursor, it auto-sizes to its items, clamps itself inside the surface, and dismisses on outside-click.
  • Overlay layout containerdone (v0.18): Overlay stacks z-ordered Layers above a primary Content child, so Popover / Toast / Notification / Tooltip / ContextMenu float without hosts arranging z-order. Events route top-down; Modal makes a miss swallow (backdrop) instead of falling through.
  • A11y bridgedone (v0.19): an opt-in Accessible interface (A11y() A11yInfo — role + name + value) implemented by the core widgets (Button/Label/Entry/CheckButton/RadioButton/ Switch/Scale), plus CollectA11y([]Widget) so a host republishes them to the platform layer (WAI-ARIA on wasm, TTY metadata on tui).

Architecture

+----------------------+
| Theme                |  Palette + metrics + font ref
+----------------------+
           |
+----------------------+    +-------------------+
| Widget interface     |    | Event             |
|   Draw(rgba, theme)  |<---|   Kind: click/key |
|   HitTest(x, y)      |    |   X,Y / Code      |
|   OnEvent(ev)        |    +-------------------+
+----------------------+
           |
   +-------+-------+-------+-------+
   |               |       |       |
+--+---+        +--+--+  +-+--+  +-+--+
|Button|        |Label|  |HBox|  | ...|
+------+        +-----+  +----+  +----+

License

BSD 3-Clause. See LICENSE.

Documentation

Overview

Package toolkit provides a pure-Go widget set for wasmdesk native apps. Widgets render per-pixel into an RGBA byte buffer (the SAB backed framebuffer wasmbox clients write to) and dispatch input events received from the wasmbox compositor.

Design notes:

  • Every widget exposes the same three-method interface, so a container (HBox, VBox, ScrollView, ...) can hold any leaf.
  • Drawing is allocation-free in the steady state: the widget writes into a caller-owned RGBA slice + reads its theme by reference. Per-frame work is bounded by the widget's bbox.
  • Coordinates are integer pixels in the caller's surface space; the widget's Rect is its placement within that surface.
  • Events are pre-translated into widget-local (X, Y) before dispatch by the parent container (HBox/VBox/ScrollView do the hit-testing + offset adjustment).

Index

Examples

Constants

View Source
const (
	// ActionRowPadX is the horizontal inset for the title / subtitle
	// text from the row's left edge (or from the prefix slot when a
	// Prefix widget is present).
	ActionRowPadX = 12
	// ActionRowPadY is the vertical inset for the title above the
	// row's top edge; the subtitle flows below the title.
	ActionRowPadY = 8
	// ActionRowSubtitleGap is the extra vertical gap between the title
	// glyph row and the subtitle glyph row.
	ActionRowSubtitleGap = 2
	// ActionRowSlotW is the fixed width of the Prefix / Suffix slots.
	ActionRowSlotW = 32
)

Sizing constants. PadX / PadY inset text from the row edges; SubtitleGap is the vertical gap between the title's baseline and the subtitle's first row; SlotW is the width reserved for the optional Prefix / Suffix child widget slots.

View Source
const (
	AlertPadX = 12
	AlertPadY = 8
)

AlertPadX / AlertPadY set the internal margin between the banner edges and the text. Matches Notification's PadX/PadY so the two widgets read as siblings when they're used side-by-side.

View Source
const (
	BadgePadX = 4
	BadgePadY = 1
)

BadgePadX / BadgePadY are the horizontal and vertical insets between the pill body and the text glyphs. Small: a badge should read as a compact tag, not a button. Vertical padding is intentionally 1 so the pill stays short next to same-line body text.

View Source
const (
	BannerPadX       = 12
	BannerPadY       = 8
	BannerButtonPadX = 8
)

Banner sizing constants. BannerPadX/PadY are the internal margin between the strip edges and the text; BannerButtonPadX is the inner horizontal inset between the button label and its border box.

View Source
const (
	CalendarHeaderH = 22
	CalendarCellW   = 24
	CalendarCellH   = 18
)

Sizing.

View Source
const (
	// CardPadX is the horizontal inset for header / body / footer text.
	CardPadX = 8
	// CardPadY is the vertical inset for the body text above the first
	// line + between the footer text and its strip border.
	CardPadY = 6
	// CardLineSpacing is the extra vertical gap inserted between two
	// body lines so successive glyph rows don't touch.
	CardLineSpacing = 2
)

Card sizing constants. Header + Footer strips are the same size so the card reads as a symmetric frame; the body gets a matching inner pad on the left and top.

View Source
const (
	// ChatBubblePadX is the horizontal inset between the bubble edge
	// and the text.
	ChatBubblePadX = 10
	// ChatBubblePadY is the vertical inset above the first text line
	// and below the last.
	ChatBubblePadY = 6
	// ChatBubbleMaxW caps the bubble's rendered width in pixels so a
	// pathologically long line stays inside a reasonable column.
	ChatBubbleMaxW = 220
	// ChatBubbleLineSpacing is the extra vertical gap between two
	// text lines in a multi-line bubble.
	ChatBubbleLineSpacing = 2
)

Sizing constants for the bubble geometry.

View Source
const (
	ChipPadX     = 8
	ChipPadY     = 2
	ChipCloseW   = 12
	ChipCloseGap = 4
)

Chip sizing constants. PadX / PadY are the inner insets from the pill edge to the Text glyphs (kept small so a row of chips reads as compact tags); CloseW is the pixel width of the "x" click slot at the right edge; CloseGap is the pixel gap between the Text and the close slot when Closable is true.

View Source
const (
	ColorChooserChannelH    = 22
	ColorChooserPreviewH    = 36
	ColorChooserPadX        = 8
	ColorChooserChannelPadY = 4
)

Sizing.

View Source
const (
	DropZonePadX    = 12
	DropZonePadY    = 12
	DropZoneDashLen = 4
	DropZoneDashGap = 4
	DropZoneBorderW = 2
)

DropZone sizing constants. PadX / PadY are the outer insets from the bounds to where inner content (the prompt text) sits; DashLen + DashGap describe the stripe pattern of the dashed border, and BorderW is its per-edge pixel thickness. Kept generous so the dashed rectangle reads as a container, not a thin outline.

View Source
const (
	FileChooserTreeRatio    = 35 // % of width for the tree pane
	FileChooserButtonStripH = 32
	FileChooserPathH        = 24
)

Sizing constants.

View Source
const (
	// FontChooserPad is the panel's inner inset.
	FontChooserPad = 4
	// FontChooserRowPad is the vertical padding above+below each row's glyphs.
	FontChooserRowPad = 3
)

FontChooser sizing.

View Source
const (
	KbdPadX = 4
	KbdPadY = 2
)

KbdPadX / KbdPadY are the internal margin between the border box and the key text glyphs. Small values keep the chip compact enough to nest inside a menu row.

View Source
const (
	NotebookTabStripH = 24
	NotebookTabWidth  = 80
)

Geometry constants for the tab strip.

View Source
const (
	NotificationPadX = 12
	NotificationPadY = 8
	NotificationLife = 180
)

NotificationPadX / NotificationPadY / NotificationLife are the visual + timing defaults; a caller wanting shorter or louder toasts overrides them per-instance (via SetBounds + direct Life assignment).

View Source
const (
	PanedHorizontal = 0 // First left, Second right
	PanedVertical   = 1 // First top, Second bottom
)

Paned orientations.

View Source
const (
	PopoverPadX    = 8
	PopoverPadY    = 6
	PopoverBorderR = 1
)

Popover sizing constants. PopoverPadX / PopoverPadY are the inner margin between the Popover's outer edge and the child's frame; PopoverBorderR is the border stroke width, matching what strokeRect paints so a host that lays out around the popover can budget the right number of pixels for the frame.

View Source
const (
	// RatingStarW is the per-cell edge in pixels.
	RatingStarW = 14
	// RatingStarGap is the horizontal spacing between two successive
	// cells (pixels of surface visible between them).
	RatingStarGap = 2
)

Rating sizing constants. Cells are square so the strip reads as a row of tiles; the small gap keeps them visually distinct without eating layout width.

View Source
const (
	// SkeletonLineH is the pixel height of a single SkeletonText bar.
	SkeletonLineH = 10
	// SkeletonLineGap is the vertical gap between two SkeletonText bars.
	SkeletonLineGap = 6
	// SkeletonLinePad is the inset applied to SkeletonBlock so the fill
	// stops shy of the Bounds edge — matches Card's body pad.
	SkeletonLinePad = 4
)

Skeleton sizing constants. Values chosen to line up with the toolkit's GlyphHeight() so a SkeletonText row visually replaces a row of body text without shifting the surrounding layout.

View Source
const (
	// StatPadX is the horizontal inset between the border and the
	// left edge of the Title / Value / Change text.
	StatPadX = 12
	// StatPadY is the vertical inset between the top border and the
	// first row of Title text (and between the last row of Change
	// text and the bottom border).
	StatPadY = 8
	// StatTitleGap is the vertical space inserted between the Title
	// row's bottom and the Value row's top.
	StatTitleGap = 4
	// StatValueGap is the vertical space inserted between the Value
	// row's bottom and the Change row's top.
	StatValueGap = 4
)

Stat sizing constants. Padding matches Alert (12, 8) so a Stat composes cleanly next to an Alert banner; the two gap constants keep the three text rows readable at 5x7 glyphs without the tall vertical footprint of a full Card.

View Source
const (
	StatusbarH           = 18
	StatusbarSegmentMinW = 80
	StatusbarPadX        = 6
)

Sizing constants.

View Source
const (
	// StepBoxW is the pixel width of each badge.
	StepBoxW = 16
	// StepBoxH is the pixel height of each badge.
	StepBoxH = 16
	// StepConnectorW is the horizontal length of the connector line
	// between two badges.
	StepConnectorW = 20
	// StepLabelGap is the vertical gap between a badge's bottom edge
	// and the caption text below it.
	StepLabelGap = 3
)

Steps sizing constants. Chosen so the badges + connectors fit inside a 40-px-tall bar (a common toolbar strip height).

View Source
const (
	// TimelineMarkerW is the reserved horizontal column width for
	// the rail + marker before the event's text begins.
	TimelineMarkerW = 12
	// TimelineMarkerSize is the pixel side of each event's filled
	// square marker painted on the rail.
	TimelineMarkerSize = 6
	// TimelineDetailGap is the vertical space inserted between an
	// event's Title row and its Detail row when Detail != "".
	TimelineDetailGap = 2
	// TimelinePadX is the horizontal inset between the widget's
	// left edge and the rail's marker column.
	TimelinePadX = 8
	// TimelinePadY is the vertical inset between the widget's top
	// edge and the first event row (and between the last event row
	// and the bottom edge).
	TimelinePadY = 8
)

Timeline sizing constants. Marker column is 12 px wide, the marker itself 6 px so it sits centred on the rail with a 3-px gutter either side; event rows are one glyph plus a 4-px vertical spacer so successive titles don't touch, and Detail rows sit 2 px below their Title with a matching glyph height.

View Source
const (
	ToastPadX = 10
	ToastPadY = 6
)

ToastPadX / ToastPadY are the internal margin between the pill edges and the text. Slightly tighter than Notification's 12/8 so several stacked pills read as a compact column.

View Source
const (
	ToolbarButtonW = 24
	ToolbarButtonH = 24
	ToolbarSepW    = 8
)

Sizing constants. Square buttons read as a true icon-toolbar (vs the MenuBar's wider text cells).

View Source
const (
	TooltipPadX = 8
	TooltipPadY = 4
)

TooltipPadX / TooltipPadY are the inner text-padding constants.

View Source
const (
	// ViewSwitcherH is the default vertical extent in pixels.
	ViewSwitcherH = 32
	// ViewSwitcherPadX is the horizontal padding at the strip's left
	// and right edges. Reserved for future asymmetric layouts; the
	// current segment layout divides the full width evenly.
	ViewSwitcherPadX = 12
)

Sizing constants for the strip's default vertical extent and its horizontal end padding. ViewSwitcher's Draw does not require Bounds.H == ViewSwitcherH; the constant is exposed so callers building a HeaderBar-like layout can allocate a matching strip.

View Source
const AvatarSize = 32

AvatarSize is the default square dimension in pixels when Bounds() is zero-sized. Matches the 32-px avatar most GTK / Material chat rows use so an Avatar drops naturally next to a Label without extra layout.

View Source
const BarGutter = 1

BarGutter is the horizontal gap (painter units) between adjacent bars.

View Source
const BreadcrumbGap = 4

BreadcrumbGap is the horizontal pixel gap inserted on either side of the separator glyph so the chevron doesn't touch the segment ink.

View Source
const BreadcrumbSep = ">"

BreadcrumbSep is the character(s) drawn between two segments. Kept as a package constant so a caller who wants "/" or "»" replaces one symbol without touching Draw.

View Source
const ChartPad = 6

ChartPad is the margin (painter units) reserved for the axes on the left and bottom edges of a chart's plot area.

View Source
const ContextMenuMinW = 96

ContextMenuMinW is the floor on a context menu's width so a menu of very short labels still reads as a panel.

View Source
const DialogButtonStripH = 32

DialogButtonStripH is the pixel height of the bottom action strip.

View Source
const DialogButtonW = 90

DialogButtonW is the width allocated per action button.

View Source
const DialogTitleH = 28

DialogTitleH is the pixel height of the title bar.

View Source
const DiffPadX = 4

DiffPadX is the horizontal padding between the widget's outer border and the leading prefix glyph.

View Source
const DiffPadY = 2

DiffPadY is the vertical padding above the first line and below the last line.

View Source
const DropPayloadSep = "\n"

DropPayloadSep separates individual items within a multi-item drag payload.

View Source
const ExpanderHeaderH = 24

ExpanderHeaderH is the pixel height of the clickable header row.

View Source
const FormFieldChildGap = 4

FormFieldChildGap is the vertical gap in pixels between the bottom of the label row and the top of the composed Child widget.

View Source
const FormFieldHelpGap = 2

FormFieldHelpGap is the vertical gap in pixels between the bottom of the Child widget and the top of the help / error caption row.

View Source
const FormFieldPadX = 0

FormFieldPadX is the horizontal padding applied on both sides of the FormField body. Kept at 0 by default: a form is expected to live inside a container (VBox, Card, ...) that supplies its own margin. Callers that need extra breathing room can wrap the field in a Card.

View Source
const FormFieldPadY = 4

FormFieldPadY is the vertical padding applied at the top + bottom of the FormField body. Small: keeps a stack of fields compact without having every caller compute inter-field spacing.

View Source
const HeaderBarHeight = 40

HeaderBarHeight is the default vertical extent of a HeaderBar in pixels. HeaderBar's Draw code assumes Bounds.H == HeaderBarHeight but scales cleanly for taller / shorter bars: children are inset by HeaderBarPad/2 top+bottom and the title / subtitle are centred in whatever remains.

View Source
const HeaderBarPad = 8

HeaderBarPad is the horizontal padding at the bar's left + right edges (space between the bar's edge and the first Start / End child). Also drives the vertical inset around child widgets: children get Bounds.Y = bar.Y + HeaderBarPad/2 and Bounds.H = bar.H - HeaderBarPad, matching GTK's typical inner spacing.

View Source
const HeaderBarSubtitleGap = 2

HeaderBarSubtitleGap is the vertical gap (in pixels) between the title's last row and the subtitle's first row in the two-line layout. Kept as a package constant so the two-line block height stays predictable across themes + font sizes.

View Source
const IconButtonSize = 28

IconButtonSize is the default square dimension in pixels when Bounds() is zero-sized. Matches the 28-px toolbar icon buttons GTK / Aqua headers use so an IconButton drops naturally next to a Label or a Button without extra layout maths.

View Source
const MenuBarH = 22

MenuBarH is the pixel height of the bar strip.

View Source
const MenuBarItemPadX = 8

MenuBarItemPadX is the horizontal padding around a top-level name when its natural width exceeds MenuBarItemW — i.e. the extra breathing room beyond the raw glyph run.

View Source
const MenuBarItemW = 60

MenuBarItemW is the DEFAULT (minimum) pixel width allocated per top-level name. Names whose TextWidth exceeds this bound scale up (with 2×MenuBarItemPadX horizontal padding on each side); shorter names take exactly this width so the bar looks stable across varying label lengths.

View Source
const MenuRowH = 22

MenuRowH is the pixel height of a menu row.

View Source
const MenuSeparatorH = 6

MenuSeparatorH is the height of a separator row.

View Source
const PaginationBtnH = 24

PaginationBtnH is the pixel height of each button.

View Source
const PaginationBtnW = 28

PaginationBtnW is the pixel width of each button (prev, next, and every page number).

View Source
const PaginationGap = 2

PaginationGap is the horizontal pixel gap between successive buttons.

View Source
const PanedHandleW = 6

PanedHandleW is the pixel thickness of the splitter handle.

View Source
const PopoverMaxRows = 12

PopoverMaxRows caps the dropdown popover height; longer option lists can wrap in a ScrollView the caller supplies.

View Source
const ProgressCircleSize = 40

ProgressCircleSize is the default side-length in pixels of a ProgressCircle rendered with a zero-sized Bounds. Roughly matches the "large" circular-progress indicator in Material / Adwaita dashboards; small enough to sit next to a status label yet big enough for the "XX%" caption to read on the toolkit's 5x7 font.

View Source
const ProgressCircleStroke = 4

ProgressCircleStroke is the ring thickness in pixels: the offset between the outer track square and the inner "hole" that carries the percentage caption. A thicker stroke leaves less room for the text; 4px keeps a two-digit percentage centred inside the ring with pixels to spare on either side.

View Source
const SearchEntryIconW = 16

SearchEntryIconW is the pixel width reserved for the leading prefix glyph and the trailing clear affordance. Both slots share the same width so hit-testing stays symmetric.

View Source
const SearchEntryPadX = 4

SearchEntryPadX is the horizontal padding between the widget's outer border and the inner content (the search prefix, the text field, the clear affordance).

View Source
const SplitButtonArrowW = 20

SplitButtonArrowW is the pixel width of the arrow slot on the right edge when Arrow is true. Sized to comfortably fit the 5x7 arrow glyph plus symmetric padding on either side.

View Source
const SplitButtonPadX = 12

SplitButtonPadX is the horizontal padding a caller should reserve on either side of the label when positioning a sibling widget flush with the main slot's inner edge. The label itself is rendered centred; this constant is exported for external layout code that wants to align against the same inset.

View Source
const TableCellPadX = 4

TableCellPadX is the left/right pixel padding applied inside every header + body cell before its text lands.

View Source
const TableHeaderHeight = 24

TableHeaderHeight is the pixel height of the header row.

View Source
const TableRowHeight = 22

TableRowHeight is the pixel height of one body row.

View Source
const TreeChevronW = 14

TreeChevronW is the pixel column the chevron lives in.

View Source
const TreeIndentW = 16

TreeIndentW is the per-depth pixel indent.

Variables

This section is empty.

Functions

func CardFooterH added in v0.7.0

func CardFooterH() int

CardFooterH is the height of the footer strip when Footer != "".

func CardHeaderH added in v0.7.0

func CardHeaderH() int

CardHeaderH is the height of the header strip when Title != "" (a function, as it derives from the active font's GlyphHeight).

func DatePickerFieldH added in v0.11.0

func DatePickerFieldH() int

DatePickerFieldH is the pixel height of the closed field.

func DaysInMonth

func DaysInMonth(year, month int) int

DaysInMonth returns the day count for (year, month).

func DeleteSelection

func DeleteSelection(lines []string, sel Selection) []string

DeleteSelection removes the selected range from lines + returns the new lines slice. The result always has at least one line (an empty line at minimum).

func DiffLineH added in v0.8.0

func DiffLineH() int

DiffLineH is the vertical stride between successive lines: one glyph tall plus two pixels of separation.

func DrawIconCopy

func DrawIconCopy(p painter.Painter, r Rect, ink RGBA)

DrawIconCopy paints two overlapping document outlines.

func DrawIconCut

func DrawIconCut(p painter.Painter, r Rect, ink RGBA)

DrawIconCut paints a pair-of-scissors icon (two open-circle handles + crossed blades).

func DrawIconNew

func DrawIconNew(p painter.Painter, r Rect, ink RGBA)

DrawIconNew paints a document-outline icon (rectangle with a folded top-right corner).

func DrawIconOpen

func DrawIconOpen(p painter.Painter, r Rect, ink RGBA)

DrawIconOpen paints a folder-outline icon (rectangle with a small tab on the top-left).

func DrawIconPaste

func DrawIconPaste(p painter.Painter, r Rect, ink RGBA)

DrawIconPaste paints a clipboard outline with a clip on top.

func DrawIconRedo

func DrawIconRedo(p painter.Painter, r Rect, ink RGBA)

DrawIconRedo paints a curved arrow pointing right (mirror of Undo).

func DrawIconSave

func DrawIconSave(p painter.Painter, r Rect, ink RGBA)

DrawIconSave paints a floppy-disk-outline icon (outer square with a small label rectangle on top).

func DrawIconSearch

func DrawIconSearch(p painter.Painter, r Rect, ink RGBA)

DrawIconSearch paints a magnifying-glass icon (a circle + a diagonal handle).

func DrawIconSettings

func DrawIconSettings(p painter.Painter, r Rect, ink RGBA)

DrawIconSettings paints a gear-outline icon (approximated as a square with corner "teeth").

func DrawIconUndo

func DrawIconUndo(p painter.Painter, r Rect, ink RGBA)

DrawIconUndo paints a curved arrow pointing left (approximated as a horizontal stroke + a triangular head).

func DrawText

func DrawText(p painter.Painter, x, y int, text string, ink RGBA)

DrawText paints text left-to-right starting at (x, y) in widget-local coordinates, using the active font (see SetFont). It is a thin wrapper over the active Font's Draw so every widget's text rendering follows a font swap.

func FormFieldLabelH added in v0.9.0

func FormFieldLabelH() int

FormFieldLabelH is the height in pixels of the label row drawn at the top of a FormField. One glyph row plus 2px of breathing space keeps the label snug against the input beneath it without touching the glyph's descender pixels.

func GlyphAdvance

func GlyphAdvance() int

GlyphAdvance is the active font's horizontal step from one glyph to the next.

func GlyphHeight

func GlyphHeight() int

GlyphHeight is the active font's glyph box height. It is a function (not a const) so widgets re-read it after SetFont; layout dimensions that derive from it are likewise functions.

func JoinDropPayload added in v0.16.0

func JoinDropPayload(items []string) string

JoinDropPayload builds a multi-item payload string from items, the inverse of SplitDropPayload — a host uses it to package several dragged paths into one Event.Code.

func SelectionText

func SelectionText(lines []string, sel Selection) string

SelectionText returns the substring covered by sel in lines (a TextView's Lines slice). Empty selection returns "".

func SetFont added in v0.20.0

func SetFont(f Font)

SetFont makes f the active font. A nil f restores the built-in default. All subsequent layout (GlyphHeight / GlyphAdvance) and DrawText use it.

Example

ExampleSetFont swaps the active font; every widget re-lays-out against the new metrics. NewBitmapFont(2) doubles the built-in bitmap ("retina" text).

package main

import (
	"fmt"

	"github.com/go-widgets/toolkit"
)

func main() {
	fmt.Println(toolkit.GlyphHeight()) // default 5×7 bitmap
	toolkit.SetFont(toolkit.NewBitmapFont(2))
	fmt.Println(toolkit.GlyphHeight()) // doubled
	toolkit.SetFont(nil)               // restore the default
	fmt.Println(toolkit.GlyphHeight())
}
Output:
7
14
7

func SplitDropPayload added in v0.16.0

func SplitDropPayload(code string) []string

SplitDropPayload splits a drop payload (as carried in Event.Code) into its individual items, dropping empty entries so a trailing separator or an empty payload yields no phantom items.

func TextWidth

func TextWidth(text string) int

TextWidth returns the pixel width that DrawText would occupy if it rendered text in the active font. Every character (known or unknown) consumes one GlyphAdvance slot so callers can pre-size text containers from len(text) alone.

func TimelineEventH added in v0.9.0

func TimelineEventH() int

TimelineEventH is the vertical stride from one event's Title row to the next when the event has NO Detail — one glyph row plus 4 px of inter-event spacing. A function, as it derives from the active font's GlyphHeight.

func WeekdayOfFirst

func WeekdayOfFirst(year, month int) int

WeekdayOfFirst returns the weekday-index (0=Mon..6=Sun) of the first day of (year, month). Uses Zeller-ish congruence so we don't depend on time.Time.

Types

type A11yInfo added in v0.19.0

type A11yInfo struct {
	Role  Role
	Name  string
	Value string
}

A11yInfo is a widget's accessibility description. Name is the accessible name (its label/caption); Value is the current value where meaningful (a textbox's text, a checkbox's "checked"/"" state, a slider's number).

func CollectA11y added in v0.19.0

func CollectA11y(widgets []Widget) []A11yInfo

CollectA11y returns the A11yInfo for every widget in the slice that implements Accessible, preserving order. A host owns its widget tree, so it passes the flat list it composed; widgets that don't implement Accessible are skipped.

Example

ExampleCollectA11y walks a host-composed widget list and returns each Accessible widget's role, name, and value for a screen-reader bridge.

package main

import (
	"fmt"

	"github.com/go-widgets/toolkit"
)

func main() {
	widgets := []toolkit.Widget{
		toolkit.NewButton("Save", nil),
		toolkit.NewCheckButton("Wrap", true),
	}
	for _, info := range toolkit.CollectA11y(widgets) {
		fmt.Printf("%s %q %q\n", info.Role, info.Name, info.Value)
	}
}
Output:
button "Save" ""
checkbox "Wrap" "checked"

type Accessible added in v0.19.0

type Accessible interface {
	Widget
	A11y() A11yInfo
}

Accessible is implemented by widgets that expose accessibility metadata.

type ActionRow added in v0.8.0

type ActionRow struct {
	Base
	Title    string
	Subtitle string
	Prefix   Widget // optional left slot; nil = no prefix drawn
	Suffix   Widget // optional right slot; nil = no suffix drawn
}

ActionRow is a libadwaita-style structured list row: a large Title with an optional dim Subtitle, plus optional Prefix and Suffix widget slots on the left and right edges. Composes into a settings- style list: stack several ActionRows in a VBox and each row reads as one entry.

The row paints a Theme.Surface body with a 1-pixel Theme.Border divider along its bottom edge (the classic GTK list-row separator). Prefix / Suffix widget slots are fixed-width strips at the left and right; the Title (and, when non-empty, the Subtitle) flows in the remaining central column.

ActionRow forwards EventClick events to whichever child (Prefix or Suffix) the click's X coordinate lands on. Clicks in the central text region are ignored — a caller wanting an activatable row wraps the ActionRow's Bounds in a container that intercepts clicks or overlays a button in the Suffix slot.

func NewActionRow added in v0.8.0

func NewActionRow(title string) *ActionRow

NewActionRow constructs an ActionRow with the given title. Subtitle starts empty; Prefix and Suffix start nil. The caller may assign them before the first Draw.

func (*ActionRow) Draw added in v0.8.0

func (a *ActionRow) Draw(p painter.Painter, theme *Theme)

Draw paints the row body + bottom divider, then positions + draws the optional Prefix / Suffix child widgets, then paints the title (and, when non-empty, the subtitle) in the remaining central column. Positioning side effect: Prefix / Suffix widgets have their Bounds updated to reflect their slot rectangle inside the row.

func (*ActionRow) OnEvent added in v0.8.0

func (a *ActionRow) OnEvent(ev Event)

OnEvent forwards EventClick to whichever Prefix / Suffix slot the click's X coordinate lands on, translating X into the child's widget-local space. Clicks in the central text region — or clicks on a slot whose child is nil — are ignored. Non-click events are dropped so keyboard input intended for a focused inner widget is not misrouted; a caller that needs richer keyboard routing wraps the ActionRow in its own dispatcher.

type Alert added in v0.7.0

type Alert struct {
	Base
	Text string
	Kind AlertKind
}

Alert is a persistent banner sitting at the top or bottom of a view, carrying a Text message coloured by Kind. Shares Notification's filled-panel-with-border shape but differs in three ways:

  1. No Life field: an Alert stays on screen until the host removes it (the "you are offline" banner). No Tick(), no auto-hide.
  2. No Visible toggle: an Alert that exists is drawn. To hide an alert the host stops rendering it (or drops it from the tree).
  3. Coloured by Kind: Notification is always Accent; Alert varies colour by severity so success + error read differently at a glance.

The banner is not interactive; the parent view supplies a dismiss button as a separate Button if the design calls for one.

func NewAlert added in v0.7.0

func NewAlert(text string, kind AlertKind) *Alert

NewAlert constructs an Alert with the given Text + Kind. Bounds are zero-initialised; the host is responsible for positioning + sizing the banner (typically full-width across the top of the parent view).

func (*Alert) Draw added in v0.7.0

func (a *Alert) Draw(p painter.Painter, theme *Theme)

Draw paints the filled panel + border + text. The ink is Theme.Background so it stays legible against every Kind's face — the same inversion trick Notification uses against its own Accent panel.

type AlertKind added in v0.7.0

type AlertKind int

AlertKind selects the semantic colour of an Alert banner. Info reuses the theme's Accent (the same blue used by focus rings + link text); the other three carry hard-coded shades tuned for meaning — green for success, amber for warning, red for error — because the theme palette doesn't carry semantic slots and adding them would blow up the Theme surface for every app.

const (
	// AlertInfo is a neutral heads-up ("Backup started"). Rendered in
	// Theme.Accent so it matches the app's own accent colour.
	AlertInfo AlertKind = iota
	// AlertSuccess signals a completed operation ("Saved!"). Green.
	AlertSuccess
	// AlertWarning flags a non-fatal issue ("Battery low"). Amber.
	AlertWarning
	// AlertError signals a failure the user must address ("Sync
	// failed"). Red.
	AlertError
)

type Avatar added in v0.8.0

type Avatar struct {
	Base
	Initials string
	// Color is the body fill. Leave at the zero RGBA to fall through to
	// Theme.Accent (the theme-tracking default); set to any opaque RGBA
	// to pin the avatar to a per-user tint.
	Color RGBA
}

Avatar renders a user identity chip: a rounded-square body filled in a solid colour with the user's Initials centred inside it in the accent-inverted ink. Colour resolves to Theme.Accent when the caller-supplied Color is the zero RGBA (a natural default that respects the theme); a caller wanting a per-user tint sets Color to any opaque RGBA and it will be honoured verbatim.

The rounded shape is faked by clipping the four corner pixels — the same three-band recipe Badge uses to look like a pill without touching a curve primitive. This keeps Avatar allocation-free and portable to every Painter back-end (PixelPainter, CellPainter, SvgPainter).

Auto-sizing: if Bounds().W is zero the first Draw() resizes the avatar to AvatarSize x AvatarSize (or AvatarSize x preserved-H when H is non-zero). A pre-sized Bounds is honoured verbatim so a fixed layout column doesn't shift when the widget is dropped in.

Avatar is passive: it displays and does not respond to input. The parent view is responsible for positioning it (typically top-left of a message row or the leading edge of a menu item).

func NewAvatar added in v0.8.0

func NewAvatar(initials string) *Avatar

NewAvatar constructs an Avatar carrying the given initials. Bounds default to zero so the first Draw() auto-sizes the widget to AvatarSize x AvatarSize. Color defaults to the zero RGBA so the body tracks Theme.Accent unless the caller pins it.

func (*Avatar) Draw added in v0.8.0

func (a *Avatar) Draw(p painter.Painter, theme *Theme)

Draw paints the rounded-square body then centres Initials on top. If Bounds().W is zero the widget resizes itself to AvatarSize x AvatarSize (H preserved when already non-zero) before painting.

Body colour is Color when non-zero, otherwise Theme.Accent. Ink is accentInk(theme) so a GTK-loaded theme's OnAccent override is honoured with a fall-through to Theme.Background — the same rule Table + Button use for their accent-face branches.

type Badge added in v0.7.0

type Badge struct {
	Base
	Text string
}

Badge is a small pill-shaped counter or indicator — the "12" that hangs off an inbox icon, the "NEW" beside a menu item. Renders Text inside a rounded-pill body filled in Theme.Accent with the ink in Theme.Background for contrast.

A Badge is passive: it displays a value + does not respond to input. The parent widget (button, menu item, ...) is responsible for positioning it in the top-right corner or wherever the design puts it.

Auto-sizing: if the caller sets Bounds().W to 0, the first Draw() resizes the Bounds to the text width plus BadgePadX on each side (plus GlyphHeight() + BadgePadY on each side vertically if H is also 0). This spares the caller from having to compute glyph widths just to paint a two-digit counter. A pre-sized Bounds is honoured verbatim so a fixed-width layout column doesn't shift when the digit count changes.

func NewBadge added in v0.7.0

func NewBadge(text string) *Badge

NewBadge constructs a Badge with the given text. Bounds default to zero so the first Draw() auto-sizes the pill to the text.

func (*Badge) Draw added in v0.7.0

func (b *Badge) Draw(p painter.Painter, theme *Theme)

Draw paints the pill body + centred text. If Bounds().W is zero the widget resizes itself to fit its Text (and Bounds().H is filled in too if it was zero) before painting; a pre-sized Bounds is preserved.

The pill shape is approximated by clipping the four corner pixels: the body fills the full rectangle minus a one-pixel bite off each corner, which reads as "rounded" against the low-resolution 5x7 glyph aesthetic without touching the painter's curve primitives.

type Banner struct {
	Base
	Text        string
	ButtonLabel string
	Revealed    bool
	OnAction    func()
}

Banner is a full-width persistent inline message strip, modelled on GTK 4's AdwBanner. Distinct from Alert (persistent, coloured by severity) in two ways:

  1. Banner is REVEAL-driven: Revealed toggles the whole strip on and off, letting the host wire dismiss and re-show without dropping the widget from the tree.
  2. Banner carries an optional right-aligned action button; a click inside the button fires OnAction. Alert has no interactive slot.

The banner paints in Theme.Accent so it reads as a system message rather than a semantic-severity Alert; the action button is drawn as a bordered box in the accent-inverted ink so it stays legible.

func NewBanner added in v0.8.0

func NewBanner(text string) *Banner

NewBanner constructs a Banner with the given Text. Revealed starts true so a freshly-constructed banner is visible; ButtonLabel is empty by default (no action slot rendered).

func (*Banner) Draw added in v0.8.0

func (b *Banner) Draw(p painter.Painter, theme *Theme)

Draw paints the accent-filled strip + the Text ink. When ButtonLabel is non-empty an outlined action button is drawn right-aligned inside BannerPadX of the trailing edge. Nothing drawn when !Revealed.

func (*Banner) OnEvent added in v0.8.0

func (b *Banner) OnEvent(ev Event)

OnEvent handles a click inside the action button. Events with a Kind other than EventClick are ignored; a click that falls outside the button rect is dropped; a click on a Banner without an action button (empty ButtonLabel) is dropped; a click with a nil OnAction is dropped silently -- the button is drawable but inert.

type BarChart added in v0.13.0

type BarChart struct {
	Base
	Values []float64
	Max    float64 // top of the Y axis; when <= 0, taken from the data
}

BarChart plots one series of non-negative Values as vertical bars over a left+bottom axis frame -- the categorical companion to LineChart. Bars share the plot width evenly with a 1-unit gutter between them and scale to the tallest value (or an explicit Max). Display-only.

It renders through painter.Painter, so the same chart draws as pixels (WUI/GUI) or promoted cells (TUI). An empty series draws just the axes.

Example

ExampleBarChart plots non-negative values as vertical bars.

package main

import (
	"github.com/go-widgets/painter"
	"github.com/go-widgets/toolkit"
)

// newSurface returns a PixelPainter over a fresh w×h RGBA buffer — the render
// target the examples draw into. A CellPainter would render the same widgets to
// a terminal grid instead.
func newSurface(w, h int) *painter.PixelPainter {
	return painter.NewPixelPainter(make([]byte, 4*w*h), w, h)
}

func main() {
	chart := toolkit.NewBarChart([]float64{4, 7, 2, 8, 5})
	chart.SetBounds(toolkit.Rect{X: 0, Y: 0, W: 200, H: 80})
	chart.Draw(newSurface(200, 80), toolkit.DefaultLight())
}

func NewBarChart added in v0.13.0

func NewBarChart(values []float64) *BarChart

NewBarChart builds a BarChart over the given values with an auto Y max.

func (*BarChart) Draw added in v0.13.0

func (c *BarChart) Draw(p painter.Painter, theme *Theme)

Draw paints the axis frame then one Accent bar per value.

type Base

type Base struct {
	// contains filtered or unexported fields
}

Base provides default Bounds/SetBounds/HitTest impls so a widget embedding it only has to implement Draw + OnEvent. Embedding is optional but convenient.

func (*Base) Bounds

func (b *Base) Bounds() Rect

func (*Base) Draw

func (b *Base) Draw(p painter.Painter, theme *Theme)

func (*Base) HitTest

func (b *Base) HitTest(px, py int) bool

func (*Base) OnEvent

func (b *Base) OnEvent(ev Event)

func (*Base) SetBounds

func (b *Base) SetBounds(r Rect)
type Breadcrumbs struct {
	Base
	Segments []string
}

Breadcrumbs is a horizontal navigation path — "Home > Docs > Reference" — rendered as a sequence of Segments separated by a chevron character. Segment text uses Theme.OnBackground; each chevron uses Theme.Border so it reads as a subtle divider rather than another clickable label.

The widget is passive display only: it computes per-segment X positions from TextWidth so a caller wanting hit-testing (e.g. clicking "Docs" to navigate up two levels) can walk the same offset table externally. HitTest / OnEvent stay as Base defaults.

func NewBreadcrumbs added in v0.7.0

func NewBreadcrumbs(segments []string) *Breadcrumbs

NewBreadcrumbs constructs a Breadcrumbs with the given segments. A nil or empty Segments slice renders as a no-op — Draw exits without painting anything.

func (b *Breadcrumbs) Draw(p painter.Painter, theme *Theme)

Draw paints each segment followed by a separator (except after the last one). Segments are vertically centred inside Bounds when Bounds.H exceeds GlyphHeight(), otherwise they anchor at Bounds.Y.

type Button

type Button struct {
	Base
	Label   string
	OnClick func()
	Style   ButtonStyle // resting appearance; default is ButtonDefault
	// contains filtered or unexported fields
}

Button is a clickable rectangle with a centred label. Paints a 1-pixel border in Theme.Border on a Theme.Surface body; hovered / pressed states cycle through SurfaceAlt + Accent so the user sees click feedback before the callback fires.

Wire a handler via OnClick; the button calls it from OnEvent when it receives an EventClick. Callers re-paint via Draw after any state mutation (the toolkit doesn't drive its own frame loop -- the wasmbox compositor's tick is the redraw trigger).

func NewButton

func NewButton(label string, onClick func()) *Button

NewButton constructs a Button with the given label + click handler. Handler may be nil (a no-op button is still rendered).

func (*Button) A11y added in v0.19.0

func (b *Button) A11y() A11yInfo

A11y reports the Button as a button role named by its label.

func (*Button) Draw

func (b *Button) Draw(p painter.Painter, theme *Theme)

Draw paints the button through p using theme's palette. Face cycles through Surface / SurfaceAlt (hovered) / Accent (pressed); the Label is centred in the body using the toolkit's 5x7 bitmap font. When the button is pressed the ink swaps to the theme's Background so the label stays legible against the Accent face.

func (*Button) OnEvent

func (b *Button) OnEvent(ev Event)

OnEvent dispatches click events to the OnClick callback. Other event kinds are ignored at this level (the parent container may have already pre-filtered).

func (*Button) SetHovered

func (b *Button) SetHovered(v bool)

SetHovered/SetPressed are wired by the parent container's mouse dispatcher so the button can render its hover/press visual states. Direct setters (vs deducing from OnEvent kinds) keep the parent in control of state propagation -- enter/leave events would duplicate the same logic in every leaf widget.

func (*Button) SetPressed

func (b *Button) SetPressed(v bool)

type ButtonStyle added in v0.10.0

type ButtonStyle int

ButtonStyle selects a button's resting fill, giving a layout visual hierarchy (macOS "prominent"/default/secondary buttons). Hover + press still override the fill on top of the style.

const (
	// ButtonDefault is a Surface-faced button (the plain look).
	ButtonDefault ButtonStyle = iota
	// ButtonProminent is filled with Accent + accent-foreground text -- the
	// primary/default action (e.g. a calculator's operator keys, "OK").
	ButtonProminent
	// ButtonSecondary is filled with SurfaceAlt -- a muted grey key that sits
	// between Default and Prominent (e.g. a calculator's C / +/- / % keys).
	ButtonSecondary
)

type Calendar

type Calendar struct {
	Base
	Year     int
	Month    int // 1..12
	Day      int // selected day in [1, daysInMonth]
	TodayY   int
	TodayM   int
	TodayD   int
	OnSelect func(y, m, d int)
}

Calendar renders a month grid (Mon..Sun columns, up to 6 rows) for a given (Year, Month). The currently-selected day is highlighted; click on a day-cell selects it + fires OnSelect with the absolute (Y, M, D) triple.

Calendar takes no time-source dep; the host must pass it the current year/month/day. A "today" pill can be drawn by setting Today (year/month/day); set to (0, 0, 0) to disable it.

func NewCalendar

func NewCalendar(year, month, day int) *Calendar

NewCalendar builds a Calendar for the given (year, month, day).

func (*Calendar) Draw

func (c *Calendar) Draw(p painter.Painter, theme *Theme)

Draw paints header (Y M) + weekday row + day grid.

func (*Calendar) OnEvent

func (c *Calendar) OnEvent(ev Event)

OnEvent dispatches a click on a day cell to OnSelect.

func (*Calendar) SetDate

func (c *Calendar) SetDate(year, month, day int)

SetDate moves the calendar to (year, month, day).

func (*Calendar) SetToday

func (c *Calendar) SetToday(y, m, d int)

SetToday records the "today" pill the calendar should highlight regardless of which (Y/M) is being viewed.

type Card added in v0.7.0

type Card struct {
	Base
	Title  string
	Body   string
	Footer string
}

Card is a bordered container laid out as three optional zones: a header strip at the top (title text on a SurfaceAlt background), a body area with multi-line text (each '\n'-separated line rendered on its own row) and a footer strip at the bottom (SurfaceAlt like the header). The whole card sits on a Theme.Surface fill and is framed by a 1-px Theme.Border stroke — the same visual grammar Button and Menu use so a Card composes cleanly next to them.

Any zone may be empty:

  • Title == "" -> the header strip is skipped, the body starts at r.Y.
  • Body == "" -> no text lines are drawn (the surface fill still shows).
  • Footer== "" -> the footer strip is skipped, the body flows to r.Y+r.H.

Card is a passive display container — it does not intercept input (HitTest / OnEvent stay as Base defaults) so a caller that needs an interactive Card wraps it with an outer container or overlays a Button on top.

func NewCard added in v0.7.0

func NewCard(title, body, footer string) *Card

NewCard constructs a Card with the given title, body + footer. Any of the three may be "" to skip that zone.

func (*Card) Draw added in v0.7.0

func (c *Card) Draw(p painter.Painter, theme *Theme)

Draw paints the surface fill, the optional header and footer strips, each body line and finally the outer border stroke. Draw order is bottom-to-top (fill, then decorations, then border) so the 1-px border always sits on top and clips overlapping strips.

type ChatBubble added in v0.8.0

type ChatBubble struct {
	Base
	Text   string
	Sender ChatSender
}

ChatBubble is a chat-transcript speech bubble: a small rounded rectangle (borrowed shape only — the toolkit's raster stays sharp- cornered) holding a short message string. Multi-line text is supported by splitting Text on '\n'; each line renders on its own glyph row.

Sizing: the bubble grows to fit the widest text line plus 2*PadX, capped at ChatBubbleMaxW so a runaway paste doesn't spill the widget's Bounds. Height is len(lines) * lineH + 2*PadY where lineH = GlyphHeight() + ChatBubbleLineSpacing.

ChatBubble is a passive display widget — it does not intercept input (HitTest / OnEvent stay as Base defaults). A caller that wants a tap-to-copy or long-press-menu bubble wraps this with an outer container that handles the gesture.

func NewChatBubble added in v0.8.0

func NewChatBubble(text string, sender ChatSender) *ChatBubble

NewChatBubble constructs a ChatBubble carrying text sent by sender.

func (*ChatBubble) Draw added in v0.8.0

func (c *ChatBubble) Draw(p painter.Painter, theme *Theme)

Draw paints the bubble: fill in Theme.Accent (user) or SurfaceAlt (other), 1-pixel Theme.Border stroke, and one DrawText per '\n'- separated line in Text. Width is derived from the widest line's TextWidth capped by ChatBubbleMaxW; height is derived from the line count. Position is right-aligned within Bounds() for ChatFromUser and left-aligned for ChatFromOther, mirroring the canonical chat-transcript convention.

type ChatSender added in v0.8.0

type ChatSender int

ChatSender enumerates which side of a chat transcript a ChatBubble belongs to. The two roles paint differently: user messages align to the right of the widget's Bounds in Theme.Accent, other-party messages align to the left in Theme.SurfaceAlt. The distinction is purely visual — the sender does not affect layout otherwise.

const (
	// ChatFromUser marks a message sent by the local user. Bubble
	// right-aligned in Theme.Accent; ink = accent-inverted colour.
	ChatFromUser ChatSender = iota
	// ChatFromOther marks a message from a remote party. Bubble
	// left-aligned in Theme.SurfaceAlt; ink = Theme.OnSurface.
	ChatFromOther
)

type CheckButton

type CheckButton struct {
	Base
	Label    string
	Checked  bool
	OnToggle func(checked bool)
}

CheckButton is a square checkbox + a label. Click toggles Checked + fires OnToggle. Visual: 12 x 12 px box (left-aligned), Theme.Border outline, Theme.Surface fill, Theme.Accent fill + two diagonal "checkmark" strokes in Theme.Background when Checked. Label rendered in Theme.OnBackground to the right of the box.

func NewCheckButton

func NewCheckButton(label string, checked bool) *CheckButton

NewCheckButton constructs a CheckButton with the given label + initial Checked state.

func (*CheckButton) A11y added in v0.19.0

func (c *CheckButton) A11y() A11yInfo

A11y reports the CheckButton as a checkbox with its checked state.

func (*CheckButton) Draw

func (c *CheckButton) Draw(p painter.Painter, theme *Theme)

Draw paints the box + checkmark + label.

func (*CheckButton) OnEvent

func (c *CheckButton) OnEvent(ev Event)

OnEvent flips Checked + fires OnToggle on click.

type Chip added in v0.9.0

type Chip struct {
	Base
	Text     string
	Closable bool
	OnClose  func()
}

Chip is a small labelled pill with an optional "x" close affordance. Unlike Badge -- which is a passive counter / status indicator with no interaction surface -- Chip is a removable tag: when Closable is true the widget renders a click target at the right edge that fires OnClose when tapped. Two constructors keep the two personalities distinct at the callsite: NewChip for a passive tag, NewClosableChip for the removable variant.

Auto-sizing follows Badge's convention: if Bounds().W is zero the first Draw() sets W to the text width plus ChipPadX on each side (plus the close slot's ChipCloseGap + ChipCloseW when Closable is true), and H to GlyphHeight() + 2*ChipPadY when it is also zero. A pre-sized Bounds is honoured verbatim so a fixed-width layout row does not shift when the chip label changes.

func NewChip added in v0.9.0

func NewChip(text string) *Chip

NewChip constructs a passive (non-closable) Chip carrying the given Text. OnClose stays nil; the widget ignores clicks. Bounds default to zero so the first Draw() auto-sizes the pill.

func NewClosableChip added in v0.9.0

func NewClosableChip(text string, onClose func()) *Chip

NewClosableChip constructs a Chip whose right edge exposes an "x" close affordance. onClose may be nil (clicks on the affordance become a no-op rather than a panic) so callers can wire the callback after construction without ordering constraints.

func (*Chip) Draw added in v0.9.0

func (c *Chip) Draw(p painter.Painter, theme *Theme)

Draw paints the pill body + text + optional close affordance. Auto- sizes Bounds when W is zero. The pill body is a filled SurfaceAlt rectangle stroked with a Border outline; the Text is drawn left- aligned inside the pad, and (when Closable) an "x" glyph in Border colour marks the close slot at the right edge.

func (*Chip) OnEvent added in v0.9.0

func (c *Chip) OnEvent(ev Event)

OnEvent fires OnClose when an EventClick lands in the right-hand close slot and Closable is true. Non-click events, non-closable chips, and clicks outside the slot are ignored. A nil OnClose is treated as a no-op so callers can toggle Closable without wiring a callback in the same statement.

Event coordinates are widget-local (as documented on Event), so the slot's horizontal extent is measured against r.W rather than r.X; no localisation is required at the callsite.

type ColorChooser

type ColorChooser struct {
	Base
	Color    RGBA
	OnChange func(c RGBA)
}

ColorChooser is a 3-channel R/G/B picker with a live preview. Each channel is rendered as a horizontal track with a 1-pixel knob the user drags to change the value. The OnChange callback fires with the new RGBA whenever any channel moves.

The widget owns the RGBA value; the host reads .Color() to get the current pick + may also stash a hex string via SetHex if there is a sibling Entry the user can type into.

func NewColorChooser

func NewColorChooser(initial RGBA) *ColorChooser

NewColorChooser builds a chooser starting at initial. Alpha is forced to 0xFF so a freshly-constructed chooser always reads as fully-opaque.

func (*ColorChooser) Draw

func (c *ColorChooser) Draw(p painter.Painter, theme *Theme)

Draw paints the 3 sliders + preview swatch + hex label.

func (*ColorChooser) Hex

func (c *ColorChooser) Hex() string

Hex returns the color as "#RRGGBB".

func (*ColorChooser) OnEvent

func (c *ColorChooser) OnEvent(ev Event)

OnEvent handles clicks on the 3 tracks to move the channel knob.

func (*ColorChooser) SetHex

func (c *ColorChooser) SetHex(s string)

SetHex parses "#RRGGBB" or "RRGGBB" into the chooser's color. Bad input is silently ignored so a malformed Entry payload can't break the picker state.

type ContextMenu added in v0.17.0

type ContextMenu struct {
	Base
	Menu             *Menu
	Open             bool
	AnchorX, AnchorY int
}

ContextMenu is a right-click popup: a Menu that appears at an arbitrary point (the cursor), auto-sizes to its items, clamps itself inside the surface so it never spills off an edge, and dismisses when the user clicks outside it. It is the overlay wrapper the widget model was missing around the bare Menu, mirroring how DropDown/DatePicker own their pop-ups.

The ContextMenu's own Bounds is the whole surface it may cover (so it can catch an outside-click anywhere); AnchorX/AnchorY and incoming event coordinates are in that same frame. Call Popup(x, y) to show it at a point.

Example

ExampleContextMenu shows a right-click popup that auto-sizes, clamps inside the surface, and dismisses on an outside click.

package main

import (
	"github.com/go-widgets/painter"
	"github.com/go-widgets/toolkit"
)

// newSurface returns a PixelPainter over a fresh w×h RGBA buffer — the render
// target the examples draw into. A CellPainter would render the same widgets to
// a terminal grid instead.
func newSurface(w, h int) *painter.PixelPainter {
	return painter.NewPixelPainter(make([]byte, 4*w*h), w, h)
}

func main() {
	menu := toolkit.NewMenu([]toolkit.MenuItem{
		{Label: "Cut", Action: func() {}},
		{Label: "Copy", Action: func() {}, Shortcut: "Ctrl+C"},
	})
	cm := toolkit.NewContextMenu(menu)
	cm.SetBounds(toolkit.Rect{X: 0, Y: 0, W: 200, H: 160})
	cm.Popup(8, 8) // open at the cursor
	cm.Draw(newSurface(200, 160), toolkit.DefaultLight())
}

func NewContextMenu added in v0.17.0

func NewContextMenu(menu *Menu) *ContextMenu

NewContextMenu wraps the given Menu as a (closed) context menu.

func (*ContextMenu) Close added in v0.17.0

func (c *ContextMenu) Close()

Close hides the menu.

func (*ContextMenu) Draw added in v0.17.0

func (c *ContextMenu) Draw(p painter.Painter, theme *Theme)

Draw paints the Menu at its clamped bounds when open; nothing when closed.

func (*ContextMenu) MenuBounds added in v0.17.0

func (c *ContextMenu) MenuBounds() Rect

MenuBounds is the rect the Menu occupies: the measured size placed at the anchor, then shifted so it stays fully inside the surface (c.Bounds()).

func (*ContextMenu) OnEvent added in v0.17.0

func (c *ContextMenu) OnEvent(ev Event)

OnEvent routes a click inside the menu to the Menu (translated to its local frame, so the hit row's Action fires and closes the overlay via OnClose); a click anywhere outside dismisses the menu.

func (*ContextMenu) Popup added in v0.17.0

func (c *ContextMenu) Popup(x, y int)

Popup opens the menu anchored at (x, y) and wires the Menu's OnClose so that activating an item (or the menu closing itself) also closes the overlay.

type DatePicker added in v0.11.0

type DatePicker struct {
	Base
	Cal      *Calendar
	Open     bool
	OnChange func(y, m, d int)
}

DatePicker is a form input for a single calendar date: a field showing the selected date as ISO YYYY-MM-DD text with a small grid icon, and a drop-down Calendar that opens beneath it when the field is clicked. Picking a day in the calendar updates the field, closes the popup, and fires OnChange.

Where the display-only Calendar just renders a month, DatePicker is the composite entry control built around it — the pixel sibling of a native date field. It owns its Calendar (exposed as Cal) and renders the popup itself when Open, so it works standalone; a host that composites overlays on a separate surface can instead read Open + PopoverBounds and draw Cal there.

Example

ExampleDatePicker shows a date field with a drop-down calendar.

package main

import (
	"fmt"

	"github.com/go-widgets/toolkit"
)

func main() {
	dp := toolkit.NewDatePicker(2026, 7, 10)
	dp.OnChange = func(y, m, d int) { fmt.Printf("picked %04d-%02d-%02d\n", y, m, d) }
	dp.SetBounds(toolkit.Rect{X: 0, Y: 0, W: 170, H: toolkit.DatePickerFieldH()})
	fmt.Println(dp.Text())
}
Output:
2026-07-10

func NewDatePicker added in v0.11.0

func NewDatePicker(year, month, day int) *DatePicker

NewDatePicker builds a DatePicker initialised to (year, month, day).

func (*DatePicker) Date added in v0.11.0

func (dp *DatePicker) Date() (y, m, d int)

Date returns the currently-selected (year, month, day).

func (*DatePicker) Draw added in v0.11.0

func (dp *DatePicker) Draw(p painter.Painter, theme *Theme)

Draw paints the field (border + date text + a grid icon) and, when Open, the Calendar popup positioned by PopoverBounds.

func (*DatePicker) OnEvent added in v0.11.0

func (dp *DatePicker) OnEvent(ev Event)

OnEvent: a click on the field toggles the popup; while open, a click inside the popup is forwarded (translated to Calendar-local coordinates) to the Calendar, whose OnSelect closes the popup and fires OnChange.

func (*DatePicker) PopoverBounds added in v0.11.0

func (dp *DatePicker) PopoverBounds() Rect

PopoverBounds is the Rect the Calendar occupies when Open: same X and full calendar width below the field. Six week-rows is the worst case.

func (*DatePicker) SetDate added in v0.11.0

func (dp *DatePicker) SetDate(year, month, day int)

SetDate moves the selection to (year, month, day) without opening the popup.

func (*DatePicker) Text added in v0.11.0

func (dp *DatePicker) Text() string

Text is the field's displayed value: ISO 8601 YYYY-MM-DD.

type Dialog

type Dialog struct {
	Base
	Title   string
	Content Widget
	Buttons []*Button
	OnClose func()
}

Dialog is a modal overlay: a centred Surface card with an optional Title bar, a Content widget filling the body, and an action-button strip at the bottom. The compositor draws a semi-darkened backdrop over the rest of the surface so the user's attention focuses on the dialog.

v0.3 ships the structure; the host app is responsible for routing input events only to the dialog while it's open (existing wasmbox modal-grab behaviour).

func NewDialog

func NewDialog(title string, content Widget, buttons ...*Button) *Dialog

NewDialog builds a Dialog with the given title, content + action buttons. Buttons are laid out right-aligned in the bottom strip.

func NewMessageDialog

func NewMessageDialog(title, message string, onOK func()) *Dialog

NewMessageDialog is a convenience constructor for the most common dialog: a title, a Label as content, and an OK button that calls onOK + closes the dialog via the caller's OnClose hook.

func (*Dialog) Draw

func (d *Dialog) Draw(p painter.Painter, theme *Theme)

Draw paints card + title + content + buttons.

func (*Dialog) OnEvent

func (d *Dialog) OnEvent(ev Event)

OnEvent forwards to content + buttons. A click that doesn't land on any button or the content falls through silently (the app keeps the dialog open).

func (*Dialog) SetBounds

func (d *Dialog) SetBounds(r Rect)

SetBounds also lays out the content + button positions.

type Diff added in v0.8.0

type Diff struct {
	Base
	Lines []DiffLine
}

Diff renders a coloured, line-by-line unified diff view. Each line carries a kind (context / added / removed); Draw fills the row with a light green tint for added lines, a light red tint for removed lines, and the theme's Surface for context lines. A one-character prefix (' ', '+', '-') anchors the row at the left so the widget stays legible even when its background rows are omitted (as when a caller reuses this on top of a striped background).

The widget is intentionally passive: it exposes no scroll, no selection, and no editing. Host apps that need those wrap Diff in a ScrollView + track selection externally.

func NewDiff added in v0.8.0

func NewDiff(lines []DiffLine) *Diff

NewDiff builds a Diff view over the supplied lines. A nil slice is normalised to a zero-length slice so Draw never has to nil-guard.

func (*Diff) Draw added in v0.8.0

func (d *Diff) Draw(p painter.Painter, theme *Theme)

Draw paints the widget body, each row (with its per-kind tint and prefix glyph), and the outer border.

type DiffKind added in v0.8.0

type DiffKind int

DiffKind enumerates the three per-line change categories a unified diff produces.

const (
	// DiffContext marks an unchanged, contextual line — rendered on
	// Theme.Surface with a leading space.
	DiffContext DiffKind = iota
	// DiffAdded marks a line inserted by the change — rendered on a
	// light green tint with a leading '+'.
	DiffAdded
	// DiffRemoved marks a line dropped by the change — rendered on a
	// light red tint with a leading '-'.
	DiffRemoved
)

type DiffLine added in v0.8.0

type DiffLine struct {
	Text string
	Kind DiffKind
}

DiffLine is one row in a Diff view: the raw text plus the change kind that colours it.

type DragSource added in v0.16.0

type DragSource interface {
	Widget
	DragData() string
}

DragSource is a widget a drag can originate from. DragData returns the payload string the host should carry for the drag (e.g. a file path or a row id).

type DropDown struct {
	Base
	Options  []string
	Selected int
	Open     bool
	OnSelect func(idx int)
}

DropDown is a one-of-N selector that shows the current choice in a button-like rectangle. Clicking opens a popover ListBox of all Options just below the widget; selecting one closes the popover + fires OnSelect.

Like Dialog, the popover's rendering surface is owned by the host app; the toolkit exposes Open + Selected so the host knows what to draw. This keeps DropDown independent of how the compositor handles overlay surfaces (some apps use a separate canvas, some draw the popover directly into the same buffer).

func NewDropDown

func NewDropDown(options []string, selected int) *DropDown

NewDropDown builds a DropDown with the given options + an initial selection (clamped to a valid index, or 0 when options is empty).

func (d *DropDown) Current() string

Current returns the currently-selected option's string, or "" when Options is empty.

func (d *DropDown) Draw(p painter.Painter, theme *Theme)

Draw paints the closed widget. The popover, when Open, is the host app's responsibility (host can render a ListBox on top using PopoverBounds).

func (d *DropDown) OnEvent(ev Event)

OnEvent toggles Open on click. Selection happens via Select() which the host wires to its popover ListBox's OnActivate.

func (d *DropDown) PopoverBounds() Rect

PopoverBounds returns the Rect the host should give to its popover ListBox: same X+W as the widget, positioned just below, height proportional to the option count (clamped to PopoverMaxRows rows).

func (d *DropDown) Select(idx int)

Select picks idx, closes the popover + fires OnSelect.

type DropTarget added in v0.16.0

type DropTarget interface {
	Widget
	AcceptsDrop(payload string) bool
}

DropTarget is a widget that can receive a drop. AcceptsDrop reports whether the given payload is droppable here — a host consults it on EventDragStart to decide whether to show an "accepted" cursor and whether to deliver the later EventDrop.

type DropZone added in v0.9.0

type DropZone struct {
	Base
	Prompt string
	Hover  bool
	OnDrop func(paths []string)
}

DropZone is an inline "drag files here" target rendered as a bordered rectangle with a centred prompt string. It is the passive counterpart of FileChooser: FileChooser opens a modal directory browser, DropZone waits in place for the host to hand it dropped file paths (typically via a native HTML5 drag+drop listener the wasmbox compositor wires to the widget).

The Hover flag toggles the dashed-border colour + surface fill so the user sees drag-over feedback before releasing the drop. DropZone is a DropTarget: it drives Hover from the formal drag lifecycle — EventDragStart / EventDragMove raise it, EventDragLeave clears it, and EventDrop delivers the payload (multiple paths newline-separated, recovered with SplitDropPayload) to OnDrop and clears Hover. As a convenience for demos and tests, EventClick also flips Hover in place.

func NewDropZone added in v0.9.0

func NewDropZone(prompt string) *DropZone

NewDropZone constructs a DropZone with the given prompt text. An empty prompt is replaced with the default "Drop files here" so a zero-argument caller still renders a legible target. Bounds default to zero; the caller positions the DropZone via SetBounds.

func (*DropZone) AcceptsDrop added in v0.16.0

func (d *DropZone) AcceptsDrop(payload string) bool

AcceptsDrop reports whether a payload is droppable here. A DropZone is a generic file target, so it accepts any non-empty payload.

func (*DropZone) Draw added in v0.9.0

func (d *DropZone) Draw(p painter.Painter, theme *Theme)

Draw paints the surface fill, the four dashed edges + the centred prompt text. Fill + border colour swap on Hover so the drag-over state is visible without the caller having to swap in a different widget on drag-enter. Dashes are emitted as short filled rects so the toolkit stays on its two existing raster primitives (fillRect / strokeRect) rather than growing a Painter.DashedLine primitive.

func (*DropZone) OnEvent added in v0.9.0

func (d *DropZone) OnEvent(ev Event)

OnEvent implements the drag lifecycle: EventDragStart / EventDragMove raise Hover, EventDragLeave clears it, and EventDrop fires OnDrop with the payload's items (split from ev.Code) then clears Hover. EventClick flips Hover in place as a demo/test hook. All other event kinds are ignored so a keyboard event bound for a sibling widget does not accidentally trigger a drop.

type Entry

type Entry struct {
	Base
	Text     string
	Cursor   int // rune index in [0, len(runes)]
	Focused  bool
	OnChange func(text string)
	OnSubmit func(text string)
}

Entry is a single-line text input. Receives focus on click, edits Text via EventKeyDown (Backspace, ArrowLeft/Right, Home, End, Enter) + EventChar (printable runes). A 1-pixel vertical cursor renders at the cursor offset when Focused.

The widget treats Text as a rune index space so multi-byte UTF-8 characters move the cursor by one position even when they take several bytes on the wire.

func NewEntry

func NewEntry(initial string) *Entry

NewEntry builds an Entry with initial text + cursor parked at end.

func (*Entry) A11y added in v0.19.0

func (e *Entry) A11y() A11yInfo

A11y reports the Entry as a textbox carrying its current text.

func (*Entry) Draw

func (e *Entry) Draw(p painter.Painter, theme *Theme)

Draw paints the border, fill, text + (when Focused) a 1-px cursor stroke at the cursor's pixel position.

func (*Entry) OnEvent

func (e *Entry) OnEvent(ev Event)

OnEvent handles focus, keyboard navigation, character insertion + delete.

type Event

type Event struct {
	Kind EventKind
	X, Y int
	Code string
}

Event is one input event delivered to a widget. The parent container translated mouse coordinates into widget-local pixels; Code is the key/char text for keyboard events.

type EventKind

type EventKind int

EventKind enumerates the input event types a widget can receive. The wasmbox compositor routes DOM events through this enum so widgets don't depend on the browser's exact event names.

const (
	// EventClick fires on a mousedown+mouseup pair inside the widget.
	// X/Y carry widget-local coordinates.
	EventClick EventKind = iota
	// EventKeyDown fires when a key is pressed while the widget has
	// focus. Code carries the key name (e.g. "Enter", "ArrowLeft").
	EventKeyDown
	// EventKeyUp is the symmetric release event.
	EventKeyUp
	// EventChar fires for printable character input (post-IME).
	// Code carries the character as a one-rune string.
	EventChar
	// EventCompositionStart fires when an IME composition begins
	// (typically a dead-key press or a CJK IME popup opening). Widgets
	// that echo text (Entry / TextView) should render Code as the
	// "in-progress" preview string, underlined or ghosted, WITHOUT
	// committing it to their buffer. The host is responsible for
	// resolving the composition via EventCompositionUpdate ticks and
	// finally an EventChar (post-commit).
	EventCompositionStart
	// EventCompositionUpdate refreshes the preview string mid-flow.
	// Code carries the current, un-committed composed text.
	EventCompositionUpdate
	// EventCompositionEnd fires when the composition is either
	// committed (host follows up with EventChar carrying the same
	// text) or cancelled (host does NOT send an EventChar and the
	// widget discards the preview).
	EventCompositionEnd
	// EventMouseDrag fires when the mouse moves while a button is
	// still pressed. X/Y carry the current widget-local position.
	// The initial button press was already dispatched as EventClick,
	// so a widget that wants drag semantics remembers "am I being
	// dragged" from the EventClick and consults it on drag ticks.
	EventMouseDrag
	// EventMouseUp fires when the button is released. Widgets that
	// track drag state clear it here. X/Y carry the release
	// position (widget-local).
	EventMouseUp
	// EventDragStart fires on a DropTarget when a drag first enters it
	// (drag-enter). The target typically raises a hover cue. Code
	// carries the drag payload the host is offering, so the target can
	// decide via AcceptsDrop whether to signal acceptance.
	EventDragStart
	// EventDragMove fires as the drag pointer moves while still over the
	// same DropTarget. X/Y carry the widget-local position (for an
	// insertion indicator); Code still carries the payload.
	EventDragMove
	// EventDragLeave fires when the drag pointer exits a DropTarget
	// without dropping. The target clears its hover cue. It completes
	// the enter/move/leave/drop lifecycle so a target never stays stuck
	// in the hover state.
	EventDragLeave
	// EventDrop fires when the drag is released over a DropTarget. Code
	// carries the payload (multiple items newline-separated — see
	// SplitDropPayload); the target consumes it and clears its hover cue.
	EventDrop
)

type Expander

type Expander struct {
	Base
	Label    string
	Expanded bool
	Content  Widget
	OnExpand func(expanded bool)
}

Expander is a header row that toggles a content area's visibility. The header is ExpanderHeaderH px tall, shows a chevron + label; clicking the header flips Expanded + fires OnExpand.

When Expanded, Content occupies the remaining bounds below the header. When collapsed, only the header is drawn.

func NewExpander

func NewExpander(label string, content Widget) *Expander

NewExpander builds an Expander with a label + initial content widget (may be nil to render header-only).

func (*Expander) Draw

func (e *Expander) Draw(p painter.Painter, theme *Theme)

Draw paints the header (chevron + label) + the content widget when Expanded.

func (*Expander) OnEvent

func (e *Expander) OnEvent(ev Event)

OnEvent: click on the header toggles Expanded + fires OnExpand; clicks below the header forward to Content (when expanded).

type FileChooser

type FileChooser struct {
	Base
	Root      *TreeNode
	ListFiles func(dir *TreeNode) []string
	OnAccept  func(path string)
	OnCancel  func()
	// contains filtered or unexported fields
}

FileChooser is a directory-tree + file-list + path-entry composite. It does NO I/O — the host hands it a virtual root (a TreeNode tree representing directories) + a func that lists files in a given directory. Selection is reported via OnAccept.

FileChooser is the canonical use-case for TreeView + ListBox + Entry composed together. It is what an "Open File…" dialog renders inside a wasmbox app that has no JS file picker access.

func NewFileChooser

func NewFileChooser(root *TreeNode, listFiles func(dir *TreeNode) []string) *FileChooser

NewFileChooser builds a FileChooser rooted at root with the given directory-listing func.

func (*FileChooser) Draw

func (f *FileChooser) Draw(p painter.Painter, theme *Theme)

Draw paints the composite.

func (*FileChooser) OnEvent

func (f *FileChooser) OnEvent(ev Event)

OnEvent dispatches to the child widgets based on which one the event falls inside.

func (*FileChooser) Path

func (f *FileChooser) Path() string

Path returns the entry text — the current effective selection.

func (*FileChooser) SetBounds

func (f *FileChooser) SetBounds(r Rect)

SetBounds positions the child widgets at the chosen split.

type Font added in v0.20.0

type Font interface {
	Advance() int
	Height() int
	Draw(p painter.Painter, x, y int, text string, ink RGBA)
}

Font is the toolkit's text metrics + rendering abstraction. Widgets lay themselves out against the ACTIVE font's metrics (via GlyphHeight / GlyphAdvance) and paint text through DrawText, so swapping the active font with SetFont rescales the whole UI's typography without touching any widget.

  • Advance is the horizontal step from one glyph origin to the next.
  • Height is the glyph box height.
  • Draw paints text left-to-right at (x, y) in the given ink.

All built-in widgets assume a monospace font (fixed Advance), which keeps grid-aligned layout math trivial.

func CurrentFont added in v0.20.0

func CurrentFont() Font

CurrentFont returns the active font.

func NewBitmapFont added in v0.20.0

func NewBitmapFont(scale int) Font

NewBitmapFont returns the built-in 5x7 font scaled by the given integer factor (clamped to at least 1). SetFont(NewBitmapFont(2)) doubles all text.

type FontChooser added in v0.21.0

type FontChooser struct {
	Base
	Options  []FontOption
	Selected int
	OnChoose func(idx int, f Font)
}

FontChooser is a vertical picker of fonts: each option's name is drawn in that very font, so the list doubles as a live size/style preview. Clicking a row selects it, applies it as the active font via SetFont, and fires OnChoose. It is the picker the Font interface (v0.20) unblocked — the long-deferred sibling of ColorChooser / FileChooser.

With no options supplied it defaults to three scales of the built-in bitmap font (Regular / Large / Extra Large), so an app gets a working font size picker for free.

func NewFontChooser added in v0.21.0

func NewFontChooser(options []FontOption) *FontChooser

NewFontChooser builds a FontChooser over the given options (defaulting to the built-in scale ladder when none are supplied).

func (*FontChooser) Draw added in v0.21.0

func (fc *FontChooser) Draw(p painter.Painter, theme *Theme)

Draw paints the panel and each option's name rendered in its own font, the Selected row on an Accent band.

func (*FontChooser) OnEvent added in v0.21.0

func (fc *FontChooser) OnEvent(ev Event)

OnEvent: a click on a row selects it, applies it as the active font (SetFont), and fires OnChoose.

type FontOption added in v0.21.0

type FontOption struct {
	Name string
	Font Font
}

FontOption is one named font in a FontChooser.

type FormField added in v0.9.0

type FormField struct {
	Base
	Label string
	Help  string // optional dim caption below the child
	Error string // optional error caption below the child (takes precedence)
	Child Widget // the actual input; may be nil
}

FormField is a labelled input row: a Label above (in theme.OnBack- ground), an optional Child input widget below, and an optional caption row underneath the Child that shows either an Error (in fixed red) or Help text (in theme.Border for a muted look). Error takes precedence over Help when both are set.

FormField sits directly on theme.Background (it is a form container, not a card) and does not fill its own body — the label glyphs and the composed Child provide their own inks. Callers wanting a filled body can wrap the FormField in a Card.

Child composition: SetBounds on the Child is called during Draw so callers only have to position the FormField itself. OnEvent forwards clicks (and other event kinds' point events) to the Child when (X, Y) falls inside the Child rect, translating coordinates into Child-local space. Non-point events (keyboard) are forwarded unconditionally so the Child can react to focus-driven input.

func NewFormField added in v0.9.0

func NewFormField(label string, child Widget) *FormField

NewFormField constructs a FormField wrapping child with a label above. Help + Error remain empty; the caller assigns them as the field's state changes.

func (*FormField) Draw added in v0.9.0

func (f *FormField) Draw(p painter.Painter, theme *Theme)

Draw paints the label row, positions + draws the Child (when non- nil), and paints the caption row (Error > Help > nothing).

func (*FormField) OnEvent added in v0.9.0

func (f *FormField) OnEvent(ev Event)

OnEvent forwards the event to Child when Child is non-nil. Point events (EventClick) are gated on the Child rect so a click outside the input body is dropped; non-point events (keyboard/composition) are forwarded unconditionally so a focused Child sees them. Nil Child is a no-op.

type Frame

type Frame struct {
	Base
	// Padding is the inset (in pixels) between Frame's border + its
	// child. Defaults to 4 when left at zero; negative values are
	// clamped to zero at layout time.
	Padding int
	// contains filtered or unexported fields
}

Frame draws a 1-pixel border around a single child widget + inset the child by Padding pixels inside that border. Useful as a group- box / panel separator when an app wants to visually fence off a region of widgets.

Frame is a Widget: Draw paints the border + delegates to the child; OnEvent forwards to the child with translated coordinates.

func NewFrame

func NewFrame(child Widget) *Frame

NewFrame wraps child in a Frame. child may be nil (the Frame then just draws its border + accepts no events).

func (*Frame) Draw

func (f *Frame) Draw(p painter.Painter, theme *Theme)

Draw paints the 1-pixel border then the child (if any).

func (*Frame) OnEvent

func (f *Frame) OnEvent(ev Event)

OnEvent forwards to the child if the event lands inside its Bounds.

func (*Frame) SetBounds

func (f *Frame) SetBounds(r Rect)

SetBounds positions the Frame + resizes its child to fit inside the border + padding.

type Grid

type Grid struct {
	Base
	// contains filtered or unexported fields
}

Grid lays children out in a fixed cols x rows table. Each cell is the same size (container W/cols, H/rows). Children are placed via Attach(child, col, row); a cell with no attached child stays empty.

Grid is a Widget: Draw fans out to every attached child + OnEvent hit-tests then forwards.

func NewGrid

func NewGrid(cols, rows int) *Grid

NewGrid constructs an empty cols x rows grid. cols + rows must be positive; the constructor clamps non-positive inputs to 1 to keep the divide-by-zero out of SetBounds.

func (*Grid) Attach

func (g *Grid) Attach(w Widget, col, row int)

Attach places w at (col, row). Out-of-range coordinates are clamped into the grid so a typo doesn't silently vanish + the child still ends up somewhere visible. Re-runs layout immediately.

func (*Grid) Draw

func (g *Grid) Draw(p painter.Painter, theme *Theme)

Draw paints every attached child in attach order.

func (*Grid) OnEvent

func (g *Grid) OnEvent(ev Event)

OnEvent hit-tests attached children + forwards with translated coordinates.

func (*Grid) SetBounds

func (g *Grid) SetBounds(r Rect)

SetBounds positions the Grid + sizes every attached child to its (col, row) cell.

type HBox

type HBox struct {
	Base
	// Spacing is the gap in pixels between adjacent children. Defaults
	// to 4 when left at zero (set to a negative value to truly disable
	// the gap; negative values are clamped to zero at layout time).
	Spacing int
	// contains filtered or unexported fields
}

HBox is a horizontal flow container. Children are laid out left-to- right + share the container's width equally (minus Spacing gaps between adjacent children). Children's Y + height fill the box's vertical extent.

HBox is a Widget itself: Draw fans out to every child + OnEvent hit-tests by child Bounds, translating coordinates into the matched child's local space before forwarding.

func NewHBox

func NewHBox() *HBox

NewHBox constructs an empty HBox. Callers add children via Append + then call SetBounds to trigger layout.

func (*HBox) Append

func (h *HBox) Append(w Widget)

Append adds w to the right of any existing children. Re-runs layout so the new child is positioned immediately + the caller doesn't have to remember to re-call SetBounds.

func (*HBox) Draw

func (h *HBox) Draw(p painter.Painter, theme *Theme)

Draw paints every child in append order. Children render directly into the surface using their own Bounds; the HBox itself draws no background or border (it's a pure layout container).

func (*HBox) OnEvent

func (h *HBox) OnEvent(ev Event)

OnEvent hit-tests by child Bounds + forwards the event with coordinates translated into the child's local space. The first child whose Bounds contains the event point wins (children should not overlap inside an HBox, but a stable order is still useful).

func (*HBox) SetBounds

func (h *HBox) SetBounds(r Rect)

SetBounds positions the HBox + lays out its children. Width is divided equally among children; Spacing pixels separate adjacent cells. Children's Y matches the box's Y; height matches H.

type HeaderBar added in v0.7.0

type HeaderBar struct {
	Base
	Title    string
	Subtitle string
	Start    []Widget // rendered left-to-right along the left edge
	End      []Widget // rendered right-to-left along the right edge
}

HeaderBar is the GTK "client-side decorations" bar: an optional row of Start widgets (usually navigation — back, menu), a centred Title (+ optional Subtitle) and an optional row of End widgets (usually actions — search, close). Composes cleanly above a Notebook + Statusbar so an app can assemble a stock GNOME window out of just three toolkit widgets.

Start widgets paint left-to-right from the bar's left edge; End widgets paint right-to-left from the bar's right edge. The title (and subtitle, when non-empty) are centred horizontally in whatever space remains between the two child regions.

HeaderBar does not intercept events itself; children receive events via the parent container's usual dispatch after HeaderBar has positioned them (Draw does the layout side-effect).

func NewHeaderBar added in v0.7.0

func NewHeaderBar(title string) *HeaderBar

NewHeaderBar constructs a HeaderBar carrying title. Subtitle, Start and End remain zero-valued; the caller populates them before the first Draw.

func (*HeaderBar) Draw added in v0.7.0

func (h *HeaderBar) Draw(p painter.Painter, theme *Theme)

Draw paints the bar body, positions + draws every Start / End child, then paints Title (+ Subtitle when non-empty) centred in whatever horizontal space is left between the two child regions.

Positioning side effect: every Start / End widget's Bounds is updated to reflect its position inside the bar. The widget's original Bounds.W is preserved; Bounds.H is fitted to the bar's inner height (bar.H - HeaderBarPad). This mirrors GTK's HdyHeaderBar pattern — children carry their own preferred width but let the bar decide vertical placement.

type IconButton added in v0.9.0

type IconButton struct {
	Base
	Icon    string
	OnClick func()
}

IconButton is a compact toolbar button whose entire face is one short glyph string ("+", "OK", "v", ...). Distinct from Button (which carries a text label with hover/press states) and ToggleButton (which carries toggle state) — IconButton is a passive Surface-faced tile meant for dense toolbars where the glyph itself is the semantic content.

The face is theme.Surface with a 1-px theme.Border stroke; the glyph renders in theme.OnSurface. No accent fill by default — this keeps the button reading as a subtle toolbar affordance rather than a primary action.

Auto-sizing: if Bounds().W is zero the first Draw() resizes the button to IconButtonSize x IconButtonSize (H preserved when non-zero). A pre-sized Bounds is honoured verbatim so a fixed toolbar column doesn't shift when the widget is dropped in.

func NewIconButton added in v0.9.0

func NewIconButton(icon string, onClick func()) *IconButton

NewIconButton constructs an IconButton carrying the given glyph + click handler. onClick may be nil (a no-op button is still rendered). Bounds default to zero so the first Draw() auto-sizes the widget to IconButtonSize x IconButtonSize.

func (*IconButton) Draw added in v0.9.0

func (i *IconButton) Draw(p painter.Painter, theme *Theme)

Draw paints the surface + border and centres Icon inside using the toolkit's 5x7 bitmap font. If Bounds().W is zero the widget resizes itself to IconButtonSize x IconButtonSize (H preserved when already non-zero) before painting.

func (*IconButton) OnEvent added in v0.9.0

func (i *IconButton) OnEvent(ev Event)

OnEvent fires OnClick on EventClick; other event kinds are ignored. OnClick is nil-safe.

type Image

type Image struct {
	Base
	Pixels []byte // RGBA bytes, W*H*4 in length
	W, H   int    // source dimensions
}

Image paints a caller-supplied RGBA byte buffer into its bounds. If source dims == bounds, the blit is 1:1; otherwise the image is nearest-neighbour scaled to fit the bounds (no aspect-ratio preservation in v0.2).

func NewImage

func NewImage(pixels []byte, w, h int) *Image

NewImage wraps pixels (length must equal w*h*4) + the source dimensions. Caller owns the pixels; the toolkit just reads them.

func (*Image) Draw

func (i *Image) Draw(p painter.Painter, theme *Theme)

Draw paints the image into bounds. Scaling is nearest-neighbour.

type Kbd added in v0.7.0

type Kbd struct {
	Base
	Keys string
}

Kbd renders a keyboard-shortcut hint like the "⌘K" chip beside a menu item: a small bordered box with the key text centred inside. Uses Theme.Surface for the face + Theme.Border for the stroke so the chip reads as a raised inlay against the parent panel.

Kbd is passive (no OnEvent handling); the caller sets Bounds to position it — a Kbd typically lives to the right of a menu label, vertically centred with the menu row. Nothing about the widget depends on the input actually being pressed; it's a purely visual mnemonic.

func NewKbd added in v0.7.0

func NewKbd(keys string) *Kbd

NewKbd constructs a Kbd carrying the given key text. Callers set Bounds via SetBounds before Draw; a natural fit is {W: TextWidth(keys) + 2*KbdPadX, H: GlyphHeight() + 2*KbdPadY}.

func (*Kbd) Draw added in v0.7.0

func (k *Kbd) Draw(p painter.Painter, theme *Theme)

Draw paints the chip: filled Surface body, 1-px Border stroke, Keys text centred in OnSurface. Zero-size Bounds degrade to a no-op via fillRect/strokeRect's own dimension guards.

type Label

type Label struct {
	Base
	Text string
}

Label is a passive widget that displays Text in the theme's OnSurface colour, drawn with the toolkit's 5x7 bitmap font. Left- aligned inside Bounds, vertically centred if Bounds.H exceeds GlyphHeight().

Label is non-interactive: HitTest returns false so clicks pass through to the widget beneath. Apps that want a clickable label should compose a Button with the text instead.

func NewLabel

func NewLabel(text string) *Label

NewLabel constructs a Label carrying text.

func (*Label) A11y added in v0.19.0

func (l *Label) A11y() A11yInfo

A11y reports the Label as static text.

func (*Label) Draw

func (l *Label) Draw(p painter.Painter, theme *Theme)

Draw paints the Label's text with the toolkit's bitmap font. If Bounds.H > GlyphHeight() the text is vertically centred; otherwise it lands at Bounds.Y.

func (*Label) HitTest

func (l *Label) HitTest(_, _ int) bool

HitTest returns false unconditionally: a Label is decorative, not interactive. Override (or compose with a Button) to make a label receive events.

type LevelBar

type LevelBar struct {
	Base
	Value, Max int
}

LevelBar is the discrete cousin of ProgressBar: Max equal cells, the first Value cells filled in Accent + the rest in SurfaceAlt. Useful for battery / signal-strength style indicators.

func NewLevelBar

func NewLevelBar(max int) *LevelBar

NewLevelBar builds a LevelBar with the given Max (Value defaults to 0).

func (*LevelBar) Draw

func (l *LevelBar) Draw(p painter.Painter, theme *Theme)

Draw paints Max cells with a 1-px gap; the first Value cells use Theme.Accent, the rest Theme.SurfaceAlt.

type LineChart added in v0.12.0

type LineChart struct {
	Base
	Series   []float64
	Min, Max float64 // Y bounds; when equal, taken from the data
}

LineChart plots one series of Y values as a polyline over a left+bottom axis frame -- the full-size sibling of the inline Sparkline. Values are spread evenly across the plot width and scaled vertically between Min and Max (auto- derived from the data when Min == Max). Display-only.

It renders through painter.Painter, so the same chart draws as anti-aliased pixels (WUI/GUI) or promoted cells (TUI). A single point renders as a dot; an empty series draws just the axes.

Example

ExampleLineChart plots a series as a polyline over an axis frame.

package main

import (
	"github.com/go-widgets/painter"
	"github.com/go-widgets/toolkit"
)

// newSurface returns a PixelPainter over a fresh w×h RGBA buffer — the render
// target the examples draw into. A CellPainter would render the same widgets to
// a terminal grid instead.
func newSurface(w, h int) *painter.PixelPainter {
	return painter.NewPixelPainter(make([]byte, 4*w*h), w, h)
}

func main() {
	chart := toolkit.NewLineChart([]float64{3, 7, 2, 8, 5, 9, 4})
	chart.SetBounds(toolkit.Rect{X: 0, Y: 0, W: 220, H: 80})
	chart.Draw(newSurface(220, 80), toolkit.DefaultLight())
}

func NewLineChart added in v0.12.0

func NewLineChart(series []float64) *LineChart

NewLineChart builds a LineChart over the given series with auto Y bounds.

func (*LineChart) Draw added in v0.12.0

func (c *LineChart) Draw(p painter.Painter, theme *Theme)

Draw paints the axis frame then the polyline (or a dot for a lone point).

type ListBox

type ListBox struct {
	Base
	Items      []string
	Selected   int // -1 = no selection
	RowHeight  int // pixels per row; default 18 via NewListBox
	OnActivate func(idx int)
}

ListBox is a vertical list of selectable string rows. Click on a row selects it + fires OnActivate.

Visual: each row is RowHeight pixels tall. The selected row uses Theme.Accent as background + Theme.Background as ink; unselected rows use Theme.Surface + Theme.OnSurface. Rows are rendered via font.DrawText with a 4 px left margin.

func NewListBox

func NewListBox(items []string) *ListBox

NewListBox builds a ListBox containing items. Selected starts at -1 (no row selected) and RowHeight defaults to 18 (a comfortable 7-px font + 11 px vertical padding).

func (*ListBox) Draw

func (l *ListBox) Draw(p painter.Painter, theme *Theme)

Draw paints every row inside the widget's bounds. Rows that fall outside the bounds (because the list is longer than the viewport) are still drawn but clipped per-pixel by the raster helpers; wrap a ScrollView around the ListBox for proper scrollable behaviour.

func (*ListBox) OnEvent

func (l *ListBox) OnEvent(ev Event)

OnEvent dispatches click events: a click at (X, Y) selects the row idx = Y / RowHeight (clamped to the list length); OnActivate fires with that idx.

type MarkdownView added in v0.15.0

type MarkdownView struct {
	Base
	Source string
}

MarkdownView renders a subset of Markdown as laid-out text -- the read-only document widget the toolkit lacked. It handles the block structure that matters on a fixed-width bitmap font: ATX headings (`#`..`######`), bullet lists (`-`/`*`/`+`), fenced code blocks (```), and blank-line-separated paragraphs, word-wrapped to the widget width.

The 5x7 font has a single weight and size, so hierarchy is shown through layout rather than type scale: headings get an Accent underline (levels 1-2) and the Accent ink, bullets get a "• " marker with a hanging indent, and code blocks sit on a SurfaceAlt band. Inline emphasis (*bold*, _italic_) is not styled -- the bitmap font has no variants -- but its text still renders. Display-only; wrap in a ScrollView for long documents.

Example

ExampleMarkdownView renders a Markdown subset (headings, lists, code, paragraphs) laid out for the bitmap font.

package main

import (
	"github.com/go-widgets/painter"
	"github.com/go-widgets/toolkit"
)

// newSurface returns a PixelPainter over a fresh w×h RGBA buffer — the render
// target the examples draw into. A CellPainter would render the same widgets to
// a terminal grid instead.
func newSurface(w, h int) *painter.PixelPainter {
	return painter.NewPixelPainter(make([]byte, 4*w*h), w, h)
}

func main() {
	md := toolkit.NewMarkdownView("# Title\n\nA paragraph.\n\n- one\n- two")
	md.SetBounds(toolkit.Rect{X: 0, Y: 0, W: 260, H: 120})
	md.Draw(newSurface(260, 120), toolkit.DefaultLight())
}

func NewMarkdownView added in v0.15.0

func NewMarkdownView(source string) *MarkdownView

NewMarkdownView builds a MarkdownView over the given Markdown source.

func (*MarkdownView) Draw added in v0.15.0

func (m *MarkdownView) Draw(p painter.Painter, theme *Theme)

Draw lays the parsed blocks top-to-bottom within Bounds.

type Menu struct {
	Base
	Items   []MenuItem
	Hover   int // index of hovered row, -1 if none
	OnClose func()
}

Menu is a vertical popover-style list of MenuItems. Used by the compositor's right-click root menu, by MenuBar drop-downs and by any widget that needs an Openbox-style picker.

func NewMenu

func NewMenu(items []MenuItem) *Menu

NewMenu builds a Menu with the given items + Hover at -1.

func (m *Menu) Draw(p painter.Painter, theme *Theme)

Draw paints the menu's body + every row + a hover highlight on the currently-hovered row.

func (m *Menu) OnEvent(ev Event)

OnEvent: a click on an enabled row fires its Action + closes the menu via OnClose (if wired).

func (m *Menu) SetHover(y int)

SetHover updates Hover based on a mouse-Y coordinate (widget-local). Useful for keyboard / mouse-move handlers that want to highlight the row the user is pointing at.

type MenuBar struct {
	Base
	Names  []string
	Menus  []*Menu
	Active int // -1 if none open
}

MenuBar is a horizontal strip of top-level menu names (File, Edit, View, ...). Clicking a name opens its associated Menu as a popover just below the strip.

The MenuBar itself doesn't own the open Menu's drawing (the containing app composes it with whatever overlay surface it has); MenuBar just exposes Active so the app knows which menu to render.

func NewMenuBar

func NewMenuBar() *MenuBar

NewMenuBar builds a MenuBar (Active = -1).

func (b *MenuBar) AddMenu(name string, m *Menu)

AddMenu appends (name, menu) to the bar.

func (b *MenuBar) Draw(p painter.Painter, theme *Theme)

Draw paints the bar + every name + a highlight on the Active name.

func (b *MenuBar) HandleShortcut(code string) bool

HandleShortcut walks every menu's items and fires the Action of the first item whose Shortcut equals code (case-sensitive; the host is expected to normalise Ctrl+N vs Cmd+N before calling). Returns true if an item fired, false if no match. Menu ordering + item ordering give a deterministic priority — first match wins.

Skipped: separators, disabled items (nil Action). A matching item with a submenu still fires its Action (if any); the submenu is not opened by a shortcut.

Typical usage from a wasmbox client's Go main:

case "keydown":
    code := formatShortcut(ev)   // host builds "Ctrl+N" etc.
    if state.menuBar.HandleShortcut(code) { render(); return }
    state.editor.OnEvent(...)    // fallthrough: forward to focus
func (b *MenuBar) Mnemonic(i int) byte

Mnemonic returns the first letter of the i-th menu name (upper-case, or 0 if the index is out of range / the name is empty). Useful for a host that wants to draw "_F_ile"-style underlines under the mnemonic character.

func (b *MenuBar) NameOriginX(i int) int

NameOriginX returns the X offset of the i-th top-level name within the bar (cumulative sum of NameWidth up to i, exclusive). Same motivation as NameWidth: a host that positions a popover under a clicked name reads this to align on the correct column.

func (b *MenuBar) NameWidth(i int) int

NameWidth returns the pixel width of the i-th top-level name after the auto-size rule: max(MenuBarItemW, TextWidth(name) + 2*pad). Exposed so hosts that render their own popover under a clicked name know how wide the "click zone" was.

func (b *MenuBar) OnEvent(ev Event)

OnEvent: a click on a name toggles its menu (Active = idx or -1). Also honours mnemonic keyboard shortcuts on EventKeyDown when the Code carries an "Alt+X" hint (X = one of the top-level names' first letter, case-insensitive) — matches the GNOME/Windows menu-bar Alt+letter convention. The host is responsible for formatting the key event's Code as "Alt+F" etc. before forwarding.

type MenuItem struct {
	Label     string
	Action    func()
	Submenu   *Menu
	Separator bool
	Shortcut  string
}

MenuItem is one row in a Menu. Label is the human text; Action is the callback fired on click. A nil Action turns the row into a disabled (greyed-out) entry; a non-empty Submenu lets it open a nested Menu (popover) on hover or click.

Separator items render as a thin SurfaceAlt line + are not clickable. They have empty Label + nil Action.

Shortcut is a hint string ("Ctrl+N", "Cmd+O", …) drawn right-aligned on the row in the muted SurfaceAlt tone. Purely visual: the host app is responsible for actually wiring the key combo to the item's Action (there is no cross-platform "Ctrl vs Cmd" logic in the toolkit — different apps route keys through different SDKs).

type Notebook

type Notebook struct {
	Base
	Tabs         []NotebookTab
	Active       int
	OnTabChanged func(idx int)
}

Notebook is a tabbed container. The top NotebookTabStripH pixels host the tab strip; the rest is the active page's body. Clicking a tab swaps Active + fires OnTabChanged.

func NewNotebook

func NewNotebook() *Notebook

NewNotebook returns an empty Notebook with no tabs + Active = 0.

func (*Notebook) AddTab

func (n *Notebook) AddTab(label string, page Widget)

AddTab appends a tab to the strip with label + the page widget shown when that tab is active.

func (*Notebook) Draw

func (n *Notebook) Draw(p painter.Painter, theme *Theme)

Draw paints the strip + the active page.

func (*Notebook) OnEvent

func (n *Notebook) OnEvent(ev Event)

OnEvent: click on the strip selects a tab; click in the body routes to the active page.

type NotebookTab

type NotebookTab struct {
	Label string
	Page  Widget
}

NotebookTab is one entry in a Notebook. Label is the human title painted on the tab; Page is the widget shown when the tab is active.

type Notification

type Notification struct {
	Base
	Text    string
	Visible bool

	// Life is the number of Tick() calls remaining before the
	// notification auto-hides. NotificationLife (~180 ≈ 3 s at 60 Hz)
	// is a reasonable default; Show() re-arms it. Set directly for
	// long-lived notifications (e.g. Life = 3600 for a persistent
	// "network offline" banner the host manually Hide()s later).
	Life int
}

Notification is a transient toast — an auto-dismissing banner that slides in over the app's normal frame, holds for a few ticks, then hides itself. Cousin of Tooltip (both are informational overlays) but with three key differences:

  1. Notification is time-bounded (Tick decrements Life; hides at 0).
  2. Notification is positioned by the host (typically top-right or bottom-centre), NOT anchored to a source widget.
  3. Notification stays up while the user is doing something else — Tooltip requires the mouse to hover over its anchor.

The host drives Life via Tick() from its own animation loop (typically a rAF tick). One Notification instance can be reused — call Show(text) to re-arm it with a fresh Life budget.

func NewNotification

func NewNotification(text string) *Notification

NewNotification builds a hidden notification with the given text + the default Life budget pre-armed (so a caller who forgets to call Show still gets a sensible time-out on the first Tick loop).

func (*Notification) Draw

func (n *Notification) Draw(p painter.Painter, theme *Theme)

Draw paints the toast when Visible. Filled Accent panel with a 1-px Border stroke, Text in the Background ink (inverted for contrast). Nothing drawn when hidden.

func (*Notification) Hide

func (n *Notification) Hide()

Hide dismisses the notification immediately (independent of Life).

func (*Notification) Show

func (n *Notification) Show(text string)

Show makes the notification visible + resets Life to NotificationLife. Bounds are auto-sized to the text width + the standard padding; the host is responsible for positioning (SetBounds) BEFORE calling Show — Show only refreshes the width to match the current Text.

func (*Notification) Tick

func (n *Notification) Tick()

Tick decrements Life by 1. When Life reaches 0, the notification auto-hides. The host calls this from its animation loop; a rAF-driven caller ticks 60 Hz so NotificationLife = 180 ≈ 3 s. Callers wanting a paused notification (freeze on user hover) just skip the Tick during the pause.

type Overlay added in v0.18.0

type Overlay struct {
	Base
	Content Widget
	Layers  []Widget
	Modal   bool
}

Overlay is a z-ordered stacking container: a primary Content child that fills the bounds, plus a stack of Layers painted on top of it in order (the last Layer is topmost). It is the piece the widget model was missing so transient widgets -- Popover, Toast, Notification, Tooltip, ContextMenu -- can float above the main UI without the host arranging screen positions or z-order.

Unlike Stack (which shows exactly one page at a time), an Overlay draws every layer, and events route top-down: the topmost Layer whose HitTest covers the point handles the event; if none do, the event falls through to Content -- unless Modal is set, in which case a miss while any Layer is up is swallowed (a modal backdrop). Layers self-position via their own Bounds; only Content is resized to fill the Overlay.

Example

ExampleOverlay stacks z-ordered layers above a primary child. Events route top-down; a Modal overlay swallows clicks that miss every layer.

package main

import (
	"github.com/go-widgets/painter"
	"github.com/go-widgets/toolkit"
)

// newSurface returns a PixelPainter over a fresh w×h RGBA buffer — the render
// target the examples draw into. A CellPainter would render the same widgets to
// a terminal grid instead.
func newSurface(w, h int) *painter.PixelPainter {
	return painter.NewPixelPainter(make([]byte, 4*w*h), w, h)
}

func main() {
	base := toolkit.NewLabel("main content")
	ov := toolkit.NewOverlay(base)
	ov.Push(toolkit.NewTooltip("floating layer"))
	ov.SetBounds(toolkit.Rect{X: 0, Y: 0, W: 200, H: 120})
	ov.Draw(newSurface(200, 120), toolkit.DefaultLight())
}

func NewOverlay added in v0.18.0

func NewOverlay(content Widget) *Overlay

NewOverlay builds an Overlay around the given primary child (which may be nil and set later).

func (*Overlay) Clear added in v0.18.0

func (o *Overlay) Clear()

Clear removes every layer, leaving just the Content.

func (*Overlay) Draw added in v0.18.0

func (o *Overlay) Draw(p painter.Painter, theme *Theme)

Draw paints Content first, then each layer bottom-to-top.

func (*Overlay) OnEvent added in v0.18.0

func (o *Overlay) OnEvent(ev Event)

OnEvent routes to the topmost layer whose HitTest covers the point; failing that, to Content -- or, when Modal and a layer is present, nowhere (the backdrop swallows the click). Coordinates are passed through unchanged (an Overlay is a surface-frame container, like Paned).

func (*Overlay) Pop added in v0.18.0

func (o *Overlay) Pop() Widget

Pop removes and returns the topmost layer, or nil when there are none.

func (*Overlay) Push added in v0.18.0

func (o *Overlay) Push(w Widget)

Push adds w as the new topmost layer.

func (*Overlay) SetBounds added in v0.18.0

func (o *Overlay) SetBounds(r Rect)

SetBounds resizes Content to fill the Overlay; layers keep their own bounds (they self-position at a point).

func (*Overlay) Top added in v0.18.0

func (o *Overlay) Top() Widget

Top returns the topmost layer without removing it, or nil when there are none.

type Pagination added in v0.8.0

type Pagination struct {
	Base
	Current  int
	Total    int
	OnChange func(page int)
}

Pagination is a page-navigator strip: a "<" prev button, a series of page-number buttons, and a ">" next button. Clicking a page number jumps Current to that page and fires OnChange; clicking prev or next steps by one (clamped). When Current is at either extreme the corresponding step button renders in a disabled tone and swallows clicks.

When Total exceeds paginationMaxButtons the middle of the range collapses into a "1 ... k-1 k k+1 ... Total" window so the widget's footprint stays bounded. Non-numeric window slots ("...") are drawn but not clickable — the hit-test skips them.

func NewPagination added in v0.8.0

func NewPagination(current, total int) *Pagination

NewPagination builds a Pagination with the given current and total page counts. Current is clamped to [1, Total] when Total > 0, and to 1 when Total <= 0 (the widget then renders empty and swallows events).

func (*Pagination) Draw added in v0.8.0

func (pg *Pagination) Draw(p painter.Painter, theme *Theme)

Draw paints the widget body, each button in its correct tint, and the button labels. Total <= 0 paints only the body — no buttons. Bounds that cannot accommodate a single button are treated the same as Total <= 0 so a mis-sized Pagination degrades gracefully.

func (*Pagination) OnEvent added in v0.8.0

func (pg *Pagination) OnEvent(ev Event)

OnEvent routes an EventClick to whichever button contains (X, Y). Prev/next step Current by one when enabled; a numeric slot sets Current to its page. Ellipsis slots and out-of-band clicks are no-ops.

type Paned

type Paned struct {
	Base
	First, Second     Widget
	Orientation       int
	Position          int
	OnPositionChanged func(pos int)
}

Paned splits its bounds into two child regions separated by a PanedHandleW-px draggable handle. Position is the handle's offset (in pixels) from the leading edge of First; orientation chooses whether that's measured along X (PanedHorizontal) or Y (PanedVertical).

The toolkit's full event model is click-only in v0.2, so drag is exposed via direct OnDragHandle helpers callers wire to their own mouse-tracking state.

func NewHPaned

func NewHPaned(first, second Widget) *Paned

NewHPaned builds a horizontal Paned with a sensible default Position (mid-bounds, applied at first SetBounds).

func NewVPaned

func NewVPaned(first, second Widget) *Paned

NewVPaned builds a vertical Paned with the same defaults as NewHPaned.

func (*Paned) Draw

func (pd *Paned) Draw(p painter.Painter, theme *Theme)

Draw paints both children + the handle.

func (*Paned) MoveHandle

func (p *Paned) MoveHandle(pos int)

MoveHandle slides the splitter to pos (clamped) and re-lays out children. Fires OnPositionChanged with the new value.

func (*Paned) OnEvent

func (p *Paned) OnEvent(ev Event)

OnEvent forwards to the appropriate child based on click position.

func (*Paned) SetBounds

func (p *Paned) SetBounds(r Rect)

SetBounds lays out First/Second around the handle.

type PieChart added in v0.14.0

type PieChart struct {
	Base
	Values []float64
	Colors []RGBA // optional per-slice palette override; cycles by index
}

PieChart plots proportional Values as wedges of a filled disc -- the part-of- whole complement to LineChart/BarChart. Wedges start at 12 o'clock and run clockwise, sized by each value's share of the total. Colours cycle through a built-in categorical palette unless Colors is set. Display-only.

It fills each wedge per-pixel over painter.Painter's putPixel (no arc primitive needed), so it renders as pixels (WUI/GUI) or promoted cells (TUI). A zero or empty total draws nothing.

Example

ExamplePieChart fills a disc with one proportional wedge per value.

package main

import (
	"github.com/go-widgets/painter"
	"github.com/go-widgets/toolkit"
)

// newSurface returns a PixelPainter over a fresh w×h RGBA buffer — the render
// target the examples draw into. A CellPainter would render the same widgets to
// a terminal grid instead.
func newSurface(w, h int) *painter.PixelPainter {
	return painter.NewPixelPainter(make([]byte, 4*w*h), w, h)
}

func main() {
	chart := toolkit.NewPieChart([]float64{3, 5, 2, 4})
	chart.SetBounds(toolkit.Rect{X: 0, Y: 0, W: 120, H: 120})
	chart.Draw(newSurface(120, 120), toolkit.DefaultLight())
}

func NewPieChart added in v0.14.0

func NewPieChart(values []float64) *PieChart

NewPieChart builds a PieChart over the given values with the default palette.

func (*PieChart) Draw added in v0.14.0

func (c *PieChart) Draw(p painter.Painter, theme *Theme)

Draw fills the disc, colouring each pixel by the wedge its angle falls in.

type Popover added in v0.8.0

type Popover struct {
	Base
	Visible bool
	Child   Widget
	Title   string
}

Popover is a Visible floating container for a single child widget, modelled on GTK 4's Popover -- a rectangular panel with a border stroke and an optional Title header. Popover is the natural home for dropdown menus, ephemeral pickers and detail overlays that the host wants to show and hide without tearing down and rebuilding the underlying child.

Distinct from Card (a passive display container) in two ways:

  1. Popover has a Visible toggle: the whole widget short-circuits Draw + OnEvent when hidden so the host does not have to unlink the child from the tree between showings.
  2. Popover forwards input events to its child with coordinates translated for the pad + optional title header, so the child sees widget-local coords in its own frame.

func NewPopover added in v0.8.0

func NewPopover(child Widget) *Popover

NewPopover constructs a hidden Popover wrapping child. child may be nil, in which case the Popover renders as an empty framed panel.

func (*Popover) Draw added in v0.8.0

func (p *Popover) Draw(pnt painter.Painter, theme *Theme)

Draw paints the surface fill + border, optionally draws the Title at the top-left inside PopoverPad, then draws Child (if non-nil) into the inset child rect. Nothing drawn when !Visible.

func (*Popover) OnEvent added in v0.8.0

func (p *Popover) OnEvent(ev Event)

OnEvent forwards the event to Child with coordinates translated into the child's local frame. No-op when !Visible or Child is nil. Mirrors the translateEvent pattern used by HBox / VBox / Grid.

type ProgressBar

type ProgressBar struct {
	Base
	Fraction float64
	Label    string
}

ProgressBar is a horizontal bar with a filled portion proportional to Fraction in [0,1]. An optional Label is centred over the bar in Theme.OnSurface ink.

func NewProgressBar

func NewProgressBar() *ProgressBar

NewProgressBar builds an empty (Fraction=0) ProgressBar with no label.

func (*ProgressBar) Draw

func (pb *ProgressBar) Draw(p painter.Painter, theme *Theme)

Draw paints border + track + fill + optional centered label.

func (*ProgressBar) SetFraction

func (p *ProgressBar) SetFraction(f float64)

SetFraction clamps + assigns Fraction. 0 = empty, 1 = full.

type ProgressCircle added in v0.9.0

type ProgressCircle struct {
	Base
	Fraction float64 // 0..1; clamped by Draw
}

ProgressCircle is a fake-circular progress indicator: a rounded square track with a "cup-filling" band that rises from the bottom as Fraction grows from 0 to 1. Not a true arc — the pixel-blitting toolkit does not carry a curve rasteriser — but it conveys the same "circular progress" intent at the same abstraction level as Spinner (a rotating radial line) and Avatar (a rounded square).

Layout: an outer square filled in theme.SurfaceAlt (the "track"), an inner square inset by ProgressCircleStroke on all sides filled in theme.Surface (the "hole" that the caption sits in), and a horizontal Accent band inside the ring whose height is proportional to Fraction. The band grows from the bottom edge upward for the familiar "filling up" visual. The percentage caption ("XX%") is drawn in theme.OnSurface centred inside the inner square.

func NewProgressCircle added in v0.9.0

func NewProgressCircle() *ProgressCircle

NewProgressCircle constructs a ProgressCircle at Fraction=0.

func (*ProgressCircle) Draw added in v0.9.0

func (pc *ProgressCircle) Draw(p painter.Painter, theme *Theme)

Draw paints the track, the hole, the fill band, and the centred percentage caption. Draw clamps Fraction defensively so callers bypassing SetFraction still render a valid frame.

func (*ProgressCircle) SetFraction added in v0.9.0

func (pc *ProgressCircle) SetFraction(f float64)

SetFraction clamps + assigns Fraction. 0 = empty, 1 = full. Kept as a symmetrical helper to ProgressBar.SetFraction so both widgets present the same knob to callers.

type RGBA

type RGBA = painter.RGBA

RGBA is a 32-bit colour value packed as bytes (Red, Green, Blue, Alpha). Alias of painter.RGBA so widgets emit values that flow unchanged through any Painter back-end (pixel buffer, cell grid, SVG stream). Alpha is honoured by the pixel rasteriser; the stock widgets all paint opaque pixels (A=0xFF).

func RGB

func RGB(r, g, b uint8) RGBA

RGB constructs an opaque colour with A=0xFF. Kept as a package- level helper so theme literals don't have to import painter.

type RadioButton

type RadioButton struct {
	Base
	Label    string
	Checked  bool
	OnToggle func(checked bool)
	// contains filtered or unexported fields
}

RadioButton is a circular toggle paired with a label. RadioButtons are typically grouped via RadioGroup so exactly one in the group is Checked at any time. A standalone RadioButton (not added to a group) behaves like a CheckButton (toggleable on click).

func NewRadioButton

func NewRadioButton(label string) *RadioButton

NewRadioButton constructs a standalone RadioButton with the given label. Add it to a RadioGroup with group.Add(r) for mutual-exclusion behaviour.

func (*RadioButton) A11y added in v0.19.0

func (r *RadioButton) A11y() A11yInfo

A11y reports the RadioButton as a radio with its checked state.

func (*RadioButton) Draw

func (r *RadioButton) Draw(p painter.Painter, theme *Theme)

Draw paints the circular mark + label. The "circle" is a 12 x 12 box with a 1-pixel inset on every side, painted as a stroked rectangle (approximate to avoid bringing in trig). When Checked, a smaller Accent-filled rect sits inside as the radio dot.

func (*RadioButton) OnEvent

func (r *RadioButton) OnEvent(ev Event)

OnEvent: on click, route through the group (if any) so siblings clear; otherwise toggle Checked locally.

type RadioGroup

type RadioGroup struct {
	Members []*RadioButton
	Active  int
}

RadioGroup makes a set of RadioButtons mutually exclusive. Active is the index of the currently-checked member, or -1 when none has been clicked yet.

func NewRadioGroup

func NewRadioGroup() *RadioGroup

NewRadioGroup builds an empty group with Active = -1.

func (*RadioGroup) Add

func (g *RadioGroup) Add(r *RadioButton)

Add appends r to the group + remembers its membership so a click on any member can clear the others.

type RangeSlider added in v0.10.0

type RangeSlider struct {
	Base
	Min, Max  float64
	Low, High float64
	OnChange  func(low, high float64)
	// contains filtered or unexported fields
}

RangeSlider is a two-handle slider selecting a sub-interval [Low, High] within a continuous Min..Max range -- a price band, a date window, a volume gate. It is the two-thumb sibling of Scale: the same rounded track and circular white thumbs, but the Accent fill spans the selected band between the handles rather than from the left edge.

A click grabs whichever handle is nearest the cursor and jumps it there; a subsequent drag moves that same handle, clamped so Low never crosses High.

Example

ExampleRangeSlider selects a sub-interval; SetRange orders + clamps its ends.

package main

import (
	"fmt"

	"github.com/go-widgets/toolkit"
)

func main() {
	rs := toolkit.NewRangeSlider(0, 100, 20, 80)
	rs.SetRange(90, 10) // passed out of order → normalised to Low <= High
	fmt.Printf("%.0f-%.0f\n", rs.Low, rs.High)
}
Output:
10-90

func NewRangeSlider added in v0.10.0

func NewRangeSlider(min, max, low, high float64) *RangeSlider

NewRangeSlider builds a RangeSlider spanning [min, max] with the given initial band. The band is clamped and ordered so Low <= High.

func (*RangeSlider) Draw added in v0.10.0

func (s *RangeSlider) Draw(p painter.Painter, theme *Theme)

Draw paints the rounded track, the Accent band between the two handles, and a circular white thumb at each handle -- matching Scale's macOS styling.

func (*RangeSlider) OnEvent added in v0.10.0

func (s *RangeSlider) OnEvent(ev Event)

OnEvent: a click grabs the nearer handle and jumps it to the cursor; a drag moves the grabbed handle; a mouse-up releases it. Each move re-clamps so the handles never cross, and fires OnChange.

func (*RangeSlider) SetRange added in v0.10.0

func (s *RangeSlider) SetRange(low, high float64)

SetRange clamps both bounds to [Min, Max] and swaps them if low > high, so the invariant Low <= High always holds.

type Rating added in v0.8.0

type Rating struct {
	Base
	Value    int
	Max      int
	OnChange func(v int)
}

Rating is a horizontal star-rating strip: Max square cells drawn left-to-right, each carrying an ASCII asterisk overlay. Cells with index < Value fill in Theme.Accent (the "filled" state); cells with index >= Value fill in Theme.SurfaceAlt (the "empty" state). The star glyph itself is drawn as the ASCII "*" character because the toolkit's 5x7 bitmap font only covers ASCII — a Unicode "★" would render blank via DrawText's font5x7 lookup fall-through.

A click on cell index i sets Value = i+1 (so the leftmost cell yields 1, the rightmost Max) and fires OnChange when non-nil. Clicks outside the strip (Y outside the cell row, X to the right of the last cell) are ignored — the parent container already routes only hits inside Bounds() but a stray x >= Max*(RatingStarW+RatingStarGap) would otherwise resolve to an out-of-range index.

func NewRating added in v0.8.0

func NewRating(value, max int) *Rating

NewRating constructs a Rating with the given value and max. Max defaults to 5 when non-positive; Value is clamped to the [0, Max] interval so a bogus caller input can never render more filled cells than Max.

func (*Rating) Draw added in v0.8.0

func (r *Rating) Draw(p painter.Painter, theme *Theme)

Draw paints Max cells left-to-right. Filled cells use Theme.Accent + the accent-inverted ink; empty cells use Theme.SurfaceAlt + Theme.OnSurface. Every cell carries an ASCII "*" overlay so the row reads as stars even when the palette is monochrome.

func (*Rating) OnEvent added in v0.8.0

func (r *Rating) OnEvent(ev Event)

OnEvent handles a click by resolving the star index from ev.X and setting Value = index+1. Non-click events are ignored (matches Switch / ToggleButton). Clicks with X to the right of the last cell (index >= Max) are ignored so a spurious hit doesn't push Value past Max.

type Rect

type Rect = painter.Rect

Rect is an axis-aligned rectangle in pixel coordinates. X/Y is the top-left corner; W/H are width/height. Aliased to painter.Rect so widgets can render on any painter.Painter (PixelPainter for a pixel buffer, CellPainter for a terminal grid) without a type conversion. Contains() is inherited from painter.Rect.

type Role added in v0.19.0

type Role string

Role is a widget's accessibility role, named after the WAI-ARIA roles a host maps them onto.

const (
	RoleButton   Role = "button"
	RoleText     Role = "text"
	RoleTextbox  Role = "textbox"
	RoleCheckbox Role = "checkbox"
	RoleRadio    Role = "radio"
	RoleSwitch   Role = "switch"
	RoleSlider   Role = "slider"
)

The roles the built-in widgets report.

type Scale

type Scale struct {
	Base
	Min, Max float64
	Value    float64
	OnChange func(v float64)
}

Scale is a horizontal slider over a continuous Min..Max range. Click on the track jumps the thumb to that x-position + fires OnChange. The 4-px track sits across the vertical midpoint in Theme.SurfaceAlt; the 10-px square thumb sits at the value's position in Theme.Accent.

func NewScale

func NewScale(min, max, initial float64) *Scale

NewScale builds a Scale spanning [min, max] with the given initial value. Min == Max is allowed but renders a non-interactive track.

func (*Scale) A11y added in v0.19.0

func (s *Scale) A11y() A11yInfo

A11y reports the Scale as a slider carrying its current value.

func (*Scale) Draw

func (s *Scale) Draw(p painter.Painter, theme *Theme)

Draw paints a macOS-style slider: a rounded track whose filled portion (up to the thumb) is Accent and whose remainder is SurfaceAlt, with a circular white thumb -- matching the Switch's pill track + circular knob.

func (*Scale) OnEvent

func (s *Scale) OnEvent(ev Event)

OnEvent: click jumps the thumb to the clicked x-position + fires OnChange.

func (*Scale) SetValue

func (s *Scale) SetValue(v float64)

SetValue clamps to [Min, Max] before assigning.

type ScrollView

type ScrollView struct {
	Base
	Child            Widget
	OffsetX, OffsetY int
	// contains filtered or unexported fields
}

ScrollView is a viewport over a child widget whose content may be larger than the visible area. The child's own Bounds is logical (= content size); ScrollView paints the child clipped to its own Bounds, with origin shifted by -OffsetX/-OffsetY.

A thin scrollbar track (8 px wide) is painted on the right edge in Theme.SurfaceAlt; a Theme.Accent thumb sized proportionally to the viewport/content ratio shows the current scroll position.

Wheel events scroll vertically; horizontal scrolling is supported via direct Scroll(dx, dy) calls (no horizontal scrollbar drawn in v0.2).

func NewScrollView

func NewScrollView(child Widget) *ScrollView

NewScrollView builds a ScrollView around child. Call SetContentSize after construction to declare the child's logical extent so the thumb is sized correctly + scrolling is clamped.

func (*ScrollView) Draw

func (s *ScrollView) Draw(p painter.Painter, theme *Theme)

Draw paints the child clipped to the viewport, then the scrollbar track + thumb on the right edge.

func (*ScrollView) HitTest

func (s *ScrollView) HitTest(px, py int) bool

HitTest covers the full bounds (the scrollbar is interactive too).

func (*ScrollView) Scroll

func (s *ScrollView) Scroll(dx, dy int)

Scroll mutates the offsets by (dx, dy) and clamps to [0, contentSize - viewportSize] so the thumb never falls off the track. Negative offsets are clamped to 0.

func (*ScrollView) SetContentSize

func (s *ScrollView) SetContentSize(w, h int)

SetContentSize tells the ScrollView how big the child's logical drawing area is. Used by Scroll() to clamp + by Draw() to size the thumb. Caller is responsible for invoking this when the child grows / shrinks.

type SearchEntry added in v0.8.0

type SearchEntry struct {
	Base
	Text     string
	OnChange func(s string)
}

SearchEntry is a single-line text input decorated with a leading search-prefix glyph and, when Text is non-empty, a trailing "clear" affordance on the right. Think GTK's SearchEntry: an Entry whose visual chrome hints at its role and offers a one-click reset. The widget is intentionally passive about focus (no cursor, no caret) — it simply appends printable characters, deletes on Backspace, and clears on a click in the right-side X slot. Callers who need cursor navigation or IME support should reach for Entry / TextView instead.

func NewSearchEntry added in v0.8.0

func NewSearchEntry(text string) *SearchEntry

NewSearchEntry builds a SearchEntry pre-loaded with initial text. The constructor does not run OnChange for the initial value so callers can wire the callback after construction without a spurious notification.

func (*SearchEntry) Draw added in v0.8.0

func (s *SearchEntry) Draw(p painter.Painter, theme *Theme)

Draw paints the entry body, the leading prefix glyph, the current Text, and (when Text is non-empty) the trailing clear affordance.

func (*SearchEntry) OnEvent added in v0.8.0

func (s *SearchEntry) OnEvent(ev Event)

OnEvent handles character insertion (EventChar), Backspace deletion (EventKeyDown / "Backspace"), and click-to-clear in the right icon slot (EventClick, when Text is non-empty). Other events are ignored.

type Selection

type Selection struct {
	StartLine, StartCol int
	EndLine, EndCol     int
}

Selection is a (start, end) range of TextView positions. Positions are (line, col) pairs in rune coordinates -- same model the TextView cursor uses. Start <= End in canonical order; SelectionRange normalises any (anchor, cursor) pair the caller hands it.

Selection is pure data; the TextView holds one + uses it to drive painting + range-delete + clipboard ops.

func SelectionRange

func SelectionRange(anchorLine, anchorCol, cursorLine, cursorCol int) Selection

SelectionRange returns a canonical Selection from an anchor + a cursor: whichever pair is "earlier" in document order becomes the start.

func (Selection) IsEmpty

func (s Selection) IsEmpty() bool

IsEmpty reports whether the selection covers zero characters.

type Skeleton added in v0.8.0

type Skeleton struct {
	Base
	Kind  SkeletonKind
	Lines int
}

Skeleton is a placeholder rendered while real content is loading. Every Skeleton fills in Theme.SurfaceAlt so the shape reads as "content coming" without demanding attention; there is no text or animation — the visual weight alone signals the pending state.

A caller typically swaps a Skeleton for the real widget once data arrives; there is no Visible flag because dropping the widget from the tree is cheaper than gating every Draw on a bool.

Skeleton is passive: it displays and does not respond to input.

func NewSkeleton added in v0.8.0

func NewSkeleton(kind SkeletonKind, lines int) *Skeleton

NewSkeleton constructs a Skeleton of the given kind + line count. The lines argument is honoured only when kind == SkeletonText; if it is non-positive in that case it defaults to 3 (a natural stand-in for a paragraph). For SkeletonAvatar / SkeletonBlock the value is stored verbatim but ignored by Draw.

func (*Skeleton) Draw added in v0.8.0

func (s *Skeleton) Draw(p painter.Painter, theme *Theme)

Draw paints the placeholder appropriate for Kind. Nothing else about the widget is theme-aware: every fill lands in Theme.SurfaceAlt so the placeholder recedes into the panel without demanding attention.

type SkeletonKind added in v0.8.0

type SkeletonKind int

SkeletonKind selects the placeholder shape. Three kinds cover the dominant loading-state patterns: SkeletonText for a paragraph or a list row, SkeletonAvatar for an identity chip (paired with SkeletonText in a message row), SkeletonBlock for a media thumbnail or a card body.

const (
	// SkeletonText draws N horizontal bars stacked vertically. The last
	// bar is 60% width so the shape reads as "wrapped text" rather than
	// a solid block.
	SkeletonText SkeletonKind = iota
	// SkeletonAvatar draws a rounded square in SurfaceAlt matching the
	// Avatar widget's shape — so a Skeleton row lines up pixel-for-pixel
	// with the real Avatar it will be swapped for once the data loads.
	SkeletonAvatar
	// SkeletonBlock draws one filled rectangle covering Bounds() inset
	// by SkeletonLinePad — the "media thumbnail loading" affordance.
	SkeletonBlock
)

type SpinButton

type SpinButton struct {
	Base
	Min, Max int
	Value    int
	Step     int
	OnChange func(v int)
}

SpinButton is an integer input with `+` and `−` buttons on the right. Click `+` adds Step, click `−` subtracts Step (clamped to [Min, Max]). The value is rendered as a decimal string in the left portion of the body.

func NewSpinButton

func NewSpinButton(min, max, initial, step int) *SpinButton

NewSpinButton builds a SpinButton spanning [min, max] with the given initial + step. Step <= 0 is clamped to 1 so clicks never no-op silently.

func (*SpinButton) Draw

func (s *SpinButton) Draw(p painter.Painter, theme *Theme)

Draw paints the body (with the value text) + the two stacked buttons on the right.

func (*SpinButton) OnEvent

func (s *SpinButton) OnEvent(ev Event)

OnEvent: click on the upper-right button increments; click on the lower-right button decrements.

func (*SpinButton) SetValue

func (s *SpinButton) SetValue(v int)

SetValue clamps + assigns.

type Spinner

type Spinner struct {
	Base
	Active bool
	Phase  float64 // 0..1, full cycle
}

Spinner is an indeterminate loading indicator. When Active, Draw paints a "clock hand" from the centre of bounds rotated by Phase * 2π in Theme.Accent. Caller drives Phase via Tick(dt) so the animation cadence stays tied to the host's frame loop (no goroutine, no timer).

func NewSpinner

func NewSpinner() *Spinner

NewSpinner builds a Spinner stopped at Phase=0.

func (*Spinner) Draw

func (s *Spinner) Draw(p painter.Painter, theme *Theme)

Draw paints the rotating hand when Active. Hand length = 40% of the smaller of (W, H); painted as a thin radial line of single pixels from centre outward.

func (*Spinner) Tick

func (s *Spinner) Tick(deltaSeconds float64)

Tick advances Phase by deltaSeconds, wrapping modulo 1 so the value stays bounded.

type SplitButton added in v0.9.0

type SplitButton struct {
	Base
	Label   string
	Arrow   bool
	OnClick func()
	OnArrow func()
}

SplitButton is a two-part button: a primary action face on the left plus an attached narrow arrow face on the right that opens a secondary action (typically a menu). Mirrors GTK's SplitButton and GtkMenuButton — one click target for the default action, a separate click target for "show the alternatives".

When Arrow is false the arrow slot is not drawn and OnArrow is ignored — the widget degrades to a solid Accent-face action button, so a caller can toggle the split visual at runtime without swapping widgets.

The two faces share theme.Accent as their fill; the label + arrow glyph render in accentInk(theme) — theme.Extra["OnAccent"] with a fall-through to theme.Background, matching Button + Table + Avatar.

func NewSplitButton added in v0.9.0

func NewSplitButton(label string, onClick func()) *SplitButton

NewSplitButton constructs a SplitButton with Arrow enabled by default and OnArrow left nil. onClick may be nil (a no-op primary action is still rendered).

func (*SplitButton) Draw added in v0.9.0

func (s *SplitButton) Draw(p painter.Painter, theme *Theme)

Draw paints the two-slot face. Both slots fill in theme.Accent; when Arrow is true a 1-px theme.Border separator is drawn between them and a small "v" glyph is centred in the arrow slot. Ink for both the label and the arrow glyph is accentInk(theme) so the text stays legible against the Accent face and honours any theme.Extra["OnAccent"] override.

func (*SplitButton) OnEvent added in v0.9.0

func (s *SplitButton) OnEvent(ev Event)

OnEvent routes clicks to OnClick or OnArrow depending on where the click landed. ev.X is widget-local; when Arrow is true a click in the right SplitButtonArrowW pixels fires OnArrow, otherwise OnClick. Both callbacks are nil-safe. Non-click event kinds are ignored.

type Stack

type Stack struct {
	Base
	Pages   map[string]Widget
	Visible string
}

Stack holds N named pages (Widgets) but shows only ONE at a time -- the page named by Visible. Use AddPage / SetVisible to navigate. Events route to the visible page only.

Suitable for application "screens" (settings vs main vs about), wizard steps, or anywhere the user expects a CLEAN swap with no transition.

func NewStack

func NewStack() *Stack

NewStack builds an empty Stack with no pages + no visible name.

func (*Stack) AddPage

func (s *Stack) AddPage(name string, w Widget)

AddPage registers a page under name. If this is the first page, it auto-becomes Visible so an unconfigured Stack still draws something.

func (*Stack) Draw

func (s *Stack) Draw(p painter.Painter, theme *Theme)

Draw paints only the visible page.

func (*Stack) OnEvent

func (s *Stack) OnEvent(ev Event)

OnEvent routes to the visible page.

func (*Stack) SetBounds

func (s *Stack) SetBounds(r Rect)

SetBounds also propagates to the visible page so it fills the Stack's rect. Other pages have stale bounds until SetVisible brings them forward -- they re-bound at draw time.

func (*Stack) SetVisible

func (s *Stack) SetVisible(name string)

SetVisible swaps the showing page. Names not in Pages are silently ignored so the caller can SetVisible blind.

type Stat added in v0.9.0

type Stat struct {
	Base
	Title  string
	Value  string
	Change string
	Trend  StatTrend
}

Stat is a compact KPI card — a small dim Title on top, a large Value in the middle drawn with a one-pixel horizontal thickening pass to fake a bold weight, and an optional Change indicator at the bottom coloured by Trend. Modelled on DaisyUI's `<div class="stat">` block: three vertically stacked text rows on a bordered Surface panel.

Stat is passive display only — the parent view supplies any interaction (a click-through link, a tooltip) as a separate widget on top. HitTest / OnEvent stay as Base defaults.

func NewStat added in v0.9.0

func NewStat(title, value string) *Stat

NewStat constructs a Stat with the given title + value. Change defaults to "" (no change row painted) and Trend defaults to StatFlat; the caller assigns those fields directly to enable the bottom row.

func (*Stat) Draw added in v0.9.0

func (s *Stat) Draw(p painter.Painter, theme *Theme)

Draw paints the surface fill, the three text rows and finally the outer border stroke. Draw order matches Card (fill, decorations, border last) so the 1-px border always sits on top and clips overlapping ink.

The Value row is drawn TWICE — once at (x, y) and again at (x+1, y) — to fake a bold weight. Since the 5x7 bitmap font ships one stroke width, the double-draw thickens each column by one pixel so the Value visually outweighs the surrounding Title + Change rows without a second glyph table.

type StatTrend added in v0.9.0

type StatTrend int

StatTrend selects the semantic direction of a Stat's optional Change indicator. StatFlat renders the change text in the theme's dim border ink (the same "muted-ink" convention HeaderBar uses for its subtitle); StatUp and StatDown paint fixed green and red shades so the direction reads at a glance regardless of the app's accent choice. Widgets that want a neutral or accent-tinted change value use StatFlat and let the theme drive the tone.

const (
	// StatFlat is the neutral "no direction" trend. Change ink comes
	// from Theme.Border so the value blends with the surrounding
	// muted labels.
	StatFlat StatTrend = iota
	// StatUp signals a positive change ("+12%", "revenue up"). Ink is
	// a fixed sea-green so up-trends read the same across every theme.
	StatUp
	// StatDown signals a negative change ("-4%", "errors up"). Ink is
	// a fixed brick-red so down-trends read the same across every theme.
	StatDown
)

type Statusbar

type Statusbar struct {
	Base
	Segments []string

	// SegmentMinW is the minimum width any non-last segment takes. The
	// last segment ALWAYS fills the rest of the bar.
	SegmentMinW int // default StatusbarSegmentMinW
}

Statusbar is a thin horizontal strip at the bottom of a window that shows N text segments (e.g. "Line 12, Col 4" + "UTF-8" + "Plain text" in an editor). Segments paint left-to-right with a 1-pixel divider between them; the LAST segment expands to fill any remaining width so an empty Statusbar still looks deliberate.

Statusbar is the natural pairing for MenuBar + Toolbar above and a document area in the middle — together they assemble the "stock GTK" window frame.

func NewStatusbar

func NewStatusbar(segs []string) *Statusbar

NewStatusbar builds a Statusbar with the given segments.

func (*Statusbar) Draw

func (s *Statusbar) Draw(p painter.Painter, theme *Theme)

Draw paints the strip + every segment.

func (*Statusbar) SetSegment

func (s *Statusbar) SetSegment(i int, text string)

SetSegment replaces the i-th segment in place. Indexes out of range are appended (filling intermediate slots with "") so callers can grow the bar lazily.

type Steps added in v0.7.0

type Steps struct {
	Base
	Labels  []string
	Current int
}

Steps is a horizontal step indicator — [1]—[2]—[3]—[4] — for multi-step flows (a wizard, an on-boarding tour, a checkout page). Each entry is drawn as a small square badge carrying its 1-based index number, with a 1-px connector line between successive badges. A Labels entry that is not "" renders below its badge as caption text in Theme.OnBackground.

Current is the 0-indexed cursor into Labels; badges up to AND including Current fill with Theme.Accent (the "done / active" colour), later badges fill with Theme.SurfaceAlt (the "pending" colour). A Current outside [0, len(Labels)) means either "no step active yet" (Current < 0 -> every badge is pending) or "all done" (Current >= len -> every badge is filled).

Steps is a passive display container: hit-testing / event routing are not implemented — a caller who needs a click-to-jump interaction walks the same layout math externally.

func NewSteps added in v0.7.0

func NewSteps(labels []string, current int) *Steps

NewSteps constructs a Steps indicator with the given labels + the initial current-step cursor.

func (*Steps) Draw added in v0.7.0

func (s *Steps) Draw(p painter.Painter, theme *Theme)

Draw paints each badge, its connector to the previous badge (if any) and the optional caption below it. The badge fill switches from Accent (index <= Current) to SurfaceAlt (index > Current); the number ink inverts accordingly so it stays legible.

type Switch added in v0.7.0

type Switch struct {
	Base
	On       bool
	OnToggle func(on bool)
}

Switch is a compact iOS-style toggle: a wide horizontal track with a small square knob that sits on the left when Off and on the right when On. Distinct from ToggleButton in shape + intent: ToggleButton is a full-face button whose entire body flips colour with state, so it reads as "an action that stays pressed"; Switch is decorative chrome — a settings-row indicator whose knob position is the entire affordance ("is this feature on?").

Track fill flips between SurfaceAlt (Off) and Accent (On) so the on-state stands out at a glance; the knob is drawn in Surface with a Border stroke so it stays visible against either track colour.

Click flips On + fires OnToggle. Non-click events are ignored.

func NewSwitch added in v0.7.0

func NewSwitch(on bool) *Switch

NewSwitch constructs a Switch with the given initial state. The OnToggle callback is nil by default; assign it after construction if the caller wants a click hook.

func (*Switch) A11y added in v0.19.0

func (s *Switch) A11y() A11yInfo

A11y reports the Switch as a switch with an on/off value.

func (*Switch) Draw added in v0.7.0

func (s *Switch) Draw(p painter.Painter, theme *Theme)

Draw paints the track + knob. Track colour is picked by On; the knob slides between left + right edges by rewriting knobX in the On branch. Zero-height or extremely narrow Bounds degrade to a no-op via fillRect's own dimension guard.

func (*Switch) OnEvent added in v0.7.0

func (s *Switch) OnEvent(ev Event)

OnEvent flips On + fires OnToggle on click. All other event kinds pass through without effect (matches ToggleButton / CheckButton).

type Table added in v0.7.0

type Table struct {
	Base
	// Columns are the header cells (title + optional pixel width).
	// A zero Width means "auto" -- the column claims an equal share of
	// whatever pixel budget is left after the fixed-Width columns.
	Columns []TableColumn
	// Rows is the body content. Each inner slice SHOULD have
	// len == len(Columns); rows shorter than that render only the
	// cells they carry (missing trailing cells are drawn as blank
	// space, the row background still paints edge-to-edge).
	Rows [][]string
	// Selected is the 0-indexed row highlighted with Theme.Accent;
	// -1 (or any out-of-range value) means "no selection" and the
	// zebra stripe pattern paints unmodified.
	Selected int
}

Table renders a structured data grid: a fixed header row of column titles above a body of text rows. The widget is the missing piece vs GTK's ColumnView + DaisyUI's Table -- the toolkit's ListBox + TreeView give a single column of items, whereas Table lays cells out horizontally under labelled columns.

Visual (per row):

+----------------+--------+-------------+
| Header A       | Hdr B  | Header C    |  <- TableHeaderHeight, SurfaceAlt
+----------------+--------+-------------+
| row 0 cell 0   | 0.1    | 0.2         |  <- TableRowHeight, Surface
| row 1 cell 0   | 1.1    | 1.2         |  <- TableRowHeight, Background
| ...
+----------------+--------+-------------+

Selected row (if 0 <= Selected < len(Rows)) paints in Theme.Accent with the accent-inverted ink -- theme.Extra["OnAccent"] when the GTK loader supplied one, otherwise theme.Background (the same fallback the Button + ListBox + TreeView selected states already use, so the visual reads consistent across widgets).

The widget is content-only: no per-cell events, no header sorting, no column drag-resize. A future revision layered on top may add them; the MVP is a passive spreadsheet-shaped viewer.

func NewTable added in v0.7.0

func NewTable(cols []TableColumn, rows [][]string) *Table

NewTable builds a Table with the given columns + rows. Selected starts at -1 (no row selected) so a freshly constructed Table renders with plain zebra striping.

func (*Table) Draw added in v0.7.0

func (t *Table) Draw(p painter.Painter, theme *Theme)

Draw paints the header + body + column separators through p using theme's palette. Widths for auto columns are computed here, so resizing the widget's Bounds() between frames re-flows the columns automatically.

type TableColumn added in v0.7.0

type TableColumn struct {
	Title string
	Width int // pixels; 0 = auto (equal share of remaining space)
}

TableColumn is one column definition: a header title + an optional fixed pixel Width. A Width of 0 marks the column as "auto" -- its width is computed at Draw time by evenly dividing the remaining pixel budget among all auto columns.

type TextView

type TextView struct {
	Base
	Lines      []string
	CursorLine int
	CursorCol  int
	Focused    bool
	OnChange   func()

	// Selection is the (start, end) range the host paints highlighted
	// + range-deletes via DeleteSelection / cut+paste via
	// CopySelection / CutSelection / Paste. An empty selection (Start
	// == End) means "no selection"; HasSelection() is the convenience
	// predicate.
	Selection Selection

	// Composition holds the in-progress IME preview string (dead-key
	// output, CJK candidate, …). Non-empty while an IME composition
	// is active; cleared on EventCompositionEnd. The Draw method
	// paints it in a muted colour at the cursor position — the
	// preview is NOT part of the buffer until the host commits via
	// EventChar. Widgets that read Lines/Text() see only committed
	// text, so downstream logic (search, syntax, autosave) never
	// operates on half-formed input.
	Composition string
}

TextView is the multi-line cousin of Entry. Lines are stored as a []string (one element per visible line); Cursor is a (line, col) position in rune coordinates. Wraps Entry's keyboard model with an added vertical axis (ArrowUp / ArrowDown / PageUp / PageDown).

This is the foundation a native wasmdesk editor builds on top of: syntax highlighting, search/replace and find can live above TextView without it growing those concerns. v0.3 ships the raw buffer; v0.4 will add a SelectionStart/End pair for range ops.

func NewTextView

func NewTextView(initial string) *TextView

NewTextView builds a TextView pre-loaded with initial text (split on "\n"). Empty initial text creates a single empty line so the cursor always has a row to live on.

func (*TextView) ClearSelection

func (t *TextView) ClearSelection()

ClearSelection collapses the selection to (CursorLine, CursorCol).

func (*TextView) CopySelection

func (t *TextView) CopySelection() string

CopySelection returns the selected text + leaves the buffer untouched. Wired to a host clipboard via the host (the toolkit has no global clipboard).

func (*TextView) CutSelection

func (t *TextView) CutSelection() string

CutSelection returns the selected text + removes it from the buffer.

func (*TextView) DeleteSelection

func (t *TextView) DeleteSelection()

DeleteSelection removes the selected text + parks the cursor at the deletion point. No-op when the selection is empty.

func (*TextView) Draw

func (t *TextView) Draw(p painter.Painter, theme *Theme)

Draw paints border + fill + every visible line + (when Focused) a 1-px vertical cursor stroke at the cursor's screen position.

Lines that would render past the bottom of the bounds are painted-but-clipped by the raster helpers; wrap in a ScrollView for proper scrollable behaviour.

func (*TextView) HasSelection

func (t *TextView) HasSelection() bool

HasSelection reports whether the TextView's selection covers > 0 characters.

func (*TextView) OnEvent

func (t *TextView) OnEvent(ev Event)

OnEvent dispatches the editing operations.

func (*TextView) Paste

func (t *TextView) Paste(text string)

Paste inserts text at the cursor (after first deleting the selection if any). "\n" splits lines.

func (*TextView) SelectAll

func (t *TextView) SelectAll()

SelectAll selects the entire buffer + parks the cursor at its end.

func (*TextView) SelectionText

func (t *TextView) SelectionText() string

SelectionText returns the selected substring, or "".

func (*TextView) SetSelection

func (t *TextView) SetSelection(sel Selection)

SetSelection records a new (start, end) selection without moving the cursor.

func (*TextView) SetText

func (t *TextView) SetText(s string)

SetText replaces the entire buffer + parks the cursor at (0,0).

func (*TextView) Text

func (t *TextView) Text() string

Text returns the buffer's concatenated content with "\n" line terminators. Mirrors strings.Join(Lines, "\n").

type Theme

type Theme struct {
	Background   RGBA
	Surface      RGBA
	SurfaceAlt   RGBA
	OnBackground RGBA
	OnSurface    RGBA
	Accent       RGBA
	Border       RGBA

	// Extra holds @define-color entries from GTK-source themes that don't
	// map to one of the canonical fields above (headerbar_bg_color,
	// success_color, ...). Populated by LoadGTKTheme; nil for code-built
	// themes. A host that needs a custom colour (e.g. for its window-
	// decoration painter) looks it up here without growing this struct
	// for every GTK colour name in the wild.
	Extra map[string]RGBA
}

Theme bundles every visual constant a widget needs to render itself. One Theme value cascades through every widget in an app, so swapping to a dark / Aqua / Fluxbox theme is a single assignment.

Field naming follows Material/Fluxbox conventions:

  • Background = the surface a widget sits on (panel/window body)
  • Surface = the widget's own filled body (button face, ...)
  • SurfaceAlt = a contrasting tone (hovered button, alternating row)
  • OnBackground / OnSurface = ink/text on those grounds
  • Accent = focus rings, the active-tab underline, the link colour
  • Border = a thin separator line drawn around or between surface regions

func DefaultDark

func DefaultDark() *Theme

DefaultDark is a low-contrast dark theme. Same shape as DefaultLight; used by themed wasmaqua apps + test coverage.

func DefaultLight

func DefaultLight() *Theme

DefaultLight is a low-stakes light theme used by tests + as the fall-through when an app doesn't supply its own. Numbers are the Fluxbox Light palette wasmbox's dock already uses, so a widget dropped into the dock without an explicit theme renders cleanly.

func LoadGTKTheme

func LoadGTKTheme(css string) (*Theme, error)

LoadGTKTheme parses a GTK theme source (the gtk.css or gtk-3.0/gtk.css or gtk-4.0/gtk.css that ships with a libadwaita / GTK3 theme) and returns a Theme that mirrors the theme's palette.

We recognise BOTH the GTK3 names (theme_bg_color / theme_fg_color / …) AND the libadwaita / GTK4 names (window_bg_color / accent_bg_color / …); when both are present the GTK4 name wins because it is the newer convention and a theme that defines both intends the GTK4 name as canonical. Unknown @define-color declarations are kept in the returned Theme's Extra map so themes that ship custom color names (e.g. "headerbar_bg_color" for a window-decoration painter) can still be looked up by a host without growing the canonical Theme struct.

Anything beyond @define-color (selectors, properties, gradients, image references) is ignored — the toolkit is a flat-paint compositor that only consumes solid RGBA values. We do not implement a full CSS parser for the same reason.

The mapping from GTK names to toolkit Theme fields:

GTK4 (preferred)         | GTK3 (fallback)          | Theme field
-------------------------|--------------------------|--------------
window_bg_color          | theme_bg_color           | Background
window_fg_color          | theme_fg_color           | OnBackground
view_bg_color            | theme_base_color         | Surface
view_fg_color            | theme_text_color         | OnSurface
card_bg_color            | insensitive_bg_color     | SurfaceAlt
accent_bg_color          | theme_selected_bg_color  | Accent
borders                  | borders                  | Border

Returns an error only if the input is empty (defensively) — malformed declarations are skipped, not fatal, so a real-world gtk.css with a stray syntax error still yields the rest of its palette.

func WhiteSurDark added in v0.9.3

func WhiteSurDark() *Theme

WhiteSurDark returns the WhiteSur dark palette as a Theme, the drop-in dark sibling of WhiteSurLight.

func WhiteSurLight added in v0.9.3

func WhiteSurLight() *Theme

WhiteSurLight returns the WhiteSur light palette as a Theme. It never fails (the embedded CSS is non-empty and well-formed), so unlike LoadGTKTheme it has no error return -- callers use it as a drop-in for DefaultLight.

type Timeline added in v0.9.0

type Timeline struct {
	Base
	Events []TimelineEvent
}

Timeline is a vertical event log — think a GitHub PR activity stream or a Discord message list. The widget draws a 1-px vertical rail on the left, one filled square marker per event on that rail, and the event's Title (+ optional Detail) rendered to the right of the marker.

Timeline is a passive display container — hit-testing / event routing are not implemented. A caller who wants click-to-focus walks the same layout math externally.

func NewTimeline added in v0.9.0

func NewTimeline(events []TimelineEvent) *Timeline

NewTimeline constructs a Timeline carrying the given events. A nil events slice is normalised to a non-nil empty slice so downstream code (range loops, len() checks) never has to guard for nil separately.

func (*Timeline) Draw added in v0.9.0

func (tl *Timeline) Draw(p painter.Painter, theme *Theme)

Draw paints the surface fill, the vertical rail line, one marker per event and each event's Title (+ optional Detail). The rail is painted BEFORE the markers so a marker overwrites the rail pixel where they intersect, giving the marker its full square silhouette without a separate clipping pass.

type TimelineEvent added in v0.9.0

type TimelineEvent struct {
	Title  string
	Detail string
	Kind   TimelineKind
}

TimelineEvent is one row in a Timeline's Events slice. Title is the always-visible headline; Detail is an optional second line rendered underneath in the dim Border ink (matching HeaderBar's subtitle convention). Kind drives the marker colour.

type TimelineKind added in v0.9.0

type TimelineKind int

TimelineKind selects the semantic colour of a timeline event's marker square. TimelineDefault reuses the theme's Accent so a neutral event matches the app's palette; the other three carry fixed shades — green for success, amber for warning, red for error — reusing the exact RGB tuples Alert already ships, so an Alert banner and a Timeline row read as the same colour language.

const (
	// TimelineDefault is a neutral event. Marker fill = Theme.Accent.
	TimelineDefault TimelineKind = iota
	// TimelineSuccess flags a completed step ("Deploy OK"). Green.
	TimelineSuccess
	// TimelineWarning flags a non-fatal event ("High latency"). Amber.
	TimelineWarning
	// TimelineError flags a failure ("Build failed"). Red.
	TimelineError
)

type Toast added in v0.8.0

type Toast struct {
	Base
	Text    string
	Kind    ToastKind
	Visible bool

	// Life is the number of Tick() calls remaining before the toast
	// auto-hides. The zero value is a sentinel meaning "sticky": Tick
	// is a no-op until the host assigns a positive Life. When Life is
	// positive, each Tick decrements it; when the countdown reaches
	// zero Visible is cleared.
	Life int
}

Toast is a short-lived, self-dismissing pill that slides in over the app's normal frame, holds for a few ticks, then hides itself. Distinct from Notification in three ways:

  1. Toast carries a Kind (like Alert) so the pill's fill colour conveys severity at a glance; Notification is always Accent.
  2. Toast's Life = 0 sentinel means "sticky" (do not auto-hide), letting a host post a persistent pill without a matching Life-budget assignment.
  3. Toast is designed to STACK: several Toast values can share the same host, each Bounds()'d to its own row; the host mutates Visible + Life directly and iterates Tick over the collection.

The host drives Life via Tick() from its own animation loop (typically a rAF tick).

func NewToast added in v0.8.0

func NewToast(text string, kind ToastKind) *Toast

NewToast builds a hidden Toast with the given text + kind. The host sets Visible=true (typically via a Show helper it wraps around the widget) + assigns Life to arm the auto-dismiss countdown.

func (*Toast) Draw added in v0.8.0

func (t *Toast) Draw(p painter.Painter, theme *Theme)

Draw paints the pill when Visible. Filled Kind-coloured panel with a 1-px Border stroke; Text in the accent-inverted ink so it stays legible against every Kind's face. Nothing drawn when hidden.

func (*Toast) Tick added in v0.8.0

func (t *Toast) Tick()

Tick decrements Life by 1 when Life is positive. When the countdown reaches 0 the toast auto-hides. Life == 0 is a sticky sentinel and leaves Visible untouched, so a host may post a persistent toast by leaving Life at its zero value.

type ToastKind added in v0.8.0

type ToastKind int

ToastKind selects the semantic colour of a Toast pill. ToastInfo reuses the theme's Accent (the same tint used by focus rings + the Notification banner); the other three carry hard-coded shades tuned for meaning -- green for success, amber for warning, red for error -- mirroring AlertKind so a Toast and an Alert with the same kind read as visual siblings.

const (
	// ToastInfo is a neutral heads-up ("Copied to clipboard"). Rendered
	// in Theme.Accent so it matches the app's own accent colour.
	ToastInfo ToastKind = iota
	// ToastSuccess signals a completed operation ("File uploaded"). Green.
	ToastSuccess
	// ToastWarning flags a non-fatal issue ("Battery low"). Amber.
	ToastWarning
	// ToastError signals a failure the user must address ("Network
	// unreachable"). Red.
	ToastError
)

type ToggleButton

type ToggleButton struct {
	Base
	Label    string
	Pressed  bool
	OnToggle func(pressed bool)
}

ToggleButton is a Button with a sticky on/off state. Click flips Pressed + fires OnToggle. Pressed = Theme.Accent face, unpressed = Theme.Surface; the label is rendered centered in the button.

func NewToggleButton

func NewToggleButton(label string, pressed bool) *ToggleButton

NewToggleButton constructs a ToggleButton with the given label + initial state.

func (*ToggleButton) Draw

func (t *ToggleButton) Draw(p painter.Painter, theme *Theme)

Draw paints the face + border + centred label.

func (*ToggleButton) OnEvent

func (t *ToggleButton) OnEvent(ev Event)

OnEvent: click flips Pressed + fires OnToggle.

type Toolbar

type Toolbar struct {
	Base
	Items   []ToolbarItem
	ButtonW int // default ToolbarButtonW
	ButtonH int // default ToolbarButtonH
	// contains filtered or unexported fields
}

Toolbar is a horizontal strip of square icon-buttons + optional separators. Each entry has a Label (used as the fallback glyph character), an optional Icon (drawn as an RGBA blit when non-empty), an OnClick callback + a Disabled flag.

Toolbar is the icon-strip that sits below a MenuBar; it composes cleanly with both Notebook + Statusbar so a "stock GTK" window can be assembled out of MenuBar + Toolbar + Notebook + Statusbar.

func NewToolbar

func NewToolbar(items []ToolbarItem) *Toolbar

NewToolbar builds a Toolbar with the given items.

func (*Toolbar) Draw

func (t *Toolbar) Draw(p painter.Painter, theme *Theme)

Draw paints the toolbar strip.

func (*Toolbar) OnEvent

func (t *Toolbar) OnEvent(ev Event)

OnEvent dispatches click events to the matching item.

type ToolbarItem

type ToolbarItem struct {
	Label    string
	Icon     []byte // optional ButtonW x ButtonH RGBA; nil = draw Label initial
	OnClick  func()
	Disabled bool

	// Separator, when true, draws a 1-pixel vertical divider instead of
	// a button. Label/Icon/OnClick are ignored.
	Separator bool
}

ToolbarItem is one cell in a Toolbar.

type Tooltip

type Tooltip struct {
	Base
	Text    string
	Visible bool
	Anchor  Rect // widget the tooltip belongs to; positions below it
}

Tooltip is a small text bubble shown near the cursor when the user hovers over a target widget. The host app drives Visible + Anchor (typically toggled by a mouse-enter/leave handler with a 500 ms delay); the toolkit's role is the rendering geometry.

Auto-sized to the Text width + 8 px horizontal padding + 6 px vertical padding; appears just below/right of (Anchor.X, Anchor.Y).

func NewTooltip

func NewTooltip(text string) *Tooltip

NewTooltip builds a hidden tooltip with the given text.

func (*Tooltip) Draw

func (t *Tooltip) Draw(p painter.Painter, theme *Theme)

Draw paints the bubble when Visible.

func (*Tooltip) Hide

func (t *Tooltip) Hide()

Hide removes the tooltip from view.

func (*Tooltip) Show

func (t *Tooltip) Show(anchor Rect)

Show makes the tooltip visible, anchored to the given widget rect.

type TreeNode

type TreeNode struct {
	Label    string
	Expanded bool
	Children []*TreeNode

	// Anything the host wants to associate with this node (typically a
	// path, an id, or the model object). The toolkit doesn't read it.
	Data any
}

TreeNode is one entry in a TreeView. Children are nested arbitrarily deep; Expanded controls whether the children are rendered.

type TreeView

type TreeView struct {
	Base
	Root       *TreeNode
	Selected   *TreeNode
	OnActivate func(node *TreeNode)
	RowHeight  int // default 18
	// contains filtered or unexported fields
}

TreeView renders a hierarchical TreeNode set as indented rows. Click on a row's ▶/▼ chevron toggles Expanded; click anywhere else on the row selects it + fires OnActivate with the clicked node.

Use for file browsers, settings hierarchies, JSON inspectors, outline views.

func NewTreeView

func NewTreeView(root *TreeNode) *TreeView

NewTreeView builds a TreeView rooted at root (which may be nil for an empty initial view).

func (*TreeView) Draw

func (t *TreeView) Draw(p painter.Painter, theme *Theme)

Draw paints every visible row.

func (*TreeView) OnEvent

func (t *TreeView) OnEvent(ev Event)

OnEvent: a click on the chevron toggles Expanded; a click anywhere else on the row selects the node + fires OnActivate.

type VBox

type VBox struct {
	Base
	// Spacing is the gap in pixels between adjacent children; same
	// semantics as HBox.Spacing.
	Spacing int
	// contains filtered or unexported fields
}

VBox is the vertical analogue of HBox: children stack top-to-bottom, sharing the container's height + filling its width.

func NewVBox

func NewVBox() *VBox

NewVBox constructs an empty VBox.

func (*VBox) Append

func (v *VBox) Append(w Widget)

Append adds w below any existing children + re-runs layout.

func (*VBox) Draw

func (v *VBox) Draw(p painter.Painter, theme *Theme)

Draw paints every child in append order.

func (*VBox) OnEvent

func (v *VBox) OnEvent(ev Event)

OnEvent hit-tests + forwards just like HBox.

func (*VBox) SetBounds

func (v *VBox) SetBounds(r Rect)

SetBounds positions the VBox + stacks its children vertically.

type ViewSwitcher added in v0.8.0

type ViewSwitcher struct {
	Base
	Views    []string
	Current  int
	OnChange func(i int)
}

ViewSwitcher is a libadwaita/GTK-style horizontal segmented tab picker: an evenly-divided strip of same-width segments where exactly one is highlighted as the active view. Clicking a segment swaps Current and fires OnChange with the new index.

The strip's background is Theme.SurfaceAlt; the active segment paints in Theme.Accent with the accent-inverted ink (theme.Extra["OnAccent"] with a Theme.Background fallback, matching what Button, ListBox, TreeView, and Table already do). A 1-pixel Theme.Border line sits along the strip's bottom edge so the switcher reads as a discrete band above the switched content.

A ViewSwitcher with no Views paints only the background + bottom border; clicks are ignored. This lets a caller assemble the widget before it knows which views the app will surface without tripping a nil-Views guard downstream.

func NewViewSwitcher added in v0.8.0

func NewViewSwitcher(views []string, current int) *ViewSwitcher

NewViewSwitcher constructs a ViewSwitcher over views with the initial highlighted segment at current. current is clamped into the [0, len(views)-1] range, or forced to 0 when views is empty, so the widget is never in a hard-to-reason "index out of range" state.

func (*ViewSwitcher) Draw added in v0.8.0

func (v *ViewSwitcher) Draw(p painter.Painter, theme *Theme)

Draw paints the strip background, then each segment with the active one highlighted in Theme.Accent, then the 1-pixel bottom border. Segments share the same width via integer division of Bounds.W by len(Views); any left-over pixel column on the right remains SurfaceAlt (this matches how HeaderBar's title strip tolerates non-integer central strips).

func (*ViewSwitcher) OnEvent added in v0.8.0

func (v *ViewSwitcher) OnEvent(ev Event)

OnEvent handles a click by locating which segment the X coordinate lands on and updating Current + firing OnChange. Non-click events, clicks with an empty Views slice, clicks on a zero-width strip and clicks that fall outside every segment are all no-ops.

type Widget

type Widget interface {
	// Bounds returns the widget's placement within its parent surface.
	// Used by containers for hit-testing + relative-coordinate translation.
	Bounds() Rect

	// SetBounds updates the placement. Containers call this during
	// layout to position children.
	SetBounds(r Rect)

	// Draw paints the widget onto the Painter using the supplied
	// theme. The Painter's back-end decides whether the primitives
	// land as pixels (browser canvas, native window, image file) or
	// cells (terminal grid). Widgets MUST NOT draw outside their
	// Bounds() rectangle.
	Draw(p painter.Painter, theme *Theme)

	// HitTest reports whether (px, py) (in surface coordinates) falls
	// on a sensitive part of the widget. Most widgets just return
	// Bounds().Contains(px, py); transparent or overlapping widgets
	// may return false even within their bounds.
	HitTest(px, py int) bool

	// OnEvent delivers an input event whose X/Y are WIDGET-LOCAL.
	// The widget mutates its internal state + may schedule a redraw
	// (the caller is responsible for invoking Draw again).
	OnEvent(ev Event)
}

Widget is the toolkit's single core abstraction. Every widget -- Button, Label, TextInput, HBox, ScrollView, ... -- implements it. Containers themselves are widgets too: a VBox passes Draw / OnEvent to its children after offsetting coordinates by the child's Rect.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL