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}