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// RenderContext is a dialog rendering context that can be used to render
42// common dialog layouts.
43type RenderContext struct {
44 // Styles is the styles to use for rendering.
45 Styles *styles.Styles
46 // TitleStyle is the style of the dialog title by default it uses Styles.Dialog.Title
47 TitleStyle lipgloss.Style
48 // ViewStyle is the style of the dialog title by default it uses Styles.Dialog.View
49 ViewStyle lipgloss.Style
50 // TitleGradientFromColor is the color the title gradient starts by defaults its Style.Primary
51 TitleGradientFromColor color.Color
52 // TitleGradientToColor is the color the title gradient starts by defaults its Style.Secondary
53 TitleGradientToColor color.Color
54 // Width is the total width of the dialog including any margins, borders,
55 // and paddings.
56 Width int
57 // Gap is the gap between content parts. Zero means no gap.
58 Gap int
59 // Title is the title of the dialog. This will be styled using the default
60 // dialog title style and prepended to the content parts slice.
61 Title string
62 // TitleInfo is additional information to display next to the title. This
63 // part is displayed as is, any styling must be applied before setting this
64 // field.
65 TitleInfo string
66 // Parts are the rendered parts of the dialog.
67 Parts []string
68 // Help is the help view content. This will be appended to the content parts
69 // slice using the default dialog help style.
70 Help string
71 // IsOnboarding indicates whether to render the dialog as part of the
72 // onboarding flow. This means that the content will be rendered at the
73 // bottom left of the screen.
74 IsOnboarding bool
75}
76
77// NewRenderContext creates a new RenderContext with the provided styles and width.
78func NewRenderContext(t *styles.Styles, width int) *RenderContext {
79 return &RenderContext{
80 Styles: t,
81 TitleStyle: t.Dialog.Title,
82 ViewStyle: t.Dialog.View,
83 TitleGradientFromColor: t.Primary,
84 TitleGradientToColor: t.Secondary,
85 Width: width,
86 Parts: []string{},
87 }
88}
89
90// AddPart adds a rendered part to the dialog.
91func (rc *RenderContext) AddPart(part string) {
92 if len(part) > 0 {
93 rc.Parts = append(rc.Parts, part)
94 }
95}
96
97// Render renders the dialog using the provided context.
98func (rc *RenderContext) Render() string {
99 titleStyle := rc.TitleStyle
100 dialogStyle := rc.ViewStyle.Width(rc.Width)
101
102 var parts []string
103
104 if len(rc.Title) > 0 {
105 var titleInfoWidth int
106 if len(rc.TitleInfo) > 0 {
107 titleInfoWidth = lipgloss.Width(rc.TitleInfo)
108 }
109 title := common.DialogTitle(rc.Styles, rc.Title,
110 max(0, rc.Width-dialogStyle.GetHorizontalFrameSize()-
111 titleStyle.GetHorizontalFrameSize()-
112 titleInfoWidth), rc.TitleGradientFromColor, rc.TitleGradientToColor)
113 if len(rc.TitleInfo) > 0 {
114 title += rc.TitleInfo
115 }
116 parts = append(parts, titleStyle.Render(title))
117 if rc.Gap > 0 {
118 parts = append(parts, make([]string, rc.Gap)...)
119 }
120 }
121
122 if rc.Gap <= 0 {
123 parts = append(parts, rc.Parts...)
124 } else {
125 for i, p := range rc.Parts {
126 if len(p) > 0 {
127 parts = append(parts, p)
128 }
129 if i < len(rc.Parts)-1 {
130 parts = append(parts, make([]string, rc.Gap)...)
131 }
132 }
133 }
134
135 if len(rc.Help) > 0 {
136 if rc.Gap > 0 {
137 parts = append(parts, make([]string, rc.Gap)...)
138 }
139 helpStyle := rc.Styles.Dialog.HelpView
140 helpStyle = helpStyle.Width(rc.Width - dialogStyle.GetHorizontalFrameSize())
141 helpView := ansi.Truncate(helpStyle.Render(rc.Help), rc.Width, "")
142 parts = append(parts, helpView)
143 }
144
145 content := strings.Join(parts, "\n")
146 if rc.IsOnboarding {
147 return content
148 }
149 return dialogStyle.Render(content)
150}