ui

package
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package ui is Nimbus Native's component layer: apps describe their screens as Go components, and the runtime keeps real native views (UIKit, Android Views) in step with them.

Everything happens on one goroutine, the UI loop: host events, state updates, rendering, layout and effects. A render reconciles the new element tree against the one on screen, lays it out with the layout package, and sends the differences to the host as one render.Commit.

Index

Constants

View Source
const (
	Column = layout.Column
	Row    = layout.Row

	JustifyStart        = layout.JustifyStart
	JustifyCenter       = layout.JustifyCenter
	JustifyEnd          = layout.JustifyEnd
	JustifySpaceBetween = layout.JustifySpaceBetween
	JustifySpaceAround  = layout.JustifySpaceAround
	JustifySpaceEvenly  = layout.JustifySpaceEvenly

	AlignStretch = layout.AlignStretch
	AlignStart   = layout.AlignStart
	AlignCenter  = layout.AlignCenter
	AlignEnd     = layout.AlignEnd
)

Variables

View Source
var (
	LightTheme = Theme{
		Background: Hex("#f9fafb"), Surface: Hex("#ffffff"), Text: Hex("#111827"), TextMuted: Hex("#6b7280"),
		Border: Hex("#e5e7eb"), Primary: Hex("#2563eb"), OnPrimary: Hex("#ffffff"), Danger: Hex("#dc2626"),
		Bar: Hex("#ffffff"), BarText: Hex("#111827"),
		Radius: 12, Title: 24, Heading: 17, Body: 15, Caption: 13, SpaceS: 8, SpaceM: 12, SpaceL: 20,
	}
	DarkTheme = Theme{
		Dark:       true,
		Background: Hex("#0b0f17"), Surface: Hex("#151b26"), Text: Hex("#f3f4f6"), TextMuted: Hex("#9ca3af"),
		Border: Hex("#273041"), Primary: Hex("#3b82f6"), OnPrimary: Hex("#ffffff"), Danger: Hex("#f87171"),
		Bar: Hex("#151b26"), BarText: Hex("#f3f4f6"),
		Radius: 12, Title: 24, Heading: 17, Body: 15, Caption: 13, SpaceS: 8, SpaceM: 12, SpaceL: 20,
	}
)

LightTheme and DarkTheme are the built-in themes.

View Source
var Once any = once{}

Once as an Effect dependency runs the effect only after the first render.

View Source
var RestoreRoute func(path string) (title string, screen Component, ok bool)

RestoreRoute turns a path back into a screen. router.Register sets it.

View Source
var StaleAfter = 30 * time.Second

StaleAfter is how old cached query data may be before a component that mounts with it fetches again in the background.

Functions

func Effect

func Effect(c *Ctx, fn func(ctx context.Context) func(), deps ...any)

Effect runs fn after a render that changed deps: every render if no deps are given, only the first with ui.Once. fn's context is cancelled — and its returned cleanup called — before it runs again and when the component leaves the screen. Effects run on the UI goroutine; start your own goroutine for slow work and set State from it.

func Main

func Main()

Main is for the app's main function: func main() { ui.Main() }. On a device the host owns the process and main never runs; under `nimbus native dev` the app runs on the laptop and Main serves the device until it reloads.

func Memo

func Memo[T any](c *Ctx, fn func() T, deps ...any) T

Memo returns fn's result, computing it again only when deps change.

func Mount

func Mount(root Component)

Mount makes root the app's UI. Call it from init; the UI loop starts when the host starts the runtime.

func Reducer

func Reducer[T any](c *Ctx, initial T) (T, func(func(T) T))

Reducer is State for values updated by a function of the old value: update(func(n int) int { return n + 1 }) never loses an increment, even when called many times before the next render.

func Ref

func Ref[T any](c *Ctx, initial T) *T

Ref is a value that survives re-renders without causing them.

func RefetchQuery

func RefetchQuery(c *Ctx, key string)

RefetchQuery fetches the query with this key again (after a change, say), re-rendering every component that shows it.

func SessionValue

func SessionValue(c *Ctx, key any) any

