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}