list.go.bak

  1package lazylist
  2
  3import (
  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.
 11type 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.
 40type renderedItem struct {
 41	content string
 42	height  int
 43}
 44
 45// itemPosition holds the start and end line of an item in the list.
 46type itemPosition struct {
 47	startLine int
 48	endLine   int
 49}
 50
 51// Height returns the height of item based on its start and end lines.
 52func (ip itemPosition) Height() int {
 53	return ip.endLine - ip.startLine
 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.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.
 67func (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.
 80func (l *List) SetGap(gap int) {
 81	l.gap = gap
 82}
 83
 84// Width returns the width of the list viewport.
 85func (l *List) Width() int {
 86	return l.width
 87}
 88
 89// Height returns the height of the list viewport.
 90func (l *List) Height() int {
 91	return l.height
 92}
 93
 94// Len returns the number of items in the list.
 95func (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.
101func (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.
139func (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.
148func (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.
196func (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.
226func (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.
246func (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.
275func (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.
294func (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.
302func (l *List) Focus() {
303	l.focused = true
304}
305
306// Blur removes the focus state from the list.
307func (l *List) Blur() {
308	l.focused = false
309}
310
311// ScrollToTop scrolls the list to the top.
312func (l *List) ScrollToTop() {
313	l.offsetIdx = 0
314	l.offsetLine = 0
315}
316
317// ScrollToBottom scrolls the list to the bottom.
318func (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.
329func (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.
338func (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.
347func (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.
356func (l *List) SelectPrev() {
357	if l.selectedIdx > 0 {
358		l.selectedIdx--
359	}
360}
361
362// SelectNext selects the next item in the list.
363func (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.
370func (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.
377func (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.
384func (l *List) SelectFirstInView() {
385	startIdx, _ := l.findVisibleItems()
386	l.selectedIdx = startIdx
387}
388
389// SelectLastInView selects the last item currently in view.
390func (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.
396func (l *List) HandleMouseDown(x, y int) {
397}
398
399// HandleMouseUp handles mouse up events at the given line in the viewport.
400func (l *List) HandleMouseUp(x, y int) {
401}
402
403// HandleMouseDrag handles mouse drag events at the given line in the viewport.
404func (l *List) HandleMouseDrag(x, y int) {
405}
406
407// countLines counts the number of lines in a string.
408func countLines(s string) int {
409	if s == "" {
410		return 0
411	}
412	return strings.Count(s, "\n") + 1
413}