1package list
2
3import (
4 "fmt"
5 "strings"
6 "testing"
7
8 tea "github.com/charmbracelet/bubbletea/v2"
9 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
10 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/x/exp/golden"
12 "github.com/google/uuid"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15)
16
17func TestList(t *testing.T) {
18 t.Parallel()
19 t.Run("should have correct positions in list that fits the items", func(t *testing.T) {
20 t.Parallel()
21 items := []Item{}
22 for i := range 5 {
23 item := NewSelectableItem(fmt.Sprintf("Item %d", i))
24 items = append(items, item)
25 }
26 l := New(items, WithDirectionForward(), WithSize(10, 20)).(*list[Item])
27 execCmd(l, l.Init())
28
29 // should select the last item
30 assert.Equal(t, items[0].ID(), l.selectedItem)
31 assert.Equal(t, 0, l.offset)
32 require.Equal(t, 5, l.indexMap.Len())
33 require.Equal(t, 5, l.items.Len())
34 require.Equal(t, 5, l.renderedItems.Len())
35 assert.Equal(t, 5, lipgloss.Height(l.rendered))
36 assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
37 start, end := l.viewPosition()
38 assert.Equal(t, 0, start)
39 assert.Equal(t, 4, end)
40 for i := range 5 {
41 item, ok := l.renderedItems.Get(items[i].ID())
42 require.True(t, ok)
43 assert.Equal(t, i, item.start)
44 assert.Equal(t, i, item.end)
45 }
46
47 golden.RequireEqual(t, []byte(l.View()))
48 })
49 t.Run("should have correct positions in list that fits the items backwards", func(t *testing.T) {
50 t.Parallel()
51 items := []Item{}
52 for i := range 5 {
53 item := NewSelectableItem(fmt.Sprintf("Item %d", i))
54 items = append(items, item)
55 }
56 l := New(items, WithDirectionBackward(), WithSize(10, 20)).(*list[Item])
57 execCmd(l, l.Init())
58
59 // should select the last item
60 assert.Equal(t, items[4].ID(), l.selectedItem)
61 assert.Equal(t, 0, l.offset)
62 require.Equal(t, 5, l.indexMap.Len())
63 require.Equal(t, 5, l.items.Len())
64 require.Equal(t, 5, l.renderedItems.Len())
65 assert.Equal(t, 5, lipgloss.Height(l.rendered))
66 assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
67 start, end := l.viewPosition()
68 assert.Equal(t, 0, start)
69 assert.Equal(t, 4, end)
70 for i := range 5 {
71 item, ok := l.renderedItems.Get(items[i].ID())
72 require.True(t, ok)
73 assert.Equal(t, i, item.start)
74 assert.Equal(t, i, item.end)
75 }
76
77 golden.RequireEqual(t, []byte(l.View()))
78 })
79
80 t.Run("should have correct positions in list that does not fits the items", func(t *testing.T) {
81 t.Parallel()
82 items := []Item{}
83 for i := range 30 {
84 item := NewSelectableItem(fmt.Sprintf("Item %d", i))
85 items = append(items, item)
86 }
87 l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
88 execCmd(l, l.Init())
89
90 // should select the last item
91 assert.Equal(t, items[0].ID(), l.selectedItem)
92 assert.Equal(t, 0, l.offset)
93 require.Equal(t, 30, l.indexMap.Len())
94 require.Equal(t, 30, l.items.Len())
95 require.Equal(t, 30, l.renderedItems.Len())
96 assert.Equal(t, 30, lipgloss.Height(l.rendered))
97 assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
98 start, end := l.viewPosition()
99 assert.Equal(t, 0, start)
100 assert.Equal(t, 9, end)
101 for i := range 30 {
102 item, ok := l.renderedItems.Get(items[i].ID())
103 require.True(t, ok)
104 assert.Equal(t, i, item.start)
105 assert.Equal(t, i, item.end)
106 }
107
108 golden.RequireEqual(t, []byte(l.View()))
109 })
110 t.Run("should have correct positions in list that does not fits the items backwards", func(t *testing.T) {
111 t.Parallel()
112 items := []Item{}
113 for i := range 30 {
114 item := NewSelectableItem(fmt.Sprintf("Item %d", i))
115 items = append(items, item)
116 }
117 l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
118 execCmd(l, l.Init())
119
120 // should select the last item
121 assert.Equal(t, items[29].ID(), l.selectedItem)
122 assert.Equal(t, 0, l.offset)
123 require.Equal(t, 30, l.indexMap.Len())
124 require.Equal(t, 30, l.items.Len())
125 require.Equal(t, 30, l.renderedItems.Len())
126 assert.Equal(t, 30, lipgloss.Height(l.rendered))
127 assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
128 start, end := l.viewPosition()
129 assert.Equal(t, 20, start)
130 assert.Equal(t, 29, end)
131 for i := range 30 {
132 item, ok := l.renderedItems.Get(items[i].ID())
133 require.True(t, ok)
134 assert.Equal(t, i, item.start)
135 assert.Equal(t, i, item.end)
136 }
137
138 golden.RequireEqual(t, []byte(l.View()))
139 })
140
141 t.Run("should have correct positions in list that does not fits the items and has multi line items", func(t *testing.T) {
142 t.Parallel()
143 items := []Item{}
144 for i := range 30 {
145 content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
146 content = strings.TrimSuffix(content, "\n")
147 item := NewSelectableItem(content)
148 items = append(items, item)
149 }
150 l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
151 execCmd(l, l.Init())
152
153 // should select the last item
154 assert.Equal(t, items[0].ID(), l.selectedItem)
155 assert.Equal(t, 0, l.offset)
156 require.Equal(t, 30, l.indexMap.Len())
157 require.Equal(t, 30, l.items.Len())
158 require.Equal(t, 30, l.renderedItems.Len())
159 expectedLines := 0
160 for i := range 30 {
161 expectedLines += (i + 1) * 1
162 }
163 assert.Equal(t, expectedLines, lipgloss.Height(l.rendered))
164 assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
165 start, end := l.viewPosition()
166 assert.Equal(t, 0, start)
167 assert.Equal(t, 9, end)
168 currentPosition := 0
169 for i := range 30 {
170 rItem, ok := l.renderedItems.Get(items[i].ID())
171 require.True(t, ok)
172 assert.Equal(t, currentPosition, rItem.start)
173 assert.Equal(t, currentPosition+i, rItem.end)
174 currentPosition += i + 1
175 }
176
177 golden.RequireEqual(t, []byte(l.View()))
178 })
179 t.Run("should have correct positions in list that does not fits the items and has multi line items backwards", func(t *testing.T) {
180 t.Parallel()
181 items := []Item{}
182 for i := range 30 {
183 content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
184 content = strings.TrimSuffix(content, "\n")
185 item := NewSelectableItem(content)
186 items = append(items, item)
187 }
188 l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
189 execCmd(l, l.Init())
190
191 // should select the last item
192 assert.Equal(t, items[29].ID(), l.selectedItem)
193 assert.Equal(t, 0, l.offset)
194 require.Equal(t, 30, l.indexMap.Len())
195 require.Equal(t, 30, l.items.Len())
196 require.Equal(t, 30, l.renderedItems.Len())
197 expectedLines := 0
198 for i := range 30 {
199 expectedLines += (i + 1) * 1
200 }
201 assert.Equal(t, expectedLines, lipgloss.Height(l.rendered))
202 assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
203 start, end := l.viewPosition()
204 assert.Equal(t, expectedLines-10, start)
205 assert.Equal(t, expectedLines-1, end)
206 currentPosition := 0
207 for i := range 30 {
208 rItem, ok := l.renderedItems.Get(items[i].ID())
209 require.True(t, ok)
210 assert.Equal(t, currentPosition, rItem.start)
211 assert.Equal(t, currentPosition+i, rItem.end)
212 currentPosition += i + 1
213 }
214
215 golden.RequireEqual(t, []byte(l.View()))
216 })
217
218 t.Run("should go to selected item at the beginning", func(t *testing.T) {
219 t.Parallel()
220 items := []Item{}
221 for i := range 30 {
222 content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
223 content = strings.TrimSuffix(content, "\n")
224 item := NewSelectableItem(content)
225 items = append(items, item)
226 }
227 l := New(items, WithDirectionForward(), WithSize(10, 10), WithSelectedItem(items[10].ID())).(*list[Item])
228 execCmd(l, l.Init())
229
230 // should select the last item
231 assert.Equal(t, items[10].ID(), l.selectedItem)
232
233 golden.RequireEqual(t, []byte(l.View()))
234 })
235
236 t.Run("should go to selected item at the beginning backwards", func(t *testing.T) {
237 t.Parallel()
238 items := []Item{}
239 for i := range 30 {
240 content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
241 content = strings.TrimSuffix(content, "\n")
242 item := NewSelectableItem(content)
243 items = append(items, item)
244 }
245 l := New(items, WithDirectionBackward(), WithSize(10, 10), WithSelectedItem(items[10].ID())).(*list[Item])
246 execCmd(l, l.Init())
247
248 // should select the last item
249 assert.Equal(t, items[10].ID(), l.selectedItem)
250
251 golden.RequireEqual(t, []byte(l.View()))
252 })
253}
254
255func TestListMovement(t *testing.T) {
256 t.Parallel()
257 t.Run("should move viewport up", func(t *testing.T) {
258 t.Parallel()
259 items := []Item{}
260 for i := range 30 {
261 content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
262 content = strings.TrimSuffix(content, "\n")
263 item := NewSelectableItem(content)
264 items = append(items, item)
265 }
266 l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
267 execCmd(l, l.Init())
268
269 execCmd(l, l.MoveUp(25))
270
271 assert.Equal(t, 25, l.offset)
272 golden.RequireEqual(t, []byte(l.View()))
273 })
274 t.Run("should move viewport up and down", func(t *testing.T) {
275 t.Parallel()
276 items := []Item{}
277 for i := range 30 {
278 content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
279 content = strings.TrimSuffix(content, "\n")
280 item := NewSelectableItem(content)
281 items = append(items, item)
282 }
283 l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
284 execCmd(l, l.Init())
285
286 execCmd(l, l.MoveUp(25))
287 execCmd(l, l.MoveDown(25))
288
289 assert.Equal(t, 0, l.offset)
290 golden.RequireEqual(t, []byte(l.View()))
291 })
292
293 t.Run("should move viewport down", func(t *testing.T) {
294 t.Parallel()
295 items := []Item{}
296 for i := range 30 {
297 content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
298 content = strings.TrimSuffix(content, "\n")
299 item := NewSelectableItem(content)
300 items = append(items, item)
301 }
302 l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
303 execCmd(l, l.Init())
304
305 execCmd(l, l.MoveDown(25))
306
307 assert.Equal(t, 25, l.offset)
308 golden.RequireEqual(t, []byte(l.View()))
309 })
310 t.Run("should move viewport down and up", func(t *testing.T) {
311 t.Parallel()
312 items := []Item{}
313 for i := range 30 {
314 content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
315 content = strings.TrimSuffix(content, "\n")
316 item := NewSelectableItem(content)
317 items = append(items, item)
318 }
319 l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
320 execCmd(l, l.Init())
321
322 execCmd(l, l.MoveDown(25))
323 execCmd(l, l.MoveUp(25))
324
325 assert.Equal(t, 0, l.offset)
326 golden.RequireEqual(t, []byte(l.View()))
327 })
328
329 t.Run("should not change offset when new items are appended and we are at the bottom in backwards list", func(t *testing.T) {
330 t.Parallel()
331 items := []Item{}
332 for i := range 30 {
333 content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
334 content = strings.TrimSuffix(content, "\n")
335 item := NewSelectableItem(content)
336 items = append(items, item)
337 }
338 l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
339 execCmd(l, l.Init())
340 execCmd(l, l.AppendItem(NewSelectableItem("Testing")))
341
342 assert.Equal(t, 0, l.offset)
343 golden.RequireEqual(t, []byte(l.View()))
344 })
345
346 t.Run("should stay at the position it is when new items are added but we moved up in backwards list", func(t *testing.T) {
347 t.Parallel()
348 items := []Item{}
349 for i := range 30 {
350 item := NewSelectableItem(fmt.Sprintf("Item %d", i))
351 items = append(items, item)
352 }
353 l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
354 execCmd(l, l.Init())
355
356 execCmd(l, l.MoveUp(2))
357 viewBefore := l.View()
358 execCmd(l, l.AppendItem(NewSelectableItem("Testing\nHello\n")))
359 viewAfter := l.View()
360 assert.Equal(t, viewBefore, viewAfter)
361 assert.Equal(t, 5, l.offset)
362 assert.Equal(t, 33, lipgloss.Height(l.rendered))
363 golden.RequireEqual(t, []byte(l.View()))
364 })
365 t.Run("should stay at the position it is when the hight of an item below is increased in backwards list", func(t *testing.T) {
366 t.Parallel()
367 items := []Item{}
368 for i := range 30 {
369 item := NewSelectableItem(fmt.Sprintf("Item %d", i))
370 items = append(items, item)
371 }
372 l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
373 execCmd(l, l.Init())
374
375 execCmd(l, l.MoveUp(2))
376 viewBefore := l.View()
377 item := items[29]
378 execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 29\nLine 2\nLine 3")))
379 viewAfter := l.View()
380 assert.Equal(t, viewBefore, viewAfter)
381 assert.Equal(t, 4, l.offset)
382 assert.Equal(t, 32, lipgloss.Height(l.rendered))
383 golden.RequireEqual(t, []byte(l.View()))
384 })
385 t.Run("should stay at the position it is when the hight of an item below is decreases in backwards list", func(t *testing.T) {
386 t.Parallel()
387 items := []Item{}
388 for i := range 30 {
389 item := NewSelectableItem(fmt.Sprintf("Item %d", i))
390 items = append(items, item)
391 }
392 items = append(items, NewSelectableItem("Item 30\nLine 2\nLine 3"))
393 l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
394 execCmd(l, l.Init())
395
396 execCmd(l, l.MoveUp(2))
397 viewBefore := l.View()
398 item := items[30]
399 execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 30")))
400 viewAfter := l.View()
401 assert.Equal(t, viewBefore, viewAfter)
402 assert.Equal(t, 0, l.offset)
403 assert.Equal(t, 31, lipgloss.Height(l.rendered))
404 golden.RequireEqual(t, []byte(l.View()))
405 })
406 t.Run("should stay at the position it is when the hight of an item above is increased in backwards list", func(t *testing.T) {
407 t.Parallel()
408 items := []Item{}
409 for i := range 30 {
410 item := NewSelectableItem(fmt.Sprintf("Item %d", i))
411 items = append(items, item)
412 }
413 l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
414 execCmd(l, l.Init())
415
416 execCmd(l, l.MoveUp(2))
417 viewBefore := l.View()
418 item := items[1]
419 execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 1\nLine 2\nLine 3")))
420 viewAfter := l.View()
421 assert.Equal(t, viewBefore, viewAfter)
422 assert.Equal(t, 2, l.offset)
423 assert.Equal(t, 32, lipgloss.Height(l.rendered))
424 golden.RequireEqual(t, []byte(l.View()))
425 })
426 t.Run("should stay at the position it is if an item is prepended and we are in backwards list", func(t *testing.T) {
427 t.Parallel()
428 items := []Item{}
429 for i := range 30 {
430 item := NewSelectableItem(fmt.Sprintf("Item %d", i))
431 items = append(items, item)
432 }
433 l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
434 execCmd(l, l.Init())
435
436 execCmd(l, l.MoveUp(2))
437 viewBefore := l.View()
438 execCmd(l, l.PrependItem(NewSelectableItem("New")))
439 viewAfter := l.View()
440 assert.Equal(t, viewBefore, viewAfter)
441 assert.Equal(t, 2, l.offset)
442 assert.Equal(t, 31, lipgloss.Height(l.rendered))
443 golden.RequireEqual(t, []byte(l.View()))
444 })
445
446 t.Run("should not change offset when new items are prepended and we are at the top in forward list", func(t *testing.T) {
447 t.Parallel()
448 items := []Item{}
449 for i := range 30 {
450 content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
451 content = strings.TrimSuffix(content, "\n")
452 item := NewSelectableItem(content)
453 items = append(items, item)
454 }
455 l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
456 execCmd(l, l.Init())
457 execCmd(l, l.PrependItem(NewSelectableItem("Testing")))
458
459 assert.Equal(t, 0, l.offset)
460 golden.RequireEqual(t, []byte(l.View()))
461 })
462
463 t.Run("should stay at the position it is when new items are added but we moved down in forward list", func(t *testing.T) {
464 t.Parallel()
465 items := []Item{}
466 for i := range 30 {
467 item := NewSelectableItem(fmt.Sprintf("Item %d", i))
468 items = append(items, item)
469 }
470 l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
471 execCmd(l, l.Init())
472
473 execCmd(l, l.MoveDown(2))
474 viewBefore := l.View()
475 execCmd(l, l.PrependItem(NewSelectableItem("Testing\nHello\n")))
476 viewAfter := l.View()
477 assert.Equal(t, viewBefore, viewAfter)
478 assert.Equal(t, 5, l.offset)
479 assert.Equal(t, 33, lipgloss.Height(l.rendered))
480 golden.RequireEqual(t, []byte(l.View()))
481 })
482
483 t.Run("should stay at the position it is when the hight of an item above is increased in forward list", func(t *testing.T) {
484 t.Parallel()
485 items := []Item{}
486 for i := range 30 {
487 item := NewSelectableItem(fmt.Sprintf("Item %d", i))
488 items = append(items, item)
489 }
490 l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
491 execCmd(l, l.Init())
492
493 execCmd(l, l.MoveDown(2))
494 viewBefore := l.View()
495 item := items[0]
496 execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 29\nLine 2\nLine 3")))
497 viewAfter := l.View()
498 assert.Equal(t, viewBefore, viewAfter)
499 assert.Equal(t, 4, l.offset)
500 assert.Equal(t, 32, lipgloss.Height(l.rendered))
501 golden.RequireEqual(t, []byte(l.View()))
502 })
503
504 t.Run("should stay at the position it is when the hight of an item above is decreases in forward list", func(t *testing.T) {
505 t.Parallel()
506 items := []Item{}
507 items = append(items, NewSelectableItem("At top\nLine 2\nLine 3"))
508 for i := range 30 {
509 item := NewSelectableItem(fmt.Sprintf("Item %d", i))
510 items = append(items, item)
511 }
512 l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
513 execCmd(l, l.Init())
514
515 execCmd(l, l.MoveDown(3))
516 viewBefore := l.View()
517 item := items[0]
518 execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("At top")))
519 viewAfter := l.View()
520 assert.Equal(t, viewBefore, viewAfter)
521 assert.Equal(t, 1, l.offset)
522 assert.Equal(t, 31, lipgloss.Height(l.rendered))
523 golden.RequireEqual(t, []byte(l.View()))
524 })
525
526 t.Run("should stay at the position it is when the hight of an item below is increased in forward list", func(t *testing.T) {
527 t.Parallel()
528 items := []Item{}
529 for i := range 30 {
530 item := NewSelectableItem(fmt.Sprintf("Item %d", i))
531 items = append(items, item)
532 }
533 l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
534 execCmd(l, l.Init())
535
536 execCmd(l, l.MoveDown(2))
537 viewBefore := l.View()
538 item := items[29]
539 execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 29\nLine 2\nLine 3")))
540 viewAfter := l.View()
541 assert.Equal(t, viewBefore, viewAfter)
542 assert.Equal(t, 2, l.offset)
543 assert.Equal(t, 32, lipgloss.Height(l.rendered))
544 golden.RequireEqual(t, []byte(l.View()))
545 })
546 t.Run("should stay at the position it is if an item is appended and we are in forward list", func(t *testing.T) {
547 t.Parallel()
548 items := []Item{}
549 for i := range 30 {
550 item := NewSelectableItem(fmt.Sprintf("Item %d", i))
551 items = append(items, item)
552 }
553 l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
554 execCmd(l, l.Init())
555
556 execCmd(l, l.MoveDown(2))
557 viewBefore := l.View()
558 execCmd(l, l.AppendItem(NewSelectableItem("New")))
559 viewAfter := l.View()
560 assert.Equal(t, viewBefore, viewAfter)
561 assert.Equal(t, 2, l.offset)
562 assert.Equal(t, 31, lipgloss.Height(l.rendered))
563 golden.RequireEqual(t, []byte(l.View()))
564 })
565}
566
567type SelectableItem interface {
568 Item
569 layout.Focusable
570}
571
572type simpleItem struct {
573 width int
574 content string
575 id string
576}
577type selectableItem struct {
578 *simpleItem
579 focused bool
580}
581
582func NewSimpleItem(content string) *simpleItem {
583 return &simpleItem{
584 id: uuid.NewString(),
585 width: 0,
586 content: content,
587 }
588}
589
590func NewSelectableItem(content string) SelectableItem {
591 return &selectableItem{
592 simpleItem: NewSimpleItem(content),
593 focused: false,
594 }
595}
596
597func (s *simpleItem) ID() string {
598 return s.id
599}
600
601func (s *simpleItem) Init() tea.Cmd {
602 return nil
603}
604
605func (s *simpleItem) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
606 return s, nil
607}
608
609func (s *simpleItem) View() string {
610 return lipgloss.NewStyle().Width(s.width).Render(s.content)
611}
612
613func (l *simpleItem) GetSize() (int, int) {
614 return l.width, 0
615}
616
617// SetSize implements Item.
618func (s *simpleItem) SetSize(width int, height int) tea.Cmd {
619 s.width = width
620 return nil
621}
622
623func (s *selectableItem) View() string {
624 if s.focused {
625 return lipgloss.NewStyle().BorderLeft(true).BorderStyle(lipgloss.NormalBorder()).Width(s.width).Render(s.content)
626 }
627 return lipgloss.NewStyle().Width(s.width).Render(s.content)
628}
629
630// Blur implements SimpleItem.
631func (s *selectableItem) Blur() tea.Cmd {
632 s.focused = false
633 return nil
634}
635
636// Focus implements SimpleItem.
637func (s *selectableItem) Focus() tea.Cmd {
638 s.focused = true
639 return nil
640}
641
642// IsFocused implements SimpleItem.
643func (s *selectableItem) IsFocused() bool {
644 return s.focused
645}
646
647func execCmd(m tea.Model, cmd tea.Cmd) {
648 for cmd != nil {
649 msg := cmd()
650 m, cmd = m.Update(msg)
651 }
652}