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// BringToFront brings the dialog with the specified ID to the front.
 75func (d *Overlay) BringToFront(dialogID string) {
 76	for i, dialog := range d.dialogs {
 77		if dialog.ID() == dialogID {
 78			// Move the dialog to the end of the slice
 79			d.dialogs = append(d.dialogs[:i], d.dialogs[i+1:]...)
 80			d.dialogs = append(d.dialogs, dialog)
 81			return
 82		}
 83	}
 84}
 85
 86// Update handles dialog updates.
 87func (d *Overlay) Update(msg tea.Msg) (*Overlay, tea.Cmd) {
 88	if len(d.dialogs) == 0 {
 89		return d, nil
 90	}
 91
 92	idx := len(d.dialogs) - 1 // active dialog is the last one
 93	dialog := d.dialogs[idx]
 94	switch msg := msg.(type) {
 95	case tea.KeyPressMsg:
 96		if key.Matches(msg, CloseKey) {
 97			// Close the current dialog
 98			d.removeDialog(idx)
 99			return d, nil
100		}
101	}
102
103	if cmd := dialog.Update(msg); cmd != nil {
104		// Close the current dialog
105		d.removeDialog(idx)
106		return d, cmd
107	}
108
109	return d, nil
110}
111
112// Draw renders the overlay and its dialogs.
113func (d *Overlay) Draw(scr uv.Screen, area uv.Rectangle) {
114	for _, dialog := range d.dialogs {
115		view := dialog.View()
116		viewWidth := lipgloss.Width(view)
117		viewHeight := lipgloss.Height(view)
118		center := common.CenterRect(area, viewWidth, viewHeight)
119		if area.Overlaps(center) {
120			uv.NewStyledString(view).Draw(scr, center)
121		}
122	}
123}
124
125// removeDialog removes a dialog from the stack.
126func (d *Overlay) removeDialog(idx int) {
127	if idx < 0 || idx >= len(d.dialogs) {
128		return
129	}
130	d.dialogs = append(d.dialogs[:idx], d.dialogs[idx+1:]...)
131}