Skip to main content

Core Concepts

Every oat-latte application is built from three kinds of thing: a Canvas, Layouts, and Widgets. Understanding what each one does — and how they relate — is everything you need to build UIs with oat-latte.


Canvas​

The Canvas is the application root. It owns the terminal screen, runs the event loop, manages keyboard focus, and handles overlays (dialogs). You create exactly one Canvas per application.

app := oat.NewCanvas(
oat.WithTheme(latte.ThemeDark),
oat.WithBody(body), // the root layout
)
if err := app.Run(); err != nil {
log.Fatal(err)
}

Run() takes over the terminal and blocks until the user quits. The Canvas divides the screen into three vertical regions: an optional header, the body (fills all remaining space), and an optional footer (used by the status bar).

Useful Canvas options​

OptionWhat it does
WithTheme(t)Apply a colour theme to the entire tree
WithBody(c)Set the root layout component
WithHeader(c)Set an optional header component
WithAutoStatusBar(bar)Mount a status bar in the footer
WithPrimary(f)Which widget receives focus first
WithNotificationManager(n)Wire up toast notifications
WithGlobalKeyBinding(b)Register an app-wide keyboard shortcut

Useful Canvas methods​

MethodWhat it does
Quit()Exit the event loop gracefully
SetTheme(t)Switch themes at runtime
GetTheme()Return a pointer to the active theme (nil if none set)
ShowDialog(d)Push a modal dialog; focus moves into it
HideDialog()Dismiss the topmost dialog
FocusByRef(f)Jump focus directly to a specific widget

Layouts​

Layouts are containers. They hold other components — other layouts, or widgets — and decide how to position and size them. You nest layouts to build any UI structure.

oat-latte ships six layout primitives:

VBox — vertical stack​

Arranges children top-to-bottom. Use AddFlexChild to let a child stretch and fill remaining space.

vbox := layout.NewVBox(
widget.NewText("Label"),
widget.NewEditText().WithHint("Name"),
)
vbox.AddFlexChild(widget.NewText(longContent), 1) // stretches to fill

Use WithHAlign to align children horizontally within their slot (instead of filling the full width):

// Every child centred horizontally
vbox := layout.NewVBox(title, body, footer).WithHAlign(oat.HAlignCenter)

HBox — horizontal row​

Arranges children left-to-right. Same flex system as VBox.

hbox := layout.NewHBox()
hbox.AddFlexChild(listPanel, 1)
hbox.AddFlexChild(detailPanel, 3) // 3× wider than the list

Use WithVAlign to align children vertically within their slot:

// Status badges pinned to the bottom of each column
hbox := layout.NewHBox(iconText, nameText, statusChip).WithVAlign(oat.VAlignBottom)

Border — framed panel​

Wraps any component in a titled border. Border panels automatically highlight their frame when a descendant is focused.

panel := layout.NewBorder(innerComponent).
WithTitle("My Panel").
WithRoundedCorner(true)

Padding​

Adds whitespace around a component without drawing a border.

padded := layout.NewPaddingUniform(child, 1)  // 1 cell on all sides

Grid​

Places children into a fixed row × column grid with optional spanning.

g := layout.NewGrid(2, 3) // 2 rows, 3 cols
g.AddChildAt(widget, 0, 0, 1, 2) // row 0, col 0, span 1 row × 2 cols

Spacers — VFill and HFill​

Empty spacers that consume leftover space. Use them to push widgets to the edges of a box.

btnRow := layout.NewHBox()
btnRow.AddChild(layout.NewHFill()) // pushes buttons to the right
btnRow.AddChild(cancelBtn)
btnRow.AddChild(layout.NewHFill().WithMaxSize(2)) // fixed 2-cell gap
btnRow.AddChild(okBtn)

AlignChild — per-child alignment override​

AlignChild wraps a single component and overrides its alignment within the parent box — useful when one child needs different alignment from the rest, or when you want alignment set inline rather than on the widget itself.

// Save pinned right, Cancel pinned left — no spacer widgets needed
vbox := layout.NewVBox(
layout.NewAlignChild(saveBtn, oat.HAlignRight, oat.VAlignFill),
layout.NewAlignChild(cancelBtn, oat.HAlignLeft, oat.VAlignFill),
)

See the Layout reference for the full API and more examples.

Composing layouts​

Layouts nest freely. A typical two-panel app:

list   := widget.NewList(items)
detail := widget.NewText("")

list.WithOnCursorChange(func(_ int, item widget.ListItem) {
detail.SetText(fmt.Sprint(item.Value))
})

body := layout.NewHBox()
body.AddFlexChild(layout.NewBorder(list).WithTitle("Items"), 1)
body.AddFlexChild(layout.NewBorder(detail).WithTitle("Detail"), 3)

Widgets​

Widgets are the leaves of the tree — the elements users actually see and interact with. oat-latte ships a full set out of the box. See the Widgets section in the sidebar for the full reference for each one.

Display widgets​

WidgetWhat it renders
widget.NewText(s)Static text with word-wrap and scroll
widget.NewTitle(s)Bold heading with an optional rule beneath
widget.NewLabel(tags)Row of inline badge chips
widget.NewProgressBar()Horizontal fill bar with optional percent label
widget.NewDivider(axis)Horizontal or vertical rule

Interactive widgets​

WidgetWhat it does
widget.NewButton(label, fn)Pressable button; fires fn on Enter or Space
widget.NewEditText()Single-line text input
widget.NewMultiLineEditText()Multi-line text input
widget.NewCheckBox(label)Boolean toggle
widget.NewList(items)Scrollable, selectable list

App-level widgets​

WidgetWhat it does
widget.NewDialog(title)Modal overlay with scrim backdrop
widget.NewStatusBar()Footer bar that shows key-binding hints
widget.NewNotificationManager()Toast notifications pushed from anywhere

Wiring widgets together​

Widgets communicate through callbacks, not through a central model. Set a callback on one widget to update another:

input := widget.NewEditText().WithHint("Search")
list := widget.NewList(allItems)

input.WithOnChange(func(text string) {
list.SetItems(filter(allItems, text))
})

Reading widget values​

Every interactive widget implements ValueGetter. The Canvas can look up any widget's current value by its ID:

nameInput := widget.NewEditText().WithID("name")
// ... later ...
val, _ := app.GetValue("name") // returns the current text as interface{}

Putting it together​

A complete application assembles these three layers:

func main() {
// 1. Widgets
input := widget.NewEditText().WithHint("Name").WithID("name")
result := widget.NewText("—")
btn := widget.NewButton("Save", func() {
result.SetText("Saved: " + input.GetText())
})

// 2. Layouts
body := layout.NewBorder(
layout.NewPaddingUniform(
layout.NewVBox(input, btn, result),
1,
),
).WithTitle("New item")

// 3. Canvas
app := oat.NewCanvas(
oat.WithTheme(latte.ThemeDark),
oat.WithBody(body),
oat.WithPrimary(input),
)
if err := app.Run(); err != nil {
log.Fatal(err)
}
}

Once you are comfortable with these three layers, the Tutorial walks you through building a full task-list application step by step.