Better ergonomics for ScrollbarState and improved documentation (#456)

* feat(scrollbar): Better ergonomics for ScrollbarState and improved documentation 

* feat(scrollbar)!: Use usize instead of u16 for scrollbar  💥
This commit is contained in:
Dheepak Krishnamurthy 2023-09-01 02:47:14 -04:00 committed by GitHub
parent 927a5d8251
commit 0696f484e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 90 additions and 26 deletions

View file

@ -66,27 +66,23 @@ fn run_app<B: Backend>(
KeyCode::Char('q') => return Ok(()), KeyCode::Char('q') => return Ok(()),
KeyCode::Char('j') => { KeyCode::Char('j') => {
app.vertical_scroll = app.vertical_scroll.saturating_add(1); app.vertical_scroll = app.vertical_scroll.saturating_add(1);
app.vertical_scroll_state = app app.vertical_scroll_state =
.vertical_scroll_state app.vertical_scroll_state.position(app.vertical_scroll);
.position(app.vertical_scroll as u16);
} }
KeyCode::Char('k') => { KeyCode::Char('k') => {
app.vertical_scroll = app.vertical_scroll.saturating_sub(1); app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
app.vertical_scroll_state = app app.vertical_scroll_state =
.vertical_scroll_state app.vertical_scroll_state.position(app.vertical_scroll);
.position(app.vertical_scroll as u16);
} }
KeyCode::Char('h') => { KeyCode::Char('h') => {
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1); app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
app.horizontal_scroll_state = app app.horizontal_scroll_state =
.horizontal_scroll_state app.horizontal_scroll_state.position(app.horizontal_scroll);
.position(app.horizontal_scroll as u16);
} }
KeyCode::Char('l') => { KeyCode::Char('l') => {
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1); app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
app.horizontal_scroll_state = app app.horizontal_scroll_state =
.horizontal_scroll_state app.horizontal_scroll_state.position(app.horizontal_scroll);
.position(app.horizontal_scroll as u16);
} }
_ => {} _ => {}
} }
@ -151,10 +147,8 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
), ),
]), ]),
]; ];
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len() as u16); app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
app.horizontal_scroll_state = app app.horizontal_scroll_state = app.horizontal_scroll_state.content_length(long_line.len());
.horizontal_scroll_state
.content_length(long_line.len() as u16);
let create_block = |title| { let create_block = |title| {
Block::default() Block::default()

View file

@ -13,6 +13,7 @@
//! - [`BarChart`] //! - [`BarChart`]
//! - [`Gauge`] //! - [`Gauge`]
//! - [`Sparkline`] //! - [`Sparkline`]
//! - [`Scrollbar`]
//! - [`calendar::Monthly`] //! - [`calendar::Monthly`]
//! - [`Clear`] //! - [`Clear`]

View file

@ -20,10 +20,16 @@ pub enum ScrollDirection {
/// A struct representing the state of a Scrollbar widget. /// A struct representing the state of a Scrollbar widget.
/// ///
/// # Important
///
/// It's essential to set the `content_length` field when using this struct. This field
/// represents the total length of the scrollable content. The default value is zero
/// which will result in the Scrollbar not rendering.
///
/// For example, in the following list, assume there are 4 bullet points: /// For example, in the following list, assume there are 4 bullet points:
/// ///
/// - the `position` is 0
/// - the `content_length` is 4 /// - the `content_length` is 4
/// - the `position` is 0
/// - the `viewport_content_length` is 2 /// - the `viewport_content_length` is 2
/// ///
/// ```text /// ```text
@ -39,29 +45,36 @@ pub enum ScrollDirection {
/// default of 0 and it'll use the track size as a `viewport_content_length`. /// default of 0 and it'll use the track size as a `viewport_content_length`.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)] #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct ScrollbarState { pub struct ScrollbarState {
// The current position within the scrollable content.
position: u16,
// The total length of the scrollable content. // The total length of the scrollable content.
content_length: u16, content_length: usize,
// The current position within the scrollable content.
position: usize,
// The length of content in current viewport. // The length of content in current viewport.
viewport_content_length: u16, viewport_content_length: usize,
} }
impl ScrollbarState { impl ScrollbarState {
/// Constructs a new ScrollbarState with the specified content length.
pub fn new(content_length: usize) -> Self {
Self {
content_length,
..Default::default()
}
}
/// Sets the scroll position of the scrollbar and returns the modified ScrollbarState. /// Sets the scroll position of the scrollbar and returns the modified ScrollbarState.
pub fn position(mut self, position: u16) -> Self { pub fn position(mut self, position: usize) -> Self {
self.position = position; self.position = position;
self self
} }
/// Sets the length of the scrollable content and returns the modified ScrollbarState. /// Sets the length of the scrollable content and returns the modified ScrollbarState.
pub fn content_length(mut self, content_length: u16) -> Self { pub fn content_length(mut self, content_length: usize) -> Self {
self.content_length = content_length; self.content_length = content_length;
self self
} }
/// Sets the length of the viewport content and returns the modified ScrollbarState. /// Sets the length of the viewport content and returns the modified ScrollbarState.
pub fn viewport_content_length(mut self, viewport_content_length: u16) -> Self { pub fn viewport_content_length(mut self, viewport_content_length: usize) -> Self {
self.viewport_content_length = viewport_content_length; self.viewport_content_length = viewport_content_length;
self self
} }
@ -124,6 +137,37 @@ pub enum ScrollbarOrientation {
/// │ └──────── thumb /// │ └──────── thumb
/// └─────────── begin /// └─────────── begin
/// ``` /// ```
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui::widgets::*;
/// # fn render_paragraph_with_scrollbar<B: Backend>(frame: &mut Frame<B>, area: Rect) {
///
/// let vertical_scroll = 0; // from app state
///
/// let items = vec![Line::from("Item 1"), Line::from("Item 2"), Line::from("Item 3")];
/// let paragraph = Paragraph::new(items.clone())
/// .scroll((vertical_scroll as u16, 0))
/// .block(Block::new().borders(Borders::RIGHT)); // to show a background for the scrollbar
///
/// let scrollbar = Scrollbar::default()
/// .orientation(ScrollbarOrientation::VerticalRight)
/// .begin_symbol(Some("↑"))
/// .end_symbol(Some("↓"));
/// let mut scrollbar_state = ScrollbarState::new(items.iter().len()).position(vertical_scroll);
///
/// let area = frame.size();
/// frame.render_widget(paragraph, area);
/// frame.render_stateful_widget(scrollbar,
/// area.inner(&Margin {
/// vertical: 1,
/// horizontal: 0,
/// }), // using a inner vertical margin of 1 unit makes the scrollbar inside the block
/// &mut scrollbar_state);
/// # }
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Scrollbar<'a> { pub struct Scrollbar<'a> {
orientation: ScrollbarOrientation, orientation: ScrollbarOrientation,
@ -312,7 +356,7 @@ impl<'a> Scrollbar<'a> {
} }
} }
fn should_not_render(&self, track_start: u16, track_end: u16, content_length: u16) -> bool { fn should_not_render(&self, track_start: u16, track_end: u16, content_length: usize) -> bool {
if track_end - track_start == 0 || content_length == 0 { if track_end - track_start == 0 || content_length == 0 {
return true; return true;
} }
@ -363,7 +407,7 @@ impl<'a> Scrollbar<'a> {
let (track_start, track_end) = track_start_end; let (track_start, track_end) = track_start_end;
let viewport_content_length = if state.viewport_content_length == 0 { let viewport_content_length = if state.viewport_content_length == 0 {
track_end - track_start (track_end - track_start) as usize
} else { } else {
state.viewport_content_length state.viewport_content_length
}; };
@ -534,6 +578,31 @@ mod tests {
); );
} }
#[test]
fn test_renders_empty_with_content_length_is_zero() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 8));
let mut state = ScrollbarState::default().position(0);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![" ", " ", " ", " ", " ", " ", " ", " "])
);
let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 8));
let mut state = ScrollbarState::new(8).position(0);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec!["", "", "", "", "", "", "", ""])
);
}
#[test] #[test]
fn test_no_render_when_area_zero() { fn test_no_render_when_area_zero() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 0)); let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 0));