on main help sign in
fix(ui): dry highlighting items in lazylist
commit065f33999124673c5a9ab6c2e3de6f0393c31520 authorAyman Bagabas <ayman.bagabas@gmail.com> dateWed 10 Dec 2025, 11:42 (1 year ago) parente2a586c
2 files changed+57−213
internal/ui/lazylist/list.go+49−213 internal/ui/model/ui.go+8
internal/ui/lazylist/list.go+49−213
@@ -2,7 +2,6 @@ package lazylist
22
33import (
44 "image"
5− "log/slog"
65 "strings"
76
87 "charm.land/lipgloss/v2"
@@ -99,6 +98,49 @@ func (l *List) getItem(idx int) renderedItem {
9998 return l.renderItem(idx, false)
10099}
101100
101+// applyHighlight applies highlighting to the given rendered item.
102+func (l *List) applyHighlight(idx int, ri *renderedItem) {
103+ // Apply highlight if item supports it
104+ if highlightable, ok := l.items[idx].(HighlightStylable); ok {
105+ startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := l.getHighlightRange()
106+ if idx >= startItemIdx && idx <= endItemIdx {
107+ var sLine, sCol, eLine, eCol int
108+ if idx == startItemIdx && idx == endItemIdx {
109+ // Single item selection
110+ sLine = startLine
111+ sCol = startCol
112+ eLine = endLine
113+ eCol = endCol
114+ } else if idx == startItemIdx {
115+ // First item - from start position to end of item
116+ sLine = startLine
117+ sCol = startCol
118+ eLine = ri.height - 1
119+ eCol = 9999 // 9999 = end of line
120+ } else if idx == endItemIdx {
121+ // Last item - from start of item to end position
122+ sLine = 0
123+ sCol = 0
124+ eLine = endLine
125+ eCol = endCol
126+ } else {
127+ // Middle item - fully highlighted
128+ sLine = 0
129+ sCol = 0
130+ eLine = ri.height - 1
131+ eCol = 9999
132+ }
133+
134+ // Apply offset for styling frame
135+ contentArea := image.Rect(0, 0, l.width, ri.height)
136+
137+ hiStyle := highlightable.HighlightStyle()
138+ rendered := Highlight(ri.content, contentArea, sLine, sCol, eLine, eCol, ToHighlighter(hiStyle))
139+ ri.content = rendered
140+ }
141+ }
142+}
143+
102144// renderItem renders (if needed) and returns the item at the given index. If
103145// process is true, it applies focus and highlight styling.
104146func (l *List) renderItem(idx int, process bool) renderedItem {
@@ -130,55 +172,17 @@ func (l *List) renderItem(idx int, process bool) renderedItem {
130172 }
131173
132174 if !process {
175+ // Simply return cached rendered item with frame size applied
176+ if vfs := style.GetVerticalFrameSize(); vfs > 0 {
177+ ri.height += vfs
178+ }
133179 return ri
134180 }
135181
136182 // We apply highlighting before focus styling so that focus styling
137183 // overrides highlight styles.
138− // Apply highlight if item supports it
139184 if l.mouseDownItem >= 0 {
140− if highlightable, ok := l.items[idx].(HighlightStylable); ok {
141− startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := l.getHighlightRange()
142− if idx >= startItemIdx && idx <= endItemIdx {
143− var sLine, sCol, eLine, eCol int
144− if idx == startItemIdx && idx == endItemIdx {
145− // Single item selection
146− sLine = startLine
147− sCol = startCol
148− eLine = endLine
149− eCol = endCol
150− } else if idx == startItemIdx {
151− // First item - from start position to end of item
152− sLine = startLine
153− sCol = startCol
154− eLine = ri.height - 1
155− eCol = 9999 // 9999 = end of line
156− } else if idx == endItemIdx {
157− // Last item - from start of item to end position
158− sLine = 0
159− sCol = 0
160− eLine = endLine
161− eCol = endCol
162− } else {
163− // Middle item - fully highlighted
164− sLine = 0
165− sCol = 0
166− eLine = ri.height - 1
167− eCol = 9999
168− }
169−
170− // Apply offset for styling frame
171− contentArea := image.Rect(0, 0, l.width, ri.height)
172−
173− hiStyle := highlightable.HighlightStyle()
174− slog.Info("Highlighting item", "idx", idx,
175− "sLine", sLine, "sCol", sCol,
176− "eLine", eLine, "eCol", eCol,
177− )
178− rendered := Highlight(ri.content, contentArea, sLine, sCol, eLine, eCol, ToHighlighter(hiStyle))
179− ri.content = rendered
180− }
181− }
185+ l.applyHighlight(idx, &ri)
182186 }
183187
184188 if isFocusable {
@@ -344,7 +348,7 @@ func (l *List) Render() string {
344348 gapOffset := currentOffset - itemHeight
345349 gapRemaining := l.gap - gapOffset
346350 if gapRemaining > 0 {
347− for i := 0; i < gapRemaining; i++ {
351+ for range gapRemaining {
348352 lines = append(lines, "")
349353 }
350354 }
@@ -390,31 +394,11 @@ func (l *List) AppendItems(items ...Item) {
390394// Focus sets the focus state of the list.
391395func (l *List) Focus() {
392396 l.focused = true
393− if l.selectedIdx < 0 || l.selectedIdx > len(l.items)-1 {
394− return
395− }
396−
397− // item := l.items[l.selectedIdx]
398− // if focusable, ok := item.(Focusable); ok {
399− // focusable.Focus()
400− // l.items[l.selectedIdx] = focusable.(Item)
401− // l.invalidateItem(l.selectedIdx)
402− // }
403397}
404398
405399// Blur removes the focus state from the list.
406400func (l *List) Blur() {
407401 l.focused = false
408− if l.selectedIdx < 0 || l.selectedIdx > len(l.items)-1 {
409− return
410− }
411−
412− // item := l.items[l.selectedIdx]
413− // if focusable, ok := item.(Focusable); ok {
414− // focusable.Blur()
415− // l.items[l.selectedIdx] = focusable.(Item)
416− // l.invalidateItem(l.selectedIdx)
417− // }
418402}
419403
420404// ScrollToTop scrolls the list to the top.
@@ -603,60 +587,11 @@ func (l *List) HandleMouseDrag(x, y int) bool {
603587 l.mouseDragX = x
604588 l.mouseDragY = itemY
605589
606− startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := l.getHighlightRange()
607−
608− slog.Info("HandleMouseDrag", "mouseDownItem", l.mouseDownItem,
609− "mouseDragItem", l.mouseDragItem,
610− "startItemIdx", startItemIdx,
611− "endItemIdx", endItemIdx,
612− "startLine", startLine,
613− "startCol", startCol,
614− "endLine", endLine,
615− "endCol", endCol,
616− )
617−
618− // for i := startItemIdx; i <= endItemIdx; i++ {
619− // item := l.getItem(i)
620− // itemHi, ok := l.items[i].(Highlightable)
621− // if ok {
622− // if i == startItemIdx && i == endItemIdx {
623− // // Single item selection
624− // itemHi.SetHighlight(startLine, startCol, endLine, endCol)
625− // } else if i == startItemIdx {
626− // // First item - from start position to end of item
627− // itemHi.SetHighlight(startLine, startCol, item.height-1, 9999) // 9999 = end of line
628− // } else if i == endItemIdx {
629− // // Last item - from start of item to end position
630− // itemHi.SetHighlight(0, 0, endLine, endCol)
631− // } else {
632− // // Middle item - fully highlighted
633− // itemHi.SetHighlight(0, 0, item.height-1, 9999)
634− // }
635− //
636− // // Invalidate item to re-render
637− // l.items[i] = itemHi.(Item)
638− // l.invalidateItem(i)
639− // }
640− // }
641−
642− // Update highlight if item supports it
643− // l.updateHighlight()
644−
645590 return true
646591}
647592
648593// ClearHighlight clears any active text highlighting.
649594func (l *List) ClearHighlight() {
650− // for i, item := range l.renderedItems {
651− // if !item.highlighted {
652− // continue
653− // }
654− // if h, ok := l.items[i].(Highlightable); ok {
655− // h.SetHighlight(-1, -1, -1, -1)
656− // l.items[i] = h.(Item)
657− // l.invalidateItem(i)
658− // }
659− // }
660595 l.mouseDownItem = -1
661596 l.mouseDragItem = -1
662597 l.lastHighlighted = make(map[int]bool)
@@ -728,108 +663,9 @@ func (l *List) getHighlightRange() (startItemIdx, startLine, startCol, endItemId
728663 endCol = l.mouseDownX
729664 }
730665
731− slog.Info("Apply highlight",
732− "startItemIdx", startItemIdx,
733− "endItemIdx", endItemIdx,
734− "startLine", startLine,
735− "startCol", startCol,
736− "endLine", endLine,
737− "endCol", endCol,
738− )
739−
740666 return startItemIdx, startLine, startCol, endItemIdx, endLine, endCol
741667}
742668
743−// updateHighlight updates the highlight range for highlightable items.
744−// Supports highlighting across multiple items and respects drag direction.
745−func (l *List) updateHighlight() {
746− if l.mouseDownItem < 0 {
747− return
748− }
749−
750− // Get start and end item indices
751− downItemIdx := l.mouseDownItem
752− dragItemIdx := l.mouseDragItem
753−
754− // Determine selection direction
755− draggingDown := dragItemIdx > downItemIdx ||
756− (dragItemIdx == downItemIdx && l.mouseDragY > l.mouseDownY) ||
757− (dragItemIdx == downItemIdx && l.mouseDragY == l.mouseDownY && l.mouseDragX >= l.mouseDownX)
758−
759− // Determine actual start and end based on direction
760− var startItemIdx, endItemIdx int
761− var startLine, startCol, endLine, endCol int
762−
763− if draggingDown {
764− // Normal forward selection
765− startItemIdx = downItemIdx
766− endItemIdx = dragItemIdx
767− startLine = l.mouseDownY
768− startCol = l.mouseDownX
769− endLine = l.mouseDragY
770− endCol = l.mouseDragX
771− } else {
772− // Backward selection (dragging up)
773− startItemIdx = dragItemIdx
774− endItemIdx = downItemIdx
775− startLine = l.mouseDragY
776− startCol = l.mouseDragX
777− endLine = l.mouseDownY
778− endCol = l.mouseDownX
779− }
780−
781− slog.Info("Update highlight", "startItemIdx", startItemIdx, "endItemIdx", endItemIdx,
782− "startLine", startLine, "startCol", startCol,
783− "endLine", endLine, "endCol", endCol,
784− "draggingDown", draggingDown,
785− )
786−
787− // Track newly highlighted items
788− // newHighlighted := make(map[int]bool)
789−
790− // Clear highlights on items that are no longer in range
791− // for i := range l.lastHighlighted {
792− // if i < startItemIdx || i > endItemIdx {
793− // if h, ok := l.items[i].(Highlightable); ok {
794− // h.SetHighlight(-1, -1, -1, -1)
795− // l.items[i] = h.(Item)
796− // l.invalidateItem(i)
797− // }
798− // }
799− // }
800−
801− // Highlight all items in range
802− // for idx := startItemIdx; idx <= endItemIdx; idx++ {
803− // item, ok := l.items[idx].(Highlightable)
804− // if !ok {
805− // continue
806− // }
807− //
808− // renderedItem := l.getItem(idx)
809− //
810− // if idx == startItemIdx && idx == endItemIdx {
811− // // Single item selection
812− // item.SetHighlight(startLine, startCol, endLine, endCol)
813− // } else if idx == startItemIdx {
814− // // First item - from start position to end of item
815− // item.SetHighlight(startLine, startCol, renderedItem.height-1, 9999) // 9999 = end of line
816− // } else if idx == endItemIdx {
817− // // Last item - from start of item to end position
818− // item.SetHighlight(0, 0, endLine, endCol)
819− // } else {
820− // // Middle item - fully highlighted
821− // item.SetHighlight(0, 0, renderedItem.height-1, 9999)
822− // }
823− //
824− // l.items[idx] = item.(Item)
825− //
826− // l.invalidateItem(idx)
827− // newHighlighted[idx] = true
828− // }
829− //
830− // l.lastHighlighted = newHighlighted
831−}
832−
833669// countLines counts the number of lines in a string.
834670func countLines(s string) int {
835671 if s == "" {
internal/ui/model/ui.go+8
@@ -258,8 +258,16 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
258258 case uiChat:
259259 if msg.Y <= 0 {
260260 m.chat.ScrollBy(-1)
261+ if !m.chat.SelectedItemInView() {
262+ m.chat.SelectPrev()
263+ m.chat.ScrollToSelected()
264+ }
261265 } else if msg.Y >= m.chat.Height()-1 {
262266 m.chat.ScrollBy(1)
267+ if !m.chat.SelectedItemInView() {
268+ m.chat.SelectNext()
269+ m.chat.ScrollToSelected()
270+ }
263271 }
264272
265273 x, y := msg.X, msg.Y