@@ -2,8 +2,6 @@ package list
6− "github.com/charmbracelet/x/ansi"
9// List represents a list of items that can be lazily rendered. A list is
7// List represents a list of items that can be lazily rendered. A list is
@@ -23,22 +21,15 @@ type List struct {
23 focused bool
21 focused bool
24 selectedIdx int // The current selected index -1 means no selection
22 selectedIdx int // The current selected index -1 means no selection
28− mouseDownItem int // Item index where mouse was pressed
29− mouseDownX int // X position in item content (character offset)
30− mouseDownY int // Y position in item (line offset)
31− mouseDragItem int // Current item index being dragged over
32− mouseDragX int // Current X in item content
33− mouseDragY int // Current Y in item
34− lastHighlighted map[int]bool // Track which items were highlighted in last update
36 // offsetIdx is the index of the first visible item in the viewport.
24 // offsetIdx is the index of the first visible item in the viewport.
37 offsetIdx int
25 offsetIdx int
38 // offsetLine is the number of lines of the item at offsetIdx that are
26 // offsetLine is the number of lines of the item at offsetIdx that are
39 // scrolled out of view (above the viewport).
27 // scrolled out of view (above the viewport).
40 // It must always be >= 0.
28 // It must always be >= 0.
41 offsetLine int
29 offsetLine int
31+ // renderCallbacks is a list of callbacks to apply when rendering items.
32+ renderCallbacks []func(idx, selectedIdx int, item Item) Item
44// renderedItem holds the rendered content and height of an item.
35// renderedItem holds the rendered content and height of an item.
@@ -52,17 +43,19 @@ func NewList(items ...Item) *List {
52 l := new(List)
43 l := new(List)
53 l.items = items
44 l.items = items
54 l.selectedIdx = -1
45 l.selectedIdx = -1
57− l.lastHighlighted = make(map[int]bool)
49+// RegisterRenderCallback registers a callback to be called when rendering
50+// items. This can be used to modify items before they are rendered.
51+func (l *List) RegisterRenderCallback(cb func(idx, selectedIdx int, item Item) Item) {
52+ l.renderCallbacks = append(l.renderCallbacks, cb)
61// SetSize sets the size of the list viewport.
55// SetSize sets the size of the list viewport.
62func (l *List) SetSize(width, height int) {
56func (l *List) SetSize(width, height int) {
63 l.width = width
57 l.width = width
64 l.height = height
58 l.height = height
65− // l.normalizeOffsets()
68// SetGap sets the gap between items.
61// SetGap sets the gap between items.
@@ -92,39 +85,12 @@ func (l *List) getItem(idx int) renderedItem {
94 item := l.items[idx]
87 item := l.items[idx]
95− if hi, ok := item.(Highlightable); ok {
88+ if len(l.renderCallbacks) > 0 {
96− // Apply highlight
89+ for _, cb := range l.renderCallbacks {
97− startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := l.getHighlightRange()
90+ if it := cb(idx, l.selectedIdx, item); it != nil {
98− sLine, sCol, eLine, eCol := -1, -1, -1, -1
91+ item = it
99− if idx >= startItemIdx && idx <= endItemIdx {
100− if idx == startItemIdx && idx == endItemIdx {
101− // Single item selection
106− } else if idx == startItemIdx {
107− // First item - from start position to end of item
112− } else if idx == endItemIdx {
113− // Last item - from start of item to end position
119− // Middle item - fully highlighted
127− hi.Highlight(sLine, sCol, eLine, eCol)
130 if focusable, isFocusable := item.(Focusable); isFocusable {
96 if focusable, isFocusable := item.(Focusable); isFocusable {
@@ -481,80 +447,19 @@ func (l *List) SelectLastInView() {
481 l.selectedIdx = endIdx
447 l.selectedIdx = endIdx
484−// HandleMouseDown handles mouse down events at the given line in the viewport.
450+// ItemAt returns the item at the given index.
485−// x and y are viewport-relative coordinates (0,0 = top-left of visible area).
486−// Returns true if the event was handled.
487−func (l *List) HandleMouseDown(x, y int) bool {
488− if len(l.items) == 0 {
492− // Find which item was clicked
493− itemIdx, itemY := l.findItemAtY(x, y)
499− l.mouseDownItem = itemIdx
501− l.mouseDownY = itemY
502− l.mouseDragItem = itemIdx
504− l.mouseDragY = itemY
506− // Select the clicked item
507− l.SetSelected(itemIdx)
509− if clickable, ok := l.items[itemIdx].(MouseClickable); ok {
510− clickable.HandleMouseClick(ansi.MouseButton1, x, itemY)
516−// HandleMouseUp handles mouse up events at the given line in the viewport.
517−// Returns true if the event was handled.
518−func (l *List) HandleMouseUp(x, y int) bool {
451+func (l *List) ItemAt(index int) Item {
528−// HandleMouseDrag handles mouse drag events at the given line in the viewport.
529−// x and y are viewport-relative coordinates.
530−// Returns true if the event was handled.
531−func (l *List) HandleMouseDrag(x, y int) bool {
536− if len(l.items) == 0 {
452+ if index < 0 || index >= len(l.items) {
537− return false
453+ return nil
540− // Find which item we're dragging over
541− itemIdx, itemY := l.findItemAtY(x, y)
546− l.mouseDragItem = itemIdx
548− l.mouseDragY = itemY
550− return true
455+ return l.items[index]
553−// ClearHighlight clears any active text highlighting.
458+// ItemIndexAtPosition returns the item at the given viewport-relative y
554−func (l *List) ClearHighlight() {
459+// coordinate. Returns the item index and the y offset within that item. It
555− l.mouseDownItem = -1
460+// returns -1, -1 if no item is found.
556− l.mouseDragItem = -1
461+func (l *List) ItemIndexAtPosition(x, y int) (itemIdx int, itemY int) {
557− l.lastHighlighted = make(map[int]bool)
462+ return l.findItemAtY(x, y)
560// findItemAtY finds the item at the given viewport y coordinate.
465// findItemAtY finds the item at the given viewport y coordinate.
@@ -591,41 +496,6 @@ func (l *List) findItemAtY(_, y int) (itemIdx int, itemY int) {
591 return -1, -1
496 return -1, -1
594−// getHighlightRange returns the current highlight range.
595−func (l *List) getHighlightRange() (startItemIdx, startLine, startCol, endItemIdx, endLine, endCol int) {
596− if l.mouseDownItem < 0 {
597− return -1, -1, -1, -1, -1, -1
600− downItemIdx := l.mouseDownItem
601− dragItemIdx := l.mouseDragItem
603− // Determine selection direction
604− draggingDown := dragItemIdx > downItemIdx ||
605− (dragItemIdx == downItemIdx && l.mouseDragY > l.mouseDownY) ||
606− (dragItemIdx == downItemIdx && l.mouseDragY == l.mouseDownY && l.mouseDragX >= l.mouseDownX)
609− // Normal forward selection
610− startItemIdx = downItemIdx
611− startLine = l.mouseDownY
612− startCol = l.mouseDownX
613− endItemIdx = dragItemIdx
614− endLine = l.mouseDragY
615− endCol = l.mouseDragX
617− // Backward selection (dragging up)
618− startItemIdx = dragItemIdx
619− startLine = l.mouseDragY
620− startCol = l.mouseDragX
621− endItemIdx = downItemIdx
622− endLine = l.mouseDownY
623− endCol = l.mouseDownX
626− return startItemIdx, startLine, startCol, endItemIdx, endLine, endCol
629// countLines counts the number of lines in a string.
499// countLines counts the number of lines in a string.
630func countLines(s string) int {
500func countLines(s string) int {
631 if s == "" {
501 if s == "" {