@@ -1,413 +0,0 @@
8−// List represents a list of items that can be lazily rendered. A list is
9−// always rendered like a chat conversation where items are stacked vertically
10−// from top to bottom.
18− // Gap between items (0 or less means no gap)
21− // Focus and selection state
23− selectedIdx int // The current selected index -1 means no selection
25− // Item positioning. If a position exists in the map, it means the item has
26− // been rendered and measured.
27− itemPositions map[int]itemPosition
29− // Rendered content and cache
31− renderedItems map[int]renderedItem
32− offsetIdx int // Index of the first visible item in the viewport
33− offsetLine int // The offset line from the start of the offsetIdx item (can be negative)
36− dirtyItems map[int]struct{}
39−// renderedItem holds the rendered content and height of an item.
40−type renderedItem struct {
45−// itemPosition holds the start and end line of an item in the list.
46−type itemPosition struct {
51−// Height returns the height of item based on its start and end lines.
52−func (ip itemPosition) Height() int {
53− return ip.endLine - ip.startLine
56−// NewList creates a new lazy-loaded list.
57−func NewList(items ...Item) *List {
60− l.itemPositions = make(map[int]itemPosition)
61− l.renderedItems = make(map[int]renderedItem)
62− l.dirtyItems = make(map[int]struct{})
66−// SetSize sets the size of the list viewport.
67−func (l *List) SetSize(width, height int) {
68− if width != l.width {
69− // Mark all rendered items as dirty if width changes because their
70− // layout may change.
71− for idx := range l.itemPositions {
72− l.dirtyItems[idx] = struct{}{}
79−// SetGap sets the gap between items.
80−func (l *List) SetGap(gap int) {
84−// Width returns the width of the list viewport.
85−func (l *List) Width() int {
89−// Height returns the height of the list viewport.
90−func (l *List) Height() int {
94−// Len returns the number of items in the list.
95−func (l *List) Len() int {
99−// renderItem renders the item at the given index and updates its cache and
101−func (l *List) renderItem(idx int) {
102− if idx < 0 || idx >= len(l.items) {
106− item := l.items[idx]
107− rendered := item.Render(l.width)
108− height := countLines(rendered)
110− l.renderedItems[idx] = renderedItem{
115− // Calculate item position
120− prevPos, ok := l.itemPositions[idx-1]
122− l.renderItem(idx - 1)
123− prevPos = l.itemPositions[idx-1]
125− startLine = prevPos.endLine
130− endLine := startLine + height
132− l.itemPositions[idx] = itemPosition{
133− startLine: startLine,
138−// ScrollToIndex scrolls the list to the given item index.
139−func (l *List) ScrollToIndex(index int) {
140− if index < 0 || index >= len(l.items) {
147−// ScrollBy scrolls the list by the given number of lines.
148−func (l *List) ScrollBy(lines int) {
149− l.offsetLine += lines
150− if l.offsetIdx <= 0 && l.offsetLine < 0 {
156− // Adjust offset index and line if needed
157− for l.offsetLine < 0 && l.offsetIdx > 0 {
158− // Move up to previous item
160− prevPos, ok := l.itemPositions[l.offsetIdx]
162− l.renderItem(l.offsetIdx)
163− prevPos = l.itemPositions[l.offsetIdx]
165− l.offsetLine += prevPos.Height()
167− l.offsetLine += l.gap
172− currentPos, ok := l.itemPositions[l.offsetIdx]
174− l.renderItem(l.offsetIdx)
175− currentPos = l.itemPositions[l.offsetIdx]
177− if l.offsetLine >= currentPos.Height() {
178− // Move down to next item
179− l.offsetLine -= currentPos.Height()
181− l.offsetLine -= l.gap
184− if l.offsetIdx >= len(l.items) {
185− l.offsetIdx = len(l.items) - 1
186− l.offsetLine = currentPos.Height() - 1
195−// findVisibleItems finds the range of items that are visible in the viewport.
196−func (l *List) findVisibleItems() (startIdx, endIdx int) {
197− startIdx = l.offsetIdx
198− endIdx = startIdx + 1
200− // Render items until we fill the viewport
201− visibleHeight := -l.offsetLine
202− for endIdx < len(l.items) {
203− pos, ok := l.itemPositions[endIdx-1]
205− l.renderItem(endIdx - 1)
206− pos = l.itemPositions[endIdx-1]
208− visibleHeight += pos.Height()
209− if endIdx-1 < len(l.items)-1 && l.gap > 0 {
210− visibleHeight += l.gap
212− if visibleHeight >= l.height {
218− if endIdx > len(l.items)-1 {
219− endIdx = len(l.items) - 1
222− return startIdx, endIdx
225−// renderLines renders the items between startIdx and endIdx into lines.
226−func (l *List) renderLines(startIdx, endIdx int) []string {
228− for idx := startIdx; idx < endIdx+1; idx++ {
229− rendered, ok := l.renderedItems[idx]
232− rendered = l.renderedItems[idx]
234− itemLines := strings.Split(rendered.content, "\n")
235− lines = append(lines, itemLines...)
236− if l.gap > 0 && idx < endIdx {
237− for i := 0; i < l.gap; i++ {
238− lines = append(lines, "")
245−// Render renders the list and returns the visible lines.
246−func (l *List) Render() string {
247− viewStartIdx, viewEndIdx := l.findVisibleItems()
248− slog.Info("Render", "viewStartIdx", viewStartIdx, "viewEndIdx", viewEndIdx, "offsetIdx", l.offsetIdx, "offsetLine", l.offsetLine)
250− for idx := range l.dirtyItems {
251− if idx >= viewStartIdx && idx <= viewEndIdx {
253− delete(l.dirtyItems, idx)
257− lines := l.renderLines(viewStartIdx, viewEndIdx)
258− for len(lines) < l.height {
260− if viewStartIdx <= 0 {
264− lines = l.renderLines(viewStartIdx, viewEndIdx)
267− if len(lines) > l.height {
268− lines = lines[:l.height]
271− return strings.Join(lines, "\n")
274−// PrependItems prepends items to the list.
275−func (l *List) PrependItems(items ...Item) {
276− l.items = append(items, l.items...)
277− // Shift existing item positions
278− newItemPositions := make(map[int]itemPosition)
279− for idx, pos := range l.itemPositions {
280− newItemPositions[idx+len(items)] = pos
282− l.itemPositions = newItemPositions
284− // Mark all items as dirty
285− for idx := range l.items {
286− l.dirtyItems[idx] = struct{}{}
289− // Adjust offset index
290− l.offsetIdx += len(items)
293−// AppendItems appends items to the list.
294−func (l *List) AppendItems(items ...Item) {
295− l.items = append(l.items, items...)
296− for idx := len(l.items) - len(items); idx < len(l.items); idx++ {
297− l.dirtyItems[idx] = struct{}{}
301−// Focus sets the focus state of the list.
302−func (l *List) Focus() {
306−// Blur removes the focus state from the list.
307−func (l *List) Blur() {
311−// ScrollToTop scrolls the list to the top.
312−func (l *List) ScrollToTop() {
317−// ScrollToBottom scrolls the list to the bottom.
318−func (l *List) ScrollToBottom() {
319− l.offsetIdx = len(l.items) - 1
320− pos, ok := l.itemPositions[l.offsetIdx]
322− l.renderItem(l.offsetIdx)
323− pos = l.itemPositions[l.offsetIdx]
325− l.offsetLine = l.height - pos.Height()
328−// ScrollToSelected scrolls the list to the selected item.
329−func (l *List) ScrollToSelected() {
330− if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
333− l.offsetIdx = l.selectedIdx
337−// SelectedItemInView returns whether the selected item is currently in view.
338−func (l *List) SelectedItemInView() bool {
339− if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
342− startIdx, endIdx := l.findVisibleItems()
343− return l.selectedIdx >= startIdx && l.selectedIdx <= endIdx
346−// SetSelected sets the selected item index in the list.
347−func (l *List) SetSelected(index int) {
348− if index < 0 || index >= len(l.items) {
351− l.selectedIdx = index
355−// SelectPrev selects the previous item in the list.
356−func (l *List) SelectPrev() {
357− if l.selectedIdx > 0 {
362−// SelectNext selects the next item in the list.
363−func (l *List) SelectNext() {
364− if l.selectedIdx < len(l.items)-1 {
369−// SelectFirst selects the first item in the list.
370−func (l *List) SelectFirst() {
371− if len(l.items) > 0 {
376−// SelectLast selects the last item in the list.
377−func (l *List) SelectLast() {
378− if len(l.items) > 0 {
379− l.selectedIdx = len(l.items) - 1
383−// SelectFirstInView selects the first item currently in view.
384−func (l *List) SelectFirstInView() {
385− startIdx, _ := l.findVisibleItems()
386− l.selectedIdx = startIdx
389−// SelectLastInView selects the last item currently in view.
390−func (l *List) SelectLastInView() {
391− _, endIdx := l.findVisibleItems()
392− l.selectedIdx = endIdx
395−// HandleMouseDown handles mouse down events at the given line in the viewport.
396−func (l *List) HandleMouseDown(x, y int) {
399−// HandleMouseUp handles mouse up events at the given line in the viewport.
400−func (l *List) HandleMouseUp(x, y int) {
403−// HandleMouseDrag handles mouse drag events at the given line in the viewport.
404−func (l *List) HandleMouseDrag(x, y int) {
407−// countLines counts the number of lines in a string.
408−func countLines(s string) int {
412− return strings.Count(s, "\n") + 1