common.go

  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}