list.go

  1package list
  2
  3import (
  4	"image"
  5	"strings"
  6
  7	tea "charm.land/bubbletea/v2"
  8	"charm.land/lipgloss/v2"
  9	"github.com/charmbracelet/x/ansi"
 10)
 11
 12// List represents a list of items that can be lazily rendered. A list is
 13// always rendered like a chat conversation where items are stacked vertically
 14// from top to bottom.
 15type List struct {
 16	// Viewport size
 17	width, height int
 18
 19	// Items in the list
 20	items []Item
 21
 22	// Gap between items (0 or less means no gap)
 23	gap int
 24
 25	// Focus and selection state
 26	focused     bool
 27	selectedIdx int // The current selected index -1 means no selection
 28
 29	// Mouse state
 30	mouseDown       bool
 31	mouseDownItem   int          // Item index where mouse was pressed
 32	mouseDownX      int          // X position in item content (character offset)
 33	mouseDownY      int          // Y position in item (line offset)
 34	mouseDragItem   int          // Current item index being dragged over
 35	mouseDragX      int          // Current X in item content
 36	mouseDragY      int          // Current Y in item
 37	lastHighlighted map[int]bool // Track which items were highlighted in last update
 38
 39	// Rendered content and cache
 40	renderedItems map[int]renderedItem
 41
 42	// offsetIdx is the index of the first visible item in the viewport.
 43	offsetIdx int
 44	// offsetLine is the number of lines of the item at offsetIdx that are
 45	// scrolled out of view (above the viewport).
 46	// It must always be >= 0.
 47	offsetLine int
 48}
 49
 50// renderedItem holds the rendered content and height of an item.
 51type renderedItem struct {
 52	content string
 53	height  int
 54}
 55
 56// NewList creates a new lazy-loaded list.
 57func NewList(items ...Item) *List {
 58	l := new(List)
 59	l.items = items
 60	l.renderedItems = make(map[int]renderedItem)
 61	l.selectedIdx = -1
 62	l.mouseDownItem = -1
 63	l.mouseDragItem = -1
 64	l.lastHighlighted = make(map[int]bool)
 65	return l
 66}
 67
 68// SetSize sets the size of the list viewport.
 69func (l *List) SetSize(width, height int) {
 70	if width != l.width {
 71		l.renderedItems = make(map[int]renderedItem)
 72	}
 73	l.width = width
 74	l.height = height
 75	// l.normalizeOffsets()
 76}
 77
 78// SetGap sets the gap between items.
 79func (l *List) SetGap(gap int) {
 80	l.gap = gap
 81}
 82
 83// Width returns the width of the list viewport.
 84func (l *List) Width() int {
 85	return l.width
 86}
 87
 88// Height returns the height of the list viewport.
 89func (l *List) Height() int {
 90	return l.height
 91}
 92
 93// Len returns the number of items in the list.
 94func (l *List) Len() int {
 95	return len(l.items)
 96}
 97
 98// getItem renders (if needed) and returns the item at the given index.
 99func (l *List) getItem(idx int) renderedItem {
100	return l.renderItem(idx, false)
101}
102
103// applyHighlight applies highlighting to the given rendered item.
104func (l *List) applyHighlight(idx int, ri *renderedItem) {
105	// Apply highlight if item supports it
106	if highlightable, ok := l.items[idx].(HighlightStylable); ok {
107		startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := l.getHighlightRange()
108		if idx >= startItemIdx && idx <= endItemIdx {
109			var sLine, sCol, eLine, eCol int
110			if idx == startItemIdx && idx == endItemIdx {
111				// Single item selection
112				sLine = startLine
113				sCol = startCol
114				eLine = endLine
115				eCol = endCol
116			} else if idx == startItemIdx {
117				// First item - from start position to end of item
118				sLine = startLine
119				sCol = startCol
120				eLine = ri.height - 1
121				eCol = 9999 // 9999 = end of line
122			} else if idx == endItemIdx {
123				// Last item - from start of item to end position
124				sLine = 0
125				sCol = 0
126				eLine = endLine
127				eCol = endCol
128			} else {
129				// Middle item - fully highlighted
130				sLine = 0
131				sCol = 0
132				eLine = ri.height - 1
133				eCol = 9999
134			}
135
136			// Apply offset for styling frame
137			contentArea := image.Rect(0, 0, l.width, ri.height)
138
139			hiStyle := highlightable.HighlightStyle()
140			rendered := Highlight(ri.content, contentArea, sLine, sCol, eLine, eCol, ToHighlighter(hiStyle))
141			ri.content = rendered
142		}
143	}
144}
145
146// renderItem renders (if needed) and returns the item at the given index. If
147// process is true, it applies focus and highlight styling.
148func (l *List) renderItem(idx int, process bool) renderedItem {
149	if idx < 0 || idx >= len(l.items) {
150		return renderedItem{}
151	}
152
153	var style lipgloss.Style
154	focusable, isFocusable := l.items[idx].(FocusStylable)
155	if isFocusable {
156		style = focusable.BlurStyle()
157		if l.focused && idx == l.selectedIdx {
158			style = focusable.FocusStyle()
159		}
160	}
161
162	// Notify item of focus state if it cares.
163	isFocused := l.focused && idx == l.selectedIdx
164	if focusAware, ok := l.items[idx].(FocusAware); ok {
165		focusAware.SetFocused(isFocused)
166	}
167
168	ri, ok := l.renderedItems[idx]
169	if !ok {
170		item := l.items[idx]
171		rendered := item.Render(l.width - style.GetHorizontalFrameSize())
172		rendered = strings.TrimRight(rendered, "\n")
173		height := countLines(rendered)
174
175		ri = renderedItem{
176			content: rendered,
177			height:  height,
178		}
179
180		l.renderedItems[idx] = ri
181	}
182
183	if !process {
184		// Simply return cached rendered item with frame size applied
185		if vfs := style.GetVerticalFrameSize(); vfs > 0 {
186			ri.height += vfs
187		}
188		return ri
189	}
190
191	// We apply highlighting before focus styling so that focus styling
192	// overrides highlight styles.
193	if l.mouseDownItem >= 0 {
194		l.applyHighlight(idx, &ri)
195	}
196
197	if isFocusable {
198		// Apply focus/blur styling if needed
199		rendered := style.Render(ri.content)
200		height := countLines(rendered)
201		ri.content = rendered
202		ri.height = height
203	}
204
205	return ri
206}
207
208// invalidateItem invalidates the cached rendered content of the item at the
209// given index.
210func (l *List) invalidateItem(idx int) {
211	delete(l.renderedItems, idx)
212}
213
214// ScrollToIndex scrolls the list to the given item index.
215func (l *List) ScrollToIndex(index int) {
216	if index < 0 {
217		index = 0
218	}
219	if index >= len(l.items) {
220		index = len(l.items) - 1
221	}
222	l.offsetIdx = index
223	l.offsetLine = 0
224}
225
226// ScrollBy scrolls the list by the given number of lines.
227func (l *List) ScrollBy(lines int) {
228	if len(l.items) == 0 || lines == 0 {
229		return
230	}
231
232	if lines > 0 {
233		// Scroll down
234		// Calculate from the bottom how many lines needed to anchor the last
235		// item to the bottom
236		var totalLines int
237		var lastItemIdx int // the last item that can be partially visible
238		for i := len(l.items) - 1; i >= 0; i-- {
239			item := l.getItem(i)
240			totalLines += item.height
241			if l.gap > 0 && i < len(l.items)-1 {
242				totalLines += l.gap
243			}
244			if totalLines > l.height-1 {
245				lastItemIdx = i
246				break
247			}
248		}
249
250		// Now scroll down by lines
251		var item renderedItem
252		l.offsetLine += lines
253		for {
254			item = l.getItem(l.offsetIdx)
255			totalHeight := item.height
256			if l.gap > 0 {
257				totalHeight += l.gap
258			}
259
260			if l.offsetIdx >= lastItemIdx || l.offsetLine < totalHeight {
261				// Valid offset
262				break
263			}
264
265			// Move to next item
266			l.offsetLine -= totalHeight
267			l.offsetIdx++
268		}
269
270		if l.offsetLine >= item.height {
271			l.offsetLine = item.height
272		}
273	} else if lines < 0 {
274		// Scroll up
275		l.offsetLine += lines // lines is negative
276		for l.offsetLine < 0 {
277			if l.offsetIdx <= 0 {
278				// Reached top
279				l.ScrollToTop()
280				break
281			}
282
283			// Move to previous item
284			l.offsetIdx--
285			prevItem := l.getItem(l.offsetIdx)
286			totalHeight := prevItem.height
287			if l.gap > 0 {
288				totalHeight += l.gap
289			}
290			l.offsetLine += totalHeight
291		}
292	}
293}
294
295// findVisibleItems finds the range of items that are visible in the viewport.
296// This is used for checking if selected item is in view.
297func (l *List) findVisibleItems() (startIdx, endIdx int) {
298	if len(l.items) == 0 {
299		return 0, 0
300	}
301
302	startIdx = l.offsetIdx
303	currentIdx := startIdx
304	visibleHeight := -l.offsetLine
305
306	for currentIdx < len(l.items) {
307		item := l.getItem(currentIdx)
308		visibleHeight += item.height
309		if l.gap > 0 {
310			visibleHeight += l.gap
311		}
312
313		if visibleHeight >= l.height {
314			break
315		}
316		currentIdx++
317	}
318
319	endIdx = currentIdx
320	if endIdx >= len(l.items) {
321		endIdx = len(l.items) - 1
322	}
323
324	return startIdx, endIdx
325}
326
327// Render renders the list and returns the visible lines.
328func (l *List) Render() string {
329	if len(l.items) == 0 {
330		return ""
331	}
332
333	var lines []string
334	currentIdx := l.offsetIdx
335	currentOffset := l.offsetLine
336
337	linesNeeded := l.height
338
339	for linesNeeded > 0 && currentIdx < len(l.items) {
340		item := l.renderItem(currentIdx, true)
341		itemLines := strings.Split(item.content, "\n")
342		itemHeight := len(itemLines)
343
344		if currentOffset >= 0 && currentOffset < itemHeight {
345			// Add visible content lines
346			lines = append(lines, itemLines[currentOffset:]...)
347
348			// Add gap if this is not the absolute last visual element (conceptually gaps are between items)
349			// But in the loop we can just add it and trim later
350			if l.gap > 0 {
351				for i := 0; i < l.gap; i++ {
352					lines = append(lines, "")
353				}
354			}
355		} else {
356			// offsetLine starts in the gap
357			gapOffset := currentOffset - itemHeight
358			gapRemaining := l.gap - gapOffset
359			if gapRemaining > 0 {
360				for range gapRemaining {
361					lines = append(lines, "")
362				}
363			}
364		}
365
366		linesNeeded = l.height - len(lines)
367		currentIdx++
368		currentOffset = 0 // Reset offset for subsequent items
369	}
370
371	if len(lines) > l.height {
372		lines = lines[:l.height]
373	}
374
375	return strings.Join(lines, "\n")
376}
377
378// SetItems sets the items in the list.
379func (l *List) SetItems(items ...Item) {
380	l.setItems(true, items...)
381}
382
383// setItems sets the items in the list. If evict is true, it clears the
384// rendered item cache.
385func (l *List) setItems(evict bool, items ...Item) {
386	l.items = items
387	if evict {
388		l.renderedItems = make(map[int]renderedItem)
389	}
390	l.selectedIdx = min(l.selectedIdx, len(l.items)-1)
391	l.offsetIdx = min(l.offsetIdx, len(l.items)-1)
392	l.offsetLine = 0
393}
394
395// AppendItems appends items to the list.
396func (l *List) AppendItems(items ...Item) {
397	l.items = append(l.items, items...)
398}
399
400// GetItemAt returns the item at the given index. Returns nil if the index is
401// out of bounds.
402func (l *List) GetItemAt(idx int) Item {
403	if idx < 0 || idx >= len(l.items) {
404		return nil
405	}
406	return l.items[idx]
407}
408
409// InvalidateItemAt invalidates the render cache for the item at the given
410// index without replacing the item. Use this when you've mutated an item's
411// internal state and need to force a re-render.
412func (l *List) InvalidateItemAt(idx int) {
413	if idx >= 0 && idx < len(l.items) {
414		l.invalidateItem(idx)
415	}
416}
417
418// DeleteItemAt removes the item at the given index. Returns true if the index
419// was valid and the item was removed.
420func (l *List) DeleteItemAt(idx int) bool {
421	if idx < 0 || idx >= len(l.items) {
422		return false
423	}
424
425	// Remove from items slice.
426	l.items = append(l.items[:idx], l.items[idx+1:]...)
427
428	// Clear and rebuild cache with shifted indices.
429	newCache := make(map[int]renderedItem, len(l.renderedItems))
430	for i, val := range l.renderedItems {
431		if i < idx {
432			newCache[i] = val
433		} else if i > idx {
434			newCache[i-1] = val
435		}
436	}
437	l.renderedItems = newCache
438
439	// Adjust selection if needed.
440	if l.selectedIdx >= len(l.items) && len(l.items) > 0 {
441		l.selectedIdx = len(l.items) - 1
442	}
443
444	return true
445}
446
447// Focus sets the focus state of the list.
448func (l *List) Focus() {
449	l.focused = true
450	// Invalidate the selected item if it's focus-aware.
451	if l.selectedIdx >= 0 && l.selectedIdx < len(l.items) {
452		if _, ok := l.items[l.selectedIdx].(FocusAware); ok {
453			l.invalidateItem(l.selectedIdx)
454		}
455	}
456}
457
458// Blur removes the focus state from the list.
459func (l *List) Blur() {
460	l.focused = false
461	// Invalidate the selected item if it's focus-aware.
462	if l.selectedIdx >= 0 && l.selectedIdx < len(l.items) {
463		if _, ok := l.items[l.selectedIdx].(FocusAware); ok {
464			l.invalidateItem(l.selectedIdx)
465		}
466	}
467}
468
469// ScrollToTop scrolls the list to the top.
470func (l *List) ScrollToTop() {
471	l.offsetIdx = 0
472	l.offsetLine = 0
473}
474
475// ScrollToBottom scrolls the list to the bottom.
476func (l *List) ScrollToBottom() {
477	if len(l.items) == 0 {
478		return
479	}
480
481	// Scroll to the last item
482	var totalHeight int
483	for i := len(l.items) - 1; i >= 0; i-- {
484		item := l.getItem(i)
485		totalHeight += item.height
486		if l.gap > 0 && i < len(l.items)-1 {
487			totalHeight += l.gap
488		}
489		if totalHeight >= l.height {
490			l.offsetIdx = i
491			l.offsetLine = totalHeight - l.height
492			break
493		}
494	}
495	if totalHeight < l.height {
496		// All items fit in the viewport
497		l.ScrollToTop()
498	}
499}
500
501// ScrollToSelected scrolls the list to the selected item.
502func (l *List) ScrollToSelected() {
503	if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
504		return
505	}
506
507	startIdx, endIdx := l.findVisibleItems()
508	if l.selectedIdx < startIdx {
509		// Selected item is above the visible range
510		l.offsetIdx = l.selectedIdx
511		l.offsetLine = 0
512	} else if l.selectedIdx > endIdx {
513		// Selected item is below the visible range
514		// Scroll so that the selected item is at the bottom
515		var totalHeight int
516		for i := l.selectedIdx; i >= 0; i-- {
517			item := l.getItem(i)
518			totalHeight += item.height
519			if l.gap > 0 && i < l.selectedIdx {
520				totalHeight += l.gap
521			}
522			if totalHeight >= l.height {
523				l.offsetIdx = i
524				l.offsetLine = totalHeight - l.height
525				break
526			}
527		}
528		if totalHeight < l.height {
529			// All items fit in the viewport
530			l.ScrollToTop()
531		}
532	}
533}
534
535// SelectedItemInView returns whether the selected item is currently in view.
536func (l *List) SelectedItemInView() bool {
537	if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
538		return false
539	}
540	startIdx, endIdx := l.findVisibleItems()
541	return l.selectedIdx >= startIdx && l.selectedIdx <= endIdx
542}
543
544// SetSelected sets the selected item index in the list.
545func (l *List) SetSelected(index int) {
546	oldIdx := l.selectedIdx
547	if index < 0 || index >= len(l.items) {
548		l.selectedIdx = -1
549	} else {
550		l.selectedIdx = index
551	}
552	l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
553}
554
555// invalidateFocusAwareItems invalidates the cache for items that implement
556// FocusAware when their focus state changes.
557func (l *List) invalidateFocusAwareItems(oldIdx, newIdx int) {
558	if oldIdx == newIdx {
559		return
560	}
561	if oldIdx >= 0 && oldIdx < len(l.items) {
562		if _, ok := l.items[oldIdx].(FocusAware); ok {
563			l.invalidateItem(oldIdx)
564		}
565	}
566	if newIdx >= 0 && newIdx < len(l.items) {
567		if _, ok := l.items[newIdx].(FocusAware); ok {
568			l.invalidateItem(newIdx)
569		}
570	}
571}
572
573// SelectPrev selects the previous item in the list.
574func (l *List) SelectPrev() {
575	if l.selectedIdx > 0 {
576		oldIdx := l.selectedIdx
577		l.selectedIdx--
578		l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
579	}
580}
581
582// SelectNext selects the next item in the list.
583func (l *List) SelectNext() {
584	if l.selectedIdx < len(l.items)-1 {
585		oldIdx := l.selectedIdx
586		l.selectedIdx++
587		l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
588	}
589}
590
591// SelectFirst selects the first item in the list.
592func (l *List) SelectFirst() {
593	if len(l.items) > 0 {
594		oldIdx := l.selectedIdx
595		l.selectedIdx = 0
596		l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
597	}
598}
599
600// SelectLast selects the last item in the list.
601func (l *List) SelectLast() {
602	if len(l.items) > 0 {
603		oldIdx := l.selectedIdx
604		l.selectedIdx = len(l.items) - 1
605		l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
606	}
607}
608
609// SelectedItem returns the currently selected item. It may be nil if no item
610// is selected.
611func (l *List) SelectedItem() Item {
612	if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
613		return nil
614	}
615	return l.items[l.selectedIdx]
616}
617
618// SelectFirstInView selects the first item currently in view.
619func (l *List) SelectFirstInView() {
620	startIdx, _ := l.findVisibleItems()
621	oldIdx := l.selectedIdx
622	l.selectedIdx = startIdx
623	l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
624}
625
626// SelectLastInView selects the last item currently in view.
627func (l *List) SelectLastInView() {
628	_, endIdx := l.findVisibleItems()
629	oldIdx := l.selectedIdx
630	l.selectedIdx = endIdx
631	l.invalidateFocusAwareItems(oldIdx, l.selectedIdx)
632}
633
634// HandleMouseDown handles mouse down events at the given line in the viewport.
635// x and y are viewport-relative coordinates (0,0 = top-left of visible area).
636// Returns true if the event was handled.
637func (l *List) HandleMouseDown(x, y int) bool {
638	if len(l.items) == 0 {
639		return false
640	}
641
642	// Find which item was clicked
643	itemIdx, itemY := l.findItemAtY(x, y)
644	if itemIdx < 0 {
645		return false
646	}
647
648	l.mouseDown = true
649	l.mouseDownItem = itemIdx
650	l.mouseDownX = x
651	l.mouseDownY = itemY
652	l.mouseDragItem = itemIdx
653	l.mouseDragX = x
654	l.mouseDragY = itemY
655
656	// Select the clicked item
657	l.SetSelected(itemIdx)
658
659	if clickable, ok := l.items[itemIdx].(MouseClickable); ok {
660		clickable.HandleMouseClick(ansi.MouseButton1, x, itemY)
661		l.items[itemIdx] = clickable.(Item)
662		l.invalidateItem(itemIdx)
663	}
664
665	return true
666}
667
668// HandleMouseUp handles mouse up events at the given line in the viewport.
669// Returns true if the event was handled.
670func (l *List) HandleMouseUp(x, y int) bool {
671	if !l.mouseDown {
672		return false
673	}
674
675	l.mouseDown = false
676
677	return true
678}
679
680// HandleMouseDrag handles mouse drag events at the given line in the viewport.
681// x and y are viewport-relative coordinates.
682// Returns true if the event was handled.
683func (l *List) HandleMouseDrag(x, y int) bool {
684	if !l.mouseDown {
685		return false
686	}
687
688	if len(l.items) == 0 {
689		return false
690	}
691
692	// Find which item we're dragging over
693	itemIdx, itemY := l.findItemAtY(x, y)
694	if itemIdx < 0 {
695		return false
696	}
697
698	l.mouseDragItem = itemIdx
699	l.mouseDragX = x
700	l.mouseDragY = itemY
701
702	return true
703}
704
705// ClearHighlight clears any active text highlighting.
706func (l *List) ClearHighlight() {
707	l.mouseDownItem = -1
708	l.mouseDragItem = -1
709	l.lastHighlighted = make(map[int]bool)
710}
711
712// HandleKeyPress handles key press events for the currently selected item.
713// Returns true if the event was handled.
714func (l *List) HandleKeyPress(msg tea.KeyPressMsg) bool {
715	if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
716		return false
717	}
718
719	if keyable, ok := l.items[l.selectedIdx].(KeyPressable); ok {
720		handled := keyable.HandleKeyPress(msg)
721		if handled {
722			l.invalidateItem(l.selectedIdx)
723		}
724		return handled
725	}
726
727	return false
728}
729
730// UpdateItems propagates a message to all items that implement Updatable.
731// This is typically used for animation messages like anim.StepMsg.
732// Returns commands from updated items.
733func (l *List) UpdateItems(msg tea.Msg) tea.Cmd {
734	var cmds []tea.Cmd
735	for i, item := range l.items {
736		if updatable, ok := item.(Updatable); ok {
737			updated, cmd := updatable.Update(msg)
738			if cmd != nil {
739				cmds = append(cmds, cmd)
740				// Invalidate cache when animation updates, even if pointer is same.
741				l.invalidateItem(i)
742			}
743			if updated != item {
744				l.items[i] = updated
745				l.invalidateItem(i)
746			}
747		}
748	}
749	if len(cmds) == 0 {
750		return nil
751	}
752	return tea.Batch(cmds...)
753}
754
755// findItemAtY finds the item at the given viewport y coordinate.
756// Returns the item index and the y offset within that item. It returns -1, -1
757// if no item is found.
758func (l *List) findItemAtY(_, y int) (itemIdx int, itemY int) {
759	if y < 0 || y >= l.height {
760		return -1, -1
761	}
762
763	// Walk through visible items to find which one contains this y
764	currentIdx := l.offsetIdx
765	currentLine := -l.offsetLine // Negative because offsetLine is how many lines are hidden
766
767	for currentIdx < len(l.items) && currentLine < l.height {
768		item := l.getItem(currentIdx)
769		itemEndLine := currentLine + item.height
770
771		// Check if y is within this item's visible range
772		if y >= currentLine && y < itemEndLine {
773			// Found the item, calculate itemY (offset within the item)
774			itemY = y - currentLine
775			return currentIdx, itemY
776		}
777
778		// Move to next item
779		currentLine = itemEndLine
780		if l.gap > 0 {
781			currentLine += l.gap
782		}
783		currentIdx++
784	}
785
786	return -1, -1
787}
788
789// getHighlightRange returns the current highlight range.
790func (l *List) getHighlightRange() (startItemIdx, startLine, startCol, endItemIdx, endLine, endCol int) {
791	if l.mouseDownItem < 0 {
792		return -1, -1, -1, -1, -1, -1
793	}
794
795	downItemIdx := l.mouseDownItem
796	dragItemIdx := l.mouseDragItem
797
798	// Determine selection direction
799	draggingDown := dragItemIdx > downItemIdx ||
800		(dragItemIdx == downItemIdx && l.mouseDragY > l.mouseDownY) ||
801		(dragItemIdx == downItemIdx && l.mouseDragY == l.mouseDownY && l.mouseDragX >= l.mouseDownX)
802
803	if draggingDown {
804		// Normal forward selection
805		startItemIdx = downItemIdx
806		startLine = l.mouseDownY
807		startCol = l.mouseDownX
808		endItemIdx = dragItemIdx
809		endLine = l.mouseDragY
810		endCol = l.mouseDragX
811	} else {
812		// Backward selection (dragging up)
813		startItemIdx = dragItemIdx
814		startLine = l.mouseDragY
815		startCol = l.mouseDragX
816		endItemIdx = downItemIdx
817		endLine = l.mouseDownY
818		endCol = l.mouseDownX
819	}
820
821	return startItemIdx, startLine, startCol, endItemIdx, endLine, endCol
822}
823
824// countLines counts the number of lines in a string.
825func countLines(s string) int {
826	if s == "" {
827		return 0
828	}
829	return strings.Count(s, "\n") + 1
830}