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// Dialog sizing constants.
12const (
13 // defaultDialogMaxWidth is the maximum width for standard dialogs.
14 defaultDialogMaxWidth = 120
15 // defaultDialogHeight is the default height for standard dialogs.
16 defaultDialogHeight = 30
17 // titleContentHeight is the height of the title content line.
18 titleContentHeight = 1
19 // inputContentHeight is the height of the input content line.
20 inputContentHeight = 1
21)
22
23// CloseKey is the default key binding to close dialogs.
24var CloseKey = key.NewBinding(
25 key.WithKeys("esc", "alt+esc"),
26 key.WithHelp("esc", "exit"),
27)
28
29// Action represents an action taken in a dialog after handling a message.
30type Action interface{}
31
32// Dialog is a component that can be displayed on top of the UI.
33type Dialog interface {
34 // ID returns the unique identifier of the dialog.
35 ID() string
36 // HandleMsg processes a message and returns an action. An [Action] can be
37 // anything and the caller is responsible for handling it appropriately.
38 HandleMsg(msg tea.Msg) Action
39 // Draw draws the dialog onto the provided screen within the specified area
40 // and returns the desired cursor position on the screen.
41 Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor
42}
43
44// Overlay manages multiple dialogs as an overlay.
45type Overlay struct {
46 dialogs []Dialog
47}
48
49// NewOverlay creates a new [Overlay] instance.
50func NewOverlay(dialogs ...Dialog) *Overlay {
51 return &Overlay{
52 dialogs: dialogs,
53 }
54}
55
56// HasDialogs checks if there are any active dialogs.
57func (d *Overlay) HasDialogs() bool {
58 return len(d.dialogs) > 0
59}
60
61// ContainsDialog checks if a dialog with the specified ID exists.
62func (d *Overlay) ContainsDialog(dialogID string) bool {
63 for _, dialog := range d.dialogs {
64 if dialog.ID() == dialogID {
65 return true
66 }
67 }
68 return false
69}
70
71// OpenDialog opens a new dialog to the stack.
72func (d *Overlay) OpenDialog(dialog Dialog) {
73 d.dialogs = append(d.dialogs, dialog)
74}
75
76// CloseDialog closes the dialog with the specified ID from the stack.
77func (d *Overlay) CloseDialog(dialogID string) {
78 for i, dialog := range d.dialogs {
79 if dialog.ID() == dialogID {
80 d.removeDialog(i)
81 return
82 }
83 }
84}
85
86// CloseFrontDialog closes the front dialog in the stack.
87func (d *Overlay) CloseFrontDialog() {
88 if len(d.dialogs) == 0 {
89 return
90 }
91 d.removeDialog(len(d.dialogs) - 1)
92}
93
94// Dialog returns the dialog with the specified ID, or nil if not found.
95func (d *Overlay) Dialog(dialogID string) Dialog {
96 for _, dialog := range d.dialogs {
97 if dialog.ID() == dialogID {
98 return dialog
99 }
100 }
101 return nil
102}
103
104// DialogLast returns the front dialog, or nil if there are no dialogs.
105func (d *Overlay) DialogLast() Dialog {
106 if len(d.dialogs) == 0 {
107 return nil
108 }
109 return d.dialogs[len(d.dialogs)-1]
110}
111
112// BringToFront brings the dialog with the specified ID to the front.
113func (d *Overlay) BringToFront(dialogID string) {
114 for i, dialog := range d.dialogs {
115 if dialog.ID() == dialogID {
116 // Move the dialog to the end of the slice
117 d.dialogs = append(d.dialogs[:i], d.dialogs[i+1:]...)
118 d.dialogs = append(d.dialogs, dialog)
119 return
120 }
121 }
122}
123
124// Update handles dialog updates.
125func (d *Overlay) Update(msg tea.Msg) tea.Msg {
126 if len(d.dialogs) == 0 {
127 return nil
128 }
129
130 idx := len(d.dialogs) - 1 // active dialog is the last one
131 dialog := d.dialogs[idx]
132 if dialog == nil {
133 return nil
134 }
135
136 return dialog.HandleMsg(msg)
137}
138
139// DrawCenterCursor draws the given string view centered in the screen area and
140// adjusts the cursor position accordingly.
141func DrawCenterCursor(scr uv.Screen, area uv.Rectangle, view string, cur *tea.Cursor) {
142 width, height := lipgloss.Size(view)
143 center := common.CenterRect(area, width, height)
144 if cur != nil {
145 cur.X += center.Min.X
146 cur.Y += center.Min.Y
147 }
148
149 uv.NewStyledString(view).Draw(scr, center)
150}
151
152// DrawCenter draws the given string view centered in the screen area.
153func DrawCenter(scr uv.Screen, area uv.Rectangle, view string) {
154 DrawCenterCursor(scr, area, view, nil)
155}
156
157// Draw renders the overlay and its dialogs.
158func (d *Overlay) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
159 var cur *tea.Cursor
160 for _, dialog := range d.dialogs {
161 cur = dialog.Draw(scr, area)
162 }
163 return cur
164}
165
166// removeDialog removes a dialog from the stack.
167func (d *Overlay) removeDialog(idx int) {
168 if idx < 0 || idx >= len(d.dialogs) {
169 return
170 }
171 d.dialogs = append(d.dialogs[:idx], d.dialogs[idx+1:]...)
172}