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
- Variables
- func Effect(c *Ctx, fn func(ctx context.Context) func(), deps ...any)
- func LinkPath(link string) string
- func Main()
- func Memo[T any](c *Ctx, fn func() T, deps ...any) T
- func Mount(root Component)
- func Reducer[T any](c *Ctx, initial T) (T, func(func(T) T))
- func Ref[T any](c *Ctx, initial T) *T
- func RefetchQuery(c *Ctx, key string)
- func SessionValue(c *Ctx, key any) any
- func SetTheme(light, dark Theme)
- func State[T any](c *Ctx, initial T) (T, func(T))
- type Action
- type Align
- type Color
- type Component
- type Ctx
- type Direction
- type Edges
- type Element
- func C(comp Component) *Element
- func Image(url string, s Style) *Element
- func List(count int, item func(i int) *Element, s Style) *Element
- func Pressable(onPress func(), s Style, children ...*Element) *Element
- func SafeArea(s Style, children ...*Element) *Element
- func Scroll(s Style, children ...*Element) *Element
- func Slider(value, min, max float32, onChange func(float32), s Style) *Element
- func Spinner(s Style) *Element
- func Stack(title string, root Component, s Style) *Element
- func Switch(on bool, onChange func(bool), s Style) *Element
- func Tabs(s Style, tabs ...Tab) *Element
- func Text(text string, s Style) *Element
- func TextField(f Field, o FieldOptions) *Element
- func TextInput(value string, onChange func(string), s Style) *Element
- func View(s Style, children ...*Element) *Element
- func (e *Element) EstimatedItemHeight(h float32) *Element
- func (e *Element) Fit(f render.Fit) *Element
- func (e *Element) Horizontal() *Element
- func (e *Element) Href(path string) *Element
- func (e *Element) ItemKey(key func(i int) string) *Element
- func (e *Element) Key(k string) *Element
- func (e *Element) Keyboard(k render.Keyboard) *Element
- func (e *Element) Label(l string) *Element
- func (e *Element) Multiline() *Element
- func (e *Element) OnLongPress(f func()) *Element
- func (e *Element) OnRefresh(refreshing bool, onRefresh func()) *Element
- func (e *Element) Placeholder(p string) *Element
- func (e *Element) Secure() *Element
- func (e *Element) TestID(id string) *Element
- type Field
- type FieldErrors
- type FieldOptions
- type Form
- func (f *Form[T]) Errors() FieldErrors
- func (f *Form[T]) Field(name string, at func(*T) *string) Field
- func (f *Form[T]) FormError() error
- func (f *Form[T]) SetErrors(err error)
- func (f *Form[T]) Submit(onValid func(T)) func()
- func (f *Form[T]) Update(change func(*T))
- func (f *Form[T]) Valid() bool
- func (f *Form[T]) Value() T
- type Justify
- type ModalNav
- type Nav
- type Query
- type Session
- type SessionOptions
- type Store
- type Style
- type Tab
- type TabsNav
- type Theme
- type Value
- type ViewportInfo
Constants ¶
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 ¶
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.
var HandleURL func(link string) bool
HandleURL, when set, sees every link first; returning true means it was handled and nothing else happens.
var Once any = once{}
Once as an Effect dependency runs the effect only after the first render.
var RestoreRoute func(path string) (title string, screen Component, ok bool)
RestoreRoute turns a path back into a screen. router.Register sets it.
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 ¶
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 LinkPath ¶ added in v0.0.9
LinkPath is the route path a link points to: a web link's path, or for an app's own scheme the host and path together (myapp://orders/42 is /orders/42).
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 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 ¶
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 RefetchQuery ¶
RefetchQuery fetches the query with this key again (after a change, say), re-rendering every component that shows it.
func SessionValue ¶
SessionValue is the value the session was started with under key (see SessionOptions.Values), or nil on a device.
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 Color ¶
type Color uint32
Color is 0xRRGGBBAA. The zero Color means "unset".
type Component ¶
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 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 List ¶
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 SafeArea ¶
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 ¶
Scroll scrolls its children vertically (Horizontal() for sideways). Give it a size or Flex: its content is as long as its children.
func Slider ¶ added in v0.0.9
Slider picks a number between min and max: UISlider, Android's SeekBar, colored with the theme's Primary (or s.Color). onChange gets each value as the user drags; pass nil to disable it.
volume, setVolume := ui.State(c, float32(0.5))
ui.Slider(volume, 0, 1, setVolume, ui.Style{})
func Stack ¶
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 ¶
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 ¶
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 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 ¶
TextInput is a single-line text field showing value; onChange gets every edit. Keep value in State and set it from onChange.
func (*Element) EstimatedItemHeight ¶
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) Horizontal ¶
Horizontal makes a Scroll scroll sideways.
func (*Element) Href ¶
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 ¶
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 ¶
Key identifies an element among its siblings, so the runtime can keep its native view and state when the list changes order.
func (*Element) Label ¶
Label is the accessibility label screen readers speak. On a component, it labels what the component renders.
func (*Element) Multiline ¶
Multiline lets a TextInput wrap and take new lines; it is three lines tall unless its style sets a height.
func (*Element) OnLongPress ¶ added in v0.0.9
OnLongPress makes a Pressable report long presses too.
func (*Element) OnRefresh ¶
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 ¶
Placeholder sets a text input's hint.
type Field ¶
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 ¶
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 ¶
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 ¶
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 ¶
FormError is the error that belongs to no single field: from validation, or from SetErrors (a server or network failure).
func (*Form[T]) SetErrors ¶
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.
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.
type Nav ¶
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 ¶
Navigation is the Stack the component is shown in, or nil outside one.
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 ¶
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.
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 (*Store[T]) Get ¶
Get is the store's value without subscribing: for handlers, effects and actions (safe from any goroutine).
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
// Wrap moves children that don't fit onto new lines (rows of chips,
// grids of tags); LineGap spaces the lines, Gap when zero.
Wrap bool
LineGap float32
// Transition animates this view's changes of position, size, opacity
// and background over this many milliseconds, instead of jumping.
Transition int
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
FontFamily string // a font the app bundles (`nimbus native font add`); "" is the system font
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.
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.
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.
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.