@@ -2,8 +2,6 @@ package list
import (
"strings"
-
- "github.com/charmbracelet/x/ansi"
)
// List represents a list of items that can be lazily rendered. A list is
@@ -23,22 +21,15 @@ type List struct {
focused bool
selectedIdx int // The current selected index -1 means no selection
- // Mouse state
- mouseDown bool
- mouseDownItem int // Item index where mouse was pressed
- mouseDownX int // X position in item content (character offset)
- mouseDownY int // Y position in item (line offset)
- mouseDragItem int // Current item index being dragged over
- mouseDragX int // Current X in item content
- mouseDragY int // Current Y in item
- lastHighlighted map[int]bool // Track which items were highlighted in last update
-
// offsetIdx is the index of the first visible item in the viewport.
offsetIdx int
// offsetLine is the number of lines of the item at offsetIdx that are
// scrolled out of view (above the viewport).
// It must always be >= 0.
offsetLine int
+
+ // renderCallbacks is a list of callbacks to apply when rendering items.
+ renderCallbacks []func(idx, selectedIdx int, item Item) Item
}
// renderedItem holds the rendered content and height of an item.
@@ -52,17 +43,19 @@ func NewList(items ...Item) *List {
l := new(List)
l.items = items
l.selectedIdx = -1
- l.mouseDownItem = -1
- l.mouseDragItem = -1
- l.lastHighlighted = make(map[int]bool)
return l
}
+// RegisterRenderCallback registers a callback to be called when rendering
+// items. This can be used to modify items before they are rendered.
+func (l *List) RegisterRenderCallback(cb func(idx, selectedIdx int, item Item) Item) {
+ l.renderCallbacks = append(l.renderCallbacks, cb)
+}
+
// SetSize sets the size of the list viewport.
func (l *List) SetSize(width, height int) {
l.width = width
l.height = height
- // l.normalizeOffsets()
}
// SetGap sets the gap between items.
@@ -92,39 +85,12 @@ func (l *List) getItem(idx int) renderedItem {
}
item := l.items[idx]
- if hi, ok := item.(Highlightable); ok {
- // Apply highlight
- startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := l.getHighlightRange()
- sLine, sCol, eLine, eCol := -1, -1, -1, -1
- if idx >= startItemIdx && idx <= endItemIdx {
- if idx == startItemIdx && idx == endItemIdx {
- // Single item selection
- sLine = startLine
- sCol = startCol
- eLine = endLine
- eCol = endCol
- } else if idx == startItemIdx {
- // First item - from start position to end of item
- sLine = startLine
- sCol = startCol
- eLine = -1
- eCol = -1
- } else if idx == endItemIdx {
- // Last item - from start of item to end position
- sLine = 0
- sCol = 0
- eLine = endLine
- eCol = endCol
- } else {
- // Middle item - fully highlighted
- sLine = 0
- sCol = 0
- eLine = -1
- eCol = -1
+ if len(l.renderCallbacks) > 0 {
+ for _, cb := range l.renderCallbacks {
+ if it := cb(idx, l.selectedIdx, item); it != nil {
+ item = it
}
}
-
- hi.Highlight(sLine, sCol, eLine, eCol)
}
if focusable, isFocusable := item.(Focusable); isFocusable {
@@ -481,80 +447,19 @@ func (l *List) SelectLastInView() {
l.selectedIdx = endIdx
}
-// HandleMouseDown handles mouse down events at the given line in the viewport.
-// x and y are viewport-relative coordinates (0,0 = top-left of visible area).
-// Returns true if the event was handled.
-func (l *List) HandleMouseDown(x, y int) bool {
- if len(l.items) == 0 {
- return false
- }
-
- // Find which item was clicked
- itemIdx, itemY := l.findItemAtY(x, y)
- if itemIdx < 0 {
- return false
- }
-
- l.mouseDown = true
- l.mouseDownItem = itemIdx
- l.mouseDownX = x
- l.mouseDownY = itemY
- l.mouseDragItem = itemIdx
- l.mouseDragX = x
- l.mouseDragY = itemY
-
- // Select the clicked item
- l.SetSelected(itemIdx)
-
- if clickable, ok := l.items[itemIdx].(MouseClickable); ok {
- clickable.HandleMouseClick(ansi.MouseButton1, x, itemY)
- }
-
- return true
-}
-
-// HandleMouseUp handles mouse up events at the given line in the viewport.
-// Returns true if the event was handled.
-func (l *List) HandleMouseUp(x, y int) bool {
- if !l.mouseDown {
- return false
- }
-
- l.mouseDown = false
-
- return true
-}
-
-// HandleMouseDrag handles mouse drag events at the given line in the viewport.
-// x and y are viewport-relative coordinates.
-// Returns true if the event was handled.
-func (l *List) HandleMouseDrag(x, y int) bool {
- if !l.mouseDown {
- return false
- }
-
- if len(l.items) == 0 {
- return false
- }
-
- // Find which item we're dragging over
- itemIdx, itemY := l.findItemAtY(x, y)
- if itemIdx < 0 {
- return false
+// ItemAt returns the item at the given index.
+func (l *List) ItemAt(index int) Item {
+ if index < 0 || index >= len(l.items) {
+ return nil
}
-
- l.mouseDragItem = itemIdx
- l.mouseDragX = x
- l.mouseDragY = itemY
-
- return true
+ return l.items[index]
}
-// ClearHighlight clears any active text highlighting.
-func (l *List) ClearHighlight() {
- l.mouseDownItem = -1
- l.mouseDragItem = -1
- l.lastHighlighted = make(map[int]bool)
+// ItemIndexAtPosition returns the item at the given viewport-relative y
+// coordinate. Returns the item index and the y offset within that item. It
+// returns -1, -1 if no item is found.
+func (l *List) ItemIndexAtPosition(x, y int) (itemIdx int, itemY int) {
+ return l.findItemAtY(x, y)
}
// findItemAtY finds the item at the given viewport y coordinate.
@@ -591,41 +496,6 @@ func (l *List) findItemAtY(_, y int) (itemIdx int, itemY int) {
return -1, -1
}
-// getHighlightRange returns the current highlight range.
-func (l *List) getHighlightRange() (startItemIdx, startLine, startCol, endItemIdx, endLine, endCol int) {
- if l.mouseDownItem < 0 {
- return -1, -1, -1, -1, -1, -1
- }
-
- downItemIdx := l.mouseDownItem
- dragItemIdx := l.mouseDragItem
-
- // Determine selection direction
- draggingDown := dragItemIdx > downItemIdx ||
- (dragItemIdx == downItemIdx && l.mouseDragY > l.mouseDownY) ||
- (dragItemIdx == downItemIdx && l.mouseDragY == l.mouseDownY && l.mouseDragX >= l.mouseDownX)
-
- if draggingDown {
- // Normal forward selection
- startItemIdx = downItemIdx
- startLine = l.mouseDownY
- startCol = l.mouseDownX
- endItemIdx = dragItemIdx
- endLine = l.mouseDragY
- endCol = l.mouseDragX
- } else {
- // Backward selection (dragging up)
- startItemIdx = dragItemIdx
- startLine = l.mouseDragY
- startCol = l.mouseDragX
- endItemIdx = downItemIdx
- endLine = l.mouseDownY
- endCol = l.mouseDownX
- }
-
- return startItemIdx, startLine, startCol, endItemIdx, endLine, endCol
-}
-
// countLines counts the number of lines in a string.
func countLines(s string) int {
if s == "" {
@@ -4,6 +4,7 @@ import (
"github.com/charmbracelet/crush/internal/ui/common"
"github.com/charmbracelet/crush/internal/ui/list"
uv "github.com/charmbracelet/ultraviolet"
+ "github.com/charmbracelet/x/ansi"
)
// Chat represents the chat UI model that handles chat interactions and
@@ -11,17 +12,28 @@ import (
type Chat struct {
com *common.Common
list *list.List
+
+ // Mouse state
+ mouseDown bool
+ mouseDownItem int // Item index where mouse was pressed
+ mouseDownX int // X position in item content (character offset)
+ mouseDownY int // Y position in item (line offset)
+ mouseDragItem int // Current item index being dragged over
+ mouseDragX int // Current X in item content
+ mouseDragY int // Current Y in item
}
// NewChat creates a new instance of [Chat] that handles chat interactions and
// messages.
func NewChat(com *common.Common) *Chat {
+ c := &Chat{com: com}
l := list.NewList()
l.SetGap(1)
- return &Chat{
- com: com,
- list: l,
- }
+ l.RegisterRenderCallback(c.applyHighlightRange)
+ c.list = l
+ c.mouseDownItem = -1
+ c.mouseDragItem = -1
+ return c
}
// Height returns the height of the chat view port.
@@ -146,16 +158,148 @@ func (m *Chat) SelectLastInView() {
}
// HandleMouseDown handles mouse down events for the chat component.
-func (m *Chat) HandleMouseDown(x, y int) {
- m.list.HandleMouseDown(x, y)
+func (m *Chat) HandleMouseDown(x, y int) bool {
+ if m.list.Len() == 0 {
+ return false
+ }
+
+ itemIdx, itemY := m.list.ItemIndexAtPosition(x, y)
+ if itemIdx < 0 {
+ return false
+ }
+
+ m.mouseDown = true
+ m.mouseDownItem = itemIdx
+ m.mouseDownX = x
+ m.mouseDownY = itemY
+ m.mouseDragItem = itemIdx
+ m.mouseDragX = x
+ m.mouseDragY = itemY
+
+ // Select the item that was clicked
+ m.list.SetSelected(itemIdx)
+
+ if clickable, ok := m.list.SelectedItem().(list.MouseClickable); ok {
+ return clickable.HandleMouseClick(ansi.MouseButton1, x, itemY)
+ }
+
+ return true
}
// HandleMouseUp handles mouse up events for the chat component.
-func (m *Chat) HandleMouseUp(x, y int) {
- m.list.HandleMouseUp(x, y)
+func (m *Chat) HandleMouseUp(x, y int) bool {
+ if !m.mouseDown {
+ return false
+ }
+
+ // TODO: Handle the behavior when mouse is released after a drag selection
+ // (e.g., copy selected text to clipboard)
+
+ m.mouseDown = false
+ return true
}
// HandleMouseDrag handles mouse drag events for the chat component.
-func (m *Chat) HandleMouseDrag(x, y int) {
- m.list.HandleMouseDrag(x, y)
+func (m *Chat) HandleMouseDrag(x, y int) bool {
+ if !m.mouseDown {
+ return false
+ }
+
+ if m.list.Len() == 0 {
+ return false
+ }
+
+ itemIdx, itemY := m.list.ItemIndexAtPosition(x, y)
+ if itemIdx < 0 {
+ return false
+ }
+
+ m.mouseDragItem = itemIdx
+ m.mouseDragX = x
+ m.mouseDragY = itemY
+
+ return true
+}
+
+// ClearMouse clears the current mouse interaction state.
+func (m *Chat) ClearMouse() {
+ m.mouseDown = false
+ m.mouseDownItem = -1
+ m.mouseDragItem = -1
+}
+
+// applyHighlightRange applies the current highlight range to the chat items.
+func (m *Chat) applyHighlightRange(idx, selectedIdx int, item list.Item) list.Item {
+ if hi, ok := item.(list.Highlightable); ok {
+ // Apply highlight
+ startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := m.getHighlightRange()
+ sLine, sCol, eLine, eCol := -1, -1, -1, -1
+ if idx >= startItemIdx && idx <= endItemIdx {
+ if idx == startItemIdx && idx == endItemIdx {
+ // Single item selection
+ sLine = startLine
+ sCol = startCol
+ eLine = endLine
+ eCol = endCol
+ } else if idx == startItemIdx {
+ // First item - from start position to end of item
+ sLine = startLine
+ sCol = startCol
+ eLine = -1
+ eCol = -1
+ } else if idx == endItemIdx {
+ // Last item - from start of item to end position
+ sLine = 0
+ sCol = 0
+ eLine = endLine
+ eCol = endCol
+ } else {
+ // Middle item - fully highlighted
+ sLine = 0
+ sCol = 0
+ eLine = -1
+ eCol = -1
+ }
+ }
+
+ hi.Highlight(sLine, sCol, eLine, eCol)
+ return hi.(list.Item)
+ }
+
+ return item
+}
+
+// getHighlightRange returns the current highlight range.
+func (m *Chat) getHighlightRange() (startItemIdx, startLine, startCol, endItemIdx, endLine, endCol int) {
+ if m.mouseDownItem < 0 {
+ return -1, -1, -1, -1, -1, -1
+ }
+
+ downItemIdx := m.mouseDownItem
+ dragItemIdx := m.mouseDragItem
+
+ // Determine selection direction
+ draggingDown := dragItemIdx > downItemIdx ||
+ (dragItemIdx == downItemIdx && m.mouseDragY > m.mouseDownY) ||
+ (dragItemIdx == downItemIdx && m.mouseDragY == m.mouseDownY && m.mouseDragX >= m.mouseDownX)
+
+ if draggingDown {
+ // Normal forward selection
+ startItemIdx = downItemIdx
+ startLine = m.mouseDownY
+ startCol = m.mouseDownX
+ endItemIdx = dragItemIdx
+ endLine = m.mouseDragY
+ endCol = m.mouseDragX
+ } else {
+ // Backward selection (dragging up)
+ startItemIdx = dragItemIdx
+ startLine = m.mouseDragY
+ startCol = m.mouseDragX
+ endItemIdx = downItemIdx
+ endLine = m.mouseDownY
+ endCol = m.mouseDownX
+ }
+
+ return startItemIdx, startLine, startCol, endItemIdx, endLine, endCol
}