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 default
69 // its Styles.Dialog.TitleGradFromColor
70 TitleGradientFromColor color.Color
71 // TitleGradientToColor is the color the title gradient ends by default its
72 // Styles.Dialog.TitleGradToColor
73 TitleGradientToColor color.Color
74 // Width is the total width of the dialog including any margins, borders,
75 // and paddings.
76 Width int
77 // Gap is the gap between content parts. Zero means no gap.
78 Gap int
79 // Title is the title of the dialog. This will be styled using the default
80 // dialog title style and prepended to the content parts slice.
81 Title string
82 // TitleInfo is additional information to display next to the title. This
83 // part is displayed as is, any styling must be applied before setting this
84 // field.
85 TitleInfo string
86 // Parts are the rendered parts of the dialog.
87 Parts []string
88 // Help is the help view content. This will be appended to the content parts
89 // slice using the default dialog help style.
90 Help string
91 // IsOnboarding indicates whether to render the dialog as part of the
92 // onboarding flow. This means that the content will be rendered at the
93 // bottom left of the screen.
94 IsOnboarding bool
95}
96
97// NewRenderContext creates a new RenderContext with the provided styles and width.
98func NewRenderContext(t *styles.Styles, width int) *RenderContext {
99 return &RenderContext{
100 Styles: t,
101 TitleStyle: t.Dialog.Title,
102 ViewStyle: t.Dialog.View,
103 TitleGradientFromColor: t.Dialog.TitleGradFromColor,
104 TitleGradientToColor: t.Dialog.TitleGradToColor,
105 Width: width,
106 Parts: []string{},
107 }
108}
109
110// AddPart adds a rendered part to the dialog.
111func (rc *RenderContext) AddPart(part string) {
112 if len(part) > 0 {
113 rc.Parts = append(rc.Parts, part)
114 }
115}
116
117// Render renders the dialog using the provided context.
118func (rc *RenderContext) Render() string {
119 titleStyle := rc.TitleStyle
120 dialogStyle := rc.ViewStyle.Width(rc.Width)
121
122 var parts []string
123
124 if len(rc.Title) > 0 {
125 var titleInfoWidth int
126 if len(rc.TitleInfo) > 0 {
127 titleInfoWidth = lipgloss.Width(rc.TitleInfo)
128 }
129 title := common.DialogTitle(rc.Styles, rc.Title,
130 max(0, rc.Width-dialogStyle.GetHorizontalFrameSize()-
131 titleStyle.GetHorizontalFrameSize()-
132 titleInfoWidth), rc.TitleGradientFromColor, rc.TitleGradientToColor)
133 if len(rc.TitleInfo) > 0 {
134 title += rc.TitleInfo
135 }
136 parts = append(parts, titleStyle.Render(title))
137 if rc.Gap > 0 {
138 parts = append(parts, make([]string, rc.Gap)...)
139 }
140 }
141
142 if rc.Gap <= 0 {
143 parts = append(parts, rc.Parts...)
144 } else {
145 for i, p := range rc.Parts {
146 if len(p) > 0 {
147 parts = append(parts, p)
148 }
149 if i < len(rc.Parts)-1 {
150 parts = append(parts, make([]string, rc.Gap)...)
151 }
152 }
153 }
154
155 if len(rc.Help) > 0 {
156 if rc.Gap > 0 {
157 parts = append(parts, make([]string, rc.Gap)...)
158 }
159 helpWidth := rc.Width - dialogStyle.GetHorizontalFrameSize()
160 helpStyle := rc.Styles.Dialog.HelpView
161 helpStyle = helpStyle.Width(helpWidth)
162 helpView := ansi.Truncate(helpStyle.Render(rc.Help), helpWidth-1, "")
163 parts = append(parts, helpView)
164 }
165
166 content := strings.Join(parts, "\n")
167 if rc.IsOnboarding {
168 return content
169 }
170 return dialogStyle.Render(content)
171}