1package lazylist
2
3import (
4 "image"
5
6 "charm.land/lipgloss/v2"
7 uv "github.com/charmbracelet/ultraviolet"
8)
9
10// DefaultHighlighter is the default highlighter function that applies inverse style.
11var DefaultHighlighter Highlighter = func(s uv.Style) uv.Style {
12 s.Attrs |= uv.AttrReverse
13 return s
14}
15
16// Highlighter represents a function that defines how to highlight text.
17type Highlighter func(uv.Style) uv.Style
18
19// Highlight highlights a region of text within the given content and region.
20func Highlight(content string, area image.Rectangle, startLine, startCol, endLine, endCol int, highlighter Highlighter) string {
21 if startLine < 0 || startCol < 0 {
22 return content
23 }
24
25 if highlighter == nil {
26 highlighter = DefaultHighlighter
27 }
28
29 width, height := area.Dx(), area.Dy()
30 buf := uv.NewScreenBuffer(width, height)
31 styled := uv.NewStyledString(content)
32 styled.Draw(&buf, area)
33
34 for y := startLine; y <= endLine && y < height; y++ {
35 if y >= buf.Height() {
36 break
37 }
38
39 line := buf.Line(y)
40
41 // Determine column range for this line
42 colStart := 0
43 if y == startLine {
44 colStart = min(startCol, len(line))
45 }
46
47 colEnd := len(line)
48 if y == endLine {
49 colEnd = min(endCol, len(line))
50 }
51
52 // Track last non-empty position as we go
53 lastContentX := -1
54
55 // Single pass: check content and track last non-empty position
56 for x := colStart; x < colEnd; x++ {
57 cell := line.At(x)
58 if cell == nil {
59 continue
60 }
61
62 // Update last content position if non-empty
63 if cell.Content != "" && cell.Content != " " {
64 lastContentX = x
65 }
66 }
67
68 // Only apply highlight up to last content position
69 highlightEnd := colEnd
70 if lastContentX >= 0 {
71 highlightEnd = lastContentX + 1
72 } else if lastContentX == -1 {
73 highlightEnd = colStart // No content on this line
74 }
75
76 // Apply highlight style only to cells with content
77 for x := colStart; x < highlightEnd; x++ {
78 if !image.Pt(x, y).In(area) {
79 continue
80 }
81 cell := line.At(x)
82 cell.Style = highlighter(cell.Style)
83 }
84 }
85
86 return buf.Render()
87}
88
89// RenderWithHighlight renders content with optional focus styling and highlighting.
90// This is a helper that combines common rendering logic for all items.
91// The content parameter should be the raw rendered content before focus styling.
92// The style parameter should come from CurrentStyle() and may be nil.
93// func (b *BaseHighlightable) RenderWithHighlight(content string, width int, style *lipgloss.Style) string {
94// // Apply focus/blur styling if configured
95// rendered := content
96// if style != nil {
97// rendered = style.Render(rendered)
98// }
99//
100// if !b.HasHighlight() {
101// return rendered
102// }
103//
104// height := lipgloss.Height(rendered)
105//
106// // Create temp buffer to draw content with highlighting
107// tempBuf := uv.NewScreenBuffer(width, height)
108//
109// // Draw the rendered content to temp buffer
110// styled := uv.NewStyledString(rendered)
111// styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
112//
113// // Apply highlighting if active
114// b.ApplyHighlight(&tempBuf, width, height, style)
115//
116// return tempBuf.Render()
117// }
118
119// ApplyHighlight applies highlighting to a screen buffer.
120// This should be called after drawing content to the buffer.
121// func (b *BaseHighlightable) ApplyHighlight(buf *uv.ScreenBuffer, width, height int, style *lipgloss.Style) {
122// if b.highlightStartLine < 0 {
123// return
124// }
125//
126// var (
127// topMargin, topBorder, topPadding int
128// rightMargin, rightBorder, rightPadding int
129// bottomMargin, bottomBorder, bottomPadding int
130// leftMargin, leftBorder, leftPadding int
131// )
132// if style != nil {
133// topMargin, rightMargin, bottomMargin, leftMargin = style.GetMargin()
134// topBorder, rightBorder, bottomBorder, leftBorder = style.GetBorderTopSize(),
135// style.GetBorderRightSize(),
136// style.GetBorderBottomSize(),
137// style.GetBorderLeftSize()
138// topPadding, rightPadding, bottomPadding, leftPadding = style.GetPadding()
139// }
140//
141// slog.Info("Applying highlight",
142// "highlightStartLine", b.highlightStartLine,
143// "highlightStartCol", b.highlightStartCol,
144// "highlightEndLine", b.highlightEndLine,
145// "highlightEndCol", b.highlightEndCol,
146// "width", width,
147// "height", height,
148// "margins", fmt.Sprintf("%d,%d,%d,%d", topMargin, rightMargin, bottomMargin, leftMargin),
149// "borders", fmt.Sprintf("%d,%d,%d,%d", topBorder, rightBorder, bottomBorder, leftBorder),
150// "paddings", fmt.Sprintf("%d,%d,%d,%d", topPadding, rightPadding, bottomPadding, leftPadding),
151// )
152//
153// // Calculate content area offsets
154// contentArea := image.Rectangle{
155// Min: image.Point{
156// X: leftMargin + leftBorder + leftPadding,
157// Y: topMargin + topBorder + topPadding,
158// },
159// Max: image.Point{
160// X: width - (rightMargin + rightBorder + rightPadding),
161// Y: height - (bottomMargin + bottomBorder + bottomPadding),
162// },
163// }
164//
165// for y := b.highlightStartLine; y <= b.highlightEndLine && y < height; y++ {
166// if y >= buf.Height() {
167// break
168// }
169//
170// line := buf.Line(y)
171//
172// // Determine column range for this line
173// startCol := 0
174// if y == b.highlightStartLine {
175// startCol = min(b.highlightStartCol, len(line))
176// }
177//
178// endCol := len(line)
179// if y == b.highlightEndLine {
180// endCol = min(b.highlightEndCol, len(line))
181// }
182//
183// // Track last non-empty position as we go
184// lastContentX := -1
185//
186// // Single pass: check content and track last non-empty position
187// for x := startCol; x < endCol; x++ {
188// cell := line.At(x)
189// if cell == nil {
190// continue
191// }
192//
193// // Update last content position if non-empty
194// if cell.Content != "" && cell.Content != " " {
195// lastContentX = x
196// }
197// }
198//
199// // Only apply highlight up to last content position
200// highlightEnd := endCol
201// if lastContentX >= 0 {
202// highlightEnd = lastContentX + 1
203// } else if lastContentX == -1 {
204// highlightEnd = startCol // No content on this line
205// }
206//
207// // Apply highlight style only to cells with content
208// for x := startCol; x < highlightEnd; x++ {
209// if !image.Pt(x, y).In(contentArea) {
210// continue
211// }
212// cell := line.At(x)
213// cell.Style = b.highlightStyle(cell.Style)
214// }
215// }
216// }
217
218// ToHighlighter converts a [lipgloss.Style] to a [Highlighter].
219func ToHighlighter(lgStyle lipgloss.Style) Highlighter {
220 return func(uv.Style) uv.Style {
221 return ToStyle(lgStyle)
222 }
223}
224
225// ToStyle converts an inline [lipgloss.Style] to a [uv.Style].
226func ToStyle(lgStyle lipgloss.Style) uv.Style {
227 var uvStyle uv.Style
228
229 // Colors are already color.Color
230 uvStyle.Fg = lgStyle.GetForeground()
231 uvStyle.Bg = lgStyle.GetBackground()
232
233 // Build attributes using bitwise OR
234 var attrs uint8
235
236 if lgStyle.GetBold() {
237 attrs |= uv.AttrBold
238 }
239
240 if lgStyle.GetItalic() {
241 attrs |= uv.AttrItalic
242 }
243
244 if lgStyle.GetUnderline() {
245 uvStyle.Underline = uv.UnderlineSingle
246 }
247
248 if lgStyle.GetStrikethrough() {
249 attrs |= uv.AttrStrikethrough
250 }
251
252 if lgStyle.GetFaint() {
253 attrs |= uv.AttrFaint
254 }
255
256 if lgStyle.GetBlink() {
257 attrs |= uv.AttrBlink
258 }
259
260 if lgStyle.GetReverse() {
261 attrs |= uv.AttrReverse
262 }
263
264 uvStyle.Attrs = attrs
265
266 return uvStyle
267}
268
269// AdjustArea adjusts the given area rectangle by subtracting margins, borders,
270// and padding from the style.
271func AdjustArea(area image.Rectangle, style lipgloss.Style) image.Rectangle {
272 topMargin, rightMargin, bottomMargin, leftMargin := style.GetMargin()
273 topBorder, rightBorder, bottomBorder, leftBorder := style.GetBorderTopSize(),
274 style.GetBorderRightSize(),
275 style.GetBorderBottomSize(),
276 style.GetBorderLeftSize()
277 topPadding, rightPadding, bottomPadding, leftPadding := style.GetPadding()
278
279 return image.Rectangle{
280 Min: image.Point{
281 X: area.Min.X + leftMargin + leftBorder + leftPadding,
282 Y: area.Min.Y + topMargin + topBorder + topPadding,
283 },
284 Max: image.Point{
285 X: area.Max.X - (rightMargin + rightBorder + rightPadding),
286 Y: area.Max.Y - (bottomMargin + bottomBorder + bottomPadding),
287 },
288 }
289}