# Introduction (https://bloomfx.dev/getting-started/introduction.md)
bloom is a layout, compositing, and animation engine for [Bubble Tea](https://github.com/charmbracelet/bubbletea)
programs. You describe your view as a tree of nodes, built fresh from your model on every frame. bloom works
out where everything goes, draws layers like dialogs and toasts on top, runs effects and animations, and tells
you which node the mouse is over.
```go
func (m *model) View() tea.View {
return m.ui.View(m.width, m.height, bloom.Overlay(
bloom.Column(
bloom.Fill(bloom.Row(
bloom.Fixed(24, m.sidebar).Collapsible(1),
bloom.Fill(m.reader).Min(40),
)),
m.statusbar,
),
bloom.Layer(m.dialog).
Modal().
Backdrop(fx.Dim{Amount: 0.5}).
Presence(m.dialogAnim, fx.Fade{}, fx.Slide{From: bloom.Bottom}),
))
}
```
> **Pre-1.0**
>
> bloom's API may change in breaking ways until v1.0.0. Pin a version in your `go.mod` and check the release
> notes when you upgrade.
## Why bloom exists [#why-bloom-exists]
Bubble Tea gives you a model, an update loop, and a view that returns a string.
[Lip Gloss](https://github.com/charmbracelet/lipgloss) styles those strings and joins them side by side or top
to bottom. That covers a lot, but some common needs take a surprising amount of code:
- **Sizing panes.** A sidebar and a reader that share the width means measuring rendered strings, subtracting
borders and padding, and redoing the math on every resize.
- **Layering.** A dialog, dropdown, or toast has to be drawn *over* the rest of the screen. Strings can only be
joined, not stacked, so you end up splicing lines by hand, and dimming what's underneath is harder still.
- **Mouse input.** A click arrives as an X and Y. Working out which list item or button that was means keeping
your own map of where you drew everything.
- **Focus.** Tab order, a focused style, and a dialog that keeps focus inside it until it closes.
- **Animation.** Tick messages, timers, and state machines, so a dialog can fade in or a toast can hide itself
after two seconds.
bloom handles each of these in the view tree, so your model only holds application state and a few animation
values.
## The packages [#the-packages]
| Package | What it does |
| --- | --- |
| [`bloom`](https://pkg.go.dev/github.com/lrstanley/bloom) | Layout nodes (rows, columns, grids, boxes, text), overlays and layers, mouse routing, focus, and the `Root` that renders it all. |
| [`bloom/anim`](https://pkg.go.dev/github.com/lrstanley/bloom/anim) | Animated numbers kept in your model: eased or spring `Value`s, a `Presence` for showing and hiding, and `Sequence`s of steps. |
| [`bloom/fx`](https://pkg.go.dev/github.com/lrstanley/bloom/fx) | Cell effects such as `Fade`, `Slide`, `Iris`, and `Dim`, used as transitions on nodes or as backdrops behind layers. |
| [`bloom/canvas`](https://pkg.go.dev/github.com/lrstanley/bloom/canvas) | The cell surfaces effects draw on. You only need it to write your own effects. |
| [`bloom/flex`](https://pkg.go.dev/github.com/lrstanley/bloom/flex) | The space-sharing math behind rows and columns, plus the `Justify` options. |
## How it fits into Bubble Tea [#how-it-fits-into-bubble-tea]
bloom doesn't replace Bubble Tea's model or update loop. You keep writing `Init`, `Update`, and `View`; bloom
lives inside them:
- A `*bloom.Root` in your model renders the tree in `View` and returns the `tea.View`.
- Any component with a `View() string` method, including [Bubbles](https://github.com/charmbracelet/bubbles)
components and your own, can be placed in the tree as is.
- Mouse handlers go on the nodes they're about, such as `bloom.Text("save").On(on.Click(m.save))`, and bloom runs
the one under the pointer.
- Animations are values in your model, advanced by messages like any other Bubble Tea state.
## Where to go next [#where-to-go-next]
- [Installation](https://bloomfx.dev/getting-started/installation.md): Add bloom to a Go module.
- [Build your first layout](https://bloomfx.dev/getting-started/first-layout.md): A tutorial: a notes app with a sidebar, a reader, and clickable items.
- [How layout works](https://bloomfx.dev/concepts/how-layout-works.md): The model behind slots, sizes, and placement, if you prefer to start with the ideas.
- [Examples](https://bloomfx.dev/resources/examples.md): Runnable programs, from a basic layout to a 120 FPS dashboard.
# Installation (https://bloomfx.dev/getting-started/installation.md)
## Requirements [#requirements]
- **Go 1.27** or newer. bloom uses generic methods, such as `frame.Rect[K]`, which need it.
- **[Bubble Tea v2](https://github.com/charmbracelet/bubbletea)**, imported as `charm.land/bubbletea/v2`.
- **[Lip Gloss v2](https://github.com/charmbracelet/lipgloss)**, imported as `charm.land/lipgloss/v2`, for
styling text and boxes.
bloom isn't built for Bubble Tea v1. Its views are `tea.View` values, and its mouse routing uses the `OnMouse`
handler that v2 views have.
## Install [#install]
```console
go get github.com/lrstanley/bloom@latest
```
That gets all of bloom's packages. Import the ones you use:
```go
import (
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/lrstanley/bloom"
"github.com/lrstanley/bloom/anim" // animated values, for transitions
"github.com/lrstanley/bloom/fx" // effects, for transitions and backdrops
)
```
> **Expect breaking changes**
>
> bloom is pre-1.0, and its API may still change significantly before v1.0.0. Pin the version in your
> `go.mod` and read the release notes before upgrading.
## Check that it works [#check-that-it-works]
This program prints a two-column layout and exits. It uses `bloom.Render`, which turns a layout into a string
without running a Bubble Tea program, so it's an easy way to try layouts out.
```go
package main
import (
"fmt"
"charm.land/lipgloss/v2"
"github.com/lrstanley/bloom"
)
func main() {
box := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Padding(0, 1)
fmt.Println(bloom.Render(bloom.Row(
bloom.Fixed(16, bloom.Box(box, bloom.Text("sidebar"))),
bloom.Fill(bloom.Box(box, bloom.Text("content"))),
), 50, 5))
}
```
```console
$ go run .
╭──────────────╮╭────────────────────────────────╮
│ sidebar ││ content │
│ ││ │
│ ││ │
╰──────────────╯╰────────────────────────────────╯
```
Next, [build your first layout](https://bloomfx.dev/getting-started/first-layout.md) in a real Bubble Tea program.
# Build your first layout (https://bloomfx.dev/getting-started/first-layout.md)
In this tutorial you'll build a small notes browser: a list of notes on the left, the selected note on the right,
and a status bar along the bottom. You can choose a note with the arrow keys or by clicking it, and the sidebar
gets out of the way when the terminal is too narrow for both panes.
```
╭──────────────────────╮╭──────────────────────────────────────────────────────╮
│ Groceries ││ Groceries │
│ Ideas ││ Oat milk, lemons, basil, and a loaf of rye. │
│ Reading ││ │
│ ││ │
╰──────────────────────╯╰──────────────────────────────────────────────────────╯
3 notes, ↑/↓ or click to choose, q to quit
```
You should know the basics of a Bubble Tea program: a model with `Init`, `Update`, and `View` methods. If you
haven't written one before, the [Bubble Tea tutorial](https://github.com/charmbracelet/bubbletea/tree/main/tutorials)
covers it in a few minutes.
## Set up the project [#set-up-the-project]
Create a new module and add bloom, Bubble Tea, and Lip Gloss:
```console
mkdir notes && cd notes
go mod init example.com/notes
go get github.com/lrstanley/bloom@latest charm.land/bubbletea/v2 charm.land/lipgloss/v2
```
Everything below goes in `main.go`.
1. ### Start with an empty screen [#start-with-an-empty-screen]
Every bloom program keeps a `*bloom.Root` in its model. The root renders your layout, and remembers what it can
between frames so it doesn't redo work. Your model also needs the window's size, which Bubble Tea sends in a
`tea.WindowSizeMsg`.
```go
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
"github.com/lrstanley/bloom"
)
type model struct {
ui *bloom.Root
width, height int
}
func (m *model) Init() tea.Cmd {
return m.ui.Init()
}
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
case tea.KeyPressMsg:
if msg.String() == "q" || msg.String() == "ctrl+c" {
return m, tea.Quit
}
}
return m, m.ui.Update(msg)
}
func (m *model) View() tea.View {
v := m.ui.View(m.width, m.height, bloom.Text("Hello from bloom"))
v.AltScreen = true
return v
}
func main() {
m := &model{ui: bloom.New()}
if _, err := tea.NewProgram(m).Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
```
Run it with `go run .`, and press q to quit.
There are three bloom calls here, and every program has them:
- `m.ui.Init()` asks the terminal for its colors, which some effects use to blend. Return it from `Init`.
- `m.ui.Update(msg)` lets the root see what the terminal reports, and keeps layout animations ticking. Pass it
every message and return its command.
- `m.ui.View(width, height, layout)` lays out and draws `layout` at the window's size and returns a `tea.View`.
2. ### Split the screen into panes [#split-the-screen-into-panes]
Layouts are built from nodes. `bloom.Row` places its children left to right, and `bloom.Column` places them top
to bottom. How much space each child gets depends on the **slot** you wrap it in:
- `bloom.Fixed(n, child)` gets exactly `n` cells.
- `bloom.Fill(child)` gets an equal share of whatever is left over.
- A child with no slot gets its natural size, the space its content needs.
Add the panes and a status bar. Replace `View` with this:
```go
func (m *model) View() tea.View {
layout := bloom.Column(
bloom.Fill(bloom.Row(
bloom.Fixed(24, bloom.Text("sidebar")),
bloom.Fill(bloom.Text("reader")),
)),
bloom.Text(" status bar"),
)
v := m.ui.View(m.width, m.height, layout)
v.AltScreen = true
return v
}
```
The status bar has no slot, so it takes the one row its text needs. The row of panes is in a `Fill` slot, so it
takes all the remaining height. Inside the row, the sidebar is 24 columns wide and the reader fills the rest.
Resize your terminal while it runs. The panes follow, without any width arithmetic in your code.
3. ### Add borders and styles [#add-borders-and-styles]
`bloom.Box(style, child)` draws a Lip Gloss style's border, padding, and background around a child, and the child
gets the space inside it. `Text(s).Style(style)` renders text with a style at whatever size the node is given.
Styled text also wraps to fit its width, and text without a style is clipped instead.
Add `"charm.land/lipgloss/v2"` to your imports, then add some styles and the notes to show, above
`type model`:
```go
type note struct {
title, body string
}
var notes = []note{
{"Groceries", "Oat milk, lemons, basil, and a loaf of rye."},
{"Ideas", "A terminal garden that grows while you type."},
{"Reading", "The Elements of Typographic Style, chapter 2."},
}
var (
muted = lipgloss.Color("#6e6a86")
subtle = lipgloss.Color("#908caa")
rose = lipgloss.Color("#eb6f92")
gold = lipgloss.Color("#f6c177")
dark = lipgloss.Color("#191724")
panel = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(muted)
item = lipgloss.NewStyle().Padding(0, 1)
selected = item.Foreground(dark).Background(rose)
heading = lipgloss.NewStyle().Bold(true).Foreground(gold)
prose = lipgloss.NewStyle().Padding(0, 1)
status = lipgloss.NewStyle().Foreground(subtle)
)
```
Add a `selected int` field to the model, then build the sidebar from the notes in `View`:
```go
func (m *model) View() tea.View {
items := make([]bloom.Child, len(notes))
for i, n := range notes {
style := item
if i == m.selected {
style = selected
}
items[i] = bloom.Text(n.title).Style(style)
}
current := notes[m.selected]
sidebar := bloom.Box(panel, bloom.Column(items...))
reader := bloom.Box(panel, bloom.Column(
bloom.Text(current.title).Style(heading.Padding(0, 1)),
bloom.Text(current.body).Style(prose),
))
hints := fmt.Sprintf(" %d notes, ↑/↓ or click to choose, q to quit", len(notes))
layout := bloom.Column(
bloom.Fill(bloom.Row(
bloom.Fixed(24, sidebar),
bloom.Fill(reader),
)),
bloom.Text(hints).Style(status).Truncate(),
)
v := m.ui.View(m.width, m.height, layout)
v.AltScreen = true
return v
}
```
`Truncate` keeps the status bar on one line and cuts it short with "…" when the terminal is narrow, rather than
letting it wrap onto a second row.
Then handle the arrow keys in `Update`, next to the quit keys:
```go
case tea.KeyPressMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "up":
m.selected = max(m.selected-1, 0)
case "down":
m.selected = min(m.selected+1, len(notes)-1)
}
```
4. ### Make the notes clickable [#make-the-notes-clickable]
Mouse handlers go on the nodes they're about, with `On`. The handlers come from the `on` package, so import it:
```go
import "github.com/lrstanley/bloom/on"
```
Give each sidebar item a click handler in `View`:
```go
items[i] = bloom.Text(n.title).Style(style).On(on.Click(func() tea.Cmd {
m.selected = i
return nil
}))
```
And turn on mouse reporting, before returning the view:
```go
v.MouseMode = tea.MouseModeCellMotion
```
A click on a note now runs its handler. bloom works out which node the pointer is over, and runs the handler on
the Bubble Tea event loop just before `Update`, so it can change the model directly, as `Update` does. That works
because `View` has a pointer receiver: the handler captures `m`, the model itself, not a copy.
5. ### Handle narrow terminals [#handle-narrow-terminals]
Make the terminal very narrow and the reader gets squeezed until its text barely fits. Two slot options fix that:
- `.Min(30)` on the reader stops it from shrinking below 30 columns.
- `.Collapsible(1)` on the sidebar lets bloom drop it entirely when the minimum sizes don't all fit.
```go
bloom.Fill(bloom.Row(
bloom.Fixed(24, sidebar).Collapsible(1),
bloom.Fill(reader).Min(30),
)),
```
Narrow the terminal below 54 columns (24 for the sidebar plus 30 for the reader) and the sidebar disappears.
Widen it again and the sidebar comes back. When several slots are collapsible, the one with the lowest priority
goes first.
## The complete program [#the-complete-program]
```go title="main.go"
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/lrstanley/bloom"
"github.com/lrstanley/bloom/on"
)
type note struct {
title, body string
}
var notes = []note{
{"Groceries", "Oat milk, lemons, basil, and a loaf of rye."},
{"Ideas", "A terminal garden that grows while you type."},
{"Reading", "The Elements of Typographic Style, chapter 2."},
}
var (
muted = lipgloss.Color("#6e6a86")
subtle = lipgloss.Color("#908caa")
rose = lipgloss.Color("#eb6f92")
gold = lipgloss.Color("#f6c177")
dark = lipgloss.Color("#191724")
panel = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(muted)
item = lipgloss.NewStyle().Padding(0, 1)
selected = item.Foreground(dark).Background(rose)
heading = lipgloss.NewStyle().Bold(true).Foreground(gold)
prose = lipgloss.NewStyle().Padding(0, 1)
status = lipgloss.NewStyle().Foreground(subtle)
)
type model struct {
ui *bloom.Root
width, height int
selected int
}
func (m *model) Init() tea.Cmd {
return m.ui.Init()
}
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
case tea.KeyPressMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "up":
m.selected = max(m.selected-1, 0)
case "down":
m.selected = min(m.selected+1, len(notes)-1)
}
}
return m, m.ui.Update(msg)
}
func (m *model) View() tea.View {
items := make([]bloom.Child, len(notes))
for i, n := range notes {
style := item
if i == m.selected {
style = selected
}
items[i] = bloom.Text(n.title).Style(style).On(on.Click(func() tea.Cmd {
m.selected = i
return nil
}))
}
current := notes[m.selected]
sidebar := bloom.Box(panel, bloom.Column(items...))
reader := bloom.Box(panel, bloom.Column(
bloom.Text(current.title).Style(heading.Padding(0, 1)),
bloom.Text(current.body).Style(prose),
))
hints := fmt.Sprintf(" %d notes, ↑/↓ or click to choose, q to quit", len(notes))
layout := bloom.Column(
bloom.Fill(bloom.Row(
bloom.Fixed(24, sidebar).Collapsible(1),
bloom.Fill(reader).Min(30),
)),
bloom.Text(hints).Style(status).Truncate(),
)
v := m.ui.View(m.width, m.height, layout)
v.AltScreen = true
v.MouseMode = tea.MouseModeCellMotion
return v
}
func main() {
m := &model{ui: bloom.New()}
if _, err := tea.NewProgram(m).Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
```
## What you learned [#what-you-learned]
- A `*bloom.Root` renders your layout from `View`, and gets every message in `Update`.
- `Row` and `Column` share space between children, and the slot a child is in decides its share: `Fixed`,
`Fill`, or its natural size.
- `Box` draws a Lip Gloss border around a child, and `Text(...).Style(...)` styles and wraps text.
- `On` attaches mouse handlers from the `on` package to the node they're about, and they can change the model
directly.
- `Min` and `Collapsible` decide what gives way when space runs out.
Next, [add a dialog and a toast](https://bloomfx.dev/getting-started/dialogs-and-toasts.md) to this app. For more on how rows and
columns share space, see [Sizing with slots](https://bloomfx.dev/layout/slots.md).
# Add a dialog and a toast (https://bloomfx.dev/getting-started/dialogs-and-toasts.md)
This tutorial continues from [Build your first layout](https://bloomfx.dev/getting-started/first-layout.md). You'll add a way to delete
notes, with a confirmation dialog that animates in over a dimmed screen, buttons you can tab between or click, and
a notification in the corner that hides itself after two seconds.
```
╭──────────────────────╮╭──────────────────────────────────────────────╮
│ Groceries ││ Groceries │
│ Ideas ││ Oat milk, lemons, basil, and a loaf of rye. │
│ Reading ╭────────────────────────────╮ │
│ │ │ │
│ │ Delete "Groceries"? │ │
│ │ This can't be undone. │ │
│ │ │ │
│ │ Cancel Delete │ │
│ │ │ │
│ ╰────────────────────────────╯ │
╰──────────────────────╯╰──────────────────────────────────────────────╯
↑/↓ choose, d delete, q quit
```
Doing this without bloom would mean drawing one string over another, dimming every cell outside the dialog,
keeping keyboard focus inside it, and running a timer to hide the notification. Here it takes three ideas:
- An **overlay** draws **layers** over a base layout. The dialog and the toast are layers.
- A **presence**, from the `anim` package, animates something being shown and hidden.
- A **focus** remembers which node has keyboard focus, and can be confined to the dialog while it's open.
1. ### Keep the notes in the model [#keep-the-notes-in-the-model]
Deleting notes means the list has to be model state now, rather than a package variable. Change the model and
add a constructor:
```go
type model struct {
ui *bloom.Root
width, height int
notes []note
selected int
}
func newModel() *model {
return &model{
ui: bloom.New(),
notes: []note{
{"Groceries", "Oat milk, lemons, basil, and a loaf of rye."},
{"Ideas", "A terminal garden that grows while you type."},
{"Reading", "The Elements of Typographic Style, chapter 2."},
},
}
}
```
Replace `notes` with `m.notes` everywhere, and start the program with `tea.NewProgram(newModel())`.
2. ### Draw the dialog as a layer [#draw-the-dialog-as-a-layer]
`bloom.Overlay(base, layers...)` draws each layer over the base, without taking any space from it. A layer
made with `bloom.Layer(child)` sits in the middle of the screen at its natural size.
Add a color, and styles for the dialog and its buttons, to the `var` block:
```go
surface = lipgloss.Color("#393552")
dialogBox = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(rose).Padding(1, 2)
button = lipgloss.NewStyle().Padding(0, 2).Background(surface)
buttonOn = button.Foreground(dark).Background(rose).Bold(true)
```
Then, in `View`, keep the column you already have as `base`, build the dialog, and render both:
```go
content := bloom.Column(
bloom.Text(fmt.Sprintf("Delete %q?", title)).Style(heading),
bloom.Text("This can't be undone."),
bloom.Fixed(1, nil),
bloom.Row(
bloom.Space(),
bloom.Text("Cancel").Style(button),
bloom.Text("Delete").Style(button),
).Gap(2),
)
dialog := bloom.Layer(bloom.Box(dialogBox, content))
v := m.ui.View(m.width, m.height, bloom.Overlay(base, dialog))
```
`title` is the selected note's title. `bloom.Fixed(1, nil)` is a blank row, and `bloom.Space()` is empty space
that grows, pushing the buttons to the right. `Gap(2)` puts two columns between the children of the row.
Run it and the dialog is always on screen. Next, you'll make it appear only when needed.
3. ### Show and hide it with a presence [#show-and-hide-it-with-a-presence]
An `anim.Presence` is a number from 0 (hidden) to 1 (shown) that animates between the two. Add the `anim` and
`fx` imports:
```go
"github.com/lrstanley/bloom/anim"
"github.com/lrstanley/bloom/fx"
```
Add a presence to the model, and create it in `newModel`:
```go
confirm anim.Presence
```
```go
confirm: anim.NewPresence(anim.Duration(180 * time.Millisecond)),
```
`confirm.Show()` and `confirm.Hide()` start the animation and return a command that drives it. The animation
moves forward on messages, so every presence has to see every message: `anim.Update(msg, &m.confirm)` passes one
message to any number of animations and returns their commands. Return it from `Update`:
```go
return m, tea.Batch(m.ui.Update(msg), anim.Update(msg, &m.confirm))
```
Now give the layer to the presence. `Presence(p, effects...)` shows the layer only while `p` is visible, and runs
the effects with its progress: here it fades in while sliding up from the bottom of the screen.
```go
dialog := bloom.Layer(bloom.Box(dialogBox, content)).
Modal().
Backdrop(fx.Dim{Amount: 0.6}).
Presence(m.confirm, fx.Fade{}, fx.Slide{From: bloom.Bottom})
```
Two more options are set here:
- `Modal()` makes the dialog take every mouse event while it's open. Clicks outside it don't reach the notes
underneath.
- `Backdrop(fx.Dim{Amount: 0.6})` dims everything beneath the dialog. It follows the presence too, so the screen
darkens as the dialog appears.
Open it with d by returning `m.confirm.Show()` from `Update`, and you have an animated modal dialog.
4. ### Move focus into the dialog [#move-focus-into-the-dialog]
The buttons should respond to tab and enter, and focus shouldn't escape to the screen
behind while the dialog is open. Add a button ID type:
```go
type buttonID string
// confirmDialog is the ID of the dialog's layer, which is also its focus scope.
const confirmDialog = "confirm"
```
And a `bloom.Focus` to the model. Its type parameter is the type of the IDs it moves between, so this one moves
between buttons:
```go
focus bloom.Focus[buttonID]
```
A button is text with an ID that's marked `Focusable`, styled by whether it has focus. `on.Press` (from
`github.com/lrstanley/bloom/on`) makes it press when it's clicked, or when enter or space is
pressed while it has focus:
```go
func (m *model) button(id buttonID, label string) bloom.Child {
style := button
if m.focus.Is(id) {
style = buttonOn
}
return bloom.Text(label).Style(style).ID(id).Focusable().
On(on.Press(func() tea.Cmd { return m.press(id) }))
}
```
Give the dialog layer the ID `confirmDialog`. A modal layer is a **focus scope**: after
`m.focus.Push(confirmDialog)`, focus can only move between the focusable nodes inside it, until
`m.focus.Pop()` puts it back where it was.
```go
func (m *model) openDialog() tea.Cmd {
if len(m.notes) == 0 {
return nil
}
m.focus.Push(confirmDialog)
m.focus.Set("cancel")
return m.confirm.Show()
}
func (m *model) closeDialog() tea.Cmd {
if !m.confirm.Shown() {
return nil
}
m.focus.Pop()
return m.confirm.Hide()
}
```
`m.focus.Update(msg, m.ui.Frame())` handles tab and shift+tab, using the layout of the last
frame drawn, and presses the focused button for enter and space. It returns the button's
command, and reports whether it handled the message, so only look at other keys when it didn't. Keep the command
in a `cmd` variable, declared with `var cmd tea.Cmd` at the top of `Update`, and add it to the batch you return:
```go
case tea.KeyPressMsg:
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
var ok bool
if cmd, ok = m.focus.Update(msg, m.ui.Frame()); !ok {
cmd = m.key(msg)
}
```
A click on the backdrop, outside the modal dialog, should close it. `on.ClickOutside` on the dialog's layer runs
for exactly that:
```go
dialog := bloom.Layer(bloom.Box(dialogBox, content)).
ID(confirmDialog).
Modal().
On(on.ClickOutside(m.closeDialog)).
Backdrop(fx.Dim{Amount: 0.6}).
Presence(m.confirm, fx.Fade{}, fx.Slide{From: bloom.Bottom})
```
5. ### Add a toast [#add-a-toast]
A toast is one more layer. Add a color and a style for it next to the others:
```go
teal = lipgloss.Color("#9ccfd8")
toastBox = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(teal).Padding(0, 1)
```
The layer is anchored to the bottom-right corner with a one-cell margin. `Anchor(x, y)` takes where
the layer goes on each axis: `bloom.Start`, `bloom.Center`, or `bloom.End`.
```go
toast := bloom.Layer(bloom.Box(toastBox, bloom.Text("Deleted "+m.deleted))).
Anchor(bloom.End, bloom.End).
Margin(1).
Presence(m.toast, fx.Slide{From: bloom.Right})
v := m.ui.View(m.width, m.height, bloom.Overlay(base, dialog, toast))
```
Give it its own presence, `toast anim.Presence`, and a `deleted string` field for the message, with the presence
created with `anim.NewPresence()`, and add it to
`anim.Update(msg, &m.confirm, &m.toast)`. `ShowFor` shows it and hides it again after a duration, with no timer
messages of your own:
```go
func (m *model) press(id buttonID) tea.Cmd {
if id == "delete" {
m.deleted = m.notes[m.selected].title
m.notes = slices.Delete(m.notes, m.selected, m.selected+1)
m.selected = max(min(m.selected, len(m.notes)-1), 0)
return tea.Batch(m.closeDialog(), m.toast.ShowFor(2*time.Second))
}
return m.closeDialog()
}
```
## The complete program [#the-complete-program]
```go title="main.go"
package main
import (
"fmt"
"os"
"slices"
"time"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/lrstanley/bloom"
"github.com/lrstanley/bloom/anim"
"github.com/lrstanley/bloom/fx"
"github.com/lrstanley/bloom/on"
)
type note struct {
title, body string
}
// buttonID identifies a button in the dialog.
type buttonID string
// confirmDialog is the ID of the dialog's layer, which is also its focus scope.
const confirmDialog = "confirm"
var (
muted = lipgloss.Color("#6e6a86")
subtle = lipgloss.Color("#908caa")
rose = lipgloss.Color("#eb6f92")
gold = lipgloss.Color("#f6c177")
dark = lipgloss.Color("#191724")
surface = lipgloss.Color("#393552")
teal = lipgloss.Color("#9ccfd8")
panel = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(muted)
item = lipgloss.NewStyle().Padding(0, 1)
selected = item.Foreground(dark).Background(rose)
heading = lipgloss.NewStyle().Bold(true).Foreground(gold)
prose = lipgloss.NewStyle().Padding(0, 1)
status = lipgloss.NewStyle().Foreground(subtle)
dialogBox = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(rose).Padding(1, 2)
button = lipgloss.NewStyle().Padding(0, 2).Background(surface)
buttonOn = button.Foreground(dark).Background(rose).Bold(true)
toastBox = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(teal).Padding(0, 1)
)
type model struct {
ui *bloom.Root
width, height int
notes []note
selected int
focus bloom.Focus[buttonID]
confirm anim.Presence
toast anim.Presence
deleted string
}
func newModel() *model {
return &model{
ui: bloom.New(),
notes: []note{
{"Groceries", "Oat milk, lemons, basil, and a loaf of rye."},
{"Ideas", "A terminal garden that grows while you type."},
{"Reading", "The Elements of Typographic Style, chapter 2."},
},
confirm: anim.NewPresence(anim.Duration(180 * time.Millisecond)),
toast: anim.NewPresence(),
}
}
func (m *model) Init() tea.Cmd {
return m.ui.Init()
}
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
case tea.KeyPressMsg:
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
var ok bool
if cmd, ok = m.focus.Update(msg, m.ui.Frame()); !ok {
cmd = m.key(msg)
}
}
return m, tea.Batch(cmd, m.ui.Update(msg), anim.Update(msg, &m.confirm, &m.toast))
}
func (m *model) key(msg tea.KeyPressMsg) tea.Cmd {
if m.confirm.Shown() {
if msg.String() == "esc" {
return m.closeDialog()
}
return nil
}
switch msg.String() {
case "q":
return tea.Quit
case "up":
m.selected = max(m.selected-1, 0)
case "down":
m.selected = min(m.selected+1, len(m.notes)-1)
case "d":
return m.openDialog()
}
return nil
}
func (m *model) openDialog() tea.Cmd {
if len(m.notes) == 0 {
return nil
}
m.focus.Push(confirmDialog)
m.focus.Set("cancel")
return m.confirm.Show()
}
func (m *model) closeDialog() tea.Cmd {
if !m.confirm.Shown() {
return nil
}
m.focus.Pop()
return m.confirm.Hide()
}
func (m *model) press(id buttonID) tea.Cmd {
if id == "delete" {
m.deleted = m.notes[m.selected].title
m.notes = slices.Delete(m.notes, m.selected, m.selected+1)
m.selected = max(min(m.selected, len(m.notes)-1), 0)
return tea.Batch(m.closeDialog(), m.toast.ShowFor(2*time.Second))
}
return m.closeDialog()
}
func (m *model) button(id buttonID, label string) bloom.Child {
style := button
if m.focus.Is(id) {
style = buttonOn
}
return bloom.Text(label).Style(style).ID(id).Focusable().
On(on.Press(func() tea.Cmd { return m.press(id) }))
}
func (m *model) View() tea.View {
items := make([]bloom.Child, len(m.notes))
for i, n := range m.notes {
style := item
if i == m.selected {
style = selected
}
items[i] = bloom.Text(n.title).Style(style).On(on.Click(func() tea.Cmd {
m.selected = i
return nil
}))
}
sidebar := bloom.Box(panel, bloom.Column(items...))
body := bloom.Child(bloom.Text("Nothing here.").Style(prose))
if len(m.notes) > 0 {
current := m.notes[m.selected]
body = bloom.Column(
bloom.Text(current.title).Style(heading.Padding(0, 1)),
bloom.Text(current.body).Style(prose),
)
}
reader := bloom.Box(panel, body)
base := bloom.Column(
bloom.Fill(bloom.Row(
bloom.Fixed(24, sidebar).Collapsible(1),
bloom.Fill(reader).Min(30),
)),
bloom.Text(" ↑/↓ choose, d delete, q quit").Style(status).Truncate(),
)
title := ""
if len(m.notes) > 0 {
title = m.notes[m.selected].title
}
content := bloom.Column(
bloom.Text(fmt.Sprintf("Delete %q?", title)).Style(heading),
bloom.Text("This can't be undone."),
bloom.Fixed(1, nil),
bloom.Row(
bloom.Space(),
m.button("cancel", "Cancel"),
m.button("delete", "Delete"),
).Gap(2),
)
dialog := bloom.Layer(bloom.Box(dialogBox, content)).
ID(confirmDialog).
Modal().
On(on.ClickOutside(m.closeDialog)).
Backdrop(fx.Dim{Amount: 0.6}).
Presence(m.confirm, fx.Fade{}, fx.Slide{From: bloom.Bottom})
toast := bloom.Layer(bloom.Box(toastBox, bloom.Text("Deleted "+m.deleted))).
Anchor(bloom.End, bloom.End).
Margin(1).
Presence(m.toast, fx.Slide{From: bloom.Right})
v := m.ui.View(m.width, m.height, bloom.Overlay(base, dialog, toast))
v.AltScreen = true
v.MouseMode = tea.MouseModeCellMotion
return v
}
func main() {
if _, err := tea.NewProgram(newModel()).Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
```
## What you learned [#what-you-learned]
- `Overlay` draws `Layer`s over a base layout, and `Anchor` and `Margin` place them.
- `Modal` keeps every mouse event from what's beneath a layer, `on.ClickOutside` runs for a click on its
backdrop, and `Backdrop` applies an effect to everything beneath it.
- An `anim.Presence` animates showing and hiding; `Layer(...).Presence(p, effects...)` uses it as a transition.
Pass every message to `anim.Update` so animations keep moving.
- `bloom.Focus` tracks keyboard focus, and `Push` and `Pop` confine it to a dialog while it's open.
- `ShowFor` shows something for a while, then hides it.
## Next steps [#next-steps]
- [Dialogs and modals](https://bloomfx.dev/layers/dialogs.md): Stacked dialogs, shadows, and other backdrops.
- [Toast notifications](https://bloomfx.dev/layers/toasts.md): Stacking several toasts, and other corners.
- [Focus and keyboard navigation](https://bloomfx.dev/input/focus.md): Focus order, scopes, and the text cursor.
- [Transitions](https://bloomfx.dev/motion/transitions.md): Every way to animate a node in and out.
# Sizing with slots (https://bloomfx.dev/layout/slots.md)
A `bloom.Row` places children left to right, and a `bloom.Column` places them top to bottom. How much of the
row or column each child gets is decided by the **slot** you wrap it in. The child doesn't choose; whoever builds
the layout does. That's why the same component can be a fixed-width sidebar in one screen and fill the space in
another.
```go
bloom.Row(
bloom.Fixed(10, nav), // exactly 10 columns
toolbar, // no slot: its natural width
bloom.Fill(editor), // a share of what's left
bloom.Grow(2, preview), // two shares of what's left
)
```
```
┌────────┐┌────┐┌────────────┐┌────────────────────────────┐
│fixed ││auto││fill ││grow 2 │
└────────┘└────┘└────────────┘└────────────────────────────┘
```
## Choose a slot [#choose-a-slot]
| Slot | Size | Grows | Shrinks when space is short |
| --- | --- | --- | --- |
| [`Fixed(n, c)`](https://pkg.go.dev/github.com/lrstanley/bloom#Fixed) | exactly `n` cells | no | no |
| [`Auto(c)`](https://pkg.go.dev/github.com/lrstanley/bloom#Auto), or `c` with no slot | the child's natural size | no | yes, down to its `Min` |
| [`Fill(c)`](https://pkg.go.dev/github.com/lrstanley/bloom#Fill) | an equal share of the space left over | yes | yes, down to its `Min` |
| [`Grow(w, c)`](https://pkg.go.dev/github.com/lrstanley/bloom#Grow) | `w` shares of the space left over | yes, by weight | yes, down to its `Min` |
| [`Percent(p, c)`](https://pkg.go.dev/github.com/lrstanley/bloom#Percent) | `p`% of the row or column | no | yes, down to its `Min` |
"Space left over" is what remains after the `Fixed`, `Auto`, and `Percent` slots, and the gaps, are placed. `Fill`
is the same as `Grow(1, c)`.
A child's natural size is the space its content needs: the width of its longest line and its number of lines for
text, plus the border and padding for a box. See [How layout works](https://bloomfx.dev/concepts/how-layout-works.md) for how bloom
measures a child, and how a component can report its own size.
## Set a minimum and a maximum [#set-a-minimum-and-a-maximum]
`Min` and `Max` clamp a slot on the row or column's direction:
```go
bloom.Row(
bloom.Fill(sidebar).Max(32), // grows, but never past 32 columns
bloom.Fill(editor).Min(40), // shrinks, but never below 40 columns
)
```
The minimum is zero unless you set one, so without `Min` a slot can be squeezed to nothing. When the minimums
don't all fit, see [Responsive layouts](https://bloomfx.dev/layout/responsive.md) for dropping slots with `Collapsible`.
## Leave empty space [#leave-empty-space]
- `bloom.Fixed(n, nil)` is `n` empty cells, such as a blank line in a column.
- `bloom.Space()` is empty space that grows, which pushes the children after it to the far end.
```go
bloom.Row(bloom.Text("left"), bloom.Space(), bloom.Text("right"))
```
```
left right
```
To put the same space between every pair of children, use `Gap`, described in
[Text, boxes, and alignment](https://bloomfx.dev/layout/text-and-boxes.md#space-children-evenly).
## Slots in a column [#slots-in-a-column]
Everything above applies to columns too, with heights instead of widths. A common screen shape is a header and
a status bar at their natural height, and the content filling the middle:
```go
bloom.Column(
header,
bloom.Fill(content),
statusbar,
)
```
> **A column is as tall as the screen**
>
> The outermost node gets the whole window. A column that should stretch its middle child needs that child in a
> `Fill` slot; without one, the children sit at the top at their natural heights.
## Give a slot an ID [#give-a-slot-an-id]
Slots are where the other per-child options live: `ID` for focus and lookups, `On` for
[mouse handlers](https://bloomfx.dev/input/mouse.md), `Focusable` and `Scope` for [focus](https://bloomfx.dev/input/focus.md), `Align` for the cross axis, and
`Animate` for [layout animation](https://bloomfx.dev/motion/layout-animation.md).
```go
bloom.Fixed(24, sidebar).ID(paneSidebar).Collapsible(1)
```
## Related [#related]
- [`SlotNode` API reference](https://pkg.go.dev/github.com/lrstanley/bloom#SlotNode)
- [Responsive layouts](https://bloomfx.dev/layout/responsive.md)
- [How layout works](https://bloomfx.dev/concepts/how-layout-works.md)
# Responsive layouts (https://bloomfx.dev/layout/responsive.md)
Terminal windows range from a narrow split pane to a full-screen monitor. bloom gives you three tools to adapt:
collapsing panels when they don't fit, percentages, and components that build a different layout for the space
they get.
## Collapse panels when space runs out [#collapse-panels-when-space-runs-out]
Mark slots `Collapsible(priority)` and set the `Min` of the slots that must stay usable. When the minimum sizes
don't all fit, bloom drops collapsible slots, lowest priority first, along with the gap beside each.
```go
bloom.Row(
bloom.Fixed(24, nav).Collapsible(2),
bloom.Fill(reader).Min(40),
bloom.Fixed(30, details).Collapsible(1),
)
```
As the terminal narrows below 94 columns, the details panel goes first because its priority is lowest. Below
64 columns, the navigation goes too, and the reader has the whole width. Widen the terminal and they come back.
A collapsed slot isn't drawn and has no rectangle, so `frame.Rect(id)` reports `false` for it. You can use that to
show a hint, such as a key to open the missing panel.
## Size by percentage [#size-by-percentage]
`Percent(p, c)` gives a child `p` percent of its container, which keeps proportions as the terminal resizes:
```go
bloom.Row(
bloom.Percent(30, list),
bloom.Fill(preview),
)
```
Unlike `Fixed`, a percentage slot can shrink below its share when space runs short, down to its `Min`.
## Build a layout for the space you're given [#build-a-layout-for-the-space-youre-given]
Some components should change shape, not just size: a toolbar that shows icon labels when wide and only icons
when narrow, or a form that's two columns on a wide screen and one on a narrow one.
A component that implements [`bloom.Layouter`](https://pkg.go.dev/github.com/lrstanley/bloom#Layouter) builds its
own layout. bloom calls its `Layout` method with a `bloom.Context` holding the width and height it has, and draws
whatever it returns:
```go
type toolbar struct {
actions []string
}
// View is used when the toolbar is drawn on its own, outside a bloom layout.
func (t toolbar) View() string {
return strings.Join(t.actions, " ")
}
func (t toolbar) Layout(ctx bloom.Context) bloom.Child {
children := []bloom.Child{bloom.Text("◆ reader"), bloom.Space()}
for _, action := range t.actions {
if ctx.Width < 70 {
// "⟳ refresh" becomes "⟳".
action, _, _ = strings.Cut(action, " ")
}
children = append(children, bloom.Text(action).Style(actionStyle))
}
return bloom.Row(children...)
}
```
`Layout` may be called more than once a frame, while bloom measures as well as for the final layout, so it must
not change the component. Treat it like `View`: read state, return nodes.
## Switch between a row and a column [#switch-between-a-row-and-a-column]
The same idea works for a whole screen. A `Layouter` can return a row on a wide terminal and a column on a
narrow one:
```go
type editor struct {
code, preview bloom.Child
}
func (e editor) View() string {
return bloom.Render(e.Layout(bloom.Context{}), 0, 0)
}
func (e editor) Layout(ctx bloom.Context) bloom.Child {
if ctx.Width >= 100 {
return bloom.Row(bloom.Fill(e.code), bloom.Fill(e.preview)).Gap(1)
}
return bloom.Column(bloom.Fill(e.code), bloom.Fill(e.preview))
}
```
`bloom.Render` with a size of zero draws a layout at its natural size, which gives the component a sensible
`View` when it's used outside a bloom layout.
## Related [#related]
- [Sizing with slots](https://bloomfx.dev/layout/slots.md)
- [Scrolling and sizing components in Update](https://bloomfx.dev/layout/scrolling.md)
- The [`responsive` example](https://github.com/lrstanley/bloom/tree/master/_examples/responsive), which combines
all of these
# Text, boxes, and alignment (https://bloomfx.dev/layout/text-and-boxes.md)
## Show text [#show-text]
`bloom.Text(s)` shows a string, which may already contain ANSI styles. On its own it's drawn as is, and clipped
to the space it's given. Give it a Lip Gloss style with `Style` and it's rendered at the size of its node instead,
so long lines wrap and a background fills the whole node:
```go
bloom.Text(body) // clipped
bloom.Text(body).Style(lipgloss.NewStyle().Padding(0, 1)) // wraps inside 1 column of padding
bloom.Text(title).Style(lipgloss.NewStyle().Background(highlight)) // background fills the node
```
A width or height set on the style itself is kept, so `Style(s.Width(20))` is 20 columns wide wherever it's
placed.
### Keep text on one line [#keep-text-on-one-line]
`Truncate` shows a single line, turning newlines into spaces and cutting the text short with "…" when it doesn't
fit. Padding and borders count against the width. It's what you want for status bars, titles, and list items:
```go
bloom.Text(path).Style(statusStyle).Truncate()
```
```
a long line…
```
For text that should scroll rather than be cut short, such as a long title in a focused list item, see
[`Marquee`](https://bloomfx.dev/motion/feedback-effects.md#scroll-text-that-doesnt-fit).
### Plain strings and components [#plain-strings-and-components]
Any value with a `View() string` method can go in a layout, so you don't need `Text` for your own components or
for [Bubbles](https://bloomfx.dev/layout/bubbles-components.md). `Text` is for plain strings, which don't have a method.
## Draw a box around a node [#draw-a-box-around-a-node]
`bloom.Box(style, child)` draws a Lip Gloss style's frame around a child: its margins, border, padding, and
background. The child gets the space inside.
```go
panel := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#6e6a86")).
Padding(0, 1)
bloom.Box(panel, bloom.Column(title, body))
```
Use `Box` rather than calling `panel.Render(...)` yourself. A pre-rendered string has a fixed size, but a box
knows its frame and gives the right amount of space to its child at any size. A box given less space than its
own border and padding is left out, rather than drawn broken.
## Center a node [#center-a-node]
`bloom.Centered(child)` places a child at its natural size in the middle of its space:
```go
bloom.Centered(bloom.Box(panel, bloom.Text("hi")))
```
```
┌──┐
│hi│
└──┘
```
## Align children across a row or column [#align-children-across-a-row-or-column]
In a row, children stretch to the row's full height by default, and in a column, to its full width. `Align`
changes that, for every child of the row or column:
```go
bloom.Row(sidebar, content).Align(bloom.Start) // each child at its natural height, at the top
```
Or for one slot only:
```go
bloom.Row(
bloom.Fixed(10, tall),
bloom.Auto(badge).Align(bloom.Center), // vertically centered next to tall
)
```
The values are `bloom.Stretch` (the default), `bloom.Start`, `bloom.Center`, and `bloom.End`.
## Justify children along a row or column [#justify-children-along-a-row-or-column]
When no child grows, there may be space left over along the row or column. `Justify` decides where it goes,
using the values from the [`flex`](https://pkg.go.dev/github.com/lrstanley/bloom/flex#Justify) package:
```go
import "github.com/lrstanley/bloom/flex"
bloom.Row(a, b).Justify(flex.SpaceBetween)
```
```
┌─┐ ┌─┐
│a│ │b│
└─┘ └─┘
```
| Value | Leftover space goes |
| --- | --- |
| `flex.Start` | after the children (the default) |
| `flex.Center` | equally before and after them |
| `flex.End` | before them |
| `flex.SpaceBetween` | between the children, none at the edges |
| `flex.SpaceAround` | around each child, so the edges get half as much as between children |
| `flex.SpaceEvenly` | equally between children and at both edges |
## Space children evenly [#space-children-evenly]
`Gap(n)` puts `n` cells between each pair of children:
```go
bloom.Row(cancel, save).Gap(2)
bloom.Column(fields...).Gap(1)
```
A slot that can never have any cells, such as `Fixed(0, ...)` or one that has collapsed, doesn't get a gap, so
hiding a child doesn't leave a double gap behind.
## Related [#related]
- [`TextNode`](https://pkg.go.dev/github.com/lrstanley/bloom#TextNode),
[`BoxNode`](https://pkg.go.dev/github.com/lrstanley/bloom#BoxNode), and
[`FlexNode`](https://pkg.go.dev/github.com/lrstanley/bloom#FlexNode) API reference
- [Lip Gloss](https://github.com/charmbracelet/lipgloss), for the styles themselves
# Grids and wrapping (https://bloomfx.dev/layout/grids-and-wrapping.md)
Rows and columns place children on one line. For a dashboard of cards, or a set of tags that should wrap like
words in a paragraph, use `Grid` or `Wrap`.
## Lay out cards in a grid [#lay-out-cards-in-a-grid]
`bloom.Grid(cols, children...)` fills `cols` equal-width columns, row by row:
```go
bloom.Grid(3, cards...).Gap(1, 0)
```
```
┌────────┐ ┌───────┐ ┌───────┐
│1 │ │2 │ │3 │
└────────┘ └───────┘ └───────┘
┌────────┐
│4 │
└────────┘
```
- `Gap(x, y)` sets the space between columns and between rows.
- A short last row keeps the same column widths, so cards line up.
- Each row is as tall as its tallest cell. Rows share any extra height equally, and give up height in proportion
to their own size when there isn't enough.
- A `cols` below one means a single column.
### Change the column count with the width [#change-the-column-count-with-the-width]
Combine a grid with a [`Layouter`](https://bloomfx.dev/layout/responsive.md#build-a-layout-for-the-space-youre-given) to pick the number
of columns from the space available:
```go
type cards struct {
items []bloom.Child
}
func (c cards) View() string {
return bloom.Render(c.Layout(bloom.Context{}), 0, 0)
}
func (c cards) Layout(ctx bloom.Context) bloom.Child {
cols := max(ctx.Width/30, 1) // a column for every 30 cells
return bloom.Grid(cols, c.items...).Gap(2, 1)
}
```
### Size content inside a cell [#size-content-inside-a-cell]
A cell's content stretches to fill the cell. To size it differently, wrap it in a slot: a slot child in a grid
sizes its content horizontally within its cell.
```go
bloom.Grid(2,
bloom.Fixed(12, badge), // 12 columns wide, inside a wider cell
description,
)
```
## Wrap children onto new lines [#wrap-children-onto-new-lines]
`bloom.Wrap(children...)` places children left to right at their natural sizes, starting a new line when the
next one doesn't fit, like words in a paragraph. It suits tags, chips, and buttons:
```go
chip := lipgloss.NewStyle().
Foreground(lipgloss.Color("#191724")).
Background(lipgloss.Color("#c4a7e7")).
Padding(0, 1)
tags := make([]bloom.Child, len(article.Tags))
for i, tag := range article.Tags {
tags[i] = bloom.Text("#" + tag).Style(chip)
}
bloom.Wrap(tags...).Gap(1, 1)
```
```
┌─────┐ ┌────┐ ┌─────┐
│alpha│ │beta│ │gamma│
└─────┘ └────┘ └─────┘
┌─────┐
│delta│
└─────┘
```
- `Gap(x, y)` sets the space between children and between lines.
- Each line is as tall as its tallest child.
- A child wider than the whole container gets a line of its own and is clipped.
- Slot rules don't apply inside a wrap: every child keeps its natural size.
Placed in an `Auto` slot, or with no slot, a wrap is as tall as all its lines, so the content below it moves down
as tags wrap.
## Related [#related]
- [`GridNode`](https://pkg.go.dev/github.com/lrstanley/bloom#GridNode) and
[`WrapNode`](https://pkg.go.dev/github.com/lrstanley/bloom#WrapNode) API reference
- [Responsive layouts](https://bloomfx.dev/layout/responsive.md)
# Scrolling and sizing in Update (https://bloomfx.dev/layout/scrolling.md)
## Scroll content [#scroll-content]
`bloom.Scroll(child, offset)` gives a child as much height as it wants, and shows it starting `offset` rows down,
clipped to the space the scroll has. The offset is model state, so you decide how keys and the mouse wheel move
it:
```go
type model struct {
ui *bloom.Root
offset int // rows scrolled
// ...
}
func (m *model) layout() bloom.Child {
return bloom.Box(panel, bloom.Scroll(bloom.Text(m.article).Style(prose), m.offset))
}
```
`Scroll` measures and draws its whole child, which is fine for an article or a help screen. For a long log or
a table with thousands of rows, have the component only render the rows in view instead, such as with the
[Bubbles viewport](https://bloomfx.dev/layout/bubbles-components.md).
## Find a node's size in Update [#find-a-nodes-size-in-update]
To clamp the offset, or to page by a screen at a time, `Update` needs to know how tall the scrolled area is. That's
decided by the layout, which runs in `View`.
[`bloom.Solve`](https://pkg.go.dev/github.com/lrstanley/bloom#Solve) lays out a tree without drawing it, and
returns where each node with an ID would go. Layout is a pure function of the tree and the size, so `Solve` in
`Update` gives the same rectangles that `View` will draw for the same state.
Give the nodes you need IDs. Here, the box gives the size of one page, and the text inside the scroll gives the
full height of the article:
```go
type pane string
const (
paneReader pane = "reader"
paneArticle pane = "article"
)
func (m *model) layout() bloom.Child {
return bloom.Column(
header,
bloom.Fill(bloom.Box(panel, bloom.Scroll(
bloom.Text(m.article).Style(prose).ID(paneArticle),
m.offset,
))).ID(paneReader),
)
}
func (m *model) View() tea.View {
v := m.ui.View(m.width, m.height, m.layout())
v.AltScreen = true
return v
}
```
Then solve the same layout in `Update`:
```go
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
case tea.KeyPressMsg:
frame := bloom.Solve(m.layout(), m.width, m.height)
page := 1
if r, ok := frame.Rect(paneReader); ok {
page = max(r.Dy()-panel.GetVerticalFrameSize(), 1)
}
switch msg.String() {
case "down":
m.offset++
case "up":
m.offset--
case "pgdown":
m.offset += page
case "pgup":
m.offset -= page
}
if r, ok := frame.Rect(paneArticle); ok {
m.offset = min(max(m.offset, 0), max(r.Dy()-page, 0))
}
}
return m, m.ui.Update(msg)
}
```
`Rect` returns an `image.Rectangle` in screen cells, and whether the node is in the frame at all. A node
in a [collapsed slot](https://bloomfx.dev/layout/responsive.md), or a layer that's hidden, isn't.
## Scroll with the mouse wheel [#scroll-with-the-mouse-wheel]
Put an [`on.Scroll`](https://pkg.go.dev/github.com/lrstanley/bloom/on#Scroll) handler on the reader, so the wheel
only scrolls what it's over. `dy` is `1` for each step down and `-1` for each step up:
```go
reader := bloom.Fill(bloom.Box(panel, bloom.Scroll(content, m.offset))).
ID(paneReader).
On(on.Scroll(func(_, dy int) tea.Cmd { m.offset += dy * 3; return nil }))
```
The handler runs just before `Update` gets the wheel event, so clamp the offset in `Update` as you do for keys.
Remember to turn on mouse reporting in `View` with `v.MouseMode = tea.MouseModeCellMotion`.
## Solve or Frame? [#solve-or-frame]
There are two ways to get geometry, and they answer different questions:
- `bloom.Solve(layout, w, h)` lays out the tree you pass, for the state your model has now. Use it in `Update`
after changing state, when the answer has to reflect that change.
- `m.ui.Frame()` returns the geometry of the frame last drawn on screen. It costs nothing, and it's what
[focus](https://bloomfx.dev/input/focus.md) moves through, but it doesn't know about changes your model made since. With
[layout animation](https://bloomfx.dev/motion/layout-animation.md), it has the rectangles currently on screen, mid-animation, while
`Solve` returns where things are headed.
## Related [#related]
- [Using Bubbles components](https://bloomfx.dev/layout/bubbles-components.md), for sizing a viewport the same way
- [Mouse events](https://bloomfx.dev/input/mouse.md)
# Using Bubbles components (https://bloomfx.dev/layout/bubbles-components.md)
[Bubbles](https://github.com/charmbracelet/bubbles) components work in bloom without any wrapping: each has a
`View() string` method, so it can be a child anywhere in a layout. bloom draws its view into whatever space it's
given and clips anything that doesn't fit.
```go
bloom.Row(
bloom.Fixed(30, m.list),
bloom.Fill(m.viewport),
)
```
Many components also need to know their size, because they decide what to render from it: a viewport shows as
many lines as its height, a list pages by its height, and a table truncates columns by its width. bloom decides
those sizes during layout, so you have to pass them to the component.
## Give a component its size [#give-a-component-its-size]
Give the slot the component is in an ID, then use [`bloom.Solve`](https://pkg.go.dev/github.com/lrstanley/bloom#Solve)
when the window size changes. `Solve` lays out the same tree `View` draws, without drawing it, and reports
where each node with an ID goes:
```go
func (m *model) layout() bloom.Child {
return bloom.Column(
bloom.Fill(bloom.Box(panel, m.log)).ID(paneLog),
statusbar,
)
}
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
if r, ok := bloom.Solve(m.layout(), m.width, m.height).Rect(paneLog); ok {
m.log.SetWidth(r.Dx() - panel.GetHorizontalFrameSize())
m.log.SetHeight(r.Dy() - panel.GetVerticalFrameSize())
}
}
// ...
}
```
The slot's rectangle includes the box's border, so subtract the style's frame size to get the space inside.
If your layout also changes for other reasons, such as a panel being toggled or collapsing, run the same code
after those changes too. Putting it in one method that you call from each place keeps it in sync.
## Show the terminal cursor [#show-the-terminal-cursor]
Text inputs in Bubbles can draw a fake cursor in their view, or report where the real terminal cursor should go.
bloom can place the real one: [`Text(s).Cursor(c)`](https://pkg.go.dev/github.com/lrstanley/bloom#TextNode.Cursor)
shows the cursor at `c`, relative to the node's top-left corner, wherever the node ends up on screen.
```go
input := textinput.New()
input.SetVirtualCursor(false) // report the cursor, rather than drawing one
input.Focus()
```
```go
bloom.Box(panel, bloom.Text(m.input.View()).Cursor(m.input.Cursor()))
```
`Cursor` returns `nil` when the input isn't focused, which shows no cursor. bloom also hides the cursor while a
[layer](https://bloomfx.dev/layers/dialogs.md) covers its cell, so a dialog opening over an input doesn't leave a cursor blinking on top.
## A complete example [#a-complete-example]
A chat-style log with an input at the bottom. Type and press enter to add a line, and the log scrolls to
the end.
```go title="main.go"
package main
import (
"fmt"
"os"
"strings"
"charm.land/bubbles/v2/textinput"
"charm.land/bubbles/v2/viewport"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/lrstanley/bloom"
)
type pane string
const (
paneLog pane = "log"
paneInput pane = "input"
)
var panel = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#6e6a86"))
type model struct {
ui *bloom.Root
width, height int
log viewport.Model
input textinput.Model
lines []string
}
func newModel() *model {
input := textinput.New()
input.Placeholder = "say something"
input.SetVirtualCursor(false)
input.Focus()
return &model{ui: bloom.New(), log: viewport.New(), input: input}
}
func (m *model) Init() tea.Cmd {
return m.ui.Init()
}
func (m *model) layout() bloom.Child {
field := bloom.Text(m.input.View()).Cursor(m.input.Cursor())
return bloom.Column(
bloom.Fill(bloom.Box(panel, m.log)).ID(paneLog),
bloom.Auto(bloom.Box(panel, field)).ID(paneInput),
)
}
// resize gives the components the size their slots will have, inside the border.
func (m *model) resize() {
frame := bloom.Solve(m.layout(), m.width, m.height)
if r, ok := frame.Rect(paneLog); ok {
m.log.SetWidth(r.Dx() - panel.GetHorizontalFrameSize())
m.log.SetHeight(r.Dy() - panel.GetVerticalFrameSize())
}
if r, ok := frame.Rect(paneInput); ok {
// Leave room for the prompt, and for the cursor after the last character.
inner := r.Dx() - panel.GetHorizontalFrameSize()
m.input.SetWidth(inner - lipgloss.Width(m.input.Prompt) - 1)
}
}
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
m.resize()
case tea.KeyPressMsg:
switch msg.String() {
case "ctrl+c":
return m, tea.Quit
case "enter":
m.lines = append(m.lines, m.input.Value())
m.log.SetContent(strings.Join(m.lines, "\n"))
m.log.GotoBottom()
m.input.Reset()
return m, nil
}
}
var cmd tea.Cmd
m.input, cmd = m.input.Update(msg)
cmds = append(cmds, cmd)
m.log, cmd = m.log.Update(msg)
cmds = append(cmds, cmd, m.ui.Update(msg))
return m, tea.Batch(cmds...)
}
func (m *model) View() tea.View {
v := m.ui.View(m.width, m.height, m.layout())
v.AltScreen = true
return v
}
func main() {
if _, err := tea.NewProgram(newModel()).Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
```
## Write your own components the same way [#write-your-own-components-the-same-way]
The components you write don't need anything bloom-specific either: a `View() string` method is enough. When a
component would benefit from knowing more, it can opt in:
- [`bloom.Measurer`](https://pkg.go.dev/github.com/lrstanley/bloom#Measurer) reports its natural size, so bloom
doesn't have to render its view to measure it. A one-line status bar can say it's one row tall at any width.
- [`bloom.Layouter`](https://pkg.go.dev/github.com/lrstanley/bloom#Layouter) builds a bloom layout for the space it's
given, which lets it [change shape with its size](https://bloomfx.dev/layout/responsive.md#build-a-layout-for-the-space-youre-given).
```go
type statusbar struct {
text string
}
func (s statusbar) View() string {
return " " + s.text
}
// Measure reports one row, as wide as the text. A column stretches it across its
// full width anyway.
func (s statusbar) Measure(bloom.Size) bloom.Size {
return bloom.Size{W: lipgloss.Width(s.View()), H: 1}
}
```
Return the size the component needs, not the space it's offered: when a layout is drawn at its natural size, the
space offered is unbounded.
## Related [#related]
- [Scrolling and sizing in Update](https://bloomfx.dev/layout/scrolling.md)
- [How layout works](https://bloomfx.dev/concepts/how-layout-works.md)
- [Bubbles](https://github.com/charmbracelet/bubbles) on GitHub
# Dialogs and modals (https://bloomfx.dev/layers/dialogs.md)
A dialog is a **layer**: a node drawn over the rest of the screen without taking space from it.
`bloom.Overlay(base, layers...)` draws a base layout and then each layer over it, in order.
```go
bloom.Overlay(
screen,
bloom.Layer(bloom.Box(dialogStyle, dialogContent)),
)
```
A layer made with `bloom.Layer(child)` is centered on the screen at its natural size. For a step-by-step version
of this page, see the [dialogs and toasts tutorial](https://bloomfx.dev/getting-started/dialogs-and-toasts.md).
## Show and hide a dialog [#show-and-hide-a-dialog]
Keep an [`anim.Presence`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#Presence) for the dialog in your
model, and give it to the layer. The layer is only drawn while the presence is visible, including while it
animates out, and its effects run with the presence's progress:
```go
type model struct {
// ...
confirm anim.Presence
}
func newModel() *model {
return &model{
ui: bloom.New(),
confirm: anim.NewPresence(anim.Duration(200 * time.Millisecond)),
}
}
```
```go
bloom.Layer(dialog).Presence(m.confirm, fx.Fade{}, fx.Slide{From: bloom.Bottom})
```
`m.confirm.Show()`, `Hide()`, and `Toggle()` start the animation and return a command for it. Pass every message to
the presence so it can move, with [`anim.Update`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#Update):
```go
return m, tea.Batch(cmd, m.ui.Update(msg), anim.Update(msg, &m.confirm))
```
`m.confirm.Shown()` reports whether the dialog is open or opening, which is what your key handling should check.
`Visible()` is also true while it animates out.
## Make it modal [#make-it-modal]
`Modal()` makes a layer take every mouse event from what's beneath it. Clicks inside it go to its nodes as usual.
Clicks outside don't reach the screen behind; they run the layer's
[`on.ClickOutside`](https://pkg.go.dev/github.com/lrstanley/bloom/on#ClickOutside) handler, so you can close the
dialog:
```go
bloom.Layer(dialog).
ID(confirmDialog).
Modal().
On(on.ClickOutside(m.closeDialog)).
Presence(m.confirm, fx.Fade{})
```
A modal layer with an ID is also a focus scope. See [focus and keyboard navigation](https://bloomfx.dev/input/focus.md#keep-focus-inside-a-dialog)
for keeping tab inside the dialog.
Keyboard events aren't routed by bloom; your `Update` sees every key. While a dialog is open, check
`m.confirm.Shown()` first, and handle keys for the dialog rather than the screen behind it.
## Dim what's beneath [#dim-whats-beneath]
`Backdrop(effects...)` applies [effects](https://bloomfx.dev/reference/effects.md) to everything drawn beneath the layer. Its strength
follows the layer's transition, so the screen dims as the dialog opens and brightens as it closes.
```go
bloom.Layer(dialog).
Modal().
Backdrop(fx.Dim{Amount: 0.6}).
Presence(m.confirm, fx.Fade{})
```
Effects that suit backdrops:
| Effect | Looks like |
| --- | --- |
| `fx.Dim{Amount: 0.6}` | colors fade toward the terminal's background |
| `fx.Desaturate{Amount: 1}` | colors fade to gray |
| `fx.Tint{Color: c, Amount: 0.3}` | colors move toward `c` |
| `fx.Recolor{Fg: fg, Bg: bg}` | everything redrawn in two colors, keeping the text |
| `fx.Spotlight{Rect: r}` | everything dimmed except the rectangle `r` |
Several effects can be combined: `Backdrop(fx.Desaturate{Amount: 1}, fx.Dim{Amount: 0.4})`.
> **Dim needs the terminal's colors**
>
> `Dim` moves colors toward the terminal's background color, and `Shimmer` and `Wave` use its foreground. The root
> asks the terminal for both. Return `m.ui.Init()` from your model's `Init` and pass every message to
> `m.ui.Update` so it hears the answer, or set defaults with
> [`bloom.WithColors`](https://pkg.go.dev/github.com/lrstanley/bloom#WithColors).
## Add a shadow [#add-a-shadow]
`Shadow(x, y, effects...)` applies effects to the area beneath the layer, offset by `x` columns and `y` rows,
like a drop shadow:
```go
bloom.Layer(dialog).Shadow(2, 1, fx.Dim{Amount: 0.7})
```
Terminal cells are about twice as tall as they are wide, so an offset of two columns and one row looks even.
## Size and place a dialog [#size-and-place-a-dialog]
- `Size(w, h)` sets the layer's size. Zero for either uses the child's natural size on that axis. Text styled with
`Style` wraps to the width you set.
- `Margin(n)` keeps the layer at least `n` cells from the screen's edges, shrinking it if the screen is small.
- `Anchor(x, y)` places it on each axis with `bloom.Start`, `bloom.Center`, or `bloom.End`. The default is the
center.
- `Offset(x, y)` moves it from where it's anchored.
```go
bloom.Layer(helpText).Size(60, 0).Margin(2) // 60 columns wide, as tall as its text
```
## Stack dialogs [#stack-dialogs]
Layers are drawn in order, so a dialog later in the list appears on top. A second confirmation opened from the
first is just another layer after it:
```go
bloom.Overlay(screen,
bloom.Layer(confirm).ID(confirmDialog).Modal().Presence(m.confirm, fx.Fade{}),
bloom.Layer(overwrite).ID(overwriteDialog).Modal().Presence(m.overwrite, fx.Fade{}),
)
```
Mouse events go to the topmost modal layer. For focus, push each dialog's scope as it opens and pop it as it
closes, and focus returns to the right place each time.
A backdrop applies to everything beneath its layer, including other dialogs. To dim the screen once, rather than
once per dialog, only give the top dialog a backdrop:
```go
first := bloom.Layer(confirm).ID(confirmDialog).Modal().Presence(m.confirm, fx.Fade{})
if !m.overwrite.Visible() {
first = first.Backdrop(fx.Dim{Amount: 0.6})
}
second := bloom.Layer(overwrite).ID(overwriteDialog).Modal().
Backdrop(fx.Dim{Amount: 0.6}).
Presence(m.overwrite, fx.Fade{})
```
## Animate without a presence [#animate-without-a-presence]
`Presence` is the simplest way to show and hide a layer, but `Transition(t, effects...)` takes any progress from
0 to 1, such as an [`anim.Value`](https://bloomfx.dev/motion/animating-values.md) or a number you compute. The layer is drawn whatever
`t` is, so leave it out of the overlay when it shouldn't be there.
```go
bloom.Layer(panel).Transition(m.panelAnim.Float(), fx.Wipe{From: bloom.Left})
```
## Related [#related]
- [`LayerNode` API reference](https://pkg.go.dev/github.com/lrstanley/bloom#LayerNode)
- [Dropdowns and popovers](https://bloomfx.dev/layers/dropdowns.md)
- [Toast notifications](https://bloomfx.dev/layers/toasts.md)
- [Transitions](https://bloomfx.dev/motion/transitions.md), for every effect a dialog can open with
- The [`forms` example](https://github.com/lrstanley/bloom/tree/master/_examples/forms), with stacked dialogs and a
springy entrance
# Dropdowns and popovers (https://bloomfx.dev/layers/dropdowns.md)
A dropdown is a layer placed against another node, rather than against the screen.
[`AnchorTo(id, side)`](https://pkg.go.dev/github.com/lrstanley/bloom#LayerNode.AnchorTo) puts a layer against a side
of the node with that ID, aligned with its start.
## Show suggestions below an input [#show-suggestions-below-an-input]
Give the input an ID, then anchor a layer to its bottom edge:
```go
type fieldID string
const fieldLanguage fieldID = "language"
func (m *model) View() tea.View {
form := bloom.Column(
bloom.Text("Language"),
bloom.Text(m.language).Style(inputStyle).ID(fieldLanguage),
)
var layers []bloom.LayerNode
if matches := m.suggestions(); len(matches) > 0 {
options := make([]bloom.Child, len(matches))
for i, match := range matches {
options[i] = bloom.Text(match).Style(optionStyle).On(on.Click(func() tea.Cmd {
m.language = match
return nil
}))
}
layers = append(layers, bloom.Layer(bloom.Box(menuStyle, bloom.Column(options...))).
AnchorTo(fieldLanguage, bloom.Bottom))
}
v := m.ui.View(m.width, m.height, bloom.Overlay(form, layers...))
v.MouseMode = tea.MouseModeCellMotion
return v
}
```
`m.suggestions()` stands for your own matching: it returns the options that match what's been typed.
The dropdown isn't modal, so the rest of the screen still gets clicks. A click on an option runs its handler,
like a click on anything else.
## Close it on a click elsewhere [#close-it-on-a-click-elsewhere]
A menu that stays open until dismissed can close itself with
[`on.ClickOutside`](https://pkg.go.dev/github.com/lrstanley/bloom/on#ClickOutside), which runs when a button goes
down anywhere but on the layer. The click still reaches what it's on:
```go
bloom.Layer(menu).AnchorTo(fieldLanguage, bloom.Bottom).On(on.ClickOutside(m.closeMenu))
```
## Choose a side [#choose-a-side]
The side is `bloom.Bottom`, `bloom.Top`, `bloom.Left`, or `bloom.Right`:
- `Bottom` and `Top` put the layer below or above the node, lined up with its left edge.
- `Left` and `Right` put it beside the node, lined up with its top edge.
The layer is shifted to stay on screen, so a menu anchored near the right edge moves left rather than being cut
off. Use `Offset(x, y)` to nudge it, such as one row down to leave a gap.
## Follow the node [#follow-the-node]
The anchor is found again every frame, so a popover stays attached when its node moves: when the list scrolls, a
panel resizes, or a slot [animates to a new place](https://bloomfx.dev/motion/layout-animation.md). Add `.ID(...).Animate()` to the
layer itself to have it glide from place to place rather than jump.
```go
bloom.Layer(marker).AnchorTo(selectedRow, bloom.Right).ID("marker").Animate()
```
While the anchor node isn't on screen, because it's scrolled away, collapsed, or not in the tree, the layer is
hidden too.
## Tooltips [#tooltips]
A tooltip is the same thing, shown while the pointer is over a node. Following the pointer needs
`v.MouseMode = tea.MouseModeAllMotion`.
[`on.Hover`](https://pkg.go.dev/github.com/lrstanley/bloom/on#Hover) runs when the pointer moves onto a node and
off it. Note which button it's over:
```go
bloom.Text("deploy").Style(button).ID(buttonDeploy).On(on.Hover(func(over bool) tea.Cmd {
if over {
m.hovered = buttonDeploy
} else if m.hovered == buttonDeploy {
m.hovered = ""
}
return nil
}))
```
Then anchor a small layer to the hovered button:
```go
if m.hovered != "" {
layers = append(layers, bloom.Layer(bloom.Text(tooltips[m.hovered]).Style(tipStyle)).
AnchorTo(m.hovered, bloom.Top))
}
```
## Related [#related]
- [Dialogs and modals](https://bloomfx.dev/layers/dialogs.md)
- [Mouse events](https://bloomfx.dev/input/mouse.md)
- The [`forms` example](https://github.com/lrstanley/bloom/tree/master/_examples/forms), with an autocomplete dropdown
# Toast notifications (https://bloomfx.dev/layers/toasts.md)
A toast is a [layer](https://bloomfx.dev/layers/dialogs.md) anchored to a corner of the screen, shown with an
[`anim.Presence`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#Presence) that hides itself.
## Show one toast [#show-one-toast]
Keep a presence and the message in your model:
```go
type model struct {
// ...
toast anim.Presence
toastText string
}
```
Create the presence with `anim.NewPresence()`, and show it with
[`ShowFor`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#Presence.ShowFor). It animates in, waits, then
animates out, with no timer messages for you to handle:
```go
func (m *model) save() tea.Cmd {
// ...
m.toastText = "Saved"
return m.toast.ShowFor(2 * time.Second)
}
```
Calling `ShowFor` again while the toast is up restarts the countdown, so repeated saves keep one toast on screen
rather than flickering it.
In `View`, anchor the layer to the bottom-right corner, one cell in from the edges:
```go
toast := bloom.Layer(bloom.Box(toastStyle, bloom.Text(m.toastText))).
Anchor(bloom.End, bloom.End).
Margin(1).
Presence(m.toast, fx.Slide{From: bloom.Right})
v := m.ui.View(m.width, m.height, bloom.Overlay(screen, toast))
```
Pass every message to the presence, with `anim.Update(msg, &m.toast)` in `Update`, so it can animate.
### Other corners and entrances [#other-corners-and-entrances]
`Anchor(x, y)` takes `bloom.Start`, `bloom.Center`, or `bloom.End` on each axis:
| Position | Anchor | A matching entrance |
| --- | --- | --- |
| bottom right | `Anchor(bloom.End, bloom.End)` | `fx.Slide{From: bloom.Right}` |
| top right | `Anchor(bloom.End, bloom.Start)` | `fx.Slide{From: bloom.Right}` |
| bottom center | `Anchor(bloom.Center, bloom.End)` | `fx.Slide{From: bloom.Bottom}` |
| top center | `Anchor(bloom.Center, bloom.Start)` | `fx.Slide{From: bloom.Top}` |
Effects combine, so `fx.Typewriter{}, fx.Fade{}` types the message in while it fades. See the
[effects reference](https://bloomfx.dev/reference/effects.md) for the full list.
## Stack several toasts [#stack-several-toasts]
To show more than one at a time, keep a list of toasts, each with its own presence. Put them in a column inside a
single layer, and wrap each in [`bloom.Present`](https://pkg.go.dev/github.com/lrstanley/bloom#Present).
`Present` is the `Presence` of an ordinary node: a hidden toast takes no space and leaves no gap, so the others
close up behind it.
```go
type toast struct {
text string
presence anim.Presence
}
// notify adds a toast that shows for three seconds.
func (m *model) notify(text string) tea.Cmd {
t := &toast{text: text, presence: anim.NewPresence()}
m.toasts = append(m.toasts, t)
return t.presence.ShowFor(3 * time.Second)
}
```
Every presence needs every message. Once a toast has finished animating out, drop it from the list:
```go
for _, t := range m.toasts {
cmds = append(cmds, t.presence.Update(msg))
}
m.toasts = slices.DeleteFunc(m.toasts, func(t *toast) bool { return !t.presence.Visible() })
```
```
press n for a notification, q to quit
╭───────────────╮
│ Saved draft 1 │
╰───────────────╯
╭───────────────╮
│ Saved draft 2 │
╰───────────────╯
```
The complete program, which adds a toast each time you press n:
```go title="main.go"
package main
import (
"fmt"
"os"
"slices"
"time"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/lrstanley/bloom"
"github.com/lrstanley/bloom/anim"
"github.com/lrstanley/bloom/fx"
)
var toastBox = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#9ccfd8")).
Padding(0, 1)
type toast struct {
text string
presence anim.Presence
}
type model struct {
ui *bloom.Root
width, height int
toasts []*toast
count int
}
func (m *model) Init() tea.Cmd { return m.ui.Init() }
// notify adds a toast that shows for three seconds.
func (m *model) notify(text string) tea.Cmd {
t := &toast{text: text, presence: anim.NewPresence()}
m.toasts = append(m.toasts, t)
return t.presence.ShowFor(3 * time.Second)
}
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
case tea.KeyPressMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "n":
m.count++
cmds = append(cmds, m.notify(fmt.Sprintf("Saved draft %d", m.count)))
}
}
for _, t := range m.toasts {
cmds = append(cmds, t.presence.Update(msg))
}
// Forget toasts once they have finished animating out.
m.toasts = slices.DeleteFunc(m.toasts, func(t *toast) bool { return !t.presence.Visible() })
cmds = append(cmds, m.ui.Update(msg))
return m, tea.Batch(cmds...)
}
func (m *model) View() tea.View {
stack := make([]bloom.Child, len(m.toasts))
for i, t := range m.toasts {
box := bloom.Box(toastBox, bloom.Text(t.text))
stack[i] = bloom.Present(t.presence, box, fx.Slide{From: bloom.Right})
}
toasts := bloom.Layer(bloom.Column(stack...).Align(bloom.End)).
Anchor(bloom.End, bloom.End).
Margin(1)
v := m.ui.View(m.width, m.height, bloom.Overlay(
bloom.Text("press n for a notification, q to quit"),
toasts,
))
v.AltScreen = true
return v
}
func main() {
if _, err := tea.NewProgram(&model{ui: bloom.New()}).Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
```
The toasts are stored as pointers, so the presence each `ShowFor` started is the one that keeps receiving
messages, even as the slice changes.
## Related [#related]
- [Animating values](https://bloomfx.dev/motion/animating-values.md), for more on presences
- [Dialogs and modals](https://bloomfx.dev/layers/dialogs.md)
# Mouse events (https://bloomfx.dev/input/mouse.md)
Bubble Tea reports mouse events in screen coordinates. bloom knows where it drew every node, so you attach handlers
to the nodes themselves, and bloom runs the right one with positions relative to that node.
## Turn on mouse reporting [#turn-on-mouse-reporting]
Set a mouse mode on the view you return:
```go
v := m.ui.View(m.width, m.height, layout)
v.MouseMode = tea.MouseModeCellMotion // clicks, the wheel, and drags
return v
```
Use `tea.MouseModeAllMotion` to also get motion while no button is pressed, for [hover](#hover).
## Attach handlers [#attach-handlers]
Every node has an `On` method, which takes handlers made by the
[`on`](https://pkg.go.dev/github.com/lrstanley/bloom/on) package:
```go
import "github.com/lrstanley/bloom/on"
```
```go
for i, s := range services {
rows[i] = bloom.Text(s).On(
on.Click(func() tea.Cmd { m.selected = i; return nil }),
on.DoubleClick(func() tea.Cmd { return m.open(i) }),
on.RightClick(on.Msg(menuMsg{i})),
)
}
```
No IDs are needed: the handler is already on the node it's about. Text, slots, boxes, rows, columns, grids, wraps,
and layers all have `On`.
| Handler | Runs on |
| --- | --- |
| `on.Click(fn)` | a left click: the button goes down and comes up over the node |
| `on.RightClick(fn)`, `on.MiddleClick(fn)` | a click with those buttons |
| `on.DoubleClick(fn)` | the second of two clicks within 400ms; both clicks still run `Click` |
| `on.Press(fn)` | a left click, or enter or space while the node has [focus](https://bloomfx.dev/input/focus.md#press-buttons-with-the-keyboard) |
| `on.ClickAt(fn)` | a left click, with where it happened |
| `on.Scroll(fn)` | the wheel, as `dx, dy` steps |
| `on.Hover(fn)` | the pointer moving onto the node (`true`) or off it (`false`) |
| `on.Drag(fn)` | the button going down on the node, the pointer moving while it's held, and it coming up |
| `on.ClickOutside(fn)` | a button going down anywhere but on the node |
| `on.Mouse(fn)` | every mouse event over the node, for anything else |
Most take a `func() tea.Cmd`, so methods such as `m.save` can be passed as they are. Clicks run when the button
comes up over the same node it went down on, like a real button: pressing a button and sliding off it before
letting go doesn't click it.
## Change state, or send a message [#change-state-or-send-a-message]
Handlers run on the Bubble Tea event loop, just before `Update` gets the same event, so a handler can change the
model directly, the same way `Update` does: change state, then return a command. The command runs in the
background, so a `tea.Tick` or I/O doesn't freeze the screen. Handlers themselves must not block.
A handler can only change the real model if it reaches it. Handlers are closures made in `View`, so they capture
whatever `View` had. With a pointer receiver, `func (m *model) View()`, that's the model itself. With a value
receiver, it's a copy, and changes made to it are lost without warning.
When a handler can't reach the state, have it send a message instead. `on.Msg(msg)` makes a handler function that
sends `msg` to `Update`:
```go
bloom.Text(name).On(on.Click(on.Msg(openMsg(i))))
```
```go
case openMsg:
m.page = int(msg)
```
> **Components kept by value**
>
> This matters most for components. A Bubbles-style component is often kept by value in its parent
> (`m.list, cmd = m.list.Update(msg)`), so a handler built in its own `View` that sets `l.selected` changes a copy,
> even when the parent is a pointer. Have the component send a message with `on.Msg` and handle it in its own
> `Update`, give it pointer receivers, or build the handler in the parent over `&m.list`.
Messages from separate events can arrive in any order, since Bubble Tea sends each in its own goroutine. Changes
made directly in handlers happen in order.
## Which node gets the event [#which-node-gets-the-event]
For each kind of event, the innermost node under the pointer with a handler for it takes it, and the nodes it's
inside don't. A row's `Click` and its pane's `Scroll` work side by side, with no need to stop anything from
spreading:
```go
pane := bloom.Column(rows...).On(on.Scroll(func(_, dy int) tea.Cmd { m.offset += dy * 3; return nil }))
```
A click on a row runs the row's `Click`; the wheel over the same row scrolls the pane. A click on the pane where
there's no row runs the pane's `Click`, if it has one. Gaps between the children of a row or column belong to the
row or column, so a click anywhere along a list row's line can select it.
Hover is the exception: every node under the pointer is hovered, as in CSS.
## Use the position inside a node [#use-the-position-inside-a-node]
`on.ClickAt` gets an [`on.Event`](https://pkg.go.dev/github.com/lrstanley/bloom/on#Event), with `X` and `Y`
relative to the node the handler is on, and `W` and `H`, its size. A seek bar needs nothing else:
```go
bloom.Fixed(1, bar).On(on.ClickAt(func(e on.Event) tea.Cmd {
m.position = m.duration * time.Duration(e.X) / time.Duration(e.W)
return nil
}))
```
The rectangle of a box includes its border and padding, so `X` and `Y` count from its outer corner.
## Scroll with the wheel [#scroll-with-the-wheel]
`on.Scroll` gets `dy` as `1` for each step down and `-1` for each step up, and `dx` the same for left and right.
Each pane scrolls on its own:
```go
bloom.Fill(m.reader).On(on.Scroll(func(_, dy int) tea.Cmd { m.offset += dy * 3; return nil }))
```
Clamp the offset in `Update`, which gets the wheel event after the handler ran.
## Hover [#hover]
`HoverStyle` on text and boxes swaps in a style while the pointer is over the node, with no state in the model:
```go
bloom.Text("save").Style(button).HoverStyle(buttonHover).On(on.Press(m.save))
```
Give it a style that takes the same space as the node's own, changing only its colors and attributes.
For anything more, such as showing a tooltip, `on.Hover` runs when the pointer moves onto the node and off it:
```go
bloom.Text("deploy").On(on.Hover(func(over bool) tea.Cmd { m.tip = over; return nil }))
```
Both need `tea.MouseModeAllMotion` to follow the pointer while no button is down. A node stops being hovered when
something covers it, such as a dialog opening over it, even without the pointer moving. bloom notices with the next
message `m.ui.Update` gets after the new frame is drawn, such as an animation frame, a key, or the pointer moving. Terminals don't report the
pointer leaving the window, so the hover also ends when the window loses focus, which needs `v.ReportFocus = true`.
## Drag [#drag]
`on.Drag` runs when the left button goes down on the node, each time the pointer moves while it's held, and once
more with `Done` set when it comes up. The node keeps getting the drag after the pointer leaves it, which a
one-cell splitter needs:
```go
divider := bloom.Fixed(1, bloom.Text("│")).On(on.Drag(func(e on.Event) tea.Cmd {
m.split = e.Screen.X
return nil
}))
```
`e.Screen` is where the pointer is on screen, `e.Start` is where the drag started, and `e.X` and `e.Y` are relative
to the node, outside it when the pointer is.
A drag also ends, with `Done` set, when the node leaves the frame or a modal layer opens over it, and when the
release gets lost, such as outside the window. bloom follows the dragged node from frame to frame by where it is in
the tree, or by its ID if it has one, so give a node that moves around in the tree while it's dragged an ID.
## Layers and the mouse [#layers-and-the-mouse]
Layers drawn on top get events first. A layer that's [modal](https://bloomfx.dev/layers/dialogs.md#make-it-modal) takes every event from
what's beneath it. `on.ClickOutside` on a modal layer runs for a click on its backdrop, which is how a dialog closes
when you click away from it:
```go
bloom.Layer(dialog).Modal().On(on.ClickOutside(m.closeDialog))
```
On a layer that isn't modal, such as a [dropdown](https://bloomfx.dev/layers/dropdowns.md), `on.ClickOutside` runs for a click anywhere
else, and the click still reaches what it's on.
A layer whose presence is hidden doesn't take events. One that's still animating out does, so a modal dialog keeps
blocking clicks until it's gone.
## Handle raw events yourself [#handle-raw-events-yourself]
The original `tea.MouseClickMsg` and the other mouse messages still reach `Update`. If you handle them as well as
using handlers, don't act on the same event twice. To find what's under a point, `m.ui.Frame().Hit(x, y)` returns
the IDs of the nodes there, outermost first:
```go
case tea.MouseClickMsg:
if path := m.ui.Frame().Hit(msg.X, msg.Y); slices.Contains(path, any(paneLog)) {
m.follow = false
}
```
For things other than events, such as drawing an effect from a button's position, `m.ui.Frame().Rect(id)` returns
where a node was drawn in the last frame, in screen coordinates:
```go
if r, ok := m.ui.Frame().Rect(buttonDeploy); ok {
center := image.Pt((r.Min.X+r.Max.X)/2, (r.Min.Y+r.Max.Y)/2)
// ...
}
```
## Fewer frames for the mouse [#fewer-frames-for-the-mouse]
Terminals can send wheel and motion events faster than they're worth drawing. `bloom.ThrottleMouse` makes a
Bubble Tea filter that drops those that come too soon after the last:
```go
p := tea.NewProgram(m, tea.WithFilter(bloom.ThrottleMouse(15*time.Millisecond)))
```
A dropped event isn't made up for: the wheel scrolls a little less, and if the pointer stops right after a dropped
motion event, hover catches up when it moves again.
## How it works [#how-it-works]
`m.ui.View` installs a `tea.View.OnMouse` handler. Bubble Tea calls it on its event loop for each mouse event,
before `Update` and the next `View`, which is what lets handlers change the model safely and have the change drawn
right away. bloom runs the handlers there, and batches the commands they return, which Bubble Tea then runs in the
background like any other.
If the view doesn't come from `m.ui.View`, or something replaces its `OnMouse`, `m.ui.Update` runs the handlers
instead when it gets the event, so pass it every message. It skips events that `OnMouse` already handled.
## Test handlers [#test-handlers]
`m.ui.Click(id)` clicks the middle of the node with an ID, and `m.ui.ClickAt(x, y)` clicks a point, running the
handlers of the last frame drawn and returning their commands. `m.ui.Update` runs handlers for any other mouse
message, so a test can drive hover, the wheel, and drags without a terminal:
```go
m.View()
m.ui.Update(tea.MouseMotionMsg{X: 3, Y: 1})
if _, ok := m.ui.Click(buttonSave); !ok || m.saves != 1 {
t.Fatal("save was not pressed")
}
```
## Related [#related]
- [Focus and keyboard navigation](https://bloomfx.dev/input/focus.md), which can focus the node you click
- [Scrolling and sizing in Update](https://bloomfx.dev/layout/scrolling.md)
- [`on` API reference](https://pkg.go.dev/github.com/lrstanley/bloom/on)
- The [`mouse` example](https://github.com/lrstanley/bloom/tree/master/_examples/mouse), with hover styles, double
clicks, the wheel, and a draggable divider
# Focus and keyboard navigation (https://bloomfx.dev/input/focus.md)
[`bloom.Focus`](https://pkg.go.dev/github.com/lrstanley/bloom#Focus) keeps track of which node has keyboard focus,
by ID. It lives in your model. The layout marks which nodes can take focus, and the order they appear in the
layout is the tab order.
## Make nodes focusable [#make-nodes-focusable]
Focus refers to nodes by ID, so give everything that can take focus an ID of the same type. A type of your own,
with a constant for each node, keeps them apart from other IDs:
```go
// control is the ID of a node that can take focus.
type control string
const (
fieldName control = "name"
fieldEmail control = "email"
buttonSave control = "save"
buttonCancel control = "cancel"
)
```
Add a `bloom.Focus` of that type to your model. Its zero value has nothing focused:
```go
type model struct {
ui *bloom.Root
focus bloom.Focus[control]
values map[control]string
// ...
}
```
Then mark the nodes `Focusable`:
```go
bloom.Text(m.values[fieldName]).Style(input).ID(fieldName).Focusable()
bloom.Text(m.values[fieldEmail]).Style(input).ID(fieldEmail).Focusable()
```
Focus moves between focusable nodes with IDs of its type, and passes over the rest. Slots can be focusable too,
such as `bloom.Fill(m.editor).ID(paneEditor).Focusable()`, which makes a whole pane a stop in the tab order.
## Move focus with tab and clicks [#move-focus-with-tab-and-clicks]
`m.focus.Update(msg, m.ui.Frame())` handles tab and shift+tab. It returns a command, and
reports whether it handled the message:
```go
case tea.KeyPressMsg:
cmd, ok := m.focus.Update(msg, m.ui.Frame())
if !ok {
cmd = m.key(msg) // other keys, for whatever has focus
}
return m, tea.Batch(cmd, m.ui.Update(msg))
```
`m.ui.Frame()` is the layout of the last frame drawn, which has the nodes that can be focused and their order.
Focus wraps around from the last node to the first.
To move focus to what's clicked, give the root your focus with `bloom.WithFocus`:
```go
func newModel() *model {
m := &model{}
m.ui = bloom.New(bloom.WithFocus(&m.focus))
return m
}
```
A click then focuses the innermost focusable node under the pointer, before any [mouse handler](https://bloomfx.dev/input/mouse.md) for
the click runs, so a handler can move focus on from there, as a "next" button would. The root keeps a pointer to
your `Focus`, so keep the model behind a pointer too.
You can also move focus yourself: `Set(id)`, `Next(frame)`, `Prev(frame)`, and `Clear()`. `Set` takes an ID of the
focus's type, so `m.focus.Set("cancel")` sets the `control` `"cancel"`.
## Press buttons with the keyboard [#press-buttons-with-the-keyboard]
A button made with [`on.Press`](https://pkg.go.dev/github.com/lrstanley/bloom/on#Press) runs its handler when
clicked, and when enter or space is pressed while it has focus. `m.focus.Update` runs it for
the keys, and returns its command with `ok` set:
```go
bloom.Text("save").Style(button).ID(buttonSave).Focusable().On(on.Press(m.save))
```
Enter and space on a focused node without a `Press` handler aren't handled, so they reach your own key handling,
such as to type a space into an input.
## Style the focused node [#style-the-focused-node]
`m.focus.Is(id)` reports whether a node has focus, so `View` can style it:
```go
func (m *model) field(id control) bloom.Child {
style := input
if m.focus.Is(id) {
style = inputFocused
}
return bloom.Text(m.values[id]).Style(style).ID(id).Focusable()
}
```
## Route keys to the focused node [#route-keys-to-the-focused-node]
`m.focus.Current()` returns the focused node's ID, and whether anything is focused. While nothing is, the ID is
the type's zero value, which matches none of your constants:
```go
id, _ := m.focus.Current()
switch id {
case fieldName, fieldEmail:
m.values[id] += msg.Text
}
```
### Nodes of different kinds [#nodes-of-different-kinds]
When the IDs of what takes focus are already different types, such as an `int` for each row of a table and a
`string` for each button, make `K` an interface they all implement. A type switch then tells them apart:
```go
// control is implemented by the ID types of everything that takes focus.
type control interface{ control() }
func (rowID) control() {}
func (buttonID) control() {}
```
```go
id, _ := m.focus.Current() // a bloom.Focus[control]
switch id := id.(type) {
case rowID:
m.open(id)
case buttonID:
return m, m.press(id)
}
```
A `bloom.Focus[any]` moves between every focusable node, whatever its ID.
## Show the cursor in an input [#show-the-cursor-in-an-input]
`Cursor` on a text node shows the terminal's cursor at a position inside the node. Put it after the text in the
focused input:
```go
in := bloom.Text(value).Style(style).ID(id).Focusable()
if m.focus.Is(id) {
// one column in, for the style's left padding
in = in.Cursor(tea.NewCursor(1+ansi.StringWidth(value), 0))
}
```
The position is relative to the node's top-left cell, wherever it ends up on screen. The cursor is hidden while a
layer covers that cell. `ansi` here is `github.com/charmbracelet/x/ansi`, which measures the width of text the way
the terminal does.
For Bubbles text inputs, use the cursor the component reports; see
[Using Bubbles components](https://bloomfx.dev/layout/bubbles-components.md#show-the-terminal-cursor).
## Keep focus inside a dialog [#keep-focus-inside-a-dialog]
While a dialog is open, tab should only move between its buttons. A **scope** confines focus to the
focusable nodes inside it. Modal layers with an ID are scopes:
```go
bloom.Layer(dialog).ID(confirmDialog).Modal()
```
`Push(scope)` confines focus to the scope and clears it, and remembers what was focused before. `Pop()` leaves the
scope and focuses that node again:
```go
func (m *model) openDialog() tea.Cmd {
m.focus.Push(confirmDialog)
m.focus.Set(buttonCancel)
return m.confirm.Show()
}
func (m *model) closeDialog() tea.Cmd {
m.focus.Pop()
return m.confirm.Hide()
}
```
Scopes nest, so a dialog opened from a dialog pushes a second scope, and closing it returns to the first.
`m.focus.Scope()` returns the innermost scope pushed, which tells you which dialog is on top.
## Group a panel into a scope [#group-a-panel-into-a-scope]
Slots can be scopes too, with `Scope()`. Without pushing it, a scope lets you ask whether focus is somewhere in a
part of the screen, such as to highlight a panel's border while any field in it is focused:
```go
bloom.Fixed(60, form).ID(scopeForm).Scope()
```
```go
if m.focus.Within(m.ui.Frame(), scopeForm) {
border = activeBorder
}
```
## Related [#related]
- [`Focus` API reference](https://pkg.go.dev/github.com/lrstanley/bloom#Focus)
- [Dialogs and modals](https://bloomfx.dev/layers/dialogs.md)
- The [`forms` example](https://github.com/lrstanley/bloom/tree/master/_examples/forms), with focusable inputs,
buttons, and stacked dialog scopes
# Animating values (https://bloomfx.dev/motion/animating-values.md)
Everything that moves in bloom is driven by a number. The
[`anim`](https://pkg.go.dev/github.com/lrstanley/bloom/anim) package animates those numbers, as ordinary fields in
your model:
| Type | Use it for |
| --- | --- |
| [`anim.Value`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#Value) | a number moving toward a target, such as a panel's width, or a one-shot effect's progress |
| [`anim.Presence`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#Presence) | something being shown and hidden, from 0 to 1, such as a dialog or toast |
| [`anim.Sequence`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#Sequence) | a series of steps on one number: animate, hold, jump, or loop forever |
## How animations run [#how-animations-run]
Animations follow the Bubble Tea pattern: starting one returns a `tea.Cmd`, which schedules a frame message, and
passing that message back to the animation moves it forward and schedules the next. Two rules keep it working:
1. **Return the command** from `Show`, `To`, `Play`, or `Loop`.
2. **Pass every message** to every animation. [`anim.Update`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#Update)
does that for any number of them and batches their commands:
```go
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyPressMsg:
if msg.String() == "?" {
cmd = m.help.Toggle()
}
}
return m, tea.Batch(cmd, anim.Update(msg, &m.help, &m.sidebar, &m.toast))
}
```
Each animation ignores frame messages that aren't its own, so you don't need to sort them. Progress follows the
clock rather than counting frames, so a slow frame never makes an animation take longer.
Animations are kept by value in your model and change in place, which is why `anim.Update` takes pointers to
them.
## Animate a number with Value [#animate-a-number-with-value]
`anim.NewValue(start, options...)` makes a value. `To(target)` animates it there from wherever it is now, and
`Float()` or `Int()` reads it in `View`:
```go
type model struct {
sidebarWidth anim.Value
// ...
}
func newModel() *model {
return &model{sidebarWidth: anim.NewValue(24)}
}
// in Update
case "b":
if m.sidebarWidth.Target() > 0 {
cmd = m.sidebarWidth.To(0)
} else {
cmd = m.sidebarWidth.To(24)
}
// in View
bloom.Fixed(m.sidebarWidth.Int(), sidebar)
```
Calling `To` again mid-animation changes course smoothly. For moving layout, consider
[layout animation](https://bloomfx.dev/motion/layout-animation.md) instead, which needs no state in your model at all.
### Run a one-shot effect [#run-a-one-shot-effect]
Some effects play from the start each time something happens, such as a shake when input is rejected.
`Play(from, to)` jumps to `from` and animates to `to`:
```go
shake: anim.NewValue(1, anim.Duration(450*time.Millisecond), anim.Ease(anim.Linear)),
// when the input is wrong
cmd = m.shake.Play(0, 1)
// in View, only while it runs
if m.shake.Animating() {
field = bloom.Transition(m.shake.Float(), field, fx.Shake{})
}
```
See [Feedback effects](https://bloomfx.dev/motion/feedback-effects.md) for more of these.
## Show and hide with Presence [#show-and-hide-with-presence]
A presence is a value from 0, hidden, to 1, shown. It's what layers, toasts, and `bloom.Present` take:
```go
help := anim.NewPresence(anim.Duration(200 * time.Millisecond))
help.Show() // animate in
help.Hide() // animate out
help.Toggle() // whichever is the opposite
help.ShowFor(time.Second) // in, then out again after a second
```
Each returns a command. To read it:
- `Shown()` is true once it's shown or animating in. Use it to decide what keys do.
- `Visible()` is true while any of it is on screen, including while it animates out. Use it to decide what to draw.
- `Progress()` is how far in it is, from 0 to 1.
- `Float()` is the same, without clamping, for springs that overshoot.
Effects given to a presence run with its progress, so the same effects play forward when it shows and backward
when it hides.
## Choose easing or a spring [#choose-easing-or-a-spring]
By default a value eases to each target over 250 milliseconds with `anim.OutCubic`. Change that with options:
```go
anim.NewValue(0, anim.Duration(600*time.Millisecond), anim.Ease(anim.InOutCubic))
anim.NewPresence(anim.Spring(9, 0.35))
```
- **Easing** takes a fixed duration and a curve. Use it for things that should take a predictable time.
The easings are `Linear`, `InQuad`, `OutQuad`, `InOutQuad`, `OutCubic`, `InOutCubic`, and `OutBack`, which
overshoots slightly. Any `func(float64) float64` works too.
- **A spring** is a damped spring of an angular frequency, where higher is faster, and a damping ratio, where 1
settles without bouncing and less than 1 bounces. A spring keeps its speed when the target changes partway,
which feels natural for things the user moves quickly, like a selection.
A spring with damping below 1 overshoots its target. For a presence, `Progress()` stays between 0 and 1, and
`Float()` goes past them, which you can use to make a dialog bounce past its place:
```go
bloom.Layer(dialog).
Offset(0, int(math.Round((1-m.dialog.Float())*-10))).
Presence(m.dialog, fx.Fade{})
```
## Chain steps with Sequence [#chain-steps-with-sequence]
A sequence runs a number through a list of steps. It replaces chains of timers, and its steps start when the
previous one was due to end, so a long sequence doesn't drift:
```go
pulse := anim.NewSequence(0,
anim.To(1, anim.Duration(900*time.Millisecond)),
anim.To(0, anim.Duration(900*time.Millisecond)),
).With(anim.Ease(anim.InOutQuad))
cmd := pulse.Loop() // or Play() to run the steps once
```
The steps are `To(target, options...)`, `Hold(duration)`, and `Jump(target)`. `With` sets the options every step
starts from, and options passed to a `To` override them for that step.
A phase that runs from 0 to 1 at an even pace and starts over, as a shimmer or scrolling text needs, has a
shortcut:
```go
shimmer := anim.NewLoop(1400 * time.Millisecond)
cmd := shimmer.Loop()
```
`Stop()` stops a sequence where it is.
## Frame rate [#frame-rate]
Values ask for 60 frames a second while they animate, which is Bubble Tea's default rendering rate. Use
`anim.FPS(n)` to change it per value, such as a lower rate for a slow pulse. If you raise it, raise Bubble Tea's
too, with `tea.WithFPS(n)` when you create the program, or the extra frames won't be drawn.
Nothing is scheduled while no animation is running, so an idle program doesn't wake up.
While the terminal isn't focused, values ask for 10 frames a second instead, as an animation nobody is looking at
needn't be smooth, and a loop such as a shimmer then costs a fraction of the CPU. Bubble Tea reports focus only
when the view sets `ReportFocus`:
```go
func (m model) View() tea.View {
v := m.ui.View(m.width, m.height, m.layout())
v.ReportFocus = true
return v
}
```
Use `anim.UnfocusedFPS(n)` to change the rate. Values learn of focus from the `tea.FocusMsg` and `tea.BlurMsg`
passed to their `Update`, so a value made while the terminal is blurred runs at its full rate until the next
change. The full rate comes back from the frame after focus returns.
## Related [#related]
- [Transitions](https://bloomfx.dev/motion/transitions.md), for using these values with effects
- [`anim` API reference](https://pkg.go.dev/github.com/lrstanley/bloom/anim)
- [Animation options reference](https://bloomfx.dev/reference/options.md#animation-options)
# Transitions (https://bloomfx.dev/motion/transitions.md)
A **transition** draws a node with [effects](https://bloomfx.dev/reference/effects.md) applied to its cells, for a progress from 0 to 1.
At 0 the node is just starting to enter, and at 1 it's fully shown, exactly as it would be without the effect. Feed
the progress from an [animated value](https://bloomfx.dev/motion/animating-values.md) and the node animates.
## Three ways to apply one [#three-ways-to-apply-one]
| Where | Use |
| --- | --- |
| A layer that comes and goes | `bloom.Layer(c).Presence(p, effects...)` |
| A node in a layout that comes and goes | `bloom.Present(p, c, effects...)` |
| Any node, with any progress | `bloom.Transition(t, c, effects...)` or `bloom.Layer(c).Transition(t, effects...)` |
The `Presence` forms take an [`anim.Presence`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#Presence) and
leave the node out completely while it's hidden. A hidden `Present` node takes no space in its row or column and
leaves no gap behind. A hidden layer has no backdrop, no shadow, and doesn't take mouse events.
```go
// a dialog
bloom.Layer(dialog).Presence(m.dialog, fx.Fade{}, fx.Slide{From: bloom.Bottom})
// a details panel that slides open below a list
bloom.Column(
list,
bloom.Present(m.details, bloom.Box(panel, details), fx.Wipe{From: bloom.Top}),
)
```
The `Transition` forms draw the node whatever the progress is. Use them for effects that run on something that's
always there, or with progress you compute yourself:
```go
bloom.Transition(m.reveal.Float(), chart, fx.Typewriter{})
```
## Showing and hiding use the same effects [#showing-and-hiding-use-the-same-effects]
When a presence hides, its progress runs back from 1 to 0, so the effects play in reverse: a dialog that slides up
from the bottom slides back down. You don't need separate exit effects.
If you want something to leave differently from how it came in, choose the effects from its state:
```go
effects := []fx.Effect{fx.Fade{}, fx.Slide{From: bloom.Bottom}}
if !m.dialog.Shown() { // animating out
effects = []fx.Effect{fx.Fade{}}
}
bloom.Layer(dialog).Presence(m.dialog, effects...)
```
## Combine effects [#combine-effects]
Effects given together are applied in order, all with the same progress. `fx.Fade{}, fx.Slide{From: bloom.Bottom}`
fades in while sliding up. `fx.Scramble{}, fx.Fade{}` settles random glyphs into the text while it fades in.
The [`fx`](https://pkg.go.dev/github.com/lrstanley/bloom/fx) package has four combinators for finer control:
### Cascade with Stagger [#cascade-with-stagger]
`Stagger` splits a node into rows or columns, and runs an effect on each one after the other. A list sliding in
one item at a time:
```go
fx.Stagger{Effect: fx.Slide{From: bloom.Left}, From: bloom.Top}
```
`From` is the edge the first part is at: `Top` and `Bottom` split the node into rows, and `Left` and `Right` into
columns. `Spread` is how much of the progress is spent starting the parts; the default, 0.5, starts the last part
when the first is halfway done.
### Run in sequence with Delay [#run-in-sequence-with-delay]
`Delay` runs an effect over part of the progress only. Several delays in a row play one after another:
```go
bloom.Layer(card).Presence(m.card,
fx.Delay{Effect: fx.Fade{}, To: 0.5}, // fade in over the first half
fx.Delay{Effect: fx.Scramble{Seed: 5}, From: 0.5}, // then unscramble the text
)
```
### Run backward with Reverse [#run-backward-with-reverse]
`Reverse` runs an effect with the progress flipped, for a node leaving while the progress rises:
```go
bloom.Transition(m.leaving.Float(), oldPage, fx.Reverse{Effect: fx.Wipe{From: bloom.Left}})
```
### Change the curve with Ease [#change-the-curve-with-ease]
`Ease` runs an effect with eased progress, using any easing function, such as those in `anim`:
```go
fx.Ease{Effect: fx.Slide{From: bloom.Left}, Easing: anim.OutBack} // overshoots, then settles
```
Easing the effect is different from easing the presence: the presence's progress drives the backdrop and every
effect, while `fx.Ease` changes one effect only. `Ease` holds a function, which Go can't compare, so a frame using
it is redrawn every time even when nothing changed. That's fine while it animates; avoid leaving it on a node that
sits still.
## Open a dialog from its button [#open-a-dialog-from-its-button]
`fx.Iris` reveals a node through a growing circle. Center the circle on the button that was pressed and the
dialog appears to open out of it. Find the button in the last frame when it's pressed:
```go
bloom.Text("share").ID(buttonShare).On(on.Press(m.openShare))
```
```go
func (m *model) openShare() tea.Cmd {
if r, ok := m.ui.Frame().Rect(buttonShare); ok {
m.shareFrom = image.Pt((r.Min.X+r.Max.X)/2, (r.Min.Y+r.Max.Y)/2)
}
return m.share.Show()
}
```
```go
bloom.Layer(shareDialog).Presence(m.share, fx.Iris{At: m.shareFrom})
```
`At` is in screen coordinates. The zero point means the center of the node.
## Random effects stay steady [#random-effects-stay-steady]
Effects with randomness, like `Dissolve`, `Scramble`, `Shade`, `Decode`, and `Glitch`, take a `Seed`. The same
seed gives the same pattern on every frame, so the effect doesn't flicker as it runs. Change the seed to vary it,
such as one per dialog or one per time it opens.
## Related [#related]
- [Effects reference](https://bloomfx.dev/reference/effects.md), with every effect and its fields
- [Animating values](https://bloomfx.dev/motion/animating-values.md)
- The [`transitions`](https://github.com/lrstanley/bloom/tree/master/_examples/transitions) and
[`effects`](https://github.com/lrstanley/bloom/tree/master/_examples/effects) examples, which show every effect
# Layout animation (https://bloomfx.dev/motion/layout-animation.md)
When a panel opens, a list re-sorts, or a split changes ratio, the layout jumps to its new shape on the next frame.
Add `.Animate()` to a slot or layer and bloom glides it there instead, reflowing its content at each size along
the way. Your model only holds the application state, such as whether the sidebar is open, and no animation
values.
```go
width := 0
if m.sidebarOpen {
width = 24
}
bloom.Row(
bloom.Fixed(width, sidebar).ID("sidebar").Animate(),
bloom.Fill(content).ID("content").Animate(),
)
```
Toggle `m.sidebarOpen` and the sidebar slides open or closed while the content resizes to match.
## Requirements [#requirements]
Layout animation is kept by the `*bloom.Root` between frames, so it needs three things:
- **An ID** on each animated slot or layer, so bloom can match it from one frame to the next.
- **A `Root`**, rendering with `m.ui.View`. `bloom.Render` and `bloom.Solve` don't keep state between calls, and
always show the final layout.
- **Every message passed to `m.ui.Update`**, with its command returned. That's what schedules frames while
anything is moving:
```go
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmd := m.ui.Update(msg)
// ...
return m, cmd
}
```
## What animates [#what-animates]
- A slot moving to a new place, such as list items re-sorting, glides there. Give each item an ID that stays with
the item, not with its position, like its name or database ID.
- A slot changing size grows or shrinks, and its children lay out again at each intermediate size.
- A slot whose size becomes zero, like the closed sidebar above, animates closed rather than vanishing.
- A layer anchored to a node, such as a marker [anchored](https://bloomfx.dev/layers/dropdowns.md#follow-the-node) to the selected row,
follows it smoothly from row to row.
Some changes happen at once: a [collapsed](https://bloomfx.dev/layout/responsive.md) slot, a node removed from the tree, and a layer whose
anchor disappears are gone on the next frame.
## Choose the motion [#choose-the-motion]
`Animate` takes the same options as an [animated value](https://bloomfx.dev/motion/animating-values.md). The default eases over 250
milliseconds; a spring suits things that move often, because it keeps its momentum when the target changes
mid-flight:
```go
bloom.Fixed(width, sidebar).ID("sidebar").Animate(anim.Spring(16, 0.85))
bloom.Layer(marker).
AnchorTo(selected, bloom.Right).
ID("marker").
Animate(anim.Spring(20, 1))
```
The options are captured when the ID first appears, so changing them later has no effect on a node already on
screen. The frame rate and clock come from the root, set with
[`bloom.WithFPS`](https://pkg.go.dev/github.com/lrstanley/bloom#WithFPS),
[`bloom.WithUnfocusedFPS`](https://pkg.go.dev/github.com/lrstanley/bloom#WithUnfocusedFPS), and
[`bloom.WithClock`](https://pkg.go.dev/github.com/lrstanley/bloom#WithClock).
## Geometry while animating [#geometry-while-animating]
`m.ui.Frame().Rect(id)` reports where a node was drawn on screen, so mid-animation it's the in-between
rectangle. `bloom.Solve` reports where the layout is headed. Use `Solve` when you need final sizes in `Update`,
such as to [size a Bubbles component](https://bloomfx.dev/layout/bubbles-components.md).
## When to use a Value instead [#when-to-use-a-value-instead]
`Animate` covers layout: position and size. Use an [`anim.Value`](https://bloomfx.dev/motion/animating-values.md) when you need the
number in your model, such as to drive an effect, to read the progress, or to animate something that isn't a
rectangle.
## Related [#related]
- [`SlotNode.Animate`](https://pkg.go.dev/github.com/lrstanley/bloom#SlotNode.Animate) and
[`LayerNode.Animate`](https://pkg.go.dev/github.com/lrstanley/bloom#LayerNode.Animate) API reference
- The [`reflow` example](https://github.com/lrstanley/bloom/tree/master/_examples/reflow): a sidebar, a re-sorting
list, a changing split, a panel coming and going, and a marker following the selection
# Feedback effects (https://bloomfx.dev/motion/feedback-effects.md)
Some effects aren't entrances. They respond to something happening: input rejected, data loading, a number
changing, a deploy succeeding. Each takes its own progress, usually from a one-shot
[`anim.Value`](https://bloomfx.dev/motion/animating-values.md#run-a-one-shot-effect) or a looping
[`anim.Sequence`](https://bloomfx.dev/motion/animating-values.md#chain-steps-with-sequence).
The pattern for one-shot effects is the same each time:
1. Keep an `anim.Value` that starts at 1, with a duration and linear easing.
2. When the event happens, `Play(0, 1)` and return its command.
3. In `View`, wrap the node with `bloom.Transition` only while the value is animating.
## Shake rejected input [#shake-rejected-input]
`fx.Shake` moves a node side to side with a decaying wobble. It's still at 0 and at 1.
```go
shake: anim.NewValue(1, anim.Duration(450*time.Millisecond), anim.Ease(anim.Linear)),
```
```go
// in Update, when the input is wrong
cmd = m.shake.Play(0, 1)
// in View
field := bloom.Child(input)
if m.shake.Animating() {
field = bloom.Transition(m.shake.Float(), field, fx.Shake{Amount: 3, Times: 3})
}
```
`Amount` is how many cells it moves at most (2 by default), and `Times` is how many times it swings each way (4 by
default).
## Skeleton loading placeholders [#skeleton-loading-placeholders]
[`bloom.Skeleton`](https://pkg.go.dev/github.com/lrstanley/bloom#Skeleton) draws placeholder bars while data
loads. Each number is a line: a bar that many cells wide, or `0` for a blank line. Give it the same number of lines
as the content it stands in for, so nothing moves when the content arrives.
`Shimmer` sweeps a band of light across the bars. Its phase comes from a looping sequence in your model:
```go
shimmer: anim.NewLoop(1400 * time.Millisecond),
```
```go
func (m *model) Init() tea.Cmd {
// fetchStats is your command that loads the data.
return tea.Batch(m.ui.Init(), m.shimmer.Loop(), fetchStats)
}
// when the data arrives
m.loaded = true
m.shimmer.Stop()
// in View: four lines, as the loaded card has four
if !m.loaded {
return bloom.Box(card, bloom.Skeleton(8, 0, 5, 11).Shimmer(m.shimmer.Float()))
}
return bloom.Box(card, bloom.Column(
bloom.Text("Requests").Style(heading),
bloom.Fixed(1, nil),
bloom.Text(strconv.Itoa(m.requests)).Style(big),
bloom.Text("per minute").Style(label),
))
```
Bars are drawn in the terminal's foreground faded most of the way into its background, so they suit any theme.
`Style` colors them yourself, and bars wider than the space they're given are cut to fit.
The band starts just off one side and ends just off the other, so the loop restarts without a visible jump. It's
[`fx.Shimmer`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Shimmer), which works on any node. For a band of
another color or width, or to shimmer a whole card, draw it in a transition instead:
```go
bloom.Transition(m.shimmer.Float(), placeholder, fx.Shimmer{Color: accent, Width: 10})
```
## Roll changing numbers [#roll-changing-numbers]
`fx.Roll` turns the digits of a number over like an odometer. Draw the new number, and give the effect the number
shown before:
```go
roll: anim.NewValue(1, anim.Duration(700*time.Millisecond), anim.Ease(anim.Linear)),
```
```go
// when the number changes
m.previous, m.requests = m.requests, latest
cmd = m.roll.Play(0, 1)
// in View
num := bloom.Child(bloom.Text(strconv.Itoa(m.requests)))
if m.roll.Animating() {
num = bloom.Transition(m.roll.Float(), num, fx.Roll{From: strconv.Itoa(m.previous)})
}
```
Digits are matched from the right and separators like commas are skipped, so going from 1,021 to 1,058 only turns
the last two digits over. `Roll` works on the text, so format the number however you like, as long as `From` is
formatted the same way.
## Burst confetti [#burst-confetti]
`fx.Burst` throws particles out from a point. Its progress is the burst's age, and nothing is drawn at 1. Apply it
to the whole screen so particles can fly anywhere, starting from a button's position in the last frame:
```go
burst: anim.NewValue(1, anim.Duration(1800*time.Millisecond), anim.Ease(anim.Linear)),
```
```go
screen := bloom.Child(layout)
if m.burst.Animating() {
if r, ok := m.ui.Frame().Rect(buttonDeploy); ok {
screen = bloom.Transition(m.burst.Float(), screen, fx.Burst{
At: image.Pt((r.Min.X+r.Max.X)/2, r.Min.Y),
Count: 90,
Seed: m.deploys,
})
}
}
```
Particles only change the glyph and color of cells they land on, and stay inside the node the effect is applied
to.
## Glitch a badge [#glitch-a-badge]
`fx.Glitch` makes a node look like a breaking-up signal: rows jitter, colors split, and cells garble, fading to
nothing at 1. It works well as an entrance for a status that just changed:
```go
badge := bloom.Child(bloom.Text("LIVE").Style(live))
if m.badge.Animating() {
badge = bloom.Transition(m.badge.Float(), badge, fx.Glitch{Seed: m.deploys})
}
```
## Spotlight a tour [#spotlight-a-tour]
`fx.Spotlight` dims everything except a rectangle. As a layer's backdrop, it walks the user through a screen one
part at a time. Put the explanation in a layer, and light the part it explains:
```go
note := bloom.Layer(bloom.Box(tourStyle, bloom.Text(steps[m.step]))).
Anchor(bloom.Center, bloom.End).
Margin(2).
Presence(m.tour, fx.Fade{})
if r, ok := m.ui.Frame().Rect(cardID(m.step)); ok {
note = note.Backdrop(fx.Spotlight{Rect: r.Inset(-1), Soft: 5})
}
```
`Soft` is how many cells the dimming takes to reach full strength, for a soft edge. `fx.Decode` suits the
explanation text, revealing it like digital rain settling into place.
## Scroll text that doesn't fit [#scroll-text-that-doesnt-fit]
`bloom.Marquee(s, phase)` shows one line of text. When it fits, it's shown as is. When it doesn't, it's cut short
with "…" while `phase` is 0, and scrolls around as `phase` runs from 0 to 1. That suits a long title in a list
item, scrolling only while the item is selected:
```go
name: anim.NewSequence(0,
anim.Jump(0),
anim.Hold(900*time.Millisecond), // wait a moment before scrolling
anim.To(1, anim.Duration(7*time.Second)),
).With(anim.Ease(anim.Linear)),
```
```go
// when an item is selected
cmd = m.name.Loop()
// in View
phase := 0.0
if i == m.selected {
phase = m.name.Float()
}
bloom.Fill(bloom.Marquee(item.title, phase).Style(itemStyle))
```
For a news ticker that always scrolls, use `anim.NewLoop(24 * time.Second)` as the phase, and `Gap` to set what's
shown between the end of the text and its start: `bloom.Marquee(headlines, phase).Gap(" ◆ ")`.
## Related [#related]
- [Effects reference](https://bloomfx.dev/reference/effects.md)
- The [`launch` example](https://github.com/lrstanley/bloom/tree/master/_examples/launch), which uses all of these
effects in a deploy console, and the [`ticker` example](https://github.com/lrstanley/bloom/tree/master/_examples/ticker)
for marquees
# Custom effects (https://bloomfx.dev/motion/custom-effects.md)
An effect is anything with an `Apply` method:
```go
type Effect interface {
Apply(s *canvas.Surface, t float64)
}
```
`s` is the area of cells to change: a node's own cells for a transition, or the frame beneath a layer for a
backdrop. `t` is the progress, from 0 to 1. For a transition, an effect at 1 should leave the cells as they are,
so the node ends up looking like itself.
## Write an effect [#write-an-effect]
This effect flashes a node in a color and fades it back to its own colors, such as to draw attention to a value
that just changed:
```go
import (
"image/color"
"github.com/lrstanley/bloom/canvas"
)
// Flash shows a node in Color at first, and fades it back to its own colors as t
// reaches 1.
type Flash struct {
Color color.Color
}
// Apply implements fx.Effect.
func (e Flash) Apply(s *canvas.Surface, t float64) {
for y := range s.Height() {
for x := range s.Width() {
c := s.Cell(x, y)
if c == nil {
continue
}
c.Style.Fg = s.Mix(e.Color, s.Fg(c), t)
}
}
}
```
Use it like any other effect:
```go
bloom.Transition(m.flash.Float(), value, Flash{Color: lipgloss.Color("#f6c177")})
```
## Keep effects comparable [#keep-effects-comparable]
bloom skips drawing a frame that's identical to the previous one, and compares effects with `==` to find out.
A struct of plain fields, like `Flash`, compares by value, so a node that isn't animating costs nothing to keep on
screen.
`fx.Func` turns a function into an effect, which is handy for a quick experiment:
```go
var revealDown = fx.Func(func(s *canvas.Surface, t float64) {
for y := int(t * float64(s.Height())); y < s.Height(); y++ {
for x := range s.Width() {
s.Hide(x, y)
}
}
})
```
But Go can't compare functions, so bloom assumes a function effect changes every frame and redraws whenever it's
on screen. Effects behind pointers are treated the same way, since they can change in place. Prefer a struct for
effects you keep.
## What a surface offers [#what-a-surface-offers]
[`canvas.Surface`](https://pkg.go.dev/github.com/lrstanley/bloom/canvas#Surface) is the grid of cells an effect
changes. Coordinates are relative to its top-left cell.
| Method or field | Use |
| --- | --- |
| `Width()`, `Height()` | the size of the surface |
| `Cell(x, y)` | the cell at a position, to change its glyph or style. Keep its width the same. |
| `Fg(c)`, `Bg(c)` | the colors a cell shows, with unset colors resolved to the terminal's defaults |
| `Mix(a, b, t)`, `Gray(c, t)` | blend two colors, or move one toward gray |
| `Hide(x, y)` | don't draw a cell, so what's beneath shows through. Only on a node's own cells. |
| `Visible(x, y)` | whether a cell will be drawn |
| `Under(x, y)` | the frame's cell beneath, for blending with what's under a node |
| `Move(r, d)` | shift the cells of a rectangle, keeping wide glyphs whole |
| `Sub(r)` | a surface over part of this one, such as a row, for effects applied piece by piece |
| `Offset` | moves the whole surface when it's drawn, as `fx.Slide` does |
| `Viewport` | the area the node is placed in, such as the screen for a dialog, which tells you how far it can move out of sight |
| `Env` | the terminal's default colors and color profile |
Cells are [Ultraviolet](https://github.com/charmbracelet/ultraviolet) cells: `c.Content` is the glyph, `c.Width`
its width in columns, and `c.Style` its colors and attributes.
## Draw directly with Drawable [#draw-directly-with-drawable]
An effect transforms cells that something else drew. To paint cells yourself, such as a chart, a meter, or a
game board, implement [Ultraviolet](https://github.com/charmbracelet/ultraviolet)'s `Drawable` interface and place it
with [`bloom.Drawable`](https://pkg.go.dev/github.com/lrstanley/bloom#Drawable):
```go
import (
"image/color"
uv "github.com/charmbracelet/ultraviolet"
)
// meter fills a fraction of its area, from the left.
type meter struct {
value float64 // from 0 to 1
color color.Color
}
func (m meter) Draw(scr uv.Screen, area uv.Rectangle) {
filled := area.Min.X + int(m.value*float64(area.Dx()))
for y := area.Min.Y; y < area.Max.Y; y++ {
for x := area.Min.X; x < area.Max.X; x++ {
glyph := "░"
if x < filled {
glyph = "█"
}
scr.SetCell(x, y, &uv.Cell{Content: glyph, Width: 1, Style: uv.Style{Fg: m.color}})
}
}
}
```
```go
bloom.Row(
bloom.Text("cpu "),
bloom.Fill(bloom.Drawable(meter{value: m.cpu, color: lipgloss.Color("#eb6f92")})),
)
```
```
cpu ████████░░░░░░░░░░░░
```
The drawable is clipped to the area it's given. Its natural size is zero unless it also implements
[`bloom.Measurer`](https://pkg.go.dev/github.com/lrstanley/bloom#Measurer), so give it a sized slot such as `Fill` or
`Fixed`.
## Related [#related]
- [`fx` package source](https://github.com/lrstanley/bloom/tree/master/fx), for how the built-in effects are written
- The [`effects` example](https://github.com/lrstanley/bloom/tree/master/_examples/effects), with two custom effects
and a `Drawable`
# How layout works (https://bloomfx.dev/concepts/how-layout-works.md)
This page explains the ideas behind bloom's layout. You don't need it to use bloom, but it makes the rules on the
other pages predictable rather than something to memorize.
## A tree, rebuilt every frame [#a-tree-rebuilt-every-frame]
In Bubble Tea, `View` turns the model into a string on every frame. bloom keeps that shape: `View` turns the model
into a tree of nodes, and bloom turns the tree into the frame.
The tree is a value, built fresh each time from the model's current state. Nothing in it needs to be kept, updated,
or torn down; a dialog that's closed is simply not in the tree (or is in it with a hidden presence). This is the
same idea as Bubble Tea's own: state lives in the model, and the view is a function of it.
Building a tree every frame sounds expensive, but nodes are small values, and bloom caches the expensive work
(parsing strings into cells, rendering Lip Gloss styles) between frames, keyed by what the node contains. See
[Rendering and performance](https://bloomfx.dev/concepts/rendering.md).
## Constraints down, sizes up, parents place [#constraints-down-sizes-up-parents-place]
Layout is one pass down the tree and back up:
```mermaid
flowchart TD
R["Column (80×24)"] -->|"you have 80×24"| A["Row"]
R -->|"you have 80×1"| S["status bar"]
A -->|"you have 24 columns"| B["sidebar"]
A -->|"you have 56 columns"| C["reader"]
S -.->|"I need 1 row"| R
B -.->|"I need 3 rows"| A
```
1. **Constraints go down.** A parent tells each child how much space it may use.
2. **Sizes come up.** A child that has a natural size reports it: a text node its lines, a box its child plus its
frame, a component that implements `Measurer` whatever it says.
3. **Parents place.** The parent decides each child's final rectangle from those answers and its own rules,
whether the child is shown at all, and clips the child to it.
A child never decides its own position, and never gets more than its parent gives it.
## The slot belongs to the parent [#the-slot-belongs-to-the-parent]
How a child takes part in sharing space, whether it's fixed, fills, or grows, is set by the **slot** it's placed
in, not by the child:
```go
bloom.Row(bloom.Fixed(24, list), bloom.Fill(preview)) // list is a sidebar
bloom.Column(bloom.Fill(list), bloom.Fixed(8, preview)) // the same list fills the screen
```
That keeps components reusable. A component doesn't know whether it's a sidebar or a full screen, and doesn't need
options for every arrangement it might end up in. The screen that uses it decides.
## How a row shares space [#how-a-row-shares-space]
For a row (and, turned on its side, a column), bloom works out sizes in this order:
1. `Fixed` slots get exactly their size.
2. `Auto` slots (and children with no slot) get their natural size, and `Percent` slots their percentage.
3. The gaps between children are subtracted.
4. What's left is shared between `Fill` and `Grow` slots by weight, within each slot's `Min` and `Max`.
5. If there wasn't enough, slots that can shrink give up space in proportion to their size, down to their `Min`.
Rounding leftovers go to the earlier slots.
6. If the minimums still don't fit, `Collapsible` slots are dropped, lowest priority first (the last one, on a
tie), and the rest is worked out again.
On the other axis, children stretch to the row's height unless an `Align` says otherwise.
## Components that know their size [#components-that-know-their-size]
bloom can measure any component by rendering its `View()` and counting lines and columns. Two optional interfaces
let a component do better:
- [`Measurer`](https://pkg.go.dev/github.com/lrstanley/bloom#Measurer) answers "how big do you want to be, given
this much space?" without rendering, which is cheaper and lets a component ask for less than its view.
- [`Layouter`](https://pkg.go.dev/github.com/lrstanley/bloom#Layouter) answers "what layout do you want, given this
much space?" by returning nodes. This is how a component changes its shape with its size, rather than only
stretching.
A `Layouter`'s `Layout` method can be called several times per frame, while measuring and for the final layout,
which is why it must not change the component.
## Layout is a pure function [#layout-is-a-pure-function]
Given the same tree and the same size, layout always gives the same rectangles. bloom relies on this in two ways:
- [`bloom.Solve`](https://pkg.go.dev/github.com/lrstanley/bloom#Solve) can run the layout in `Update`, without
drawing, and get exactly the rectangles `View` will draw. That's how components like viewports learn their size
before they render.
- Mouse events are matched against the frame that's on screen, so a click always lands on what the user saw, even
if the model changed since.
[Layout animation](https://bloomfx.dev/motion/layout-animation.md) is the one place bloom keeps layout state between frames: the `Root`
remembers where animated nodes were, so it can move them smoothly to where the pure layout says they should be.
## Cells, not pixels [#cells-not-pixels]
Terminal layout works in whole cells, and that shows up in a few places:
- Shares that don't divide evenly are rounded, and the leftover cells go to some slots and not others, so two
`Fill` slots can differ by a cell.
- Some characters, such as many emoji and CJK characters, are two cells wide. bloom measures text the way the
terminal does, and never cuts a wide character in half: one cut by an edge is drawn as blanks instead.
- Cells are about twice as tall as they are wide, which is why shadows look even at an offset of `(2, 1)`, and why
`fx.Spotlight` counts horizontal distance as half.
## Related [#related]
- [Sizing with slots](https://bloomfx.dev/layout/slots.md)
- [Responsive layouts](https://bloomfx.dev/layout/responsive.md)
- [Rendering and performance](https://bloomfx.dev/concepts/rendering.md)
# Rendering and performance (https://bloomfx.dev/concepts/rendering.md)
## From tree to frame [#from-tree-to-frame]
Each time `m.ui.View` runs, bloom:
1. **Lays out** the tree, giving every node a rectangle, as described in [How layout works](https://bloomfx.dev/concepts/how-layout-works.md).
2. **Draws** each node's content into cells. Strings from `View()` methods and `Text` are parsed into cells,
styles are rendered by Lip Gloss at the node's size, and boxes draw their frames.
3. **Composites** the nodes in tree order, each clipped to its parent, then each layer of an overlay on top.
4. **Runs effects**: a transition's effects run on a node's own cells before they're composited, and a backdrop's
effects run on the frame beneath its layer.
5. **Records geometry**: where each node with an ID went, which nodes can take focus, and what the mouse can hit,
with the nodes' mouse handlers. That's the `Frame` returned by `m.ui.Frame()`.
6. **Returns a `tea.View`** with the content, the terminal cursor if a node placed one, and an `OnMouse` handler that
runs the handlers of the nodes under the pointer, using the recorded geometry.
## What's cached [#whats-cached]
Most frames change little, so bloom avoids repeating work:
- A string is parsed into cells and measured once, and reused for as long as the same string is drawn.
- A Lip Gloss render is reused while the node's style, content, and size don't change.
- A frame that would be identical to the last one isn't composited again at all.
On a 200×60 dashboard, an unchanged frame takes about 0.04ms, a frame where most of the content changed about
0.44ms, and a dialog animating in over a dimmed backdrop about 0.58ms. At 120 frames a second, each frame has
8.3ms. You can run the benchmarks yourself with `go test -bench . ./...` in the repository.
## Keep frames cheap to compare [#keep-frames-cheap-to-compare]
To know that a frame is unchanged, bloom compares what it's about to draw with the last frame, including effects,
which are compared with `==`. Effects that are plain structs, like all of the [`fx`](https://bloomfx.dev/reference/effects.md) effects, compare by
value. A few things can't be compared, so bloom assumes they change every frame:
- Effects that are functions, such as `fx.Func` or `fx.Ease` (which holds an easing function).
- Effects behind pointers, which could be changed in place.
That only costs anything while they're on screen. Wrapping a node in a transition only while it animates, as in
`if m.shake.Animating() { ... }`, keeps still frames free.
## Colors and the terminal [#colors-and-the-terminal]
Some effects blend toward the terminal's own colors: `Dim` toward its background, and `Shimmer` and `Wave` toward
its foreground. `m.ui.Init()` asks the terminal for those colors, and `m.ui.Update` hears the reply, along with the
terminal's color profile. Until the reply arrives, or if the terminal never answers, bloom uses defaults that you can
set with [`bloom.WithColors`](https://pkg.go.dev/github.com/lrstanley/bloom#WithColors).
Text widths are measured with grapheme clustering by default, as Lip Gloss and modern terminals do. If your
program targets terminals that measure differently, set the method with
[`bloom.WithWidthMethod`](https://pkg.go.dev/github.com/lrstanley/bloom#WithWidthMethod) so bloom agrees with the
terminal about where each cell is.
## Frame rate [#frame-rate]
Bubble Tea draws at most 60 frames a second by default. Animations in `anim` ask for 60 too. For smoother motion on
fast terminals, raise both: `tea.WithFPS(120)` on the program, and `anim.FPS(120)` on your values or
`bloom.WithFPS(120)` on the root for layout animation. Nothing is scheduled while nothing moves, so an idle program
stays idle.
With `ReportFocus` set on the view, animations drop to 10 frames a second while the terminal isn't focused. Change
it with `anim.UnfocusedFPS(n)` and `bloom.WithUnfocusedFPS(n)`; see
[Animating values](https://bloomfx.dev/motion/animating-values.md#frame-rate).
## Render without a program [#render-without-a-program]
[`bloom.Render`](https://pkg.go.dev/github.com/lrstanley/bloom#Render) lays out and draws a tree once, and returns
a string. It doesn't keep anything between calls and shows animations at their targets, so it suits places where a
string is wanted: printing to stdout, inside a Lip Gloss layout, a component's `View` method, or tests. A width or
height of zero uses the tree's natural size on that axis.
`m.ui.Render(w, h, tree)` does the same with the root's caches, for programs that draw inline rather than in the
alternate screen, where a height of zero means "as tall as the content".
## Related [#related]
- [Testing](https://bloomfx.dev/resources/testing.md)
- [Root options reference](https://bloomfx.dev/reference/options.md#root-options)
# Effects (https://bloomfx.dev/reference/effects.md)
All effects are in [`github.com/lrstanley/bloom/fx`](https://pkg.go.dev/github.com/lrstanley/bloom/fx). Each takes a
progress `t` from 0 to 1:
- As a **transition** (`bloom.Transition`, `bloom.Present`, `Layer.Presence`, `Layer.Transition`), it changes the
node's own cells. `t` runs from 0, starting to enter, to 1, fully shown and unchanged.
- As a **backdrop** or **shadow** (`Layer.Backdrop`, `Layer.Shadow`), it changes the frame beneath a layer. `t` is
the strength, following the layer's transition.
- Some take their own progress, such as a shake's or a burst's, and are applied with `bloom.Transition` while it runs.
`Edge` fields take `bloom.Top`, `bloom.Bottom`, `bloom.Left`, or `bloom.Right` (also available as `fx.Top` and so
on). The zero value is `Bottom`. `Seed` fields pick a random pattern that stays the same for the same seed.
## Entrances [#entrances]
| Effect | Fields | Brings a node in by |
| --- | --- | --- |
| [`Fade`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Fade) | | crossfading from what's beneath it |
| [`Slide`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Slide) | `From Edge` | moving in from beyond an edge of its viewport |
| [`Wipe`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Wipe) | `From Edge` | revealing it from one edge to the other |
| [`Dissolve`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Dissolve) | `Seed` | revealing its cells in a random order |
| [`Typewriter`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Typewriter) | | revealing its cells in reading order |
| [`Scramble`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Scramble) | `Seed` | random glyphs settling into its own |
| [`Decode`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Decode) | `Seed` | columns falling into place like digital rain |
| [`Unfold`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Unfold) | | opening from its middle row outward, like a CRT turning on |
| [`Iris`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Iris) | `At image.Point` | a circle growing from `At` (screen coordinates; zero is the node's center) |
| [`Shade`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Shade) | `Seed` | each cell stepping through ░ ▒ ▓ to its glyph |
| [`Blinds`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Blinds) | `From Edge`, `Size int` (default 3) | opening slats, like window blinds |
| [`Wave`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Wave) | `From Edge` | a wobbling front sweeping across, with a bright crest |
| [`Glitch`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Glitch) | `Seed` | a breaking-up signal that settles, with jitter and split colors |
## Backdrops [#backdrops]
| Effect | Fields | Changes what's beneath a layer by |
| --- | --- | --- |
| [`Dim`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Dim) | `Amount float64` | moving colors toward the terminal's background |
| [`Desaturate`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Desaturate) | `Amount float64` | moving colors toward gray |
| [`Tint`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Tint) | `Color color.Color`, `Amount float64` | moving colors toward `Color` |
| [`Recolor`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Recolor) | `Fg, Bg color.Color` | drawing every cell in two colors, keeping the glyphs |
| [`Spotlight`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Spotlight) | `Rect image.Rectangle`, `Amount float64` (default 0.7), `Soft int` | dimming everything outside `Rect`, with a soft edge `Soft` cells wide |
`Amount` runs from 0, no change, to 1, all the way. Backdrops also work as transitions; `Tint` on a node fades it in
from a color, for example.
## Feedback [#feedback]
These take their own progress rather than an entrance's. See [Feedback effects](https://bloomfx.dev/motion/feedback-effects.md) for how to
drive them.
| Effect | Fields | Does |
| --- | --- | --- |
| [`Shake`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Shake) | `Amount int` (default 2), `Times int` (default 4) | moves the node side to side with a decaying wobble; still at 0 and 1 |
| [`Shimmer`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Shimmer) | `Color color.Color`, `Width int` (default 6) | sweeps a highlight band across the node; `t` is the band's position |
| [`Roll`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Roll) | `From string` | turns digits over like an odometer, from the number in `From` |
| [`Burst`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Burst) | `At image.Point`, `Count int` (default 40), `Seed` | throws confetti from `At`; `t` is its age, gone at 1 |
## Combinators [#combinators]
These wrap other effects.
| Effect | Fields | Does |
| --- | --- | --- |
| [`Stagger`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Stagger) | `Effect`, `From Edge`, `Spread float64` (default 0.5) | runs `Effect` on each row (`From` top or bottom) or column (left or right), one after another |
| [`Delay`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Delay) | `Effect`, `From, To float64` (`To` defaults to 1) | runs `Effect` only between `From` and `To` of the progress |
| [`Reverse`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Reverse) | `Effect` | runs `Effect` with `1-t` |
| [`Ease`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Ease) | `Effect`, `Easing func(float64) float64` | runs `Effect` with eased progress, such as `anim.OutBack` |
## Your own [#your-own]
| Type | Does |
| --- | --- |
| [`Func`](https://pkg.go.dev/github.com/lrstanley/bloom/fx#Func) | turns a `func(*canvas.Surface, float64)` into an effect |
Any type with an `Apply(*canvas.Surface, float64)` method is an effect. See [Custom effects](https://bloomfx.dev/motion/custom-effects.md).
## Things to know [#things-to-know]
- Effects listed together run in order with the same `t`.
- Effects that need the terminal's colors (`Dim`, `Shimmer` with no color, `Wave`) work best when the root has
heard them: return `m.ui.Init()` from `Init` and pass messages to `m.ui.Update`.
- Effects that replace glyphs (`Scramble`, `Decode`, `Shade`, `Glitch`) leave wide characters, such as emoji, alone or
reveal them in one step, so they never draw half a character.
- `Ease` and `Func` hold functions, which can't be compared, so frames using them are redrawn every time. Keep them
on nodes that are animating.
# Nodes and options (https://bloomfx.dev/reference/options.md)
This page lists what's available, grouped by what it's for. The full documentation, with every method's details,
is on [pkg.go.dev](https://pkg.go.dev/github.com/lrstanley/bloom).
## Nodes [#nodes]
| Function | Makes |
| --- | --- |
| [`Row(children...)`](https://pkg.go.dev/github.com/lrstanley/bloom#Row) | children left to right |
| [`Column(children...)`](https://pkg.go.dev/github.com/lrstanley/bloom#Column) | children top to bottom |
| [`Grid(cols, children...)`](https://pkg.go.dev/github.com/lrstanley/bloom#Grid) | equal-width columns, filled row by row |
| [`Wrap(children...)`](https://pkg.go.dev/github.com/lrstanley/bloom#Wrap) | children flowing onto new lines at their natural sizes |
| [`Text(s)`](https://pkg.go.dev/github.com/lrstanley/bloom#Text) | a string, optionally styled |
| [`Marquee(s, phase)`](https://pkg.go.dev/github.com/lrstanley/bloom#Marquee) | one line that scrolls, or is cut short, when it doesn't fit |
| [`Skeleton(lines...)`](https://pkg.go.dev/github.com/lrstanley/bloom#Skeleton) | placeholder bars for content still loading, a line for each width, 0 for a blank line |
| [`Box(style, child)`](https://pkg.go.dev/github.com/lrstanley/bloom#Box) | a Lip Gloss frame around a child |
| [`Centered(child)`](https://pkg.go.dev/github.com/lrstanley/bloom#Centered) | a child at its natural size in the middle of its space |
| [`Scroll(child, offset)`](https://pkg.go.dev/github.com/lrstanley/bloom#Scroll) | a child shown from row `offset` |
| [`Overlay(base, layers...)`](https://pkg.go.dev/github.com/lrstanley/bloom#Overlay) | layers drawn over a base |
| [`Layer(child)`](https://pkg.go.dev/github.com/lrstanley/bloom#Layer) | a layer for an overlay, centered at its natural size |
| [`Transition(t, child, effects...)`](https://pkg.go.dev/github.com/lrstanley/bloom#Transition) | a child drawn with effects at progress `t` |
| [`Present(p, child, effects...)`](https://pkg.go.dev/github.com/lrstanley/bloom#Present) | a child shown only while a presence is visible |
| [`Drawable(d)`](https://pkg.go.dev/github.com/lrstanley/bloom#Drawable) | an Ultraviolet `Drawable`, painting cells directly |
Any value with a `View() string` method can also be a child.
## Container options [#container-options]
| Method | On | Does |
| --- | --- | --- |
| `Gap(n)` | `Row`, `Column` | `n` cells between children |
| `Gap(x, y)` | `Grid`, `Wrap` | space between columns and between rows |
| `Align(a)` | `Row`, `Column` | cross-axis placement of children: `Stretch` (default), `Start`, `Center`, `End` |
| `Justify(j)` | `Row`, `Column` | main-axis placement when nothing grows: `flex.Start`, `Center`, `End`, `SpaceBetween`, `SpaceAround`, `SpaceEvenly` |
## Slots [#slots]
| Function | Size |
| --- | --- |
| `Fixed(n, child)` | exactly `n` cells; a `nil` child is empty space |
| `Auto(child)` | the child's natural size |
| `Fill(child)` | an equal share of what's left |
| `Grow(weight, child)` | a weighted share of what's left |
| `Percent(p, child)` | `p`% of the container |
| `Space()` | empty space that fills what's left |
| Method | Does |
| --- | --- |
| `Min(n)`, `Max(n)` | clamp the size on the main axis |
| `Collapsible(priority)` | allow dropping the slot when minimums don't fit, lowest priority first |
| `Align(a)` | this slot's cross-axis placement |
| `ID(id)` | the ID for focus, layout animation, and `Frame.Rect` |
| `On(handlers...)` | [mouse handlers](https://bloomfx.dev/input/mouse.md) from the `on` package |
| `Focusable()` | a stop in the focus order |
| `Scope()` | a focus scope, for `Focus.Within` and `Focus.Push` |
| `Animate(opts...)` | glide to new positions and sizes; needs an ID and a `Root` |
## Text [#text]
| Method | Does |
| --- | --- |
| `Style(style)` | render with a Lip Gloss style at the node's size, wrapping long lines |
| `Truncate()` | one line, cut short with "…" |
| `Cursor(c)` | show the terminal cursor at `c`, relative to the node |
| `HoverStyle(style)` | render with `style` while the pointer is over the node |
| `ID(id)`, `Focusable()`, `On(handlers...)` | as for slots |
Boxes, rows, columns, grids, and wraps have `ID(id)` and `On(handlers...)` too, and boxes have `HoverStyle(style)`
for their frame.
`Marquee` has `Style(style)` and `Gap(s)`, the text shown between loops. `Skeleton` has `Style(style)`, which
colors the bars, and `Shimmer(phase)`, which sweeps a band of light across them.
## Layers [#layers]
| Method | Does |
| --- | --- |
| `Anchor(x, y)` | place on each axis: `Start`, `Center` (default), `End` |
| `AnchorTo(id, side)` | place against a side of another node |
| `Offset(x, y)` | move from where it's anchored |
| `Size(w, h)` | set the size; zero uses the natural size |
| `Margin(n)` | stay `n` cells from the screen's edges |
| `Modal()` | take every mouse event from what's beneath, and be a focus scope |
| `Backdrop(effects...)` | apply effects to everything beneath |
| `Shadow(x, y, effects...)` | apply effects beneath, offset like a drop shadow |
| `Presence(p, effects...)` | show while a presence is visible, with effects |
| `Transition(t, effects...)` | draw with effects at progress `t` |
| `ID(id)` | the ID for focus, anchoring, layout animation, and `Frame.Rect` |
| `On(handlers...)` | mouse handlers; `on.ClickOutside` runs for a modal layer's backdrop |
| `Animate(opts...)` | glide to new positions and sizes; needs an ID and a `Root` |
## Root [#root]
| | Does |
| --- | --- |
| `bloom.New(opts...)` | make a root; keep it in your model by pointer |
| `ui.Init()` | ask the terminal for its colors; return from `Init` |
| `ui.Update(msg)` | hear the terminal, run layout animation, and keep hover up to date; pass every message, return the command |
| `ui.View(w, h, tree)` | lay out, draw, and return a `tea.View` that runs mouse handlers |
| `ui.Render(w, h, tree)` | lay out and draw to a string; a height of zero uses the natural height |
| `ui.Frame()` | the geometry of the last frame drawn |
| `ui.Click(id)`, `ui.ClickAt(x, y)` | run the handlers for a click, for tests |
### Root options [#root-options]
| Option | Default | Sets |
| --- | --- | --- |
| [`WithColors(fg, bg)`](https://pkg.go.dev/github.com/lrstanley/bloom#WithColors) | white on black | the terminal colors effects blend with, until the terminal reports its own |
| [`WithFPS(n)`](https://pkg.go.dev/github.com/lrstanley/bloom#WithFPS) | 60 | the frame rate of layout animation |
| [`WithUnfocusedFPS(n)`](https://pkg.go.dev/github.com/lrstanley/bloom#WithUnfocusedFPS) | 10 | the frame rate of layout animation while the terminal isn't focused |
| [`WithWidthMethod(m)`](https://pkg.go.dev/github.com/lrstanley/bloom#WithWidthMethod) | `ansi.GraphemeWidth` | how character widths are measured; must match the terminal |
| [`WithClock(now)`](https://pkg.go.dev/github.com/lrstanley/bloom#WithClock) | `time.Now` | the clock for layout animation and double clicks, for tests |
| [`WithFocus(&m.focus)`](https://pkg.go.dev/github.com/lrstanley/bloom#WithFocus) | none | the focus that clicks move, before handlers run |
## Geometry and input [#geometry-and-input]
| | Does |
| --- | --- |
| [`Solve(tree, w, h)`](https://pkg.go.dev/github.com/lrstanley/bloom#Solve) | lay out without drawing, and return the `*Frame` |
| [`Render(tree, w, h)`](https://pkg.go.dev/github.com/lrstanley/bloom#Render) | lay out and draw to a string, without a root |
| `frame.Rect(id)` | where a node went, and whether it's in the frame |
| `frame.Hit(x, y)` | the IDs of the nodes at a point, outermost first |
| [`ThrottleMouse(every)`](https://pkg.go.dev/github.com/lrstanley/bloom#ThrottleMouse) | a `tea.WithFilter` filter that drops wheel and motion events that come too fast |
| `Focus[K]` methods | `Set`, `Clear`, `Is`, `Current`, `Next`, `Prev`, `Update`, `Push`, `Pop`, `Scope`, `Within`; `K` is the type of the IDs focus moves between |
## Mouse handlers [#mouse-handlers]
From [`github.com/lrstanley/bloom/on`](https://pkg.go.dev/github.com/lrstanley/bloom/on), for `On(...)`:
| Handler | Runs on |
| --- | --- |
| `on.Click(fn)`, `on.RightClick(fn)`, `on.MiddleClick(fn)` | a click with that button, when it comes up over the node |
| `on.DoubleClick(fn)` | the second of two left clicks within 400ms |
| `on.Press(fn)` | a left click, or enter or space while focused |
| `on.ClickAt(fn)` | a left click, with an `on.Event` holding where |
| `on.Scroll(fn)` | the wheel, as `dx, dy` |
| `on.Hover(fn)` | the pointer moving onto or off the node |
| `on.Drag(fn)` | a left-button drag that starts on the node, until the button comes up |
| `on.ClickOutside(fn)` | a button going down anywhere else; a modal layer's backdrop |
| `on.Mouse(fn)` | every mouse event over the node |
| `on.Msg(msg)` | a handler function that sends `msg` to `Update` |
Component interfaces: [`Measurer`](https://pkg.go.dev/github.com/lrstanley/bloom#Measurer) reports a natural size,
and [`Layouter`](https://pkg.go.dev/github.com/lrstanley/bloom#Layouter) builds a layout for the space given.
## Animation options [#animation-options]
For `anim.NewValue`, `anim.NewPresence`, `anim.NewLoop`, `Sequence.With`, `anim.To`, and `Animate`:
| Option | Default | Sets |
| --- | --- | --- |
| [`Duration(d)`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#Duration) | 250ms | how long easing to a target takes |
| [`Ease(e)`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#Ease) | `OutCubic` | the easing curve |
| [`Spring(frequency, damping)`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#Spring) | | move like a spring instead of easing |
| [`FPS(n)`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#FPS) | 60 | frames requested while animating |
| [`UnfocusedFPS(n)`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#UnfocusedFPS) | 10 | frames requested while the terminal isn't focused |
| [`Clock(now)`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#Clock) | `time.Now` | the source of time, for tests |
Easings: `Linear`, `InQuad`, `OutQuad`, `InOutQuad`, `OutCubic`, `InOutCubic`, `OutBack`, or any
`func(float64) float64`.
Sequence steps: `To(target, opts...)`, `Hold(d)`, `Jump(target)`.
# Examples (https://bloomfx.dev/resources/examples.md)
The repository's [`_examples`](https://github.com/lrstanley/bloom/tree/master/_examples) directory has complete
programs, in rough order of complexity. To run one, clone the repository and run it from that directory:
```console
git clone https://github.com/lrstanley/bloom
cd bloom/_examples
go run ./forms
```
| Example | Shows | Related pages |
| --- | --- | --- |
| [basics](https://github.com/lrstanley/bloom/tree/master/_examples/basics) | Layouts printed with `Render`, without a program: every slot, `Min` and `Max`, `Collapsible`, gaps, `Justify`, `Align`, `Space`, `Box`, wrapping text, `Centered`, and a layout inside Lip Gloss | [Sizing with slots](https://bloomfx.dev/layout/slots.md), [Text, boxes, and alignment](https://bloomfx.dev/layout/text-and-boxes.md) |
| [standard](https://github.com/lrstanley/bloom/tree/master/_examples/standard) | The usual shape of a program: a sidebar that's a `Layouter`, a plain component as the reader, a `Measurer` status bar, and clicks | [Build your first layout](https://bloomfx.dev/getting-started/first-layout.md) |
| [responsive](https://github.com/lrstanley/bloom/tree/master/_examples/responsive) | Panels collapsing by priority, `Percent`, a toolbar that rebuilds for its width, wrapping tags, paging with `Solve` in `Update`, and wheel scrolling | [Responsive layouts](https://bloomfx.dev/layout/responsive.md), [Scrolling](https://bloomfx.dev/layout/scrolling.md) |
| [mouse](https://github.com/lrstanley/bloom/tree/master/_examples/mouse) | Mouse handlers with no IDs: hover styles, click and double click, wheel scrolling, a divider that keeps its drag, and `ThrottleMouse` | [Mouse events](https://bloomfx.dev/input/mouse.md) |
| [forms](https://github.com/lrstanley/bloom/tree/master/_examples/forms) | Focusable inputs with the cursor, an anchored autocomplete dropdown, stacked modal dialogs as focus scopes, recolored backdrops, shadows, a spring entrance, and a toast | [Dialogs and modals](https://bloomfx.dev/layers/dialogs.md), [Focus](https://bloomfx.dev/input/focus.md), [Dropdowns](https://bloomfx.dev/layers/dropdowns.md) |
| [effects](https://github.com/lrstanley/bloom/tree/master/_examples/effects) | Every effect as a looping transition, two custom effects, a `Drawable`, and each backdrop | [Effects reference](https://bloomfx.dev/reference/effects.md), [Custom effects](https://bloomfx.dev/motion/custom-effects.md) |
| [transitions](https://github.com/lrstanley/bloom/tree/master/_examples/transitions) | `Unfold`, `Iris`, `Shade`, `Blinds`, `Wave`, the `Stagger`, `Delay`, `Reverse`, and `Ease` combinators, and dialogs opening from their button | [Transitions](https://bloomfx.dev/motion/transitions.md) |
| [launch](https://github.com/lrstanley/bloom/tree/master/_examples/launch) | A deploy console: shimmering placeholders, rolling numbers, a shake on wrong input, a spotlight tour with `Decode`, confetti, and a glitching badge | [Feedback effects](https://bloomfx.dev/motion/feedback-effects.md) |
| [ticker](https://github.com/lrstanley/bloom/tree/master/_examples/ticker) | `Marquee` headlines and list items, and `anim.Sequence` for a banner and a pulse | [Feedback effects](https://bloomfx.dev/motion/feedback-effects.md#scroll-text-that-doesnt-fit), [Animating values](https://bloomfx.dev/motion/animating-values.md) |
| [reflow](https://github.com/lrstanley/bloom/tree/master/_examples/reflow) | Layout animation with `Animate`: a sidebar, a re-sorting list, a changing split, a panel coming and going, and a layer following the selection | [Layout animation](https://bloomfx.dev/motion/layout-animation.md) |
| [dashboard](https://github.com/lrstanley/bloom/tree/master/_examples/dashboard) | Everything together at 120 frames a second, with the time each frame takes | [Rendering and performance](https://bloomfx.dev/concepts/rendering.md) |
Each example's comment at the top of `main.go` lists its keys. Most quit with q or ctrl+c.
The examples' tests drive each program end to end with [steep](https://bloomfx.dev/resources/related-libraries.md#steep), and can record
a GIF of each one:
```console
STEEP_EXPORT_DIR=/tmp STEEP_EXPORT_MODE=on go test -count 1 ./...
```
# Testing (https://bloomfx.dev/resources/testing.md)
Because a bloom view is a function of your model, most of it can be tested without running a terminal.
## Test what a layout draws [#test-what-a-layout-draws]
[`bloom.Render`](https://pkg.go.dev/github.com/lrstanley/bloom#Render) draws a tree at a size and returns a string.
Compare it with a golden file, or check for the text you expect:
```go
func TestSidebarCollapses(t *testing.T) {
m := newModel()
m.width, m.height = 50, 10
got := ansi.Strip(bloom.Render(m.layout(), m.width, m.height))
if strings.Contains(got, "Groceries") {
t.Errorf("sidebar should collapse at 50 columns:\n%s", got)
}
}
```
`Render` returns styled text; `ansi.Strip` from `github.com/charmbracelet/x/ansi` removes the styles when you only
care about the text. `Render` has no animation state, so it always draws the final layout, with transitions at
whatever progress you give them.
This works best when your model has a method that returns the layout, separate from `View`, which only adds the
`tea.View` settings.
## Test where things go [#test-where-things-go]
[`bloom.Solve`](https://pkg.go.dev/github.com/lrstanley/bloom#Solve) returns the geometry without drawing. Use it to
check sizes and positions, which is less brittle than comparing drawn output:
```go
func TestReaderMinimum(t *testing.T) {
frame := bloom.Solve(newModel().layout(), 60, 20)
r, ok := frame.Rect(paneReader)
if !ok {
t.Fatal("reader not in the frame")
}
if r.Dx() < 40 {
t.Errorf("reader is %d columns wide, want at least 40", r.Dx())
}
}
```
## Test a whole program [#test-a-whole-program]
For behavior that depends on the event loop, such as clicks, focus, timing, and animation, run the program in an
emulated terminal. [steep](https://bloomfx.dev/resources/related-libraries.md#steep) does that: it drives the program with keys, text,
mouse events, and resizes, waits for what it draws, and compares snapshots. bloom's own examples are tested this
way:
```go
func TestNotes(t *testing.T) {
h := steep.NewHarness(t, newModel(), steep.WithWindowSize(80, 12))
h.Wait("Groceries")
h.LeftClick(3, 2).Wait("A terminal garden")
h.Key("d").Wait("Delete \"Ideas\"?")
h.AssertSnapshot()
h.Key("ctrl+c")
h.WaitFinished()
}
```
Clicks go through the same path as a real terminal's, so this tests your IDs and mouse handling too.
## Make animations deterministic [#make-animations-deterministic]
Animations run on the wall clock, which makes timing-sensitive tests flaky. Give values a clock you control with
[`anim.Clock`](https://pkg.go.dev/github.com/lrstanley/bloom/anim#Clock), and the root one with
[`bloom.WithClock`](https://pkg.go.dev/github.com/lrstanley/bloom#WithClock) for layout animation. Or wait for the
end state instead: tests usually care that a dialog opened, not how it looked halfway.
# Related libraries (https://bloomfx.dev/resources/related-libraries.md)
bloom covers layout, layering, input routing, and motion. These libraries cover other parts of a terminal app.
## From the same author [#from-the-same-author]
### bubbletint [#bubbletint]
[bubbletint](https://github.com/lrstanley/bubbletint) gives you hundreds of ready-made terminal color themes, such as
Dracula, Catppuccin, Nord, and Rosé Pine, as Go values, and can switch between them at runtime or follow the theme
of the terminal your program runs in. Its colors are standard `color.Color` values, so they work directly in Lip
Gloss styles and bloom effects:
```go
import tint "github.com/lrstanley/bubbletint/v2"
theme := tint.TintRosePine
panel := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(theme.BrightBlack)
backdrop := fx.Recolor{Fg: theme.BrightBlack, Bg: theme.Bg}
// Effects that blend toward the terminal's colors start from the theme's.
ui := bloom.New(bloom.WithColors(theme.Fg, theme.Bg))
```
Browse the themes at [lrstanley.github.io/bubbletint](https://lrstanley.github.io/bubbletint/).
### steep [#steep]
[steep](https://github.com/lrstanley/x/tree/master/term/steep) is a test harness for Bubble Tea programs and
components. It runs your program in an emulated terminal, drives it with keys, text, mouse events, and resizes,
waits for output, and compares snapshots. It can also record what happened as a GIF. bloom's examples are tested
with it; see [Testing](https://bloomfx.dev/resources/testing.md).
```console
go get github.com/lrstanley/x/term/steep@latest
```
## The Charm ecosystem [#the-charm-ecosystem]
bloom builds on [Charm](https://charm.land)'s libraries, and works alongside the rest:
| Library | What it's for |
| --- | --- |
| [Bubble Tea](https://github.com/charmbracelet/bubbletea) | The framework: models, messages, commands, and the program loop. |
| [Lip Gloss](https://github.com/charmbracelet/lipgloss) | Styles: colors, borders, padding. bloom's `Box` and `Text.Style` take Lip Gloss styles. |
| [Bubbles](https://github.com/charmbracelet/bubbles) | Components: text inputs, viewports, lists, tables, spinners. See [Using Bubbles components](https://bloomfx.dev/layout/bubbles-components.md). |
| [Ultraviolet](https://github.com/charmbracelet/ultraviolet) | The cell-based screen library under Bubble Tea v2. bloom's cells, and `Drawable`, come from it. |
| [Glamour](https://github.com/charmbracelet/glamour) | Markdown rendered for the terminal, which you can place in a layout as text. |
| [Huh](https://github.com/charmbracelet/huh) | Forms and prompts. |
# Troubleshooting (https://bloomfx.dev/resources/troubleshooting.md)
## See what the layout did [#see-what-the-layout-did]
[`bloom.Debug`](https://pkg.go.dev/github.com/lrstanley/bloom#Debug) lays out a tree like `Solve` and returns it as
text: every node, how it's sized, its ID, and where it went. Nodes that were collapsed, scrolled away, or only partly
on screen are marked. Write it somewhere you can read while the program runs, such as a log file:
```go
func (m model) View() tea.View {
if m.debug {
log.Print(bloom.Debug(m.layout(), m.width, m.height))
}
return m.ui.View(m.width, m.height, m.layout())
}
```
```text
Column 0,0 30x5
├─ Row 0,0 30x3
│ ├─ Fixed(10) id="sidebar" hidden collapsible=1
│ │ └─ Box hidden
│ │ └─ Text "Inbox" hidden
│ └─ Fill id="reader" 0,0 30x3 min=24
│ └─ main.reader 0,0 30x3
└─ Text "ready" 0,3 30x1 truncate
```
Rectangles are `x,y` then `width x height`. `Debug` is stateless, so layout animations show where they're headed, not
where they are mid-flight, and effects that move a node, such as `fx.Slide`, aren't applied. The format is for
reading, and may change between versions.
## The screen is blank [#the-screen-is-blank]
`m.ui.View` draws nothing while the width is zero, which it is until Bubble Tea sends the first
`tea.WindowSizeMsg`. Store the size from that message in your model and pass it to `View`:
```go
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
```
## Clicks don't arrive [#clicks-dont-arrive]
Check each of these:
- The view turns on mouse reporting: `v.MouseMode = tea.MouseModeCellMotion` (or `tea.MouseModeAllMotion` for
hover).
- The handler is on the node, or on something it's inside, with `On(...)`.
- A node inside it doesn't have a handler of the same kind. The innermost one takes the event; see
[which node gets the event](https://bloomfx.dev/input/mouse.md#which-node-gets-the-event).
- A click runs when the button comes up over the node it went down on. Releasing it elsewhere cancels the click.
- A modal layer isn't open, taking the events.
- The view comes from `m.ui.View(...)`, which installs the `OnMouse` handler that runs your handlers. If you build
the `tea.View` yourself or replace its `OnMouse`, pass every message to `m.ui.Update`, which then runs them.
## A handler runs, but nothing changes [#a-handler-runs-but-nothing-changes]
Handlers are closures made in `View`, so they change whatever `View` captured. If that's a copy, the change is
lost:
- `View` has a value receiver, `func (m model) View()`. Use a pointer receiver, or have the handler send a message
with `on.Msg(msg)` and change the state in `Update`.
- The handler is built in a component kept by value in its parent, such as a Bubbles-style list updated with
`m.list, cmd = m.list.Update(msg)`. Have the component send a message with `on.Msg` and handle it in its own
`Update`, or build the handler in the parent over `&m.list`.
See [change state, or send a message](https://bloomfx.dev/input/mouse.md#change-state-or-send-a-message).
## Every click happens twice [#every-click-happens-twice]
Your handlers run, and the original `tea.MouseClickMsg` reaches `Update` too. If you also handle the raw mouse
messages, don't act on an event a handler already handled.
## The screen freezes after a click [#the-screen-freezes-after-a-click]
A handler runs on the Bubble Tea event loop, the same as `Update`, so it must not block: no sleeping, waiting, or
I/O in the handler itself. Return a command for that work instead, which runs in the background.
## Animations don't move, or stop partway [#animations-dont-move-or-stop-partway]
- Return the command from `Show`, `Hide`, `To`, `Play`, and `Loop`. Without it, the first frame is never scheduled.
- Pass **every** message to every animation, with `anim.Update(msg, &m.a, &m.b)`, and return its command. Passing
only some message types drops the frame messages.
- For layout animation (`Animate`), pass every message to `m.ui.Update` and return its command, and give the
animated slot or layer an ID.
- Animations in a slice need to be updated through pointers to the elements, as in
[stacking toasts](https://bloomfx.dev/layers/toasts.md#stack-several-toasts).
## Effects look wrong on a light terminal [#effects-look-wrong-on-a-light-terminal]
`Dim` fades toward the terminal's background, and some effects use its foreground. Until the terminal reports its
colors, bloom assumes white on black. Return `m.ui.Init()` from your model's `Init` and pass every message to
`m.ui.Update` so the root hears the reply. For terminals that never answer, set defaults with `bloom.WithColors`.
## Text doesn't wrap, or wraps when it shouldn't [#text-doesnt-wrap-or-wraps-when-it-shouldnt]
- `bloom.Text(s)` on its own is clipped, not wrapped. Add `.Style(...)`, even an empty `lipgloss.NewStyle()`, to wrap
it to its node's width.
- A status bar or title that should stay on one line needs `.Truncate()`.
- A string you rendered yourself with Lip Gloss has a fixed size already. Pass the style to `Text(...).Style(...)` or
`Box(...)` instead, so bloom renders it at the right size.
## A panel disappears [#a-panel-disappears]
A `Collapsible` slot is dropped when the minimum sizes in its row or column don't fit. Lower another slot's `Min`,
raise the collapsible slot's priority, or make the terminal bigger. `frame.Rect(id)` reports `false` for a collapsed
slot, which you can use to tell the user the panel is hidden.
## A component has the wrong size [#a-component-has-the-wrong-size]
Components that size themselves internally, such as a Bubbles viewport, need to be told the size their slot has.
Find it with `bloom.Solve` when the window size or layout changes; see
[Using Bubbles components](https://bloomfx.dev/layout/bubbles-components.md#give-a-component-its-size). Remember that a slot's rectangle
includes the border of a `Box` inside it.
## Wide characters look misaligned [#wide-characters-look-misaligned]
bloom measures text with grapheme clustering by default, as Lip Gloss does. If your terminal measures emoji or other
wide characters differently, set the method to match with `bloom.WithWidthMethod`.
## Still stuck? [#still-stuck]
Ask in [GitHub Discussions](https://github.com/lrstanley/bloom/discussions/new?category=q-a), report a bug in
[issues](https://github.com/lrstanley/bloom/issues/new/choose), or chat on [Discord](https://liam.sh/chat).