mirror of
https://github.com/anotherhadi/ilovetui.git
synced 2026-08-21 12:05:49 +02:00
aa6496901f
Signed-off-by: Hadi <hadi@example.fr>
138 lines
4.9 KiB
Markdown
138 lines
4.9 KiB
Markdown
# modal
|
|
|
|
A centered popup box on top of a dimmed background, triggered from anywhere in a bubbletea
|
|
program via an exported `tea.Msg` (`ShowMsg`/`Show`) rather than a direct reference to the
|
|
`Model` that ends up rendering it - standard Elm architecture, no IPC between processes.
|
|
|
|
It composites over an already-rendered string, so it makes no assumption about how the host
|
|
builds that string: the same `Model` works whatever the host uses to lay out its main content.
|
|
|
|
## Quick start
|
|
|
|
```go
|
|
import (
|
|
"github.com/anotherhadi/ilovetui/modal"
|
|
)
|
|
|
|
type model struct {
|
|
m modal.Model
|
|
width, height int
|
|
}
|
|
|
|
func newModel() model {
|
|
return model{m: modal.New()}
|
|
}
|
|
|
|
func (m model) Init() tea.Cmd { return m.m.Init() }
|
|
|
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.KeyPressMsg:
|
|
if msg.String() == "d" {
|
|
return m, modal.Show("Delete file?", modal.Text("This can't be undone.\n\ny: confirm esc: cancel"))
|
|
}
|
|
if msg.String() == "esc" && m.m.Open() {
|
|
return m, modal.Close()
|
|
}
|
|
}
|
|
|
|
var cmd tea.Cmd
|
|
m.m, cmd = m.m.Update(msg)
|
|
return m, cmd
|
|
}
|
|
|
|
func (m model) View() tea.View {
|
|
background := renderYourUI(m.width, m.height)
|
|
view := tea.NewView(m.m.Render(background))
|
|
view.AltScreen = true
|
|
return view
|
|
}
|
|
```
|
|
|
|
Any component in the same bubbletea program can trigger a modal via `modal.Show`, without holding
|
|
a reference to the `modal.Model` that will actually render it - that `Model` just needs to see
|
|
every `tea.Msg` the program produces (i.e. get its `Update` called from the top-level `Update`),
|
|
same as any other child model.
|
|
|
|
## Content is a model
|
|
|
|
A modal's body is a `tea.Model`, not a string. While a modal is on top of the stack it gets every
|
|
message the `modal.Model` receives, its `Init` runs when it opens, and its commands come back out
|
|
- so it can hold a form, a list, or a confirmation that reports its answer with a `tea.Msg` of
|
|
its own, which the component that opened it listens for:
|
|
|
|
```go
|
|
type confirmedMsg struct{ path string }
|
|
|
|
func (c confirm) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
if k, ok := msg.(tea.KeyPressMsg); ok && k.String() == "y" {
|
|
path := c.path
|
|
return c, func() tea.Msg { return confirmedMsg{path} }
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// somewhere else
|
|
return m, modal.Show("Delete file?", confirm{path: p})
|
|
```
|
|
|
|
Only the topmost modal is updated: everything beneath it is dimmed and frozen until the modals
|
|
above it close.
|
|
|
|
For a modal with nothing to interact with, `modal.Text` wraps a plain string:
|
|
|
|
```go
|
|
return m, modal.Show("About", modal.Text("v1.0\n\nesc to close"))
|
|
```
|
|
|
|
The box shrinks to fit whatever the content draws, so a content that wants a specific size sets
|
|
it on itself - the modal only ever sees the rendered result.
|
|
|
|
## Showing and closing
|
|
|
|
```go
|
|
return m, modal.Show("Delete file?", confirm{})
|
|
|
|
return m, modal.Close() // close the topmost modal
|
|
```
|
|
|
|
The stack is a plain LIFO, with no identity: a modal is closed by being on top, never by being
|
|
named. There is nothing to tag a modal with, and nothing that can target one in the middle of the
|
|
stack - the topmost is both the only one that receives messages and the only one `Close` can
|
|
reach, so the two rules never disagree.
|
|
|
|
- `modal.Open()` reports whether at least one modal is currently shown - handy for a host that
|
|
wants to route key presses to the modal instead of its normal UI while one is open. Note that a
|
|
host doing this also makes it impossible for a key to open a second modal, which is what keeps
|
|
the stack shallow without any bookkeeping.
|
|
- A content model closes its own modal by returning `modal.Close()`, since it only ever runs while
|
|
it is the topmost one.
|
|
|
|
## Stacking
|
|
|
|
Modals stack: showing a second one while the first is still open pushes it on top, dimming both
|
|
the background and the first modal to the same flat color. Closing the top one reveals the one
|
|
beneath, still in full color. This is what lets a "delete?" confirmation open a nested "really
|
|
sure?" modal without any special-casing - the nested one is pushed by the content on top, the only
|
|
one able to act.
|
|
|
|
## Styling
|
|
|
|
```go
|
|
m := modal.New(modal.WithMaxWidth(60), modal.WithMaxHeight(20), modal.WithStyles(myStyles))
|
|
|
|
return m, modal.Show("Title", modal.Text("Message"), modal.WithModalStyle(oneOffStyles))
|
|
```
|
|
|
|
`WithMaxWidth`/`WithMaxHeight` cap how large a modal box can grow before wrapping/truncating; a
|
|
modal narrower than the cap shrinks to fit its content instead of padding out to it. A modal can
|
|
also never overflow past the edge of whatever background it's rendered on, regardless of these
|
|
caps. `WithStyles` sets the default look for every modal shown by this `Model`; `WithModalStyle`
|
|
(a `Show` option) overrides it for one modal alone. `DefaultStyles()` builds from `style.S`: the
|
|
box borrows `PanelFocused`'s border (the modal is what has focus while it's open), the dim color
|
|
reuses `Subtle`.
|
|
|
|
## Examples
|
|
|
|
- `examples/modal` - open/dismiss, nested modals, styled from theme colors.
|