highlight.go

  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}