1package dialog
2
3import (
4 "image/color"
5 "strings"
6
7 tea "charm.land/bubbletea/v2"
8 "charm.land/lipgloss/v2"
9 "github.com/charmbracelet/crush/internal/ui/common"
10 "github.com/charmbracelet/crush/internal/ui/styles"
11 "github.com/charmbracelet/x/ansi"
12)
13
14// InputCursor adjusts the cursor position for an input field within a dialog.
15func InputCursor(t *styles.Styles, cur *tea.Cursor) *tea.Cursor {
16 if cur != nil {
17 titleStyle := t.Dialog.Title
18 dialogStyle := t.Dialog.View
19 inputStyle := t.Dialog.InputPrompt
20 // Adjust cursor position to account for dialog layout
21 cur.X += inputStyle.GetBorderLeftSize() +
22 inputStyle.GetMarginLeft() +
23 inputStyle.GetPaddingLeft() +
24 dialogStyle.GetBorderLeftSize() +
25 dialogStyle.GetPaddingLeft() +
26 dialogStyle.GetMarginLeft()
27 cur.Y += titleStyle.GetVerticalFrameSize() +
28 inputStyle.GetBorderTopSize() +
29 inputStyle.GetMarginTop() +
30 inputStyle.GetPaddingTop() +
31 inputStyle.GetBorderBottomSize() +
32 inputStyle.GetMarginBottom() +
33 inputStyle.GetPaddingBottom() +
34 dialogStyle.GetPaddingTop() +
35 dialogStyle.GetMarginTop() +
36 dialogStyle.GetBorderTopSize()
37 }
38 return cur
39}
40
41// adjustOnboardingInputCursor removes the dialog view frame offset from an
42// input cursor. Onboarding dialogs render without Dialog.View frame, while
43// InputCursor includes that frame offset for regular dialogs.
44func adjustOnboardingInputCursor(t *styles.Styles, cur *tea.Cursor) *tea.Cursor {
45 if cur == nil {
46 return nil
47 }
48
49 dialogStyle := t.Dialog.View
50 cur.X -= dialogStyle.GetBorderLeftSize() +
51 dialogStyle.GetPaddingLeft() +
52 dialogStyle.GetMarginLeft()
53 cur.Y -= dialogStyle.GetBorderTopSize() +
54 dialogStyle.GetPaddingTop() +
55 dialogStyle.GetMarginTop()
56 return cur
57}
58
59// RenderContext is a dialog rendering context that can be used to render
60// common dialog layouts.
61type RenderContext struct {
62 // Styles is the styles to use for rendering.
63 Styles *styles.Styles
64 // TitleStyle is the style of the dialog title by default it uses Styles.Dialog.Title
65 TitleStyle lipgloss.Style
66 // ViewStyle is the style of the dialog title by default it uses Styles.Dialog.View
67 ViewStyle lipgloss.Style
68 // TitleGradientFromColor is the color the title gradient starts by defaults its Style.Primary
69 TitleGradientFromColor color.Color
70 // TitleGradientToColor is the color the title gradient starts by defaults its Style.Secondary
71 TitleGradientToColor color.Color
72 // Width is the total width of the dialog including any margins, borders,
73 // and paddings.
74 Width int
75 // Gap is the gap between content parts. Zero means no gap.
76 Gap int
77 // Title is the title of the dialog. This will be styled using the default
78 // dialog title style and prepended to the content parts slice.
79 Title string
80 // TitleInfo is additional information to display next to the title. This
81 // part is displayed as is, any styling must be applied before setting this
82 // field.
83 TitleInfo string
84 // Parts are the rendered parts of the dialog.
85 Parts []string
86 // Help is the help view content. This will be appended to the content parts
87 // slice using the default dialog help style.
88 Help string
89 // IsOnboarding indicates whether to render the dialog as part of the
90 // onboarding flow. This means that the content will be rendered at the
91 // bottom left of the screen.
92 IsOnboarding bool
93}
94
95// NewRenderContext creates a new RenderContext with the provided styles and width.
96func NewRenderContext(t *styles.Styles, width int) *RenderContext {
97 return &RenderContext{
98 Styles: t,
99 TitleStyle: t.Dialog.Title,
100 ViewStyle: t.Dialog.View,
101 TitleGradientFromColor: t.Primary,
102 TitleGradientToColor: t.Secondary,
103 Width: width,
104 Parts: []string{},
105 }
106}
107
108// AddPart adds a rendered part to the dialog.
109func (rc *RenderContext) AddPart(part string) {
110 if len(part) > 0 {
111 rc.Parts = append(rc.Parts, part)
112 }
113}
114
115// Render renders the dialog using the provided context.
116func (rc *RenderContext) Render() string {
117 titleStyle := rc.TitleStyle
118 dialogStyle := rc.ViewStyle.Width(rc.Width)
119
120 var parts []string
121
122 if len(rc.Title) > 0 {
123 var titleInfoWidth int
124 if len(rc.TitleInfo) > 0 {
125 titleInfoWidth = lipgloss.Width(rc.TitleInfo)
126 }
127 title := common.DialogTitle(rc.Styles, rc.Title,
128 max(0, rc.Width-dialogStyle.GetHorizontalFrameSize()-
129 titleStyle.GetHorizontalFrameSize()-
130 titleInfoWidth), rc.TitleGradientFromColor, rc.TitleGradientToColor)
131 if len(rc.TitleInfo) > 0 {
132 title += rc.TitleInfo
133 }
134 parts = append(parts, titleStyle.Render(title))
135 if rc.Gap > 0 {
136 parts = append(parts, make([]string, rc.Gap)...)
137 }
138 }
139
140 if rc.Gap <= 0 {
141 parts = append(parts, rc.Parts...)
142 } else {
143 for i, p := range rc.Parts {
144 if len(p) > 0 {
145 parts = append(parts, p)
146 }
147 if i < len(rc.Parts)-1 {
148 parts = append(parts, make([]string, rc.Gap)...)
149 }
150 }
151 }
152
153 if len(rc.Help) > 0 {
154 if rc.Gap > 0 {
155 parts = append(parts, make([]string, rc.Gap)...)
156 }
157 helpWidth := rc.Width - dialogStyle.GetHorizontalFrameSize()
158 helpStyle := rc.Styles.Dialog.HelpView
159 helpStyle = helpStyle.Width(helpWidth)
160 helpView := ansi.Truncate(helpStyle.Render(rc.Help), helpWidth-1, "")
161 parts = append(parts, helpView)
162 }
163
164 content := strings.Join(parts, "\n")
165 if rc.IsOnboarding {
166 return content
167 }
168 return dialogStyle.Render(content)
169}