@@ -1,10 +1,8 @@
1package list
1package list
7− "charm.land/lipgloss/v2"
8 "github.com/charmbracelet/x/ansi"
6 "github.com/charmbracelet/x/ansi"
@@ -35,9 +33,6 @@ type List struct {
35 mouseDragY int // Current Y in item
33 mouseDragY int // Current Y in item
36 lastHighlighted map[int]bool // Track which items were highlighted in last update
34 lastHighlighted map[int]bool // Track which items were highlighted in last update
38− // Rendered content and cache
39− renderedItems map[int]renderedItem
41 // offsetIdx is the index of the first visible item in the viewport.
36 // offsetIdx is the index of the first visible item in the viewport.
42 offsetIdx int
37 offsetIdx int
43 // offsetLine is the number of lines of the item at offsetIdx that are
38 // offsetLine is the number of lines of the item at offsetIdx that are
@@ -56,7 +51,6 @@ type renderedItem struct {
56func NewList(items ...Item) *List {
51func NewList(items ...Item) *List {
57 l := new(List)
52 l := new(List)
58 l.items = items
53 l.items = items
59− l.renderedItems = make(map[int]renderedItem)
60 l.selectedIdx = -1
54 l.selectedIdx = -1
61 l.mouseDownItem = -1
55 l.mouseDownItem = -1
62 l.mouseDragItem = -1
56 l.mouseDragItem = -1
@@ -66,9 +60,6 @@ func NewList(items ...Item) *List {
67// SetSize sets the size of the list viewport.
61// SetSize sets the size of the list viewport.
68func (l *List) SetSize(width, height int) {
62func (l *List) SetSize(width, height int) {
69− if width != l.width {
70− l.renderedItems = make(map[int]renderedItem)
72 l.width = width
63 l.width = width
73 l.height = height
64 l.height = height
74 // l.normalizeOffsets()
65 // l.normalizeOffsets()
@@ -96,16 +87,16 @@ func (l *List) Len() int {
97// getItem renders (if needed) and returns the item at the given index.
88// getItem renders (if needed) and returns the item at the given index.
98func (l *List) getItem(idx int) renderedItem {
89func (l *List) getItem(idx int) renderedItem {
99− return l.renderItem(idx, false)
90+ if idx < 0 || idx >= len(l.items) {
91+ return renderedItem{}
102−// applyHighlight applies highlighting to the given rendered item.
94+ item := l.items[idx]
103−func (l *List) applyHighlight(idx int, ri *renderedItem) {
95+ if hi, ok := item.(Highlightable); ok {
104− // Apply highlight if item supports it
96+ // Apply highlight
105− if highlightable, ok := l.items[idx].(HighlightStylable); ok {
106 startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := l.getHighlightRange()
97 startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := l.getHighlightRange()
98+ sLine, sCol, eLine, eCol := -1, -1, -1, -1
107 if idx >= startItemIdx && idx <= endItemIdx {
99 if idx >= startItemIdx && idx <= endItemIdx {
108− var sLine, sCol, eLine, eCol int
109 if idx == startItemIdx && idx == endItemIdx {
100 if idx == startItemIdx && idx == endItemIdx {
110 // Single item selection
101 // Single item selection
111 sLine = startLine
102 sLine = startLine
@@ -116,8 +107,8 @@ func (l *List) applyHighlight(idx int, ri *renderedItem) {
116 // First item - from start position to end of item
107 // First item - from start position to end of item
117 sLine = startLine
108 sLine = startLine
118 sCol = startCol
109 sCol = startCol
119− eLine = ri.height - 1
110+ eLine = -1
120− eCol = 9999 // 9999 = end of line
111+ eCol = -1
121 } else if idx == endItemIdx {
112 } else if idx == endItemIdx {
122 // Last item - from start of item to end position
113 // Last item - from start of item to end position
123 sLine = 0
114 sLine = 0
@@ -128,82 +119,29 @@ func (l *List) applyHighlight(idx int, ri *renderedItem) {
128 // Middle item - fully highlighted
119 // Middle item - fully highlighted
129 sLine = 0
120 sLine = 0
131− eLine = ri.height - 1
122+ eLine = -1
132− eCol = 9999
123+ eCol = -1
135− // Apply offset for styling frame
136− contentArea := image.Rect(0, 0, l.width, ri.height)
138− hiStyle := highlightable.HighlightStyle()
139− rendered := Highlight(ri.content, contentArea, sLine, sCol, eLine, eCol, ToHighlighter(hiStyle))
140− ri.content = rendered
145−// renderItem renders (if needed) and returns the item at the given index. If
127+ hi.Highlight(sLine, sCol, eLine, eCol)
146−// process is true, it applies focus and highlight styling.
147−func (l *List) renderItem(idx int, process bool) renderedItem {
148− if idx < 0 || idx >= len(l.items) {
149− return renderedItem{}
152− var style lipgloss.Style
153− focusable, isFocusable := l.items[idx].(FocusStylable)
130+ if focusable, isFocusable := item.(Focusable); isFocusable {
155− style = focusable.BlurStyle()
156− if l.focused && idx == l.selectedIdx {
131+ focusable.SetFocused(l.focused && idx == l.selectedIdx)
157− style = focusable.FocusStyle()
161− ri, ok := l.renderedItems[idx]
163− item := l.items[idx]
164− rendered := item.Render(l.width - style.GetHorizontalFrameSize())
134+ rendered := item.Render(l.width)
165− rendered = strings.TrimRight(rendered, "\n")
135+ rendered = strings.TrimRight(rendered, "\n")
166− height := countLines(rendered)
136+ height := countLines(rendered)
168− ri = renderedItem{
137+ ri := renderedItem{
169− content: rendered,
138+ content: rendered,
170− height: height,
139+ height: height,
173− l.renderedItems[idx] = ri
177− // Simply return cached rendered item with frame size applied
178− if vfs := style.GetVerticalFrameSize(); vfs > 0 {
184− // We apply highlighting before focus styling so that focus styling
185− // overrides highlight styles.
186− if l.mouseDownItem >= 0 {
187− l.applyHighlight(idx, &ri)
191− // Apply focus/blur styling if needed
192− rendered := style.Render(ri.content)
193− height := countLines(rendered)
194− ri.content = rendered
198 return ri
142 return ri
201−// invalidateItem invalidates the cached rendered content of the item at the
203−func (l *List) invalidateItem(idx int) {
204− delete(l.renderedItems, idx)
207// ScrollToIndex scrolls the list to the given item index.
145// ScrollToIndex scrolls the list to the given item index.
208func (l *List) ScrollToIndex(index int) {
146func (l *List) ScrollToIndex(index int) {
209 if index < 0 {
147 if index < 0 {
@@ -330,7 +268,7 @@ func (l *List) Render() string {
330 linesNeeded := l.height
268 linesNeeded := l.height
332 for linesNeeded > 0 && currentIdx < len(l.items) {
270 for linesNeeded > 0 && currentIdx < len(l.items) {
333− item := l.renderItem(currentIdx, true)
271+ item := l.getItem(currentIdx)
334 itemLines := strings.Split(item.content, "\n")
272 itemLines := strings.Split(item.content, "\n")
335 itemHeight := len(itemLines)
273 itemHeight := len(itemLines)
@@ -372,13 +310,6 @@ func (l *List) Render() string {
372func (l *List) PrependItems(items ...Item) {
310func (l *List) PrependItems(items ...Item) {
373 l.items = append(items, l.items...)
311 l.items = append(items, l.items...)
376− newCache := make(map[int]renderedItem)
377− for idx, val := range l.renderedItems {
378− newCache[idx+len(items)] = val
380− l.renderedItems = newCache
382 // Keep view position relative to the content that was visible
313 // Keep view position relative to the content that was visible
383 l.offsetIdx += len(items)
314 l.offsetIdx += len(items)
@@ -397,9 +328,6 @@ func (l *List) SetItems(items ...Item) {
397// rendered item cache.
328// rendered item cache.
398func (l *List) setItems(evict bool, items ...Item) {
329func (l *List) setItems(evict bool, items ...Item) {
399 l.items = items
330 l.items = items
401− l.renderedItems = make(map[int]renderedItem)
403 l.selectedIdx = min(l.selectedIdx, len(l.items)-1)
331 l.selectedIdx = min(l.selectedIdx, len(l.items)-1)
404 l.offsetIdx = min(l.offsetIdx, len(l.items)-1)
332 l.offsetIdx = min(l.offsetIdx, len(l.items)-1)
405 l.offsetLine = 0
333 l.offsetLine = 0
@@ -580,8 +508,6 @@ func (l *List) HandleMouseDown(x, y int) bool {
581 if clickable, ok := l.items[itemIdx].(MouseClickable); ok {
509 if clickable, ok := l.items[itemIdx].(MouseClickable); ok {
582 clickable.HandleMouseClick(ansi.MouseButton1, x, itemY)
510 clickable.HandleMouseClick(ansi.MouseButton1, x, itemY)
583− l.items[itemIdx] = clickable.(Item)
584− l.invalidateItem(itemIdx)
587 return true
513 return true