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// Action represents an action taken in a dialog after handling a message.
 18type Action interface{}
 19
 20// Dialog is a component that can be displayed on top of the UI.
 21type Dialog interface {
 22	// ID returns the unique identifier of the dialog.
 23	ID() string
 24	// HandleMsg processes a message and returns an action. An [Action] can be
 25	// anything and the caller is responsible for handling it appropriately.
 26	HandleMsg(msg tea.Msg) Action
 27	// Draw draws the dialog onto the provided screen within the specified area
 28	// and returns the desired cursor position on the screen.
 29	Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor
 30}
 31
 32// Overlay manages multiple dialogs as an overlay.
 33type Overlay struct {
 34	dialogs []Dialog
 35}
 36
 37// NewOverlay creates a new [Overlay] instance.
 38func NewOverlay(dialogs ...Dialog) *Overlay {
 39	return &Overlay{
 40		dialogs: dialogs,
 41	}
 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// OpenDialog opens a new dialog to the stack.
 60func (d *Overlay) OpenDialog(dialog Dialog) {
 61	d.dialogs = append(d.dialogs, dialog)
 62}
 63
 64// CloseDialog closes the dialog with the specified ID from the stack.
 65func (d *Overlay) CloseDialog(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// CloseFrontDialog closes the front dialog in the stack.
 75func (d *Overlay) CloseFrontDialog() {
 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.HandleMsg(msg)
125}
126
127// DrawCenterCursor draws the given string view centered in the screen area and
128// adjusts the cursor position accordingly.
129func DrawCenterCursor(scr uv.Screen, area uv.Rectangle, view string, cur *tea.Cursor) {
130	width, height := lipgloss.Size(view)
131	center := common.CenterRect(area, width, height)
132	if cur != nil {
133		cur.X += center.Min.X
134		cur.Y += center.Min.Y
135	}
136
137	uv.NewStyledString(view).Draw(scr, center)
138}
139
140// DrawCenter draws the given string view centered in the screen area.
141func DrawCenter(scr uv.Screen, area uv.Rectangle, view string) {
142	DrawCenterCursor(scr, area, view, nil)
143}
144
145// Draw renders the overlay and its dialogs.
146func (d *Overlay) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
147	var cur *tea.Cursor
148	for _, dialog := range d.dialogs {
149		cur = dialog.Draw(scr, area)
150	}
151	return cur
152}
153
154// removeDialog removes a dialog from the stack.
155func (d *Overlay) removeDialog(idx int) {
156	if idx < 0 || idx >= len(d.dialogs) {
157		return
158	}
159	d.dialogs = append(d.dialogs[:idx], d.dialogs[idx+1:]...)
160}