common.go

  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}