on main help sign in
chore: small improvements
2a13723Kujtim Hoxha
internal/tui/exp/list/list.go+60−31
@@ -56,7 +56,6 @@ const (
56type renderedItem struct {
56type renderedItem struct {
57 id string
57 id string
58 view string
58 view string
59− dirty bool
60 height int
59 height int
61 start int
60 start int
62 end int
61 end int
@@ -84,6 +83,8 @@ type list[T Item] struct {
84 renderedItems map[string]renderedItem
83 renderedItems map[string]renderedItem
85
84
86 rendered string
85 rendered string
86+
87+ movingByItem bool
87}
88}
88
89
89type listOption func(*confOptions)
90type listOption func(*confOptions)
@@ -209,7 +210,9 @@ func (l *list[T]) View() string {
209 lines := strings.Split(view, "\n")
210 lines := strings.Split(view, "\n")
210
211
211 start, end := l.viewPosition()
212 start, end := l.viewPosition()
212− lines = lines[start : end+1]
213+ viewStart := max(0, start)
214+ viewEnd := min(len(lines), end+1)
215+ lines = lines[viewStart:viewEnd]
213 return strings.Join(lines, "\n")
216 return strings.Join(lines, "\n")
214}
217}
215
218
@@ -245,7 +248,13 @@ func (l *list[T]) render() tea.Cmd {
245 return nil
248 return nil
246 }
249 }
247 l.setDefaultSelected()
250 l.setDefaultSelected()
251+
252+ var focusChangeCmd tea.Cmd
253+ if l.focused {
248− focusCmd := l.focusSelectedItem()
254+ focusChangeCmd = l.focusSelectedItem()
255+ } else {
256+ focusChangeCmd = l.blurSelectedItem()
257+ }
249 // we are not rendering the first time
258 // we are not rendering the first time
250 if l.rendered != "" {
259 if l.rendered != "" {
251 l.rendered = ""
260 l.rendered = ""
@@ -258,7 +267,7 @@ func (l *list[T]) render() tea.Cmd {
258 if l.focused {
267 if l.focused {
259 l.scrollToSelection()
268 l.scrollToSelection()
260 }
269 }
261− return focusCmd
270+ return focusChangeCmd
262 }
271 }
263 finishIndex := l.renderIterator(0, true)
272 finishIndex := l.renderIterator(0, true)
264 // recalculate for the initial items
273 // recalculate for the initial items
@@ -276,9 +285,10 @@ func (l *list[T]) render() tea.Cmd {
276 if l.focused {
285 if l.focused {
277 l.scrollToSelection()
286 l.scrollToSelection()
278 }
287 }
288+
279 return renderedMsg{}
289 return renderedMsg{}
280 }
290 }
281− return tea.Batch(focusCmd, renderCmd)
291+ return tea.Batch(focusChangeCmd, renderCmd)
282}
292}
283
293
284func (l *list[T]) setDefaultSelected() {
294func (l *list[T]) setDefaultSelected() {
@@ -304,11 +314,21 @@ func (l *list[T]) scrollToSelection() {
304 if rItem.start <= start && rItem.end >= end {
314 if rItem.start <= start && rItem.end >= end {
305 return
315 return
306 }
316 }
317+ // if we are moving by item we want to move the offset so that the
318+ // whole item is visible not just portions of it
319+ if l.movingByItem {
320+ if rItem.start >= start && rItem.end <= end {
321+ return
322+ }
323+ defer func() { l.movingByItem = false }()
324+ } else {
307− // item already in view do nothing
325+ // item already in view do nothing
308− if rItem.start >= start && rItem.start <= end {
326+ if rItem.start >= start && rItem.start <= end {
309− return
327+ return
328+ }
310− } else if rItem.end <= end && rItem.end >= start {
329+ if rItem.end >= start && rItem.end <= end {
311− return
330+ return
331+ }
312 }
332 }
313
333
314 if rItem.height >= l.height {
334 if rItem.height >= l.height {
@@ -320,11 +340,22 @@ func (l *list[T]) scrollToSelection() {
320 return
340 return
321 }
341 }
322
342
323− itemMiddleStart := rItem.start + rItem.height/2 + 1
324− if l.direction == DirectionForward {
325− l.offset = itemMiddleStart - l.height/2
326− } else {
327− l.offset = max(0, lipgloss.Height(l.rendered)-(itemMiddleStart+l.height/2))
343+ renderedLines := lipgloss.Height(l.rendered) - 1
344+
345+ // If item is above the viewport, make it the first item
346+ if rItem.start < start {
347+ if l.direction == DirectionForward {
348+ l.offset = rItem.start
349+ } else {
350+ l.offset = max(0, renderedLines-rItem.start-l.height+1)
351+ }
352+ } else if rItem.end > end {
353+ // If item is below the viewport, make it the last item
354+ if l.direction == DirectionForward {
355+ l.offset = max(0, rItem.end-l.height+1)
356+ } else {
357+ l.offset = max(0, renderedLines-rItem.end)
358+ }
328 }
359 }
329}
360}
330
361
@@ -446,32 +477,26 @@ func (l *list[T]) focusSelectedItem() tea.Cmd {
446 if f, ok := any(item).(layout.Focusable); ok {
477 if f, ok := any(item).(layout.Focusable); ok {
447 if item.ID() == l.selectedItem && !f.IsFocused() {
478 if item.ID() == l.selectedItem && !f.IsFocused() {
448 cmds = append(cmds, f.Focus())
479 cmds = append(cmds, f.Focus())
449− if cache, ok := l.renderedItems[item.ID()]; ok {
480+ delete(l.renderedItems, item.ID())
450− cache.dirty = true
451− l.renderedItems[item.ID()] = cache
452− }
453 } else if item.ID() != l.selectedItem && f.IsFocused() {
481 } else if item.ID() != l.selectedItem && f.IsFocused() {
454 cmds = append(cmds, f.Blur())
482 cmds = append(cmds, f.Blur())
455− if cache, ok := l.renderedItems[item.ID()]; ok {
483+ delete(l.renderedItems, item.ID())
456− cache.dirty = true
457− l.renderedItems[item.ID()] = cache
458− }
459 }
484 }
460 }
485 }
461 }
486 }
462 return tea.Batch(cmds...)
487 return tea.Batch(cmds...)
463}
488}
464
489
465−func (l *list[T]) blurItems() tea.Cmd {
490+func (l *list[T]) blurSelectedItem() tea.Cmd {
491+ if l.selectedItem == "" || l.focused {
492+ return nil
493+ }
466 var cmds []tea.Cmd
494 var cmds []tea.Cmd
467 for _, item := range l.items {
495 for _, item := range l.items {
468 if f, ok := any(item).(layout.Focusable); ok {
496 if f, ok := any(item).(layout.Focusable); ok {
469 if item.ID() == l.selectedItem && f.IsFocused() {
497 if item.ID() == l.selectedItem && f.IsFocused() {
470 cmds = append(cmds, f.Blur())
498 cmds = append(cmds, f.Blur())
471− if cache, ok := l.renderedItems[item.ID()]; ok {
499+ delete(l.renderedItems, item.ID())
472− cache.dirty = true
473− l.renderedItems[item.ID()] = cache
474− }
475 }
500 }
476 }
501 }
477 }
502 }
@@ -495,7 +520,7 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
495
520
496 item := l.items[inx]
521 item := l.items[inx]
497 var rItem renderedItem
522 var rItem renderedItem
498− if cache, ok := l.renderedItems[item.ID()]; ok && !cache.dirty {
523+ if cache, ok := l.renderedItems[item.ID()]; ok {
499 rItem = cache
524 rItem = cache
500 } else {
525 } else {
501 rItem = l.renderItem(item)
526 rItem = l.renderItem(item)
@@ -534,8 +559,8 @@ func (l *list[T]) AppendItem(T) tea.Cmd {
534
559
535// Blur implements List.
560// Blur implements List.
536func (l *list[T]) Blur() tea.Cmd {
561func (l *list[T]) Blur() tea.Cmd {
537− cmd := l.blurItems()
562+ l.focused = false
538− return tea.Batch(cmd, l.render())
563+ return l.render()
539}
564}
540
565
541// DeleteItem implements List.
566// DeleteItem implements List.
@@ -644,6 +669,7 @@ func (l *list[T]) SelectItemAbove() tea.Cmd {
644 }
669 }
645 item := l.items[newIndex]
670 item := l.items[newIndex]
646 l.selectedItem = item.ID()
671 l.selectedItem = item.ID()
672+ l.movingByItem = true
647 return l.render()
673 return l.render()
648}
674}
649
675
@@ -661,6 +687,7 @@ func (l *list[T]) SelectItemBelow() tea.Cmd {
661 }
687 }
662 item := l.items[newIndex]
688 item := l.items[newIndex]
663 l.selectedItem = item.ID()
689 l.selectedItem = item.ID()
690+ l.movingByItem = true
664 return l.render()
691 return l.render()
665}
692}
666
693
@@ -692,6 +719,8 @@ func (l *list[T]) SetSelected(id string) tea.Cmd {
692func (l *list[T]) reset() tea.Cmd {
719func (l *list[T]) reset() tea.Cmd {
693 var cmds []tea.Cmd
720 var cmds []tea.Cmd
694 l.rendered = ""
721 l.rendered = ""
722+ l.offset = 0
723+ l.selectedItem = ""
695 l.indexMap = make(map[string]int)
724 l.indexMap = make(map[string]int)
696 l.renderedItems = make(map[string]renderedItem)
725 l.renderedItems = make(map[string]renderedItem)
697 for inx, item := range l.items {
726 for inx, item := range l.items {
internal/tui/exp/list/list_test.go−36
@@ -205,42 +205,6 @@ func TestList(t *testing.T) {
205 golden.RequireEqual(t, []byte(l.View()))
205 golden.RequireEqual(t, []byte(l.View()))
206 })
206 })
207
207
208− t.Run("should go to selected item and center", func(t *testing.T) {
209− t.Parallel()
210− items := []Item{}
211− for i := range 30 {
212− content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
213− content = strings.TrimSuffix(content, "\n")
214− item := NewSelectableItem(content)
215− items = append(items, item)
216− }
217− l := New(items, WithDirectionForward(), WithSize(10, 10), WithSelectedItem(items[4].ID())).(*list[Item])
218− execCmd(l, l.Init())
219−
220− // should select the last item
221− assert.Equal(t, items[4].ID(), l.selectedItem)
222−
223− golden.RequireEqual(t, []byte(l.View()))
224− })
225−
226− t.Run("should go to selected item and center backwards", func(t *testing.T) {
227− t.Parallel()
228− items := []Item{}
229− for i := range 30 {
230− content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
231− content = strings.TrimSuffix(content, "\n")
232− item := NewSelectableItem(content)
233− items = append(items, item)
234− }
235− l := New(items, WithDirectionBackward(), WithSize(10, 10), WithSelectedItem(items[4].ID())).(*list[Item])
236− execCmd(l, l.Init())
237−
238− // should select the last item
239− assert.Equal(t, items[4].ID(), l.selectedItem)
240−
241− golden.RequireEqual(t, []byte(l.View()))
242− })
243−
244 t.Run("should go to selected item at the beginning", func(t *testing.T) {
208 t.Run("should go to selected item at the beginning", func(t *testing.T) {
245 t.Parallel()
209 t.Parallel()
246 items := []Item{}
210 items := []Item{}
internal/tui/exp/list/testdata/TestList/should_go_to_selected_item_and_center.goldendeleted−10
@@ -1,10 +0,0 @@
1−Item 3
2−Item 3
3−│Item 4
4−│Item 4
5−│Item 4
6−│Item 4
7−│Item 4
8−Item 5
9−Item 5
10−Item 5
internal/tui/exp/list/testdata/TestList/should_go_to_selected_item_and_center_backwards.goldendeleted−10
@@ -1,10 +0,0 @@
1−Item 3
2−Item 3
3−│Item 4
4−│Item 4
5−│Item 4
6−│Item 4
7−│Item 4
8−Item 5
9−Item 5
10−Item 5