@@ -123,8 +123,23 @@ impl<'a> InteractiveWidget<'a> {
123123 /// Render the list items.
124124 fn render_items ( & self , area : Rect , buf : & mut Buffer ) {
125125 let visible_items = self . state . visible_items ( ) ;
126- let start = self . state . scroll_offset ;
127- let end = ( start + area. height as usize ) . min ( visible_items. len ( ) ) ;
126+ let viewport_height = area. height as usize ;
127+
128+ // Compute the effective scroll offset to ensure the selected item is visible.
129+ // This handles cases where the actual viewport is smaller than max_visible,
130+ // which can occur in small terminal windows.
131+ let start = if self . state . selected >= self . state . scroll_offset + viewport_height {
132+ // Selected item is below the visible area - scroll down
133+ self . state . selected . saturating_sub ( viewport_height - 1 )
134+ } else if self . state . selected < self . state . scroll_offset {
135+ // Selected item is above the visible area - scroll up
136+ self . state . selected
137+ } else {
138+ // Selected item is within view - use existing scroll offset
139+ self . state . scroll_offset
140+ } ;
141+
142+ let end = ( start + viewport_height) . min ( visible_items. len ( ) ) ;
128143
129144 for ( i, ( real_idx, item) ) in visible_items
130145 . iter ( )
@@ -329,4 +344,41 @@ mod tests {
329344 // 2 items + 1 title + 1 search + 1 hints + 2 border = 7
330345 assert_eq ! ( widget. required_height( ) , 7 ) ;
331346 }
347+
348+ #[ test]
349+ fn test_scroll_offset_calculation_small_viewport ( ) {
350+ // Test that scroll offset is computed correctly when viewport is smaller than max_visible.
351+ // This tests the fix for issue #1709 where items couldn't be scrolled to in small terminals.
352+ let items: Vec < InteractiveItem > = ( 0 ..20 )
353+ . map ( |i| InteractiveItem :: new ( format ! ( "{}" , i) , format ! ( "Item {}" , i) ) )
354+ . collect ( ) ;
355+
356+ let mut state =
357+ InteractiveState :: new ( "Test" , items, InteractiveAction :: Custom ( "test" . into ( ) ) )
358+ . with_max_visible ( 25 ) ; // max_visible is 25, but viewport will be smaller
359+
360+ // Select the last item (index 19)
361+ for _ in 0 ..19 {
362+ state. select_next ( ) ;
363+ }
364+ assert_eq ! ( state. selected, 19 ) ;
365+
366+ // Simulate a small viewport of height 5
367+ let viewport_height: usize = 5 ;
368+
369+ // Calculate start as the renderer would
370+ let start = if state. selected >= state. scroll_offset + viewport_height {
371+ state. selected . saturating_sub ( viewport_height - 1 )
372+ } else if state. selected < state. scroll_offset {
373+ state. selected
374+ } else {
375+ state. scroll_offset
376+ } ;
377+
378+ // With selected=19 and viewport_height=5, start should be 15
379+ // so items 15-19 are visible, including the selected item 19
380+ assert_eq ! ( start, 15 ) ;
381+ assert ! ( state. selected >= start) ;
382+ assert ! ( state. selected < start + viewport_height) ;
383+ }
332384}
0 commit comments