1package model
2
3import (
4 "github.com/charmbracelet/crush/internal/ui/common"
5 "github.com/charmbracelet/crush/internal/ui/list"
6 uv "github.com/charmbracelet/ultraviolet"
7 "github.com/charmbracelet/x/ansi"
8)
9
10// Chat represents the chat UI model that handles chat interactions and
11// messages.
12type Chat struct {
13 com *common.Common
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
24}
25
26// NewChat creates a new instance of [Chat] that handles chat interactions and
27// messages.
28func NewChat(com *common.Common) *Chat {
29 c := &Chat{com: com}
30 l := list.NewList()
31 l.SetGap(1)
32 l.RegisterRenderCallback(c.applyHighlightRange)
33 c.list = l
34 c.mouseDownItem = -1
35 c.mouseDragItem = -1
36 return c
37}
38
39// Height returns the height of the chat view port.
40func (m *Chat) Height() int {
41 return m.list.Height()
42}
43
44// Draw renders the chat UI component to the screen and the given area.
45func (m *Chat) Draw(scr uv.Screen, area uv.Rectangle) {
46 uv.NewStyledString(m.list.Render()).Draw(scr, area)
47}
48
49// SetSize sets the size of the chat view port.
50func (m *Chat) SetSize(width, height int) {
51 m.list.SetSize(width, height)
52}
53
54// Len returns the number of items in the chat list.
55func (m *Chat) Len() int {
56 return m.list.Len()
57}
58
59// PrependItems prepends new items to the chat list.
60func (m *Chat) PrependItems(items ...list.Item) {
61 m.list.PrependItems(items...)
62 m.list.ScrollToIndex(0)
63}
64
65// SetMessages sets the chat messages to the provided list of message items.
66func (m *Chat) SetMessages(msgs ...MessageItem) {
67 items := make([]list.Item, len(msgs))
68 for i, msg := range msgs {
69 items[i] = msg
70 }
71 m.list.SetItems(items...)
72 m.list.ScrollToBottom()
73}
74
75// AppendMessages appends a new message item to the chat list.
76func (m *Chat) AppendMessages(msgs ...MessageItem) {
77 items := make([]list.Item, len(msgs))
78 for i, msg := range msgs {
79 items[i] = msg
80 }
81 m.list.AppendItems(items...)
82}
83
84// AppendItems appends new items to the chat list.
85func (m *Chat) AppendItems(items ...list.Item) {
86 m.list.AppendItems(items...)
87 m.list.ScrollToIndex(m.list.Len() - 1)
88}
89
90// Focus sets the focus state of the chat component.
91func (m *Chat) Focus() {
92 m.list.Focus()
93}
94
95// Blur removes the focus state from the chat component.
96func (m *Chat) Blur() {
97 m.list.Blur()
98}
99
100// ScrollToTop scrolls the chat view to the top.
101func (m *Chat) ScrollToTop() {
102 m.list.ScrollToTop()
103}
104
105// ScrollToBottom scrolls the chat view to the bottom.
106func (m *Chat) ScrollToBottom() {
107 m.list.ScrollToBottom()
108}
109
110// ScrollBy scrolls the chat view by the given number of line deltas.
111func (m *Chat) ScrollBy(lines int) {
112 m.list.ScrollBy(lines)
113}
114
115// ScrollToSelected scrolls the chat view to the selected item.
116func (m *Chat) ScrollToSelected() {
117 m.list.ScrollToSelected()
118}
119
120// SelectedItemInView returns whether the selected item is currently in view.
121func (m *Chat) SelectedItemInView() bool {
122 return m.list.SelectedItemInView()
123}
124
125// SetSelected sets the selected message index in the chat list.
126func (m *Chat) SetSelected(index int) {
127 m.list.SetSelected(index)
128}
129
130// SelectPrev selects the previous message in the chat list.
131func (m *Chat) SelectPrev() {
132 m.list.SelectPrev()
133}
134
135// SelectNext selects the next message in the chat list.
136func (m *Chat) SelectNext() {
137 m.list.SelectNext()
138}
139
140// SelectFirst selects the first message in the chat list.
141func (m *Chat) SelectFirst() {
142 m.list.SelectFirst()
143}
144
145// SelectLast selects the last message in the chat list.
146func (m *Chat) SelectLast() {
147 m.list.SelectLast()
148}
149
150// SelectFirstInView selects the first message currently in view.
151func (m *Chat) SelectFirstInView() {
152 m.list.SelectFirstInView()
153}
154
155// SelectLastInView selects the last message currently in view.
156func (m *Chat) SelectLastInView() {
157 m.list.SelectLastInView()
158}
159
160// HandleMouseDown handles mouse down events for the chat component.
161func (m *Chat) HandleMouseDown(x, y int) bool {
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
187}
188
189// HandleMouseUp handles mouse up events for the chat component.
190func (m *Chat) HandleMouseUp(x, y int) bool {
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
200}
201
202// HandleMouseDrag handles mouse drag events for the chat component.
203func (m *Chat) HandleMouseDrag(x, y int) bool {
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.
225func (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.
232func (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.
273func (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
305}