1package list
2
3import (
4 "strings"
5
6 tea "charm.land/bubbletea/v2"
7 "charm.land/lipgloss/v2"
8 uv "github.com/charmbracelet/ultraviolet"
9 "github.com/charmbracelet/x/exp/ordered"
10)
11
12const maxGapSize = 100
13
14var newlineBuffer = strings.Repeat("\n", maxGapSize)
15
16// SimpleList is a string-based list with virtual scrolling behavior.
17// Based on exp/list but simplified for our needs.
18type SimpleList struct {
19 // Viewport dimensions.
20 width, height int
21
22 // Scroll offset (in lines from top).
23 offset int
24
25 // Items.
26 items []Item
27 itemIDs map[string]int // ID -> index mapping
28
29 // Rendered content (all items stacked).
30 rendered string
31 renderedHeight int // Total height of rendered content in lines
32 lineOffsets []int // Byte offsets for each line (for fast slicing)
33
34 // Rendered item metadata.
35 renderedItems map[string]renderedItem
36
37 // Selection.
38 selectedIdx int
39 focused bool
40
41 // Focus tracking.
42 prevSelectedIdx int
43
44 // Mouse/highlight state.
45 mouseDown bool
46 mouseDownItem int
47 mouseDownX int
48 mouseDownY int // viewport-relative Y
49 mouseDragItem int
50 mouseDragX int
51 mouseDragY int // viewport-relative Y
52 selectionStartLine int
53 selectionStartCol int
54 selectionEndLine int
55 selectionEndCol int
56
57 // Configuration.
58 gap int // Gap between items in lines
59}
60
61type renderedItem struct {
62 view string
63 height int
64 start int // Start line in rendered content
65 end int // End line in rendered content
66}
67
68// NewSimpleList creates a new simple list.
69func NewSimpleList(items ...Item) *SimpleList {
70 l := &SimpleList{
71 items: items,
72 itemIDs: make(map[string]int, len(items)),
73 renderedItems: make(map[string]renderedItem),
74 selectedIdx: -1,
75 prevSelectedIdx: -1,
76 gap: 0,
77 selectionStartLine: -1,
78 selectionStartCol: -1,
79 selectionEndLine: -1,
80 selectionEndCol: -1,
81 }
82
83 // Build ID map.
84 for i, item := range items {
85 if idItem, ok := item.(interface{ ID() string }); ok {
86 l.itemIDs[idItem.ID()] = i
87 }
88 }
89
90 return l
91}
92
93// Init initializes the list (Bubbletea lifecycle).
94func (l *SimpleList) Init() tea.Cmd {
95 return l.render()
96}
97
98// Update handles messages (Bubbletea lifecycle).
99func (l *SimpleList) Update(msg tea.Msg) (*SimpleList, tea.Cmd) {
100 return l, nil
101}
102
103// View returns the visible viewport (Bubbletea lifecycle).
104func (l *SimpleList) View() string {
105 if l.height <= 0 || l.width <= 0 {
106 return ""
107 }
108
109 start, end := l.viewPosition()
110 viewStart := max(0, start)
111 viewEnd := end
112
113 if viewStart > viewEnd {
114 return ""
115 }
116
117 view := l.getLines(viewStart, viewEnd)
118
119 // Apply width/height constraints.
120 view = lipgloss.NewStyle().
121 Height(l.height).
122 Width(l.width).
123 Render(view)
124
125 // Apply highlighting if active.
126 if l.hasSelection() {
127 return l.renderSelection(view)
128 }
129
130 return view
131}
132
133// viewPosition returns the start and end line indices for the viewport.
134func (l *SimpleList) viewPosition() (int, int) {
135 start := max(0, l.offset)
136 end := min(l.offset+l.height-1, l.renderedHeight-1)
137 start = min(start, end)
138 return start, end
139}
140
141// getLines returns lines [start, end] from rendered content.
142func (l *SimpleList) getLines(start, end int) string {
143 if len(l.lineOffsets) == 0 || start >= len(l.lineOffsets) {
144 return ""
145 }
146
147 if end >= len(l.lineOffsets) {
148 end = len(l.lineOffsets) - 1
149 }
150 if start > end {
151 return ""
152 }
153
154 startOffset := l.lineOffsets[start]
155 var endOffset int
156 if end+1 < len(l.lineOffsets) {
157 endOffset = l.lineOffsets[end+1] - 1 // Exclude newline
158 } else {
159 endOffset = len(l.rendered)
160 }
161
162 if startOffset >= len(l.rendered) {
163 return ""
164 }
165 endOffset = min(endOffset, len(l.rendered))
166
167 return l.rendered[startOffset:endOffset]
168}
169
170// render rebuilds the rendered content from all items.
171func (l *SimpleList) render() tea.Cmd {
172 if l.width <= 0 || l.height <= 0 || len(l.items) == 0 {
173 return nil
174 }
175
176 // Set default selection if none.
177 if l.selectedIdx < 0 && len(l.items) > 0 {
178 l.selectedIdx = 0
179 }
180
181 // Handle focus changes.
182 var focusCmd tea.Cmd
183 if l.focused {
184 focusCmd = l.focusSelectedItem()
185 } else {
186 focusCmd = l.blurSelectedItem()
187 }
188
189 // Render all items.
190 var b strings.Builder
191 currentLine := 0
192
193 for i, item := range l.items {
194 // Render item.
195 view := l.renderItem(item)
196 height := lipgloss.Height(view)
197
198 // Store metadata.
199 rItem := renderedItem{
200 view: view,
201 height: height,
202 start: currentLine,
203 end: currentLine + height - 1,
204 }
205
206 if idItem, ok := item.(interface{ ID() string }); ok {
207 l.renderedItems[idItem.ID()] = rItem
208 }
209
210 // Append to rendered content.
211 b.WriteString(view)
212
213 // Add gap after item (except last).
214 gap := l.gap
215 if i == len(l.items)-1 {
216 gap = 0
217 }
218
219 if gap > 0 {
220 if gap <= maxGapSize {
221 b.WriteString(newlineBuffer[:gap])
222 } else {
223 b.WriteString(strings.Repeat("\n", gap))
224 }
225 }
226
227 currentLine += height + gap
228 }
229
230 l.setRendered(b.String())
231
232 // Scroll to selected item.
233 if l.focused && l.selectedIdx >= 0 {
234 l.scrollToSelection()
235 }
236
237 return focusCmd
238}
239
240// renderItem renders a single item.
241func (l *SimpleList) renderItem(item Item) string {
242 // Create a buffer for the item.
243 buf := uv.NewScreenBuffer(l.width, 1000) // Max height
244 area := uv.Rect(0, 0, l.width, 1000)
245 item.Draw(&buf, area)
246
247 // Find actual height.
248 height := l.measureBufferHeight(&buf)
249 if height == 0 {
250 height = 1
251 }
252
253 // Render to string.
254 return buf.Render()
255}
256
257// measureBufferHeight finds the actual content height in a buffer.
258func (l *SimpleList) measureBufferHeight(buf *uv.ScreenBuffer) int {
259 height := buf.Height()
260
261 // Scan from bottom up to find last non-empty line.
262 for y := height - 1; y >= 0; y-- {
263 line := buf.Line(y)
264 if l.lineHasContent(line) {
265 return y + 1
266 }
267 }
268
269 return 0
270}
271
272// lineHasContent checks if a line has any non-empty cells.
273func (l *SimpleList) lineHasContent(line uv.Line) bool {
274 for x := 0; x < len(line); x++ {
275 cell := line.At(x)
276 if cell != nil && !cell.IsZero() && cell.Content != "" && cell.Content != " " {
277 return true
278 }
279 }
280 return false
281}
282
283// setRendered updates the rendered content and caches line offsets.
284func (l *SimpleList) setRendered(rendered string) {
285 l.rendered = rendered
286 l.renderedHeight = lipgloss.Height(rendered)
287
288 // Build line offset cache.
289 if len(rendered) > 0 {
290 l.lineOffsets = make([]int, 0, l.renderedHeight)
291 l.lineOffsets = append(l.lineOffsets, 0)
292
293 offset := 0
294 for {
295 idx := strings.IndexByte(rendered[offset:], '\n')
296 if idx == -1 {
297 break
298 }
299 offset += idx + 1
300 l.lineOffsets = append(l.lineOffsets, offset)
301 }
302 } else {
303 l.lineOffsets = nil
304 }
305}
306
307// scrollToSelection scrolls to make the selected item visible.
308func (l *SimpleList) scrollToSelection() {
309 if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
310 return
311 }
312
313 // Get selected item metadata.
314 var rItem *renderedItem
315 if idItem, ok := l.items[l.selectedIdx].(interface{ ID() string }); ok {
316 if ri, ok := l.renderedItems[idItem.ID()]; ok {
317 rItem = &ri
318 }
319 }
320
321 if rItem == nil {
322 return
323 }
324
325 start, end := l.viewPosition()
326
327 // Already visible.
328 if rItem.start >= start && rItem.end <= end {
329 return
330 }
331
332 // Item is above viewport - scroll up.
333 if rItem.start < start {
334 l.offset = rItem.start
335 return
336 }
337
338 // Item is below viewport - scroll down.
339 if rItem.end > end {
340 l.offset = max(0, rItem.end-l.height+1)
341 }
342}
343
344// Focus/blur management.
345
346func (l *SimpleList) focusSelectedItem() tea.Cmd {
347 if l.selectedIdx < 0 || !l.focused {
348 return nil
349 }
350
351 var cmds []tea.Cmd
352
353 // Blur previous.
354 if l.prevSelectedIdx >= 0 && l.prevSelectedIdx != l.selectedIdx && l.prevSelectedIdx < len(l.items) {
355 if f, ok := l.items[l.prevSelectedIdx].(Focusable); ok && f.IsFocused() {
356 f.Blur()
357 }
358 }
359
360 // Focus current.
361 if l.selectedIdx >= 0 && l.selectedIdx < len(l.items) {
362 if f, ok := l.items[l.selectedIdx].(Focusable); ok && !f.IsFocused() {
363 f.Focus()
364 }
365 }
366
367 l.prevSelectedIdx = l.selectedIdx
368 return tea.Batch(cmds...)
369}
370
371func (l *SimpleList) blurSelectedItem() tea.Cmd {
372 if l.selectedIdx < 0 || l.focused {
373 return nil
374 }
375
376 if l.selectedIdx >= 0 && l.selectedIdx < len(l.items) {
377 if f, ok := l.items[l.selectedIdx].(Focusable); ok && f.IsFocused() {
378 f.Blur()
379 }
380 }
381
382 return nil
383}
384
385// Public API.
386
387// SetSize sets the viewport dimensions.
388func (l *SimpleList) SetSize(width, height int) tea.Cmd {
389 oldWidth := l.width
390 l.width = width
391 l.height = height
392
393 if oldWidth != width {
394 // Width changed - need to re-render.
395 return l.render()
396 }
397
398 return nil
399}
400
401// Width returns the viewport width.
402func (l *SimpleList) Width() int {
403 return l.width
404}
405
406// Height returns the viewport height.
407func (l *SimpleList) Height() int {
408 return l.height
409}
410
411// GetSize returns the viewport dimensions.
412func (l *SimpleList) GetSize() (int, int) {
413 return l.width, l.height
414}
415
416// Items returns all items.
417func (l *SimpleList) Items() []Item {
418 return l.items
419}
420
421// Len returns the number of items.
422func (l *SimpleList) Len() int {
423 return len(l.items)
424}
425
426// SetItems replaces all items.
427func (l *SimpleList) SetItems(items []Item) tea.Cmd {
428 l.items = items
429 l.itemIDs = make(map[string]int, len(items))
430 l.renderedItems = make(map[string]renderedItem)
431 l.selectedIdx = -1
432 l.prevSelectedIdx = -1
433 l.offset = 0
434
435 // Build ID map.
436 for i, item := range items {
437 if idItem, ok := item.(interface{ ID() string }); ok {
438 l.itemIDs[idItem.ID()] = i
439 }
440 }
441
442 return l.render()
443}
444
445// AppendItem adds an item to the end.
446func (l *SimpleList) AppendItem(item Item) tea.Cmd {
447 l.items = append(l.items, item)
448
449 if idItem, ok := item.(interface{ ID() string }); ok {
450 l.itemIDs[idItem.ID()] = len(l.items) - 1
451 }
452
453 return l.render()
454}
455
456// PrependItem adds an item to the beginning.
457func (l *SimpleList) PrependItem(item Item) tea.Cmd {
458 l.items = append([]Item{item}, l.items...)
459
460 // Rebuild ID map (indices shifted).
461 l.itemIDs = make(map[string]int, len(l.items))
462 for i, it := range l.items {
463 if idItem, ok := it.(interface{ ID() string }); ok {
464 l.itemIDs[idItem.ID()] = i
465 }
466 }
467
468 // Adjust selection.
469 if l.selectedIdx >= 0 {
470 l.selectedIdx++
471 }
472 if l.prevSelectedIdx >= 0 {
473 l.prevSelectedIdx++
474 }
475
476 return l.render()
477}
478
479// UpdateItem replaces an item at the given index.
480func (l *SimpleList) UpdateItem(idx int, item Item) tea.Cmd {
481 if idx < 0 || idx >= len(l.items) {
482 return nil
483 }
484
485 l.items[idx] = item
486
487 // Update ID map.
488 if idItem, ok := item.(interface{ ID() string }); ok {
489 l.itemIDs[idItem.ID()] = idx
490 }
491
492 return l.render()
493}
494
495// DeleteItem removes an item at the given index.
496func (l *SimpleList) DeleteItem(idx int) tea.Cmd {
497 if idx < 0 || idx >= len(l.items) {
498 return nil
499 }
500
501 l.items = append(l.items[:idx], l.items[idx+1:]...)
502
503 // Rebuild ID map (indices shifted).
504 l.itemIDs = make(map[string]int, len(l.items))
505 for i, it := range l.items {
506 if idItem, ok := it.(interface{ ID() string }); ok {
507 l.itemIDs[idItem.ID()] = i
508 }
509 }
510
511 // Adjust selection.
512 if l.selectedIdx == idx {
513 if idx > 0 {
514 l.selectedIdx = idx - 1
515 } else if len(l.items) > 0 {
516 l.selectedIdx = 0
517 } else {
518 l.selectedIdx = -1
519 }
520 } else if l.selectedIdx > idx {
521 l.selectedIdx--
522 }
523
524 if l.prevSelectedIdx == idx {
525 l.prevSelectedIdx = -1
526 } else if l.prevSelectedIdx > idx {
527 l.prevSelectedIdx--
528 }
529
530 return l.render()
531}
532
533// Focus sets the list as focused.
534func (l *SimpleList) Focus() tea.Cmd {
535 l.focused = true
536 return l.render()
537}
538
539// Blur removes focus from the list.
540func (l *SimpleList) Blur() tea.Cmd {
541 l.focused = false
542 return l.render()
543}
544
545// Focused returns whether the list is focused.
546func (l *SimpleList) Focused() bool {
547 return l.focused
548}
549
550// Selection.
551
552// Selected returns the currently selected item index.
553func (l *SimpleList) Selected() int {
554 return l.selectedIdx
555}
556
557// SelectedIndex returns the currently selected item index.
558func (l *SimpleList) SelectedIndex() int {
559 return l.selectedIdx
560}
561
562// SelectedItem returns the currently selected item.
563func (l *SimpleList) SelectedItem() Item {
564 if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
565 return nil
566 }
567 return l.items[l.selectedIdx]
568}
569
570// SetSelected sets the selected item by index.
571func (l *SimpleList) SetSelected(idx int) tea.Cmd {
572 if idx < -1 || idx >= len(l.items) {
573 return nil
574 }
575
576 if l.selectedIdx == idx {
577 return nil
578 }
579
580 l.prevSelectedIdx = l.selectedIdx
581 l.selectedIdx = idx
582
583 return l.render()
584}
585
586// SelectFirst selects the first item.
587func (l *SimpleList) SelectFirst() tea.Cmd {
588 return l.SetSelected(0)
589}
590
591// SelectLast selects the last item.
592func (l *SimpleList) SelectLast() tea.Cmd {
593 if len(l.items) > 0 {
594 return l.SetSelected(len(l.items) - 1)
595 }
596 return nil
597}
598
599// SelectNext selects the next item.
600func (l *SimpleList) SelectNext() tea.Cmd {
601 if l.selectedIdx < len(l.items)-1 {
602 return l.SetSelected(l.selectedIdx + 1)
603 }
604 return nil
605}
606
607// SelectPrev selects the previous item.
608func (l *SimpleList) SelectPrev() tea.Cmd {
609 if l.selectedIdx > 0 {
610 return l.SetSelected(l.selectedIdx - 1)
611 }
612 return nil
613}
614
615// SelectNextWrap selects the next item (wraps to beginning).
616func (l *SimpleList) SelectNextWrap() tea.Cmd {
617 if len(l.items) == 0 {
618 return nil
619 }
620 nextIdx := (l.selectedIdx + 1) % len(l.items)
621 return l.SetSelected(nextIdx)
622}
623
624// SelectPrevWrap selects the previous item (wraps to end).
625func (l *SimpleList) SelectPrevWrap() tea.Cmd {
626 if len(l.items) == 0 {
627 return nil
628 }
629 prevIdx := (l.selectedIdx - 1 + len(l.items)) % len(l.items)
630 return l.SetSelected(prevIdx)
631}
632
633// SelectFirstInView selects the first fully visible item.
634func (l *SimpleList) SelectFirstInView() tea.Cmd {
635 if len(l.items) == 0 {
636 return nil
637 }
638
639 start, end := l.viewPosition()
640
641 for i := 0; i < len(l.items); i++ {
642 if idItem, ok := l.items[i].(interface{ ID() string }); ok {
643 if rItem, ok := l.renderedItems[idItem.ID()]; ok {
644 // Check if fully visible.
645 if rItem.start >= start && rItem.end <= end {
646 return l.SetSelected(i)
647 }
648 }
649 }
650 }
651
652 return nil
653}
654
655// SelectLastInView selects the last fully visible item.
656func (l *SimpleList) SelectLastInView() tea.Cmd {
657 if len(l.items) == 0 {
658 return nil
659 }
660
661 start, end := l.viewPosition()
662
663 for i := len(l.items) - 1; i >= 0; i-- {
664 if idItem, ok := l.items[i].(interface{ ID() string }); ok {
665 if rItem, ok := l.renderedItems[idItem.ID()]; ok {
666 // Check if fully visible.
667 if rItem.start >= start && rItem.end <= end {
668 return l.SetSelected(i)
669 }
670 }
671 }
672 }
673
674 return nil
675}
676
677// SelectedItemInView returns true if the selected item is visible.
678func (l *SimpleList) SelectedItemInView() bool {
679 if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
680 return false
681 }
682
683 var rItem *renderedItem
684 if idItem, ok := l.items[l.selectedIdx].(interface{ ID() string }); ok {
685 if ri, ok := l.renderedItems[idItem.ID()]; ok {
686 rItem = &ri
687 }
688 }
689
690 if rItem == nil {
691 return false
692 }
693
694 start, end := l.viewPosition()
695 return rItem.start < end && rItem.end > start
696}
697
698// Scrolling.
699
700// Offset returns the current scroll offset.
701func (l *SimpleList) Offset() int {
702 return l.offset
703}
704
705// TotalHeight returns the total height of all items.
706func (l *SimpleList) TotalHeight() int {
707 return l.renderedHeight
708}
709
710// ScrollBy scrolls by the given number of lines.
711func (l *SimpleList) ScrollBy(deltaLines int) tea.Cmd {
712 l.offset += deltaLines
713 l.clampOffset()
714 return nil
715}
716
717// ScrollToTop scrolls to the top.
718func (l *SimpleList) ScrollToTop() tea.Cmd {
719 l.offset = 0
720 return nil
721}
722
723// ScrollToBottom scrolls to the bottom.
724func (l *SimpleList) ScrollToBottom() tea.Cmd {
725 l.offset = l.renderedHeight - l.height
726 l.clampOffset()
727 return nil
728}
729
730// AtTop returns true if scrolled to the top.
731func (l *SimpleList) AtTop() bool {
732 return l.offset <= 0
733}
734
735// AtBottom returns true if scrolled to the bottom.
736func (l *SimpleList) AtBottom() bool {
737 return l.offset >= l.renderedHeight-l.height
738}
739
740// ScrollToItem scrolls to make an item visible.
741func (l *SimpleList) ScrollToItem(idx int) tea.Cmd {
742 if idx < 0 || idx >= len(l.items) {
743 return nil
744 }
745
746 var rItem *renderedItem
747 if idItem, ok := l.items[idx].(interface{ ID() string }); ok {
748 if ri, ok := l.renderedItems[idItem.ID()]; ok {
749 rItem = &ri
750 }
751 }
752
753 if rItem == nil {
754 return nil
755 }
756
757 start, end := l.viewPosition()
758
759 // Already visible.
760 if rItem.start >= start && rItem.end <= end {
761 return nil
762 }
763
764 // Above viewport.
765 if rItem.start < start {
766 l.offset = rItem.start
767 return nil
768 }
769
770 // Below viewport.
771 if rItem.end > end {
772 l.offset = rItem.end - l.height + 1
773 l.clampOffset()
774 }
775
776 return nil
777}
778
779// ScrollToSelected scrolls to the selected item.
780func (l *SimpleList) ScrollToSelected() tea.Cmd {
781 if l.selectedIdx >= 0 {
782 return l.ScrollToItem(l.selectedIdx)
783 }
784 return nil
785}
786
787func (l *SimpleList) clampOffset() {
788 maxOffset := l.renderedHeight - l.height
789 if maxOffset < 0 {
790 maxOffset = 0
791 }
792 l.offset = ordered.Clamp(l.offset, 0, maxOffset)
793}
794
795// Mouse and highlighting.
796
797// HandleMouseDown handles mouse press.
798func (l *SimpleList) HandleMouseDown(x, y int) bool {
799 if x < 0 || y < 0 || x >= l.width || y >= l.height {
800 return false
801 }
802
803 // Find item at viewport y.
804 contentY := l.offset + y
805 itemIdx := l.findItemAtLine(contentY)
806
807 if itemIdx < 0 {
808 return false
809 }
810
811 l.mouseDown = true
812 l.mouseDownItem = itemIdx
813 l.mouseDownX = x
814 l.mouseDownY = y
815 l.mouseDragItem = itemIdx
816 l.mouseDragX = x
817 l.mouseDragY = y
818
819 // Start selection.
820 l.selectionStartLine = y
821 l.selectionStartCol = x
822 l.selectionEndLine = y
823 l.selectionEndCol = x
824
825 // Select item.
826 l.SetSelected(itemIdx)
827
828 return true
829}
830
831// HandleMouseDrag handles mouse drag.
832func (l *SimpleList) HandleMouseDrag(x, y int) bool {
833 if !l.mouseDown {
834 return false
835 }
836
837 // Clamp coordinates to viewport bounds.
838 clampedX := max(0, min(x, l.width-1))
839 clampedY := max(0, min(y, l.height-1))
840
841 if clampedY >= 0 && clampedY < l.height {
842 contentY := l.offset + clampedY
843 itemIdx := l.findItemAtLine(contentY)
844 if itemIdx >= 0 {
845 l.mouseDragItem = itemIdx
846 l.mouseDragX = clampedX
847 l.mouseDragY = clampedY
848 }
849 }
850
851 // Update selection end (clamped to viewport).
852 l.selectionEndLine = clampedY
853 l.selectionEndCol = clampedX
854
855 return true
856}
857
858// HandleMouseUp handles mouse release.
859func (l *SimpleList) HandleMouseUp(x, y int) bool {
860 if !l.mouseDown {
861 return false
862 }
863
864 l.mouseDown = false
865
866 // Final selection update (clamped to viewport).
867 clampedX := max(0, min(x, l.width-1))
868 clampedY := max(0, min(y, l.height-1))
869 l.selectionEndLine = clampedY
870 l.selectionEndCol = clampedX
871
872 return true
873}
874
875// ClearHighlight clears the selection.
876func (l *SimpleList) ClearHighlight() {
877 l.selectionStartLine = -1
878 l.selectionStartCol = -1
879 l.selectionEndLine = -1
880 l.selectionEndCol = -1
881 l.mouseDown = false
882 l.mouseDownItem = -1
883 l.mouseDragItem = -1
884}
885
886// GetHighlightedText returns the selected text.
887func (l *SimpleList) GetHighlightedText() string {
888 if !l.hasSelection() {
889 return ""
890 }
891
892 return l.renderSelection(l.View())
893}
894
895func (l *SimpleList) hasSelection() bool {
896 return l.selectionEndCol != l.selectionStartCol || l.selectionEndLine != l.selectionStartLine
897}
898
899// renderSelection applies highlighting to the view and extracts text.
900func (l *SimpleList) renderSelection(view string) string {
901 // Create a screen buffer spanning the viewport.
902 buf := uv.NewScreenBuffer(l.width, l.height)
903 area := uv.Rect(0, 0, l.width, l.height)
904 uv.NewStyledString(view).Draw(&buf, area)
905
906 // Calculate selection bounds.
907 startLine := min(l.selectionStartLine, l.selectionEndLine)
908 endLine := max(l.selectionStartLine, l.selectionEndLine)
909 startCol := l.selectionStartCol
910 endCol := l.selectionEndCol
911
912 if l.selectionEndLine < l.selectionStartLine {
913 startCol = l.selectionEndCol
914 endCol = l.selectionStartCol
915 }
916
917 // Apply highlighting.
918 for y := startLine; y <= endLine && y < l.height; y++ {
919 if y >= buf.Height() {
920 break
921 }
922
923 line := buf.Line(y)
924
925 // Determine column range for this line.
926 colStart := 0
927 if y == startLine {
928 colStart = startCol
929 }
930
931 colEnd := len(line)
932 if y == endLine {
933 colEnd = min(endCol, len(line))
934 }
935
936 // Apply highlight style.
937 for x := colStart; x < colEnd && x < len(line); x++ {
938 cell := line.At(x)
939 if cell != nil && !cell.IsZero() {
940 cell = cell.Clone()
941 // Toggle reverse for highlight.
942 if cell.Style.Attrs&uv.AttrReverse != 0 {
943 cell.Style.Attrs &^= uv.AttrReverse
944 } else {
945 cell.Style.Attrs |= uv.AttrReverse
946 }
947 buf.SetCell(x, y, cell)
948 }
949 }
950 }
951
952 return buf.Render()
953}
954
955// findItemAtLine finds the item index at the given content line.
956func (l *SimpleList) findItemAtLine(line int) int {
957 for i := 0; i < len(l.items); i++ {
958 if idItem, ok := l.items[i].(interface{ ID() string }); ok {
959 if rItem, ok := l.renderedItems[idItem.ID()]; ok {
960 if line >= rItem.start && line <= rItem.end {
961 return i
962 }
963 }
964 }
965 }
966 return -1
967}
968
969// Render returns the view (for compatibility).
970func (l *SimpleList) Render() string {
971 return l.View()
972}