@@ -1,413 +0,0 @@
1−package lazylist
2−
3−import (
4− "log/slog"
5− "strings"
6−)
7−
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.
11−type List struct {
12− // Viewport size
13− width, height int
14−
15− // Items in the list
16− items []Item
17−
18− // Gap between items (0 or less means no gap)
19− gap int
20−
21− // Focus and selection state
22− focused bool
23− selectedIdx int // The current selected index -1 means no selection
24−
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
28−
29− // Rendered content and cache
30− lines []string
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)
34−
35− // Dirty tracking
36− dirtyItems map[int]struct{}
37−}
38−
39−// renderedItem holds the rendered content and height of an item.
40−type renderedItem struct {
41− content string
42− height int
43−}
44−
45−// itemPosition holds the start and end line of an item in the list.
46−type itemPosition struct {
47− startLine int
48− endLine int
49−}
50−
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
54−}
55−
56−// NewList creates a new lazy-loaded list.
57−func NewList(items ...Item) *List {
58− l := new(List)
59− l.items = items
60− l.itemPositions = make(map[int]itemPosition)
61− l.renderedItems = make(map[int]renderedItem)
62− l.dirtyItems = make(map[int]struct{})
63− return l
64−}
65−
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{}{}
73− }
74− }
75− l.width = width
76− l.height = height
77−}
78−
79−// SetGap sets the gap between items.
80−func (l *List) SetGap(gap int) {
81− l.gap = gap
82−}
83−
84−// Width returns the width of the list viewport.
85−func (l *List) Width() int {
86− return l.width
87−}
88−
89−// Height returns the height of the list viewport.
90−func (l *List) Height() int {
91− return l.height
92−}
93−
94−// Len returns the number of items in the list.
95−func (l *List) Len() int {
96− return len(l.items)
97−}
98−
99−// renderItem renders the item at the given index and updates its cache and
100−// position.
101−func (l *List) renderItem(idx int) {
102− if idx < 0 || idx >= len(l.items) {
103− return
104− }
105−
106− item := l.items[idx]
107− rendered := item.Render(l.width)
108− height := countLines(rendered)
109−
110− l.renderedItems[idx] = renderedItem{
111− content: rendered,
112− height: height,
113− }
114−
115− // Calculate item position
116− var startLine int
117− if idx == 0 {
118− startLine = 0
119− } else {
120− prevPos, ok := l.itemPositions[idx-1]
121− if !ok {
122− l.renderItem(idx - 1)
123− prevPos = l.itemPositions[idx-1]
124− }
125− startLine = prevPos.endLine
126− if l.gap > 0 {
127− startLine += l.gap
128− }
129− }
130− endLine := startLine + height
131−
132− l.itemPositions[idx] = itemPosition{
133− startLine: startLine,
134− endLine: endLine,
135− }
136−}
137−
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) {
141− return
142− }
143− l.offsetIdx = index
144− l.offsetLine = 0
145−}
146−
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 {
151− l.offsetIdx = 0
152− l.offsetLine = 0
153− return
154− }
155−
156− // Adjust offset index and line if needed
157− for l.offsetLine < 0 && l.offsetIdx > 0 {
158− // Move up to previous item
159− l.offsetIdx--
160− prevPos, ok := l.itemPositions[l.offsetIdx]
161− if !ok {
162− l.renderItem(l.offsetIdx)
163− prevPos = l.itemPositions[l.offsetIdx]
164− }
165− l.offsetLine += prevPos.Height()
166− if l.gap > 0 {
167− l.offsetLine += l.gap
168− }
169− }
170−
171− for {
172− currentPos, ok := l.itemPositions[l.offsetIdx]
173− if !ok {
174− l.renderItem(l.offsetIdx)
175− currentPos = l.itemPositions[l.offsetIdx]
176− }
177− if l.offsetLine >= currentPos.Height() {
178− // Move down to next item
179− l.offsetLine -= currentPos.Height()
180− if l.gap > 0 {
181− l.offsetLine -= l.gap
182− }
183− l.offsetIdx++
184− if l.offsetIdx >= len(l.items) {
185− l.offsetIdx = len(l.items) - 1
186− l.offsetLine = currentPos.Height() - 1
187− break
188− }
189− } else {
190− break
191− }
192− }
193−}
194−
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
199−
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]
204− if !ok {
205− l.renderItem(endIdx - 1)
206− pos = l.itemPositions[endIdx-1]
207− }
208− visibleHeight += pos.Height()
209− if endIdx-1 < len(l.items)-1 && l.gap > 0 {
210− visibleHeight += l.gap
211− }
212− if visibleHeight >= l.height {
213− break
214− }
215− endIdx++
216− }
217−
218− if endIdx > len(l.items)-1 {
219− endIdx = len(l.items) - 1
220− }
221−
222− return startIdx, endIdx
223−}
224−
225−// renderLines renders the items between startIdx and endIdx into lines.
226−func (l *List) renderLines(startIdx, endIdx int) []string {
227− var lines []string
228− for idx := startIdx; idx < endIdx+1; idx++ {
229− rendered, ok := l.renderedItems[idx]
230− if !ok {
231− l.renderItem(idx)
232− rendered = l.renderedItems[idx]
233− }
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, "")
239− }
240− }
241− }
242− return lines
243−}
244−
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)
249−
250− for idx := range l.dirtyItems {
251− if idx >= viewStartIdx && idx <= viewEndIdx {
252− l.renderItem(idx)
253− delete(l.dirtyItems, idx)
254− }
255− }
256−
257− lines := l.renderLines(viewStartIdx, viewEndIdx)
258− for len(lines) < l.height {
259− viewStartIdx--
260− if viewStartIdx <= 0 {
261− break
262− }
263−
264− lines = l.renderLines(viewStartIdx, viewEndIdx)
265− }
266−
267− if len(lines) > l.height {
268− lines = lines[:l.height]
269− }
270−
271− return strings.Join(lines, "\n")
272−}
273−
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
281− }
282− l.itemPositions = newItemPositions
283−
284− // Mark all items as dirty
285− for idx := range l.items {
286− l.dirtyItems[idx] = struct{}{}
287− }
288−
289− // Adjust offset index
290− l.offsetIdx += len(items)
291−}
292−
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{}{}
298− }
299−}
300−
301−// Focus sets the focus state of the list.
302−func (l *List) Focus() {
303− l.focused = true
304−}
305−
306−// Blur removes the focus state from the list.
307−func (l *List) Blur() {
308− l.focused = false
309−}
310−
311−// ScrollToTop scrolls the list to the top.
312−func (l *List) ScrollToTop() {
313− l.offsetIdx = 0
314− l.offsetLine = 0
315−}
316−
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]
321− if !ok {
322− l.renderItem(l.offsetIdx)
323− pos = l.itemPositions[l.offsetIdx]
324− }
325− l.offsetLine = l.height - pos.Height()
326−}
327−
328−// ScrollToSelected scrolls the list to the selected item.
329−func (l *List) ScrollToSelected() {
330− if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
331− return
332− }
333− l.offsetIdx = l.selectedIdx
334− l.offsetLine = 0
335−}
336−
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) {
340− return false
341− }
342− startIdx, endIdx := l.findVisibleItems()
343− return l.selectedIdx >= startIdx && l.selectedIdx <= endIdx
344−}
345−
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) {
349− l.selectedIdx = -1
350− } else {
351− l.selectedIdx = index
352− }
353−}
354−
355−// SelectPrev selects the previous item in the list.
356−func (l *List) SelectPrev() {
357− if l.selectedIdx > 0 {
358− l.selectedIdx--
359− }
360−}
361−
362−// SelectNext selects the next item in the list.
363−func (l *List) SelectNext() {
364− if l.selectedIdx < len(l.items)-1 {
365− l.selectedIdx++
366− }
367−}
368−
369−// SelectFirst selects the first item in the list.
370−func (l *List) SelectFirst() {
371− if len(l.items) > 0 {
372− l.selectedIdx = 0
373− }
374−}
375−
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
380− }
381−}
382−
383−// SelectFirstInView selects the first item currently in view.
384−func (l *List) SelectFirstInView() {
385− startIdx, _ := l.findVisibleItems()
386− l.selectedIdx = startIdx
387−}
388−
389−// SelectLastInView selects the last item currently in view.
390−func (l *List) SelectLastInView() {
391− _, endIdx := l.findVisibleItems()
392− l.selectedIdx = endIdx
393−}
394−
395−// HandleMouseDown handles mouse down events at the given line in the viewport.
396−func (l *List) HandleMouseDown(x, y int) {
397−}
398−
399−// HandleMouseUp handles mouse up events at the given line in the viewport.
400−func (l *List) HandleMouseUp(x, y int) {
401−}
402−
403−// HandleMouseDrag handles mouse drag events at the given line in the viewport.
404−func (l *List) HandleMouseDrag(x, y int) {
405−}
406−
407−// countLines counts the number of lines in a string.
408−func countLines(s string) int {
409− if s == "" {
410− return 0
411− }
412− return strings.Count(s, "\n") + 1
413−}