diff --git a/tui-scrollview/src/scroll_view.rs b/tui-scrollview/src/scroll_view.rs index 28e9af3..ccede83 100644 --- a/tui-scrollview/src/scroll_view.rs +++ b/tui-scrollview/src/scroll_view.rs @@ -194,7 +194,6 @@ impl ScrollView { /// /// This should not be confused with the `render` method, which renders the visible area of the /// ScrollView into the main buffer. - pub fn render_stateful_widget( &mut self, widget: W, @@ -451,6 +450,57 @@ mod tests { ) } + #[rstest] + fn move_to_bottom(scroll_view: ScrollView) { + let mut buf = Buffer::empty(Rect::new(0, 0, 6, 6)); + let mut state = ScrollViewState::default(); + + // Prior rendering, page and buffer size are unkown. We default to `true`. + assert!(state.is_at_bottom()); + + scroll_view.clone().render(buf.area, &mut buf, &mut state); + + // The vertical view size is five which means the page size is five. + // We have not scrolled yet, view is at the top and not the at the bottom. + // => We see the top five rows + assert!(!state.is_at_bottom()); + + // Since the content height is ten, + assert_eq!(state.size.unwrap().height, 10); + // if we scroll down one page (five rows), + state.scroll_down(); + state.scroll_down(); + state.scroll_down(); + state.scroll_down(); + state.scroll_down(); + + // we reach the bottom, + assert!(state.is_at_bottom()); + assert_eq!(state.offset.y, 5); + + // and we see the last five rows of the content. + scroll_view.render(buf.area, &mut buf, &mut state); + assert_eq!( + buf, + Buffer::with_lines(vec![ + "YZABC▲", + "IJKLM║", + "STUVW█", + "CDEFG█", + "MNOPQ▼", + "◄██═► ", + ]) + ); + + // We could also jump directly to the bottom... + state.scroll_to_bottom(); + assert!(state.is_at_bottom()); + + // ...which sets the offset to the last row of content, + // ensuring to be at the bottom regardless of the page size. + assert_eq!(state.offset.y, state.size.unwrap().height - 1); + } + #[rstest] fn hides_both_scrollbars(scroll_view: ScrollView) { let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10)); @@ -790,7 +840,7 @@ mod tests { let items: Vec = (1..=10).map(|i| format!("Item {}", i)).collect(); let list = List::new(items); scroll_view.render_stateful_widget(list, scroll_view.area(), &mut list_state); - scroll_view.render(buf.area, &mut buf, &mut state); + scroll_view.clone().render(buf.area, &mut buf, &mut state); assert_eq!( buf, Buffer::with_lines(vec![ diff --git a/tui-scrollview/src/state.rs b/tui-scrollview/src/state.rs index 4cf0c4c..ab22ab9 100644 --- a/tui-scrollview/src/state.rs +++ b/tui-scrollview/src/state.rs @@ -74,6 +74,9 @@ impl ScrollViewState { } /// Move the scroll view state to the bottom of the buffer + /// + /// If the buffer size is not yet computed (done during the first rendering), it will not + /// be taken into account and the scroll offset will be set to the maximum value: `u16::MAX` pub fn scroll_to_bottom(&mut self) { // the render call will adjust the offset to ensure that we don't scroll past the end of // the buffer, so we can set the offset to the maximum value here @@ -82,4 +85,18 @@ impl ScrollViewState { .map_or(u16::MAX, |size| size.height.saturating_sub(1)); self.offset.y = bottom; } + + /// True if the scroll view state is at the bottom of the buffer + /// + /// This takes the page size into account. It returns true if the current scroll offset plus + /// the page size matches or exceeds the buffer length. + /// + /// The buffer and the page size are unknown until computed during the first rendering. If the + /// page size is not yet known, it won't be taken into account. If the buffer is not yet known, + /// this function always returns true. + pub fn is_at_bottom(&self) -> bool { + let bottom = self.size.map_or(0, |size| size.height.saturating_sub(1)); + let page_size = self.page_size.map_or(0, |size| size.height); + self.offset.y.saturating_add(page_size) >= bottom + } }