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}
63
64// NewRenderContext creates a new RenderContext with the provided styles and width.
65func NewRenderContext(t *styles.Styles, width int) *RenderContext {
66 return &RenderContext{
67 Styles: t,
68 Width: width,
69 Parts: []string{},
70 }
71}
72
73// AddPart adds a rendered part to the dialog.
74func (rc *RenderContext) AddPart(part string) {
75 if len(part) > 0 {
76 rc.Parts = append(rc.Parts, part)
77 }
78}
79
80// Render renders the dialog using the provided context.
81func (rc *RenderContext) Render() string {
82 titleStyle := rc.Styles.Dialog.Title
83 dialogStyle := rc.Styles.Dialog.View.Width(rc.Width)
84
85 parts := []string{}
86 if len(rc.Title) > 0 {
87 var titleInfoWidth int
88 if len(rc.TitleInfo) > 0 {
89 titleInfoWidth = lipgloss.Width(rc.TitleInfo)
90 }
91 title := common.DialogTitle(rc.Styles, rc.Title,
92 max(0, rc.Width-dialogStyle.GetHorizontalFrameSize()-
93 titleStyle.GetHorizontalFrameSize()-
94 titleInfoWidth))
95 if len(rc.TitleInfo) > 0 {
96 title += rc.TitleInfo
97 }
98 parts = append(parts, titleStyle.Render(title))
99 if rc.Gap > 0 {
100 parts = append(parts, make([]string, rc.Gap)...)
101 }
102 }
103
104 if rc.Gap <= 0 {
105 parts = append(parts, rc.Parts...)
106 } else {
107 for i, p := range rc.Parts {
108 if len(p) > 0 {
109 parts = append(parts, p)
110 }
111 if i < len(rc.Parts)-1 {
112 parts = append(parts, make([]string, rc.Gap)...)
113 }
114 }
115 }
116
117 if len(rc.Help) > 0 {
118 if rc.Gap > 0 {
119 parts = append(parts, make([]string, rc.Gap)...)
120 }
121 helpStyle := rc.Styles.Dialog.HelpView
122 helpStyle = helpStyle.Width(rc.Width - dialogStyle.GetHorizontalFrameSize())
123 helpView := ansi.Truncate(helpStyle.Render(rc.Help), rc.Width, "")
124 parts = append(parts, helpView)
125 }
126
127 content := strings.Join(parts, "\n")
128
129 return dialogStyle.Render(content)
130}