SessionValue is the value the session was started with under key (see SessionOptions.Values), or nil on a device.

func SetTheme

func SetTheme(light, dark Theme)

SetTheme replaces the light and dark themes. Call it from init, next to Mount.

func State

func State[T any](c *Ctx, initial T) (T, func(T))

State is a value that survives re-renders. Calling set — from any goroutine — stores the new value and schedules a render.

Types

type Action

type Action[A, R any] struct {
	Run     func(A) // starts the action (ignored while it runs)
	Running bool
	Err     error // the last run's error
	Result  R     // the last successful run's result
}

Action is a UseAction's state.

func UseAction

func UseAction[A, R any](c *Ctx, do func(ctx context.Context, arg A) (R, error), onDone ...func(R)) Action[A, R]

UseAction runs a write — placing an order, saving a profile — off the UI goroutine and re-renders with its progress. onDone, if given, runs on the UI goroutine after a success (e.g. to RefetchQuery or close a modal).

place := ui.UseAction(c, func(ctx context.Context, o NewOrder) (Order, error) {
	return api.Call(ctx, client, shared.CreateOrder, o)
}, func(Order) { ui.RefetchQuery(c, "orders"); modals.Dismiss() })

type Align

type Align = layout.Align

Layout vocabulary, re-exported so apps only import ui.

type Color

type Color uint32

Color is 0xRRGGBBAA. The zero Color means "unset".

func Hex

func Hex(s string) Color

Hex parses "#rgb", "#rrggbb" or "#rrggbbaa". It panics on bad input, so use it for constants.

func RGB

func RGB(r, g, b uint8) Color

RGB is an opaque color.

type Component

type Component interface {
	Render(c *Ctx) *Element
}

Component is a reusable piece of UI. Its fields are its props; Render runs on every render and may call hooks (State, Effect, Memo, Ref) on c.

type Counter struct{ Start int }

func (p Counter) Render(c *ui.Ctx) *ui.Element {
	n, setN := ui.State(c, p.Start)
	return ui.Pressable(func() { setN(n + 1) }, ui.Style{},
		ui.Text(fmt.Sprint(n), ui.Style{}))
}

type Ctx

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

Ctx is a component instance's handle for hooks. Hooks must be called in the same order on every render (never inside if or for).

type Direction

type Direction = layout.Direction

Layout vocabulary, re-exported so apps only import ui.

type Edges

type Edges = layout.Edges

Layout vocabulary, re-exported so apps only import ui.

func All

func All(v float32) Edges

All is the same length on every side.

func XY

func XY(x, y float32) Edges

XY is horizontal and vertical lengths: XY(16, 8) pads 16 left and right, 8 top and bottom.

type Element

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

Element describes one piece of UI: a native view, or a component that renders to one. Elements are cheap values, rebuilt on every render; the runtime compares them with what is on screen and changes only what differs.

func C

func C(comp Component) *Element

C places a component in the tree.

func Image

func Image(url string, s Style) *Element

Image shows a picture from a URL. Give it a size: it has none of its own.

func List

func List(count int, item func(i int) *Element, s Style) *Element

List shows count items in a vertical scroll view, but only creates native views for the items on screen and about one screen above and below. Items may differ in height: each is measured once it has been shown.

ui.List(len(orders), func(i int) *ui.Element {
	return ui.C(OrderRow{orders[i]})
}, ui.Flex(1)).ItemKey(func(i int) string { return orders[i].ID })

Scrolling renders no components but the items coming into view.

func Pressable

func Pressable(onPress func(), s Style, children ...*Element) *Element

Pressable is a container that calls onPress when tapped, dimming while pressed.

func SafeArea

func SafeArea(s Style, children ...*Element) *Element

SafeArea is a View padded by the screen's safe-area insets — the notch or status bar, the home indicator, rounded corners — on top of any padding in s, so its children stay clear of them. Inside a Stack the navigation bar already covers the top. Inside Tabs the bottom is padded clear of the tab bar instead (iOS tabs run under it); a Stack in a tab already stops above the bar. While the keyboard is up, the bottom pads clear of it.

ui.SafeArea(ui.Style{Padding: ui.All(16)}, ui.Text("Hello", ui.Style{}))

