diff --git a/internal/ui/selectable_list.go b/internal/ui/selectable_list.go index 617d2cbb..d5ac9242 100644 --- a/internal/ui/selectable_list.go +++ b/internal/ui/selectable_list.go @@ -95,6 +95,23 @@ func (sl *SelectableList) HandleListEvent(ev tcell.Event, rect Rect, itemCount i sl.Selected++ } return ListEventResult{Result: EventConsumed} + case tcell.KeyPgDn: + sl.Selected += rect.H + if sl.Selected >= itemCount { + sl.Selected = itemCount - 1 + } + if sl.Selected < 0 { + sl.Selected = 0 + } + sl.EnsureVisible(rect.H) + return ListEventResult{Result: EventConsumed} + case tcell.KeyPgUp: + sl.Selected -= rect.H + if sl.Selected < 0 { + sl.Selected = 0 + } + sl.EnsureVisible(rect.H) + return ListEventResult{Result: EventConsumed} case tcell.KeyEnter: return ListEventResult{Result: EventConsumed, Action: ListActionActivate} } diff --git a/internal/ui/selectable_list_test.go b/internal/ui/selectable_list_test.go index f3bd551a..22ff2bc1 100644 --- a/internal/ui/selectable_list_test.go +++ b/internal/ui/selectable_list_test.go @@ -177,6 +177,119 @@ func TestSelectableListClampSelected(t *testing.T) { } } +func TestSelectableListPageDown(t *testing.T) { + sl := &SelectableList{Selected: 0, ScrollTop: 0} + r := Rect{X: 0, Y: 0, W: 20, H: 10} + items := 50 + + // PageDown moves by page size (rect.H = 10) + res := sl.HandleListEvent(tcell.NewEventKey(tcell.KeyPgDn, 0, 0), r, items) + if res.Result != EventConsumed { + t.Fatal("PageDown should be consumed") + } + if sl.Selected != 10 { + t.Fatalf("expected Selected 10, got %d", sl.Selected) + } + + // PageDown again + sl.HandleListEvent(tcell.NewEventKey(tcell.KeyPgDn, 0, 0), r, items) + if sl.Selected != 20 { + t.Fatalf("expected Selected 20, got %d", sl.Selected) + } + + // PageDown near end — clamps to last item + sl.Selected = 45 + sl.HandleListEvent(tcell.NewEventKey(tcell.KeyPgDn, 0, 0), r, items) + if sl.Selected != 49 { + t.Fatalf("expected Selected 49 (clamped), got %d", sl.Selected) + } + + // PageDown at last item — stays at last + sl.HandleListEvent(tcell.NewEventKey(tcell.KeyPgDn, 0, 0), r, items) + if sl.Selected != 49 { + t.Fatalf("expected Selected 49 (still), got %d", sl.Selected) + } +} + +func TestSelectableListPageUp(t *testing.T) { + sl := &SelectableList{Selected: 30, ScrollTop: 25} + r := Rect{X: 0, Y: 0, W: 20, H: 10} + items := 50 + + // PageUp moves by page size (rect.H = 10) + res := sl.HandleListEvent(tcell.NewEventKey(tcell.KeyPgUp, 0, 0), r, items) + if res.Result != EventConsumed { + t.Fatal("PageUp should be consumed") + } + if sl.Selected != 20 { + t.Fatalf("expected Selected 20, got %d", sl.Selected) + } + + // PageUp again + sl.HandleListEvent(tcell.NewEventKey(tcell.KeyPgUp, 0, 0), r, items) + if sl.Selected != 10 { + t.Fatalf("expected Selected 10, got %d", sl.Selected) + } + + // PageUp near start — clamps to 0 + sl.Selected = 5 + sl.HandleListEvent(tcell.NewEventKey(tcell.KeyPgUp, 0, 0), r, items) + if sl.Selected != 0 { + t.Fatalf("expected Selected 0 (clamped), got %d", sl.Selected) + } + + // PageUp at start — stays at 0 + sl.HandleListEvent(tcell.NewEventKey(tcell.KeyPgUp, 0, 0), r, items) + if sl.Selected != 0 { + t.Fatalf("expected Selected 0 (still), got %d", sl.Selected) + } +} + +func TestSelectableListPageDownScrollOffset(t *testing.T) { + sl := &SelectableList{Selected: 0, ScrollTop: 0} + r := Rect{X: 0, Y: 0, W: 20, H: 10} + items := 50 + + // PageDown should update ScrollTop to keep selection visible + sl.HandleListEvent(tcell.NewEventKey(tcell.KeyPgDn, 0, 0), r, items) + if sl.Selected != 10 { + t.Fatalf("expected Selected 10, got %d", sl.Selected) + } + // Selected=10 should be visible: ScrollTop should be at least 1 + if sl.ScrollTop > sl.Selected || sl.Selected >= sl.ScrollTop+r.H { + t.Fatalf("Selected %d not visible with ScrollTop %d and H %d", sl.Selected, sl.ScrollTop, r.H) + } +} + +func TestSelectableListPageUpScrollOffset(t *testing.T) { + sl := &SelectableList{Selected: 40, ScrollTop: 35} + r := Rect{X: 0, Y: 0, W: 20, H: 10} + items := 50 + + // PageUp should update ScrollTop to keep selection visible + sl.HandleListEvent(tcell.NewEventKey(tcell.KeyPgUp, 0, 0), r, items) + if sl.Selected != 30 { + t.Fatalf("expected Selected 30, got %d", sl.Selected) + } + if sl.ScrollTop > sl.Selected || sl.Selected >= sl.ScrollTop+r.H { + t.Fatalf("Selected %d not visible with ScrollTop %d and H %d", sl.Selected, sl.ScrollTop, r.H) + } +} + +func TestSelectableListPageDownEmptyList(t *testing.T) { + sl := &SelectableList{Selected: 0} + r := Rect{X: 0, Y: 0, W: 20, H: 10} + + // PageDown with 0 items should not panic, Selected stays 0 + res := sl.HandleListEvent(tcell.NewEventKey(tcell.KeyPgDn, 0, 0), r, 0) + if res.Result != EventConsumed { + t.Fatal("PageDown should be consumed even for empty list") + } + if sl.Selected != 0 { + t.Fatalf("expected Selected 0 for empty list, got %d", sl.Selected) + } +} + func TestSelectableListUnhandledKey(t *testing.T) { sl := &SelectableList{} r := Rect{X: 0, Y: 0, W: 20, H: 10}