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.Msg
 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// RemoveFrontDialog removes the front dialog from the stack.
 75func (d *Overlay) RemoveFrontDialog() {
 76	if len(d.dialogs) == 0 {
 77		return
 78	}
 79	d.removeDialog(len(d.dialogs) - 1)
 80}
 81
 82// Dialog returns the dialog with the specified ID, or nil if not found.
 83func (d *Overlay) Dialog(dialogID string) Dialog {
 84	for _, dialog := range d.dialogs {
 85		if dialog.ID() == dialogID {
 86			return dialog
 87		}
 88	}
 89	return nil
 90}
 91
 92// DialogLast returns the front dialog, or nil if there are no dialogs.
 93func (d *Overlay) DialogLast() Dialog {
 94	if len(d.dialogs) == 0 {
 95		return nil
 96	}
 97	return d.dialogs[len(d.dialogs)-1]
 98}
 99
100// BringToFront brings the dialog with the specified ID to the front.
101func (d *Overlay) BringToFront(dialogID string) {
102	for i, dialog := range d.dialogs {
103		if dialog.ID() == dialogID {
104			// Move the dialog to the end of the slice
105			d.dialogs = append(d.dialogs[:i], d.dialogs[i+1:]...)
106			d.dialogs = append(d.dialogs, dialog)
107			return
108		}
109	}
110}
111
112// Update handles dialog updates.
113func (d *Overlay) Update(msg tea.Msg) tea.Msg {
114	if len(d.dialogs) == 0 {
115		return nil
116	}
117
118	idx := len(d.dialogs) - 1 // active dialog is the last one
119	dialog := d.dialogs[idx]
120	if dialog == nil {
121		return nil
122	}
123
124	return dialog.Update(msg)
125}
126
127// CenterPosition calculates the centered position for the dialog.
128func (d *Overlay) CenterPosition(area uv.Rectangle, dialogID string) uv.Rectangle {
129	dialog := d.Dialog(dialogID)
130	if dialog == nil {
131		return uv.Rectangle{}
132	}
133	return d.centerPositionView(area, dialog.View())
134}
135
136func (d *Overlay) centerPositionView(area uv.Rectangle, view string) uv.Rectangle {
137	viewWidth := lipgloss.Width(view)
138	viewHeight := lipgloss.Height(view)
139	return common.CenterRect(area, viewWidth, viewHeight)
140}
141
142// Draw renders the overlay and its dialogs.
143func (d *Overlay) Draw(scr uv.Screen, area uv.Rectangle) {
144	for _, dialog := range d.dialogs {
145		view := dialog.View()
146		center := d.centerPositionView(area, view)
147		if area.Overlaps(center) {
148			uv.NewStyledString(view).Draw(scr, center)
149		}
150	}
151}
152
153// removeDialog removes a dialog from the stack.
154func (d *Overlay) removeDialog(idx int) {
155	if idx < 0 || idx >= len(d.dialogs) {
156		return
157	}
158	d.dialogs = append(d.dialogs[:idx], d.dialogs[idx+1:]...)
159}