func Scroll

func Scroll(s Style, children ...*Element) *Element

Scroll scrolls its children vertically (Horizontal() for sideways). Give it a size or Flex: its content is as long as its children.

func Spinner

func Spinner(s Style) *Element

Spinner is the platform's activity indicator.

func Stack

func Stack(title string, root Component, s Style) *Element

Stack is a navigation stack: screens shown one at a time under the platform's navigation bar, with its push and pop transitions and back gestures. root is the first screen. Components inside it push and pop screens through Navigation.

ui.Stack("Orders", OrdersScreen{}, ui.Flex(1))

func Switch

func Switch(on bool, onChange func(bool), s Style) *Element

Switch is the platform's on/off toggle, colored with the theme's Primary (or s.Color). onChange gets the new state; pass nil to disable it.

notify, setNotify := ui.State(c, true)
ui.Switch(notify, setNotify, ui.Style{})

func Tabs

func Tabs(s Style, tabs ...Tab) *Element

Tabs is the platform's tab bar with a screen per tab: UITabBarController on iOS, a bottom navigation bar on Android. Every tab stays mounted, so switching keeps each tab's state, scroll position and navigation stack.

ui.Tabs(ui.Flex(1),
	ui.Tab{Title: "Orders", Icon: "list.bullet", Screen: OrdersTab{}},
	ui.Tab{Title: "Account", Icon: "person", Screen: AccountTab{}})

func Text

func Text(text string, s Style) *Element

Text shows a string.

func TextField

func TextField(f Field, o FieldOptions) *Element

TextField is a labelled input for a form Field, with its error below.

ui.TextField(form.Field("email", func(s *Signup) *string { return &s.Email }),
	ui.FieldOptions{Label: "Email", Keyboard: render.KeyboardEmail})

func TextInput

func TextInput(value string, onChange func(string), s Style) *Element

TextInput is a single-line text field showing value; onChange gets every edit. Keep value in State and set it from onChange.

func View

func View(s Style, children ...*Element) *Element

View is a container: a flex column unless its style says otherwise.

func (*Element) EstimatedItemHeight

func (e *Element) EstimatedItemHeight(h float32) *Element

EstimatedItemHeight is the height a List assumes for items it has not shown yet (default: the average of the items it has shown).

func (*Element) Fit

func (e *Element) Fit(f render.Fit) *Element

Fit sets how an image fills its frame.

func (*Element) Horizontal

func (e *Element) Horizontal() *Element

Horizontal makes a Scroll scroll sideways.

func (*Element) Href

func (e *Element) Href(path string) *Element

Href is where a Pressable leads, for hosts with addresses: the web host makes it a link crawlers and new tabs can follow. Taps still run its handler. router.Link sets it.

func (*Element) ItemKey

func (e *Element) ItemKey(key func(i int) string) *Element

ItemKey names a List's items, so an item keeps its native views and measured height when the data changes order. Without it, items are keyed by index.

func (*Element) Key

func (e *Element) Key(k string) *Element

Key identifies an element among its siblings, so the runtime can keep its native view and state when the list changes order.

func (*Element) Keyboard

func (e *Element) Keyboard(k render.Keyboard) *Element

Keyboard picks a text input's soft keyboard.

func (*Element) Label

func (e *Element) Label(l string) *Element

Label is the accessibility label screen readers speak. On a component, it labels what the component renders.

func (*Element) Multiline

func (e *Element) Multiline() *Element

Multiline lets a TextInput wrap and take new lines; it is three lines tall unless its style sets a height.

func (*Element) OnRefresh

func (e *Element) OnRefresh(refreshing bool, onRefresh func()) *Element

OnRefresh adds pull to refresh to a Scroll or List: pulling calls onRefresh, and the indicator shows while refreshing is true. With a query:

ui.List(n, row, ui.Flex(1)).OnRefresh(q.Loading, q.Refetch)

func (*Element) Placeholder

func (e *Element) Placeholder(p string) *Element

Placeholder sets a text input's hint.

func (*Element) Secure

func (e *Element) Secure() *Element

