dialog.go

  1package dialog
  2
  3import (
  4	"charm.land/bubbles/v2/key"
  5	tea "charm.land/bubbletea/v2"
  6	"charm.land/lipgloss/v2"
  7	"github.com/charmbracelet/crush/internal/ui/common"
  8	uv "github.com/charmbracelet/ultraviolet"
  9)
 10
 11// CloseKey is the default key binding to close dialogs.
 12var CloseKey = key.NewBinding(
 13	key.WithKeys("esc", "alt+esc"),
 14	key.WithHelp("esc", "exit"),
 15)
 16
 17// Dialog is a component that can be displayed on top of the UI.
 18type Dialog interface {
 19	ID() string
 20	Update(msg tea.Msg) tea.Cmd
 21	View() string
 22}
 23
 24// Overlay manages multiple dialogs as an overlay.
 25type Overlay struct {
 26	dialogs []Dialog
 27}
 28
 29// NewOverlay creates a new [Overlay] instance.
 30func NewOverlay(dialogs ...Dialog) *Overlay {
 31	return &Overlay{
 32		dialogs: dialogs,
 33	}
 34}
 35
 36// IsFrontDialog checks if the dialog with the specified ID is at the front.
 37func (d *Overlay) IsFrontDialog(dialogID string) bool {
 38	if len(d.dialogs) == 0 {
 39		return false
 40	}
 41	return d.dialogs[len(d.dialogs)-1].ID() == dialogID
 42}
 43
 44// HasDialogs checks if there are any active dialogs.
 45func (d *Overlay) HasDialogs() bool {
 46	return len(d.dialogs) > 0
 47}
 48
 49// ContainsDialog checks if a dialog with the specified ID exists.
 50func (d *Overlay) ContainsDialog(dialogID string) bool {
 51	for _, dialog := range d.dialogs {
 52		if dialog.ID() == dialogID {
 53			return true
 54		}
 55	}
 56	return false
 57}
 58
 59// AddDialog adds a new dialog to the stack.
 60func (d *Overlay) AddDialog(dialog Dialog) {
 61	d.dialogs = append(d.dialogs, dialog)
 62}
 63
 64// RemoveDialog removes the dialog with the specified ID from the stack.
 65func (d *Overlay) RemoveDialog(dialogID string) {
 66	for i, dialog := range d.dialogs {
 67		if dialog.ID() == dialogID {
 68			d.removeDialog(i)
 69			return
 70		}
 71	}
 72}
 73
 74// Dialog returns the dialog with the specified ID, or nil if not found.
 75func (d *Overlay) Dialog(dialogID string) Dialog {
 76	for _, dialog := range d.dialogs {
 77		if dialog.ID() == dialogID {
 78			return dialog
 79		}
 80	}
 81	return nil
 82}
 83
 84// BringToFront brings the dialog with the specified ID to the front.
 85func (d *Overlay) BringToFront(dialogID string) {
 86	for i, dialog := range d.dialogs {
 87		if dialog.ID() == dialogID {
 88			// Move the dialog to the end of the slice
 89			d.dialogs = append(d.dialogs[:i], d.dialogs[i+1:]...)
 90			d.dialogs = append(d.dialogs, dialog)
 91			return
 92		}
 93	}
 94}
 95
 96// Update handles dialog updates.
 97func (d *Overlay) Update(msg tea.Msg) (*Overlay, tea.Cmd) {
 98	if len(d.dialogs) == 0 {
 99		return d, nil
100	}
101
102	idx := len(d.dialogs) - 1 // active dialog is the last one
103	dialog := d.dialogs[idx]
104	switch msg := msg.(type) {
105	case tea.KeyPressMsg:
106		if key.Matches(msg, CloseKey) {
107			// Close the current dialog
108			d.removeDialog(idx)
109			return d, nil
110		}
111	}
112
113	if cmd := dialog.Update(msg); cmd != nil {
114		// Close the current dialog
115		d.removeDialog(idx)
116		return d, cmd
117	}
118
119	return d, nil
120}
121
122// Draw renders the overlay and its dialogs.
123func (d *Overlay) Draw(scr uv.Screen, area uv.Rectangle) {
124	for _, dialog := range d.dialogs {
125		view := dialog.View()
126		viewWidth := lipgloss.Width(view)
127		viewHeight := lipgloss.Height(view)
128		center := common.CenterRect(area, viewWidth, viewHeight)
129		if area.Overlaps(center) {
130			uv.NewStyledString(view).Draw(scr, center)
131		}
132	}
133}
134
135// removeDialog removes a dialog from the stack.
136func (d *Overlay) removeDialog(idx int) {
137	if idx < 0 || idx >= len(d.dialogs) {
138		return
139	}
140	d.dialogs = append(d.dialogs[:idx], d.dialogs[idx+1:]...)
141}