list.go

  1// Package list implements a UI component for displaying a list of items.
  2package list
  3
  4import (
  5	"io"
  6	"slices"
  7	"strings"
  8
  9	uv "github.com/charmbracelet/ultraviolet"
 10)
 11
 12// ItemRenderer is an interface for rendering items in the list.
 13type ItemRenderer interface {
 14	// Render renders the item as a string.
 15	Render(w io.Writer, l *List, index int, item Item)
 16}
 17
 18// DefaultItemRenderer is the default implementation of [ItemRenderer].
 19type DefaultItemRenderer struct{}
 20
 21// Render renders the item as a string using its content.
 22func (r *DefaultItemRenderer) Render(w io.Writer, list *List, index int, item Item) {
 23	_, _ = io.WriteString(w, item.Content())
 24}
 25
 26// List represents a component that renders a list of [Item]s via
 27// [ItemRenderer]s. It supports focus management and styling.
 28type List struct {
 29	// items is the master list of items.
 30	items []Item
 31
 32	// rend is the item renderer for the list.
 33	rend ItemRenderer
 34
 35	// width is the width of the list.
 36	width int
 37
 38	// yOffset is the vertical scroll offset. -1 means scrolled to bottom.
 39	yOffset int
 40}
 41
 42// New creates a new [List] component with the given items.
 43func New(rend ItemRenderer, items ...Item) *List {
 44	if rend == nil {
 45		rend = &DefaultItemRenderer{}
 46	}
 47	l := &List{
 48		rend:    rend,
 49		yOffset: -1,
 50	}
 51	l.Append(items...)
 52	return l
 53}
 54
 55// SetWidth sets the width of the list.
 56func (l *List) SetWidth(width int) {
 57	l.width = width
 58}
 59
 60// Width returns the width of the list.
 61func (l *List) Width() int {
 62	return l.width
 63}
 64
 65// Len returns the number of items in the list.
 66func (l *List) Len() int {
 67	return len(l.items)
 68}
 69
 70// Items returns a new slice of all items in the list.
 71func (l *List) Items() []Item {
 72	return l.items
 73}
 74
 75// Update updates an item at the given index.
 76func (l *List) Update(index int, item Item) bool {
 77	if index < 0 || index >= len(l.items) {
 78		return false
 79	}
 80	l.items[index] = item
 81	return true
 82}
 83
 84// At returns the item at the given index.
 85func (l *List) At(index int) (Item, bool) {
 86	if index < 0 || index >= len(l.items) {
 87		return nil, false
 88	}
 89	return l.items[index], true
 90}
 91
 92// Delete removes the item at the given index.
 93func (l *List) Delete(index int) bool {
 94	if index < 0 || index >= len(l.items) {
 95		return false
 96	}
 97	l.items = slices.Delete(l.items, index, index+1)
 98	return true
 99}
100
101// Append adds new items to the end of the list.
102func (l *List) Append(items ...Item) {
103	l.items = append(l.items, items...)
104}
105
106// GotoBottom scrolls the list to the bottom.
107func (l *List) GotoBottom() {
108	l.yOffset = -1
109}
110
111// GotoTop scrolls the list to the top.
112func (l *List) GotoTop() {
113	l.yOffset = 0
114}
115
116// TotalHeight returns the total height of all items in the list.
117func (l *List) TotalHeight() int {
118	total := 0
119	for _, item := range l.items {
120		total += item.Height()
121	}
122	return total
123}
124
125// ScrollUp scrolls the list up by the given number of lines.
126func (l *List) ScrollUp(lines int) {
127	if l.yOffset == -1 {
128		// Calculate total height
129		totalHeight := l.TotalHeight()
130		l.yOffset = totalHeight
131	}
132	l.yOffset -= lines
133	if l.yOffset < 0 {
134		l.yOffset = 0
135	}
136}
137
138// ScrollDown scrolls the list down by the given number of lines.
139func (l *List) ScrollDown(lines int) {
140	if l.yOffset == -1 {
141		// Already at bottom
142		return
143	}
144	l.yOffset += lines
145	totalHeight := l.TotalHeight()
146	if l.yOffset >= totalHeight {
147		l.yOffset = -1 // Scroll to bottom
148	}
149}
150
151// YOffset returns the current vertical scroll offset.
152func (l *List) YOffset() int {
153	if l.yOffset == -1 {
154		return l.TotalHeight()
155	}
156	return l.yOffset
157}
158
159// Render renders the whole list as a string.
160func (l *List) Render() string {
161	return l.RenderRange(0, len(l.items))
162}
163
164// Draw draws the list to the given [uv.Screen] in the specified area.
165func (l *List) Draw(scr uv.Screen, area uv.Rectangle) {
166	yOffset := l.YOffset()
167	rendered := l.RenderLines(yOffset, yOffset+area.Dy())
168	uv.NewStyledString(rendered).Draw(scr, area)
169}
170
171// RenderRange renders a range of items from start to end indices.
172func (l *List) RenderRange(start, end int) string {
173	var b strings.Builder
174	for i := start; i < end && i < len(l.items); i++ {
175		item, ok := l.At(i)
176		if !ok {
177			continue
178		}
179
180		l.rend.Render(&b, l, i, item)
181		if i < end-1 && i < len(l.items)-1 {
182			b.WriteString("\n")
183		}
184	}
185
186	return b.String()
187}
188
189// RenderLines renders the list based on the start and end y offsets.
190func (l *List) RenderLines(startY, endY int) string {
191	var b strings.Builder
192	currentY := 0
193	for i := 0; i < len(l.items); i++ {
194		item, ok := l.At(i)
195		if !ok {
196			continue
197		}
198
199		itemHeight := item.Height()
200		if currentY+itemHeight <= startY {
201			// Skip this item as it's above the startY
202			currentY += itemHeight
203			continue
204		}
205		if currentY >= endY {
206			// Stop rendering as we've reached endY
207			break
208		}
209
210		// Render the item to a temporary buffer if needed
211		if currentY < startY || currentY+itemHeight > endY {
212			var tempBuf strings.Builder
213			l.rend.Render(&tempBuf, l, i, item)
214			lines := strings.Split(tempBuf.String(), "\n")
215
216			// Calculate the visible lines
217			startLine := 0
218			if currentY < startY {
219				startLine = startY - currentY
220			}
221			endLine := itemHeight
222			if currentY+itemHeight > endY {
223				endLine = endY - currentY
224			}
225
226			// Write only the visible lines
227			for j := startLine; j < endLine && j < len(lines); j++ {
228				b.WriteString(lines[j])
229				b.WriteString("\n")
230			}
231		} else {
232			// Render the whole item directly
233			l.rend.Render(&b, l, i, item)
234			b.WriteString("\n")
235		}
236
237		currentY += itemHeight
238	}
239
240	return strings.TrimRight(b.String(), "\n")
241}