on main help sign in
refactor(ui): list: remove mouse highlighting state, add render callbacks
2dc5d0bAyman Bagabas
internal/ui/list/list.go+23−153
@@ -2,8 +2,6 @@ package list
2
2
3import (
3import (
4 "strings"
4 "strings"
5−
6− "github.com/charmbracelet/x/ansi"
7)
5)
8
6
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
25
23
26− // Mouse state
27− mouseDown bool
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
35−
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
30+
31+ // renderCallbacks is a list of callbacks to apply when rendering items.
32+ renderCallbacks []func(idx, selectedIdx int, item Item) Item
42}
33}
43
34
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
55− l.mouseDownItem = -1
56− l.mouseDragItem = -1
57− l.lastHighlighted = make(map[int]bool)
58 return l
46 return l
59}
47}
60
48
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)
53+}
54+
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()
66}
59}
67
60
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 {
92 }
85 }
93
86
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
102− sLine = startLine
103− sCol = startCol
104− eLine = endLine
105− eCol = endCol
106− } else if idx == startItemIdx {
107− // First item - from start position to end of item
108− sLine = startLine
109− sCol = startCol
110− eLine = -1
111− eCol = -1
112− } else if idx == endItemIdx {
113− // Last item - from start of item to end position
114− sLine = 0
115− sCol = 0
116− eLine = endLine
117− eCol = endCol
118− } else {
119− // Middle item - fully highlighted
120− sLine = 0
121− sCol = 0
122− eLine = -1
123− eCol = -1
124 }
92 }
125 }
93 }
126−
127− hi.Highlight(sLine, sCol, eLine, eCol)
128 }
94 }
129
95
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
482}
448}
483
449
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 {
489− return false
490− }
491−
492− // Find which item was clicked
493− itemIdx, itemY := l.findItemAtY(x, y)
494− if itemIdx < 0 {
495− return false
496− }
497−
498− l.mouseDown = true
499− l.mouseDownItem = itemIdx
500− l.mouseDownX = x
501− l.mouseDownY = itemY
502− l.mouseDragItem = itemIdx
503− l.mouseDragX = x
504− l.mouseDragY = itemY
505−
506− // Select the clicked item
507− l.SetSelected(itemIdx)
508−
509− if clickable, ok := l.items[itemIdx].(MouseClickable); ok {
510− clickable.HandleMouseClick(ansi.MouseButton1, x, itemY)
511− }
512−
513− return true
514−}
515−
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 {
519− if !l.mouseDown {
520− return false
521− }
522−
523− l.mouseDown = false
524−
525− return true
526−}
527−
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 {
532− if !l.mouseDown {
533− return false
534− }
535−
536− if len(l.items) == 0 {
452+ if index < 0 || index >= len(l.items) {
537− return false
453+ return nil
538− }
539−
540− // Find which item we're dragging over
541− itemIdx, itemY := l.findItemAtY(x, y)
542− if itemIdx < 0 {
543− return false
544 }
454 }
545−
546− l.mouseDragItem = itemIdx
547− l.mouseDragX = x
548− l.mouseDragY = itemY
549−
550− return true
455+ return l.items[index]
551}
456}
552
457
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)
558}
463}
559
464
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
592}
497}
593
498
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
598− }
599−
600− downItemIdx := l.mouseDownItem
601− dragItemIdx := l.mouseDragItem
602−
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)
607−
608− if draggingDown {
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
616− } else {
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
624− }
625−
626− return startItemIdx, startLine, startCol, endItemIdx, endLine, endCol
627−}
628−
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 == "" {
internal/ui/model/chat.go+154−10
@@ -4,6 +4,7 @@ import (
4 "github.com/charmbracelet/crush/internal/ui/common"
4 "github.com/charmbracelet/crush/internal/ui/common"
5 "github.com/charmbracelet/crush/internal/ui/list"
5 "github.com/charmbracelet/crush/internal/ui/list"
6 uv "github.com/charmbracelet/ultraviolet"
6 uv "github.com/charmbracelet/ultraviolet"
7+ "github.com/charmbracelet/x/ansi"
7)
8)
8
9
9// Chat represents the chat UI model that handles chat interactions and
10// Chat represents the chat UI model that handles chat interactions and
@@ -11,17 +12,28 @@ import (
11type Chat struct {
12type Chat struct {
12 com *common.Common
13 com *common.Common
13 list *list.List
14 list *list.List
15+
16+ // Mouse state
17+ mouseDown bool
18+ mouseDownItem int // Item index where mouse was pressed
19+ mouseDownX int // X position in item content (character offset)
20+ mouseDownY int // Y position in item (line offset)
21+ mouseDragItem int // Current item index being dragged over
22+ mouseDragX int // Current X in item content
23+ mouseDragY int // Current Y in item
14}
24}
15
25
16// NewChat creates a new instance of [Chat] that handles chat interactions and
26// NewChat creates a new instance of [Chat] that handles chat interactions and
17// messages.
27// messages.
18func NewChat(com *common.Common) *Chat {
28func NewChat(com *common.Common) *Chat {
29+ c := &Chat{com: com}
19 l := list.NewList()
30 l := list.NewList()
20 l.SetGap(1)
31 l.SetGap(1)
32+ l.RegisterRenderCallback(c.applyHighlightRange)
33+ c.list = l
34+ c.mouseDownItem = -1
35+ c.mouseDragItem = -1
21− return &Chat{
36+ return c
22− com: com,
23− list: l,
24− }
25}
37}
26
38
27// Height returns the height of the chat view port.
39// Height returns the height of the chat view port.
@@ -146,16 +158,148 @@ func (m *Chat) SelectLastInView() {
146}
158}
147
159
148// HandleMouseDown handles mouse down events for the chat component.
160// HandleMouseDown handles mouse down events for the chat component.
149−func (m *Chat) HandleMouseDown(x, y int) {
161+func (m *Chat) HandleMouseDown(x, y int) bool {
150− m.list.HandleMouseDown(x, y)
162+ if m.list.Len() == 0 {
163+ return false
164+ }
165+
166+ itemIdx, itemY := m.list.ItemIndexAtPosition(x, y)
167+ if itemIdx < 0 {
168+ return false
169+ }
170+
171+ m.mouseDown = true
172+ m.mouseDownItem = itemIdx
173+ m.mouseDownX = x
174+ m.mouseDownY = itemY
175+ m.mouseDragItem = itemIdx
176+ m.mouseDragX = x
177+ m.mouseDragY = itemY
178+
179+ // Select the item that was clicked
180+ m.list.SetSelected(itemIdx)
181+
182+ if clickable, ok := m.list.SelectedItem().(list.MouseClickable); ok {
183+ return clickable.HandleMouseClick(ansi.MouseButton1, x, itemY)
184+ }
185+
186+ return true
151}
187}
152
188
153// HandleMouseUp handles mouse up events for the chat component.
189// HandleMouseUp handles mouse up events for the chat component.
154−func (m *Chat) HandleMouseUp(x, y int) {
190+func (m *Chat) HandleMouseUp(x, y int) bool {
155− m.list.HandleMouseUp(x, y)
191+ if !m.mouseDown {
192+ return false
193+ }
194+
195+ // TODO: Handle the behavior when mouse is released after a drag selection
196+ // (e.g., copy selected text to clipboard)
197+
198+ m.mouseDown = false
199+ return true
156}
200}
157
201
158// HandleMouseDrag handles mouse drag events for the chat component.
202// HandleMouseDrag handles mouse drag events for the chat component.
159−func (m *Chat) HandleMouseDrag(x, y int) {
203+func (m *Chat) HandleMouseDrag(x, y int) bool {
160− m.list.HandleMouseDrag(x, y)
204+ if !m.mouseDown {
205+ return false
206+ }
207+
208+ if m.list.Len() == 0 {
209+ return false
210+ }
211+
212+ itemIdx, itemY := m.list.ItemIndexAtPosition(x, y)
213+ if itemIdx < 0 {
214+ return false
215+ }
216+
217+ m.mouseDragItem = itemIdx
218+ m.mouseDragX = x
219+ m.mouseDragY = itemY
220+
221+ return true
222+}
223+
224+// ClearMouse clears the current mouse interaction state.
225+func (m *Chat) ClearMouse() {
226+ m.mouseDown = false
227+ m.mouseDownItem = -1
228+ m.mouseDragItem = -1
229+}
230+
231+// applyHighlightRange applies the current highlight range to the chat items.
232+func (m *Chat) applyHighlightRange(idx, selectedIdx int, item list.Item) list.Item {
233+ if hi, ok := item.(list.Highlightable); ok {
234+ // Apply highlight
235+ startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := m.getHighlightRange()
236+ sLine, sCol, eLine, eCol := -1, -1, -1, -1
237+ if idx >= startItemIdx && idx <= endItemIdx {
238+ if idx == startItemIdx && idx == endItemIdx {
239+ // Single item selection
240+ sLine = startLine
241+ sCol = startCol
242+ eLine = endLine
243+ eCol = endCol
244+ } else if idx == startItemIdx {
245+ // First item - from start position to end of item
246+ sLine = startLine
247+ sCol = startCol
248+ eLine = -1
249+ eCol = -1
250+ } else if idx == endItemIdx {
251+ // Last item - from start of item to end position
252+ sLine = 0
253+ sCol = 0
254+ eLine = endLine
255+ eCol = endCol
256+ } else {
257+ // Middle item - fully highlighted
258+ sLine = 0
259+ sCol = 0
260+ eLine = -1
261+ eCol = -1
262+ }
263+ }
264+
265+ hi.Highlight(sLine, sCol, eLine, eCol)
266+ return hi.(list.Item)
267+ }
268+
269+ return item
270+}
271+
272+// getHighlightRange returns the current highlight range.
273+func (m *Chat) getHighlightRange() (startItemIdx, startLine, startCol, endItemIdx, endLine, endCol int) {
274+ if m.mouseDownItem < 0 {
275+ return -1, -1, -1, -1, -1, -1
276+ }
277+
278+ downItemIdx := m.mouseDownItem
279+ dragItemIdx := m.mouseDragItem
280+
281+ // Determine selection direction
282+ draggingDown := dragItemIdx > downItemIdx ||
283+ (dragItemIdx == downItemIdx && m.mouseDragY > m.mouseDownY) ||
284+ (dragItemIdx == downItemIdx && m.mouseDragY == m.mouseDownY && m.mouseDragX >= m.mouseDownX)
285+
286+ if draggingDown {
287+ // Normal forward selection
288+ startItemIdx = downItemIdx
289+ startLine = m.mouseDownY
290+ startCol = m.mouseDownX
291+ endItemIdx = dragItemIdx
292+ endLine = m.mouseDragY
293+ endCol = m.mouseDragX
294+ } else {
295+ // Backward selection (dragging up)
296+ startItemIdx = dragItemIdx
297+ startLine = m.mouseDragY
298+ startCol = m.mouseDragX
299+ endItemIdx = downItemIdx
300+ endLine = m.mouseDownY
301+ endCol = m.mouseDownX
302+ }
303+
304+ return startItemIdx, startLine, startCol, endItemIdx, endLine, endCol
161}
305}