From 4d70169bef86898d331f46013ff72ef6d1c275ed Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sun, 13 Aug 2023 10:24:51 +0200 Subject: [PATCH] feat(list): add option to always allocate the "selection" column width (#394) * feat(list): add option to always allocate the "selection" column width Before this option was available, selecting a item in a list when nothing was selected previously made the row layout change (the same applies to unselecting) by adding the width of the "highlight symbol" in the front of the list, this option allows to configure this behavior. * style: change "highlight_spacing" doc comment to use inline code-block for reference --- src/widgets/list.rs | 145 +++++++++++++++++++++++++++++++++++++++++- src/widgets/table.rs | 2 +- tests/widgets_list.rs | 127 +++++++++++++++++++++++++++++++++++- 3 files changed, 269 insertions(+), 5 deletions(-) diff --git a/src/widgets/list.rs b/src/widgets/list.rs index 5b07e5397..b80f83ab7 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -5,7 +5,7 @@ use crate::{ layout::{Corner, Rect}, style::{Style, Styled}, text::Text, - widgets::{Block, StatefulWidget, Widget}, + widgets::{Block, HighlightSpacing, StatefulWidget, Widget}, }; #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] @@ -103,6 +103,8 @@ pub struct List<'a> { highlight_symbol: Option<&'a str>, /// Whether to repeat the highlight symbol for each line of the selected item repeat_highlight_symbol: bool, + /// Decides when to allocate spacing for the selection symbol + highlight_spacing: HighlightSpacing, } impl<'a> List<'a> { @@ -118,6 +120,7 @@ impl<'a> List<'a> { highlight_style: Style::default(), highlight_symbol: None, repeat_highlight_symbol: false, + highlight_spacing: HighlightSpacing::default(), } } @@ -146,6 +149,14 @@ impl<'a> List<'a> { self } + /// Set when to show the highlight spacing + /// + /// See [`HighlightSpacing`] about which variant affects spacing in which way + pub fn highlight_spacing(mut self, value: HighlightSpacing) -> Self { + self.highlight_spacing = value; + self + } + pub fn start_corner(mut self, corner: Corner) -> List<'a> { self.start_corner = corner; self @@ -228,7 +239,7 @@ impl<'a> StatefulWidget for List<'a> { let blank_symbol = " ".repeat(highlight_symbol.width()); let mut current_height = 0; - let has_selection = state.selected.is_some(); + let selection_spacing = self.highlight_spacing.should_add(state.selected.is_some()); for (i, item) in self .items .iter_mut() @@ -263,7 +274,7 @@ impl<'a> StatefulWidget for List<'a> { } else { &blank_symbol }; - let (elem_x, max_element_width) = if has_selection { + let (elem_x, max_element_width) = if selection_spacing { let (elem_x, _) = buf.set_stringn( x, y + j as u16, @@ -774,6 +785,134 @@ mod tests { assert_buffer_eq!(buffer, expected); } + #[test] + fn test_list_highlight_spacing_default_whenselected() { + // when not selected + { + let items = list_items(vec!["Item 0", "Item 1", "Item 2"]); + let list = List::new(items).highlight_symbol(">>"); + let mut state = ListState::default(); + + let buffer = render_stateful_widget(list, &mut state, 10, 5); + + let expected = Buffer::with_lines(vec![ + "Item 0 ", + "Item 1 ", + "Item 2 ", + " ", + " ", + ]); + assert_buffer_eq!(buffer, expected); + } + + // when selected + { + let items = list_items(vec!["Item 0", "Item 1", "Item 2"]); + let list = List::new(items).highlight_symbol(">>"); + let mut state = ListState::default(); + state.select(Some(1)); + + let buffer = render_stateful_widget(list, &mut state, 10, 5); + + let expected = Buffer::with_lines(vec![ + " Item 0 ", + ">>Item 1 ", + " Item 2 ", + " ", + " ", + ]); + assert_buffer_eq!(buffer, expected); + } + } + + #[test] + fn test_list_highlight_spacing_default_always() { + // when not selected + { + let items = list_items(vec!["Item 0", "Item 1", "Item 2"]); + let list = List::new(items) + .highlight_symbol(">>") + .highlight_spacing(HighlightSpacing::Always); + let mut state = ListState::default(); + + let buffer = render_stateful_widget(list, &mut state, 10, 5); + + let expected = Buffer::with_lines(vec![ + " Item 0 ", + " Item 1 ", + " Item 2 ", + " ", + " ", + ]); + assert_buffer_eq!(buffer, expected); + } + + // when selected + { + let items = list_items(vec!["Item 0", "Item 1", "Item 2"]); + let list = List::new(items) + .highlight_symbol(">>") + .highlight_spacing(HighlightSpacing::Always); + let mut state = ListState::default(); + state.select(Some(1)); + + let buffer = render_stateful_widget(list, &mut state, 10, 5); + + let expected = Buffer::with_lines(vec![ + " Item 0 ", + ">>Item 1 ", + " Item 2 ", + " ", + " ", + ]); + assert_buffer_eq!(buffer, expected); + } + } + + #[test] + fn test_list_highlight_spacing_default_never() { + // when not selected + { + let items = list_items(vec!["Item 0", "Item 1", "Item 2"]); + let list = List::new(items) + .highlight_symbol(">>") + .highlight_spacing(HighlightSpacing::Never); + let mut state = ListState::default(); + + let buffer = render_stateful_widget(list, &mut state, 10, 5); + + let expected = Buffer::with_lines(vec![ + "Item 0 ", + "Item 1 ", + "Item 2 ", + " ", + " ", + ]); + assert_buffer_eq!(buffer, expected); + } + + // when selected + { + let items = list_items(vec!["Item 0", "Item 1", "Item 2"]); + let list = List::new(items) + .highlight_symbol(">>") + .highlight_spacing(HighlightSpacing::Never); + let mut state = ListState::default(); + state.select(Some(1)); + + let buffer = render_stateful_widget(list, &mut state, 10, 5); + + let expected = Buffer::with_lines(vec![ + "Item 0 ", + "Item 1 ", + "Item 2 ", + " ", + " ", + ]); + assert_buffer_eq!(buffer, expected); + } + } + #[test] fn test_list_repeat_highlight_symbol() { let items = list_items(vec!["Item 0\nLine 2", "Item 1", "Item 2"]); diff --git a/src/widgets/table.rs b/src/widgets/table.rs index e9255b3e5..4a4b6839f 100644 --- a/src/widgets/table.rs +++ b/src/widgets/table.rs @@ -324,7 +324,7 @@ impl<'a> Table<'a> { /// Set when to show the highlight spacing /// - /// See [HighlightSpacing] about which variant affects spacing in which way + /// See [`HighlightSpacing`] about which variant affects spacing in which way pub fn highlight_spacing(mut self, value: HighlightSpacing) -> Self { self.highlight_spacing = value; self diff --git a/tests/widgets_list.rs b/tests/widgets_list.rs index a25e8fed8..2224c7993 100644 --- a/tests/widgets_list.rs +++ b/tests/widgets_list.rs @@ -7,7 +7,7 @@ use ratatui::{ style::{Color, Style}, symbols, text::Line, - widgets::{Block, Borders, List, ListItem, ListState}, + widgets::{Block, Borders, HighlightSpacing, List, ListItem, ListState}, Terminal, }; @@ -243,3 +243,128 @@ fn widget_list_should_not_ignore_empty_string_items() { terminal.backend().assert_buffer(&expected); } + +#[test] +fn widgets_list_enable_always_highlight_spacing() { + let test_case = |state: &mut ListState, space: HighlightSpacing, expected: Buffer| { + let backend = TestBackend::new(30, 8); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|f| { + let size = f.size(); + let table = List::new(vec![ + ListItem::new(vec![Line::from("Item 1"), Line::from("Item 1a")]), + ListItem::new(vec![Line::from("Item 2"), Line::from("Item 2b")]), + ListItem::new(vec![Line::from("Item 3"), Line::from("Item 3c")]), + ]) + .block(Block::default().borders(Borders::ALL)) + .highlight_symbol(">> ") + .highlight_spacing(space); + f.render_stateful_widget(table, size, state); + }) + .unwrap(); + terminal.backend().assert_buffer(&expected); + }; + + assert_eq!(HighlightSpacing::default(), HighlightSpacing::WhenSelected); + + let mut state = ListState::default(); + // no selection, "WhenSelected" should only allocate if selected + test_case( + &mut state, + HighlightSpacing::default(), + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Item 1 │", + "│Item 1a │", + "│Item 2 │", + "│Item 2b │", + "│Item 3 │", + "│Item 3c │", + "└────────────────────────────┘", + ]), + ); + + // no selection, "Always" should allocate regardless if selected or not + test_case( + &mut state, + HighlightSpacing::Always, + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│ Item 1 │", + "│ Item 1a │", + "│ Item 2 │", + "│ Item 2b │", + "│ Item 3 │", + "│ Item 3c │", + "└────────────────────────────┘", + ]), + ); + + // no selection, "Never" should never allocate regadless if selected or not + test_case( + &mut state, + HighlightSpacing::Never, + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Item 1 │", + "│Item 1a │", + "│Item 2 │", + "│Item 2b │", + "│Item 3 │", + "│Item 3c │", + "└────────────────────────────┘", + ]), + ); + + // select first, "WhenSelected" should only allocate if selected + state.select(Some(0)); + test_case( + &mut state, + HighlightSpacing::default(), + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│>> Item 1 │", + "│ Item 1a │", + "│ Item 2 │", + "│ Item 2b │", + "│ Item 3 │", + "│ Item 3c │", + "└────────────────────────────┘", + ]), + ); + + // select first, "Always" should allocate regardless if selected or not + state.select(Some(0)); + test_case( + &mut state, + HighlightSpacing::Always, + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│>> Item 1 │", + "│ Item 1a │", + "│ Item 2 │", + "│ Item 2b │", + "│ Item 3 │", + "│ Item 3c │", + "└────────────────────────────┘", + ]), + ); + + // select first, "Never" should never allocate regadless if selected or not + state.select(Some(0)); + test_case( + &mut state, + HighlightSpacing::Never, + Buffer::with_lines(vec![ + "┌────────────────────────────┐", + "│Item 1 │", + "│Item 1a │", + "│Item 2 │", + "│Item 2b │", + "│Item 3 │", + "│Item 3c │", + "└────────────────────────────┘", + ]), + ); +}