Secure hides a text input's characters.

func (*Element) TestID

func (e *Element) TestID(id string) *Element

TestID names the element for UI tests (accessibilityIdentifier on iOS, the view tag on Android). On a component, it names what the component renders.

type Field

type Field struct {
	Name     string
	Value    string
	Error    string
	OnChange func(string)
}

Field is one bound text field: its value, the error to show (empty until the user has touched it or tried to submit), and its change handler.

type FieldErrors

type FieldErrors map[string][]string

FieldErrors are validation errors by field name — the shape Nimbus's validation.ValidationErrors has, so server rules can validate app forms.

func FieldErrorsOf

func FieldErrorsOf(err error) FieldErrors

FieldErrorsOf reads field errors out of a validation error — a FieldErrors, Nimbus's validation.ValidationErrors (for example from a server's 422 response), or any error with ToMap() map[string][]string. It is nil when err carries none.

func (FieldErrors) Error

func (e FieldErrors) Error() string

Error lets FieldErrors be returned as an error.

type FieldOptions

type FieldOptions struct {
	Label       string
	Placeholder string
	Keyboard    render.Keyboard
	Secure      bool
	Multiline   bool  // several lines, three tall unless Style sets a height
	Style       Style // for the input box
}

FieldOptions configure a TextField.

type Form

type Form[T any] struct {
	// contains filtered or unexported fields
}

Form is a form's state: its value, which fields the user has touched, and the validation errors. Get one with UseForm.

func UseForm

func UseForm[T any](c *Ctx, initial T, validate func(T) error) *Form[T]

UseForm keeps a form's value in state and validates it on every change. validate returns nil when the value is valid; field errors come from a FieldErrors, from Nimbus's validation.ValidationErrors, or from any error with ToMap() map[string][]string. Pass validation.ValidateStruct to share your server's rules:

form := ui.UseForm(c, Signup{}, validation.ValidateStruct)

func (*Form[T]) Errors

func (f *Form[T]) Errors() FieldErrors

Errors are all field errors, touched or not.

func (*Form[T]) Field

func (f *Form[T]) Field(name string, at func(*T) *string) Field

Field binds one text field of the value: name is the validation key (as in the rules, usually the json name) and at points at the field.

func (*Form[T]) FormError

func (f *Form[T]) FormError() error

FormError is the error that belongs to no single field: from validation, or from SetErrors (a server or network failure).

func (*Form[T]) SetErrors

func (f *Form[T]) SetErrors(err error)

SetErrors shows errors a server returned for the value — say, the field errors in a Nimbus 422 — on their fields, until each field changes. nil clears them. Safe from any goroutine, so an action can call it directly.

func (*Form[T]) Submit

func (f *Form[T]) Submit(onValid func(T)) func()

Submit returns a handler for the submit button: when the value is valid it calls onValid with it; otherwise every field shows its error.

func (*Form[T]) Update

func (f *Form[T]) Update(change func(*T))

Update changes the value; it re-renders and re-validates.

func (*Form[T]) Valid

func (f *Form[T]) Valid() bool

Valid reports whether the value passes validation.

func (*Form[T]) Value

func (f *Form[T]) Value() T

Value is the form's current value.

type Justify

type Justify = layout.Justify

Layout vocabulary, re-exported so apps only import ui.

type ModalNav

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

ModalNav presents and dismisses modals: screens shown over everything — a sheet on iOS (swipe down to close), a full-screen layer on Android (back to close). Each modal is its own navigation stack with a title bar, so Navigation works inside it. Safe from any goroutine.

func Modals

func Modals(c *Ctx) *ModalNav

Modals presents and dismisses modals from any component.

func (*ModalNav) Count

func (m *ModalNav) Count() int

Count is how many modals are open.

func (*ModalNav) Dismiss

func (m *ModalNav) Dismiss()

Dismiss closes the modal the component is in, or else the topmost one.

func (*ModalNav) Present

func (m *ModalNav) Present(title string, screen Component)

Present shows screen in a new modal titled title, over everything else.

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

Nav pushes and pops the screens of the Stack a component is in. Its methods are safe from any goroutine; changes show on the next render.

