TinyGUI is a minimal widget toolkit for TinyGo targets that emphasizes deterministic rendering, limited allocations, and compatibility with microcontroller displays. The library organizes UI construction around three core abstractions: Widget, Context, and Container. Widgets encapsulate drawable entities with a small, testable API. Contexts provide drawing state and access to a drivers.Displayer, while containers sequence widgets and handle focus/interaction routing.
- Support resource-constrained devices by avoiding heap churn and reflection-heavy patterns.
- Keep rendering synchronous and deterministic to maintain predictable frame timings.
- Provide composable building blocks (widgets, layouts, containers) that can be extended without modifying core code (Open/Closed principle).
- Allow projects to mix rendering backends that implement
drivers.Displayerwith optional accelerated drawing interfaces (e.g.,RectangleDisplayer,LineDisplayer). - Enable command-driven interaction (serial, button matrices) that maps to a small set of
UserCommandvalues.
Widget interface (widget.go)
Draw(ctx Context)renders widget content using the cloned context calculated by its parent container.Interact(UserCommand)allows widgets to react to focused input; defaults provided byWidgetBase.- Metadata: parent pointer, width/height, selection flag. Sizing is static to avoid layout recalculations at runtime.
- Optional capability interfaces keep responsibilities explicit and opt-in only:
Selectablemarks widgets that participate in focus/navigation.VisibleHandlerreacts to visibility toggles (containers call it only when state changes).ScrollHandlerreceives scroll offset changes.SelectHandlernotifies widgets when they become (or cease to be) the selected entry.ExitHandlerfires when the navigator exits an item (e.g. user presses BACK).EnableStatelets wrappers exposeEnabled()so navigators can skip disabled entries without extra bookkeeping.
WidgetBase
- Convenience struct that implements parent tracking, sizing, and selection state.
- Default
Interacthandles escape by deselecting itself; other commands fall through to the container.
Container package (container/)
Base[T]embedsui.WidgetBaseand handles selection, activation, idle timeouts, and child traversal for any widget slice. It mirrors Fyne’s composable containers but trims allocations for MCU constraints.- Options (
WithLayout,WithChildren,WithPadding,WithMargin,WithTimeout) configure containers declaratively so constructors stay lean and intent remains explicit. Baseemits opt-in events automatically:VisibleHandler,SelectHandler,ExitHandler, andScrollHandlerare invoked only when attached widgets implement them.- Padding/margin offsets adjust the child context before layouts run so nested containers can respect spacing without hand-rolled coordinate tweaks.
ScrollcomposesBase[ui.Widget]with scroll offsets. It only draws visible children, leaving parent contexts untouched while notifying observers of offset changes.ScrollChange/ScrollObserverlet higher-level widgets (e.g., navigable lists) synchronise scrolling with focus changes.- Planned:
ScrollChoicewill build onScrolland reusewidget.InteractiveSelector[ui.Widget]so selection and viewport adjustments stay in sync. The constructor mirrorsScroll(layout + children), while options expose index binding, change callbacks, and auto-scrolling policies. Selection commands (UP,DOWN,NEXT,PREV, wraparound) are delegated to the shared selector, ensuring behavioural parity with widget-level choices but at container scope. - Tab/pager components will layer on top by composing
Baseand wiring into the navigator.
Layout package (layout/)
- Provides static
Strategyfunctions (HList,VList,Grid,HFlow,VFlow) that mutate contexts between child draws. - Layouts are fixed at construction to keep behaviour deterministic; dynamic layouts can compose on top without modifying core data structures.
Context implementations (context.go)
ContextImplholds the display handle, dimensions, and drawing origin.Cloneproduces a child context for nested widgets while maintaining absolute display coordinates.RandomContextperiodically shifts the drawing origin within the physical display bounds to mitigate OLED burn-in. ReusesContextImplcloning logic.
Drawing helpers (drawing.go)
- Provides fallbacks for drawing lines and rectangles when optimized interfaces are unavailable.
- Integrates PNG decoder via
tinygo.org/x/drivers/image/pngwith a callback-based renderer that streams decoded pixels toBitmapDisplayer.
Label,MultilineLabel, andLogsupport text rendering viatinyfont, using closures for dynamic content.Gauge[T]covers horizontal/vertical progress displays, binding directly to mutable value pointers without additional callbacks.IconwrapsDrawPngto render embedded images.Separatorrenders horizontal or vertical rules based on its dimensions, reusing accelerated displayer paths when available.- Widget constructors encapsulate size configuration, ensuring deterministic layout footprints.
- Text widgets own their font and color configuration so they remain self-contained and theme-ready.
- Interactive widgets (e.g., toggle/selector) encapsulate their behaviour by accepting getter/setter callbacks, enabling focus-driven state changes without direct hardware coupling.
InteractiveLabelembeds aLabel, annotates text with ▲/▼ while selected, and edits pointer-backed values using opt-in options (WithValue,WithRange,WithSteps, etc.) without extra allocation.InteractiveIconembedsIcon, cycling through preloaded PNGs on directional commands while optionally mirroring an external index for deterministic state.Bitmap16/Bitmap8reuse a genericBitmapBase[T]to stream raw pixel buffers (RGB565 or 8-bit) via the accelerated bitmap interfaces without extra allocations.InteractiveLabelChoicerenders string options through aLabelwhile delegating navigation to the shared selector, allowing index binding and change callbacks.InteractiveWidgetChoice[T]accepts arbitrary widgets, forwarding selection and drawing the active child while exposing the generic selector for commit/cancel coordination.ScrollChoiceexposes scrollable collections at container scope, delegating index changes to a sharedInteractiveSelectorand auto-scrolling to keep the focused child visible.InteractiveIconChoicerenders image identifiers viaIcon, reusing the same selector plumbing to keep interaction logic opt-in.MultilineLabel/Logshare a configurable base (MultilineOrder, font, colour) while interactive variants add scrollable history views.HorizontalInteractiveGauge/VerticalInteractiveGaugecompose the gauge displays for single values, while multi-value variants wrapHorizontalMultiGauge/VerticalMultiGaugeto provide segment navigation (ENTER to advance, BACK/ESC to revert) and option-driven configuration.- Phase 2 adds new composites:
Toggleand future selectors implementSelectable/EnableStateto opt into navigation.Tabwidgets encapsulate tab headers, track active state, and forward activation commands to associated pages.- Scrolling-aware widgets (log, multiline labels) leverage
container.Scrollto redraw only visible lines and can opt intoScrollHandlerfor fine control.
CommandStreamMuxparses newline-delimited commands from anio.Reader, dispatching to registered callbacks without extra allocations. Designed for serial command channels or scripting interfaces.SerialReaderadaptsmachine.Serialertoio.Reader, reading bytes while respecting buffered availability.
i2cscan: simple utility leveraging TinyGo drivers to enumerate I2C devices.png2bin: converts PNG/JPEG assets into Go source arrays (RGB565) suitable for embedding; reinforces image handling workflow forIconwidgets.
- Watchdog support is abstracted via build tags (
watchdog_rp2040.go,watchdog_esp32.go), enabling button polling to keep watchdog timers alive without coupling UI logic to specific targets. - The UI event layer expects button input via
PeekButton, with long-press detection returning duration thresholds to map intoUserCommandvariants (e.g.,LONG_UP).
- Application creates a root
Contextwith a displayer instance and desired viewport dimensions. - Containers draw child widgets sequentially, cloning contexts to adjust origins.
- Layout callbacks mutate context positions between draws, establishing simple flow layouts.
- After draw pass, application calls the underlying display’s
Display()(outside library) to flush.
- Input devices map hardware actions to
UserCommandvalues. - Root container receives
Interactcalls with commands. - Inactive containers use
NEXT/PREVto change selection;ENTERactivates the focused child. - Active child widget processes commands;
ESCor inactivity timeout returns focus to parent.
- Phase 1 adds a dedicated
Navigatorthat manages a stack ofNavigablewidgets (containers, tabs, scroll panes). Navigation commands update this stack and emit focus/activation events mirroring SurroundAmp semantics. - The navigator exposes a device-independent API (
Focus,Next,Prev,Enter,Back,WalkPath) so encoders, buttons, or scripted command streams can drive traversal without coupling to specific widgets. - Selection change events bubble via observer interfaces, enabling backlight control, logging, or persistence of the active menu path.
- Phase 2 integrates scroll commands (
SCROLL_UP,SCROLL_DOWN, etc.) so navigator-aware containers adjust viewports while maintaining predictable focus. Layout negotiation metadata (MinSize,PreferredSize) will let containers respect widget sizing hints before scrolling.
- Developers can implement new widgets by embedding
WidgetBaseand providingDraw/Interact. - Custom layouts: supply a
layout.Strategyclosure to apply grid, flex, or absolute positioning. - Alternative contexts: embed additional behavior (double-buffering, clipping) by implementing
Context. - Rendering acceleration: add interfaces similar to
RectangleDisplayerto leverage hardware features.
- Layout system only offers simple sequential positioning;
layout.Gridis a stub. - Focus management is container-centric; nested containers require manual orchestration for complex navigation trees.
- Widget sizing is entirely static—no content measurement or adaptive layout.
- Legacy gauges now replaced by generic pointer-driven
Gauge[T]to guarantee dynamic updates without closure indirection. PeekButtonsleeps for fixed intervals and busy-waits, which may block cooperative scheduling on some targets.- Lack of formal theme/styling abstraction; colors/fonts are set per widget.
- No integration with asynchronous data sources or event pumps; applications poll and redraw manually.
- Maintain lightweight footprint compatible with TinyGo and microcontroller constraints.
- Gradually expand layout and widget set while preserving deterministic behavior.
- Introduce configuration patterns (options structs, interfaces) instead of inheritance for future features.
- Encourage asynchronous-safe APIs (non-blocking input, rendering triggers) to match embedded runtime patterns.
- Navigation is device-independent: hardware adapters push abstract
UserCommands into the navigator, and focus/activation changes propagate via callback interfaces instead of concrete types. - Clipping discipline: containers calculate child bounds before draw; if a child lies outside the current viewport it is skipped, with navigator still tracking it for structural completeness.
- Status tracking: navigators emit structured events so applications can persist and restore menu paths, ensuring back-compat for existing TinyGUI apps while unlocking advanced menu flows.
- Layout extensibility: upcoming
LayoutOptionscarry alignment, spacing, and wrapping hints so containers can compose complex grids without bespoke logic. - Scroll performance: dirty-region tracking combines with viewport calculations to limit redraw to visible content, keeping frame times stable on constrained MCUs.
This document captures the current structure to inform future planning (PLAN.md) and ensure subsequent enhancements remain consistent with the library’s guiding principles.
- Option helpers (
InteractiveOption[T]) provide a shared configuration surface (WithValue,WithRange,WithSteps,WithForeground, etc.), keeping constructors minimal and aligned with the opt-in capability philosophy. - Scroll-aware choice containers reuse the shared selector plumbing to reduce duplication between list widgets and higher-level navigable containers, keeping focus rules consistent across the stack.