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}