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