Skip to content

Commit

Permalink
feat(list): add option to always allocate the "selection" column width (
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
hasezoey committed Aug 13, 2023
1 parent 10dbd6f commit 4d70169
Show file tree
Hide file tree
Showing 3 changed files with 269 additions and 5 deletions.
145 changes: 142 additions & 3 deletions src/widgets/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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> {
Expand All @@ -118,6 +120,7 @@ impl<'a> List<'a> {
highlight_style: Style::default(),
highlight_symbol: None,
repeat_highlight_symbol: false,
highlight_spacing: HighlightSpacing::default(),
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"]);
Expand Down
2 changes: 1 addition & 1 deletion src/widgets/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
127 changes: 126 additions & 1 deletion tests/widgets_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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 β”‚",
"β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜",
]),
);
}

0 comments on commit 4d70169

Please sign in to comment.