func Navigation(c *Ctx) *Nav

Navigation is the Stack the component is shown in, or nil outside one.

func (n *Nav) Depth() int

Depth is how many screens are on the stack.

func (n *Nav) Pop()

Pop goes back one screen. The root screen stays.

func (n *Nav) PopToRoot()

PopToRoot goes back to the first screen.

func (n *Nav) Push(title string, screenComp Component)

Push shows screen on top, titled title, with the platform's transition.

func (n *Nav) PushPath(title, path string, screenComp Component)

PushPath is Push for a screen with a URL path (router.Push uses it), so a dev reload can open the screen again.

type Query

type Query[T any] struct {
	Data    T
	Loading bool  // a fetch is running (Data may still hold the last result)
	Loaded  bool  // Data holds a result
	Err     error // the last fetch's error, if it failed
	Refetch func()
}

Query is a UseQuery's state.

func UseQuery

func UseQuery[T any](c *Ctx, key string, fetch func(ctx context.Context) (T, error)) Query[T]

UseQuery loads data off the UI goroutine and re-renders when it arrives. Results are cached by key and shared: components using the same key make one request and see the same data. Cached data shows at once and is fetched again in the background when older than StaleAfter.

q := ui.UseQuery(c, "orders", func(ctx context.Context) ([]Order, error) {
	return api.Call(ctx, client, shared.ListOrders, shared.ListOrdersReq{})
})
if q.Loading && !q.Loaded { return ui.Spinner(ui.Style{}) }

type Session

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

Session is a UI runtime that is not the device's: the web host runs one per browser tab. It speaks the same render protocol as the phone hosts: Receive takes host messages, and SessionOptions.Send gets commits.

func NewSession

func NewSession(root Component, o SessionOptions) *Session

NewSession starts a runtime for root. It renders once the host sends a render.Viewport.

func (*Session) Close

func (s *Session) Close()

Close stops the session: effects stop, and its loop ends.

func (*Session) Do

func (s *Session) Do(f func())

Do runs f on the session's UI loop and renders what it changed.

func (*Session) Receive

func (s *Session) Receive(msg []byte)

Receive handles an encoded message from the host.

func (*Session) Settle

func (s *Session) Settle(timeout time.Duration) bool

Settle waits, up to timeout, until the UI stops changing: no queued work and no query still loading. Server rendering uses it to send a page with its data, not a spinner. It reports whether the session settled.

type SessionOptions

type SessionOptions struct {
	// Send gets each render.Commit, encoded.
	Send func(commit []byte)
	// Style, when set, leaves layout to the host (a browser's CSS
	// flexbox): no Frame ops are sent, Style gets each view's layout style
	// whenever it changes, and Lists render every item.
	Style func(tag uint32, s layout.Style)
	// Values are what SessionValue returns to components.
	Values map[any]any
}

SessionOptions configure a Session.

type Store

type Store[T any] struct {
	// contains filtered or unexported fields
}

Store is state the whole app shares, such as a cart or the signed-in user: one value per app on a device, and one per browser tab on the web, where every visitor's UI runs in the same server. Declare it once, at package level:

var Cart = ui.NewStore(func() CartState { return CartState{} })

Components read it with Use, which re-renders them when it changes, and change it from handlers, effects and actions with Update:

cart := Cart.Use(c)
ui.Pressable(func() { Cart.Update(c, func(s CartState) CartState { return s.Add(p) }) }, …)

Keep T a value: Update gets the current one and returns the next.

func NewStore

func NewStore[T any](init func() T) *Store[T]

NewStore declares a store; init makes its first value in each app or tab.

func (*Store[T]) Get

func (s *Store[T]) Get(c *Ctx) T

Get is the store's value without subscribing: for handlers, effects and actions (safe from any goroutine).

func (*Store[T]) Update

func (s *Store[T]) Update(c *Ctx, f func(T) T)

Update replaces the value with f(current) and re-renders the components using it. Safe from any goroutine.

func (*Store[T]) Use

func (s *Store[T]) Use(c *Ctx) T

Use is the store's value, and re-renders the component when it changes. Call it while rendering, like any hook.

