From 7b0e31934306ed544ff31a6b3fb0188024944fd2 Mon Sep 17 00:00:00 2001 From: Teufelchen1 Date: Mon, 19 May 2025 13:45:56 +0200 Subject: [PATCH 1/5] feat: Query if ScrollViewState is_at_bottom() --- tui-scrollview/src/scroll_view.rs | 64 +++++++++++++++++++++++++++++-- tui-scrollview/src/state.rs | 10 +++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/tui-scrollview/src/scroll_view.rs b/tui-scrollview/src/scroll_view.rs index 28e9af3..b2c6e25 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,65 @@ 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(); + 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_eq!(state.offset.y, 0); + assert!(!state.is_at_bottom()); + assert_eq!( + buf, + Buffer::with_lines(vec![ + "ABCDE▲", + "KLMNO█", + "UVWXY█", + "EFGHI║", + "OPQRS▼", + "◄██═► ", + ]) + ); + + // 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)); @@ -787,10 +845,10 @@ mod tests { let mut buf = Buffer::empty(Rect::new(0, 0, 7, 5)); let mut state = ScrollViewState::default(); let mut list_state = ListState::default(); - let items: Vec = (1..=10).map(|i| format!("Item {}", i)).collect(); + 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..79a0433 100644 --- a/tui-scrollview/src/state.rs +++ b/tui-scrollview/src/state.rs @@ -82,4 +82,14 @@ 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 + /// Takes the pagesize into account + pub fn is_at_bottom(&self) -> bool { + let bottom = self + .size + .map_or(u16::MAX, |size| size.height.saturating_sub(1)); + let page_size = self.page_size.map_or(0, |size| size.height); + self.offset.y + page_size >= bottom + } } From 7a62a6010f0499e24fd02fb354026307956f0270 Mon Sep 17 00:00:00 2001 From: Teufelchen1 Date: Tue, 20 May 2025 10:32:00 +0200 Subject: [PATCH 2/5] review: remove accidential added change to for loop --- tui-scrollview/src/scroll_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tui-scrollview/src/scroll_view.rs b/tui-scrollview/src/scroll_view.rs index b2c6e25..9680f35 100644 --- a/tui-scrollview/src/scroll_view.rs +++ b/tui-scrollview/src/scroll_view.rs @@ -845,7 +845,7 @@ mod tests { let mut buf = Buffer::empty(Rect::new(0, 0, 7, 5)); let mut state = ScrollViewState::default(); let mut list_state = ListState::default(); - let items: Vec = (1..10).map(|i| format!("Item {}", i)).collect(); + 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.clone().render(buf.area, &mut buf, &mut state); From 805d875b1e81ecabda40fad725cb5410453b1d7a Mon Sep 17 00:00:00 2001 From: Teufelchen1 Date: Tue, 20 May 2025 11:20:10 +0200 Subject: [PATCH 3/5] review: Enhance doc strings --- tui-scrollview/src/state.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tui-scrollview/src/state.rs b/tui-scrollview/src/state.rs index 79a0433..9e2fc53 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 @@ -84,12 +87,15 @@ impl ScrollViewState { } /// True if the scroll view state is at the bottom of the buffer - /// Takes the pagesize into account + /// + /// 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 unkown 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(u16::MAX, |size| size.height.saturating_sub(1)); + 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 + page_size >= bottom + self.offset.y.saturating_add(page_size) >= bottom } } From cbfbe82ef65b3498e5144fd525f0749ddd8fc4d1 Mon Sep 17 00:00:00 2001 From: Teufelchen1 Date: Tue, 20 May 2025 11:41:02 +0200 Subject: [PATCH 4/5] review: Remove precondition test from unit test --- tui-scrollview/src/scroll_view.rs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/tui-scrollview/src/scroll_view.rs b/tui-scrollview/src/scroll_view.rs index 9680f35..ccede83 100644 --- a/tui-scrollview/src/scroll_view.rs +++ b/tui-scrollview/src/scroll_view.rs @@ -454,24 +454,16 @@ mod tests { 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_eq!(state.offset.y, 0); assert!(!state.is_at_bottom()); - assert_eq!( - buf, - Buffer::with_lines(vec![ - "ABCDE▲", - "KLMNO█", - "UVWXY█", - "EFGHI║", - "OPQRS▼", - "◄██═► ", - ]) - ); // Since the content height is ten, assert_eq!(state.size.unwrap().height, 10); @@ -500,11 +492,11 @@ mod tests { ]) ); - // We could also jump directly to the bottom + // 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, + // ...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); } From 2fe46b97575c6066b3e5864cd1d4c5fc9e895a5c Mon Sep 17 00:00:00 2001 From: Teufelchen1 Date: Tue, 20 May 2025 15:55:20 +0200 Subject: [PATCH 5/5] review: Adjust line wrapping --- tui-scrollview/src/state.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tui-scrollview/src/state.rs b/tui-scrollview/src/state.rs index 9e2fc53..ab22ab9 100644 --- a/tui-scrollview/src/state.rs +++ b/tui-scrollview/src/state.rs @@ -88,11 +88,12 @@ impl ScrollViewState { /// 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 unkown 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. + /// 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);