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// HasDialogs checks if there are any active dialogs.
 37func (d *Overlay) HasDialogs() bool {
 38	return len(d.dialogs) > 0
 39}
 40
 41// ContainsDialog checks if a dialog with the specified ID exists.
 42func (d *Overlay) ContainsDialog(dialogID string) bool {
 43	for _, dialog := range d.dialogs {
 44		if dialog.ID() == dialogID {
 45			return true
 46		}
 47	}
 48	return false
 49}
 50
 51// OpenDialog opens a new dialog to the stack.
 52func (d *Overlay) OpenDialog(dialog Dialog) {
 53	d.dialogs = append(d.dialogs, dialog)
 54}
 55
 56// CloseDialog closes the dialog with the specified ID from the stack.
 57func (d *Overlay) CloseDialog(dialogID string) {
 58	for i, dialog := range d.dialogs {
 59		if dialog.ID() == dialogID {
 60			d.removeDialog(i)
 61			return
 62		}
 63	}
 64}
 65
 66// CloseFrontDialog closes the front dialog in the stack.
 67func (d *Overlay) CloseFrontDialog() {
 68	if len(d.dialogs) == 0 {
 69		return
 70	}
 71	d.removeDialog(len(d.dialogs) - 1)
 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// DialogLast returns the front dialog, or nil if there are no dialogs.
 85func (d *Overlay) DialogLast() Dialog {
 86	if len(d.dialogs) == 0 {
 87		return nil
 88	}
 89	return d.dialogs[len(d.dialogs)-1]
 90}
 91
 92// BringToFront brings the dialog with the specified ID to the front.
 93func (d *Overlay) BringToFront(dialogID string) {
 94	for i, dialog := range d.dialogs {
 95		if dialog.ID() == dialogID {
 96			// Move the dialog to the end of the slice
 97			d.dialogs = append(d.dialogs[:i], d.dialogs[i+1:]...)
 98			d.dialogs = append(d.dialogs, dialog)
 99			return
100		}
101	}
102}
103
104// Update handles dialog updates.
105func (d *Overlay) Update(msg tea.Msg) tea.Msg {
106	if len(d.dialogs) == 0 {
107		return nil
108	}
109
110	idx := len(d.dialogs) - 1 // active dialog is the last one
111	dialog := d.dialogs[idx]
112	if dialog == nil {
113		return nil
114	}
115
116	return dialog.Update(msg)
117}
118
119// CenterPosition calculates the centered position for the dialog.
120func (d *Overlay) CenterPosition(area uv.Rectangle, dialogID string) uv.Rectangle {
121	dialog := d.Dialog(dialogID)
122	if dialog == nil {
123		return uv.Rectangle{}
124	}
125	return d.centerPositionView(area, dialog.View())
126}
127
128func (d *Overlay) centerPositionView(area uv.Rectangle, view string) uv.Rectangle {
129	viewWidth := lipgloss.Width(view)
130	viewHeight := lipgloss.Height(view)
131	return common.CenterRect(area, viewWidth, viewHeight)
132}
133
134// Draw renders the overlay and its dialogs.
135func (d *Overlay) Draw(scr uv.Screen, area uv.Rectangle) {
136	for _, dialog := range d.dialogs {
137		view := dialog.View()
138		center := d.centerPositionView(area, view)
139		if area.Overlaps(center) {
140			uv.NewStyledString(view).Draw(scr, center)
141		}
142	}
143}
144
145// removeDialog removes a dialog from the stack.
146func (d *Overlay) removeDialog(idx int) {
147	if idx < 0 || idx >= len(d.dialogs) {
148		return
149	}
150	d.dialogs = append(d.dialogs[:idx], d.dialogs[idx+1:]...)
151}