type Style

type Style struct {
	// Layout
	Direction  Direction
	Justify    Justify
	AlignItems Align
	AlignSelf  Align

	Grow, Shrink float32
	Basis        Value

	Width, Height       Value
	MinWidth, MinHeight Value
	MaxWidth, MaxHeight Value

	Padding Edges
	Margin  Edges
	Gap     float32

	Absolute                 bool
	Left, Top, Right, Bottom Value
	Hidden                   bool

	// Box
	Background  Color
	Radius      float32
	BorderWidth float32
	BorderColor Color
	Opacity     float32 // 0 means 1 (fully opaque); use Hidden to hide
	Clip        bool

	// Text and inputs
	FontSize   float32 // 0 means 16
	FontWeight uint16  // 100..900; 0 means 400
	Color      Color   // text color; 0 means the platform default
	TextAlign  Align   // AlignStart, AlignCenter, AlignEnd
	Lines      uint32  // 0 = unlimited
}

Style is how an element looks and lays out. The zero Style is an auto-sized column that stretches its children.

func Flex

func Flex(n float32) Style

Flex makes an element take its share of the free space: Flex(1) fills what is left, like grow 1, shrink 1, basis 0.

func (Style) With

func (s Style) With(o Style) Style

With returns s with the non-zero fields of o laid over it, so styles compose: base.With(ui.Style{Background: red}).

type Tab

type Tab struct {
	Title string
	// Icon is an SF Symbol name on iOS ("list.bullet"); on Android, the
	// drawable resource of the same name with dots as underscores
	// (res/drawable/list_bullet.xml). Without that resource Android shows
	// the title alone.
	Icon   string
	Screen Component
}

Tab is one tab of a Tabs bar.

type TabsNav

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

TabsNav switches the Tabs a component is in. Safe from any goroutine.

func TabsOf

func TabsOf(c *Ctx) *TabsNav

TabsOf is the Tabs the component is shown in, or nil outside one.

func (*TabsNav) Select

func (t *TabsNav) Select(i int)

Select shows the tab at index i.

func (*TabsNav) Selected

func (t *TabsNav) Selected() int

Selected is the index of the tab on screen.

type Theme

type Theme struct {
	Dark bool

	Background Color // screens
	Surface    Color // cards, rows, inputs
	Text       Color
	TextMuted  Color
	Border     Color
	Primary    Color // buttons, links, selected tabs
	OnPrimary  Color // text on Primary
	Danger     Color // errors
	Bar        Color // navigation and tab bars
	BarText    Color // titles on bars

	Radius float32 // corner radius for cards and buttons

	// Type scale, in points.
	Title, Heading, Body, Caption float32

	// Spacing scale, in points.
	SpaceS, SpaceM, SpaceL float32
}

Theme is an app's design tokens. The runtime picks the light or dark one to match the system, uses it for anything left unstyled (text color, spinners, screen backgrounds) and for what the platform draws (bars, modal backgrounds, the status bar). Components read it with UseTheme.

func UseTheme

func UseTheme(c *Ctx) Theme

UseTheme is the theme in effect: the dark one while the system is in dark mode. Components re-render when it switches.

type Value

type Value = layout.Value

Layout vocabulary, re-exported so apps only import ui.

func Pct

func Pct(v float32) Value

Pct is a percentage of the parent's inner size.

func Pt

func Pt(v float32) Value

Pt is a length in points (dp on Android).

type ViewportInfo

type ViewportInfo struct {
	W, H                                     float32
	Scale                                    float32
	SafeTop, SafeBottom, SafeLeft, SafeRight float32
	Platform                                 string // "ios" | "android"
	Dark                                     bool
	Keyboard                                 float32 // how far the keyboard covers the root's bottom
}

ViewportInfo describes the screen.

func Viewport

func Viewport(c *Ctx) ViewportInfo

Viewport is the screen the root fills: size, safe-area insets, platform.

Directories

Path Synopsis
Package render is the wire protocol between the ui runtime (Go) and a host's native renderer.
Package render is the wire protocol between the ui runtime (Go) and a host's native renderer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL