mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-21 20:23:11 +00:00
feat(widgets/list): allow padding selected using layout constraints
Consider padding when choosing offset/scrolling to selected item resolves #328
This commit is contained in:
parent
a6b25a4877
commit
eb65100e2c
3 changed files with 251 additions and 5 deletions
|
@ -43,6 +43,20 @@ impl Constraint {
|
|||
Constraint::Min(m) => length.max(m),
|
||||
}
|
||||
}
|
||||
|
||||
/// returns the new target padding based on the constraint
|
||||
pub fn apply_for_padding(&self, total_length: u16, current_padding: u16) -> u16 {
|
||||
match *self {
|
||||
Constraint::Percentage(p) => total_length * p / 100,
|
||||
Constraint::Ratio(num, den) => {
|
||||
let r = num * u32::from(total_length) / den;
|
||||
r as u16
|
||||
}
|
||||
Constraint::Length(l) => total_length.min(l),
|
||||
Constraint::Max(m) => current_padding.min(m),
|
||||
Constraint::Min(m) => current_padding.max(m),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Corner, Rect},
|
||||
layout::{Constraint, Corner, Rect},
|
||||
style::Style,
|
||||
text::Text,
|
||||
widgets::{Block, StatefulWidget, Widget},
|
||||
|
@ -10,6 +12,7 @@ use unicode_width::UnicodeWidthStr;
|
|||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ListState {
|
||||
offset: usize,
|
||||
padding: (Option<Constraint>, Option<Constraint>),
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
|
@ -24,6 +27,33 @@ impl ListState {
|
|||
self.offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply padding when scrolling selected item into view.
|
||||
///
|
||||
/// The scrolling offset algorithm prioritizes `top_padding_constraint` over `bottom_padding_constraint`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use tui::layout::Constraint;
|
||||
/// let mut state = tui::widgets::ListState::default();
|
||||
/// state.padding(
|
||||
/// Some(Constraint::Percentage(50)),
|
||||
/// Some(Constraint::Percentage(50)),
|
||||
/// );
|
||||
/// // or
|
||||
/// state.padding(None, Some(Constraint::Length(3)));
|
||||
/// // or
|
||||
/// state.padding(Some(Constraint::Max(6)), Some(Constraint::Min(3)));
|
||||
/// // etc.
|
||||
/// ```
|
||||
pub fn padding(
|
||||
&mut self,
|
||||
top_padding_constraint: Option<Constraint>,
|
||||
bottom_padding_constraint: Option<Constraint>,
|
||||
) {
|
||||
self.padding = (top_padding_constraint, bottom_padding_constraint);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
@ -131,6 +161,7 @@ impl<'a> List<'a> {
|
|||
fn get_items_bounds(
|
||||
&self,
|
||||
selected: Option<usize>,
|
||||
padding: (Option<Constraint>, Option<Constraint>),
|
||||
offset: usize,
|
||||
max_height: usize,
|
||||
) -> (usize, usize) {
|
||||
|
@ -147,7 +178,42 @@ impl<'a> List<'a> {
|
|||
}
|
||||
|
||||
let selected = selected.unwrap_or(0).min(self.items.len() - 1);
|
||||
while selected >= end {
|
||||
|
||||
// This function prioritizes the ideal start padding to the ideal end padding
|
||||
let padding_cmp_ideal = |start: usize, end: usize| {
|
||||
let end_cmp_ideal = padding
|
||||
.1
|
||||
.map(|c| {
|
||||
let current_padding = self
|
||||
.items
|
||||
.get((selected + 1)..end)
|
||||
.map(|ir| ir.iter().map(|i| i.height()).sum::<usize>() as u16)
|
||||
.unwrap_or(0);
|
||||
current_padding.cmp(&c.apply_for_padding(max_height as u16, current_padding))
|
||||
})
|
||||
.unwrap_or(Ordering::Equal);
|
||||
let start_cmp_ideal = padding
|
||||
.0
|
||||
.map(|c| {
|
||||
let current_padding = self
|
||||
.items
|
||||
.get(start..selected)
|
||||
.map(|ir| ir.iter().map(|i| i.height()).sum::<usize>() as u16)
|
||||
.unwrap_or(0);
|
||||
current_padding.cmp(&c.apply_for_padding(max_height as u16, current_padding))
|
||||
})
|
||||
.unwrap_or(Ordering::Equal);
|
||||
|
||||
if start_cmp_ideal == Ordering::Equal {
|
||||
end_cmp_ideal.reverse()
|
||||
} else {
|
||||
start_cmp_ideal
|
||||
}
|
||||
};
|
||||
|
||||
while selected >= end
|
||||
|| (padding_cmp_ideal(start, end) == Ordering::Greater && end < self.items.len())
|
||||
{
|
||||
height = height.saturating_add(self.items[end].height());
|
||||
end += 1;
|
||||
while height > max_height {
|
||||
|
@ -155,7 +221,7 @@ impl<'a> List<'a> {
|
|||
start += 1;
|
||||
}
|
||||
}
|
||||
while selected < start {
|
||||
while selected < start || (padding_cmp_ideal(start, end) == Ordering::Less && start > 0) {
|
||||
start -= 1;
|
||||
height = height.saturating_add(self.items[start].height());
|
||||
while height > max_height {
|
||||
|
@ -190,7 +256,8 @@ impl<'a> StatefulWidget for List<'a> {
|
|||
}
|
||||
let list_height = list_area.height as usize;
|
||||
|
||||
let (start, end) = self.get_items_bounds(state.selected, state.offset, list_height);
|
||||
let (start, end) =
|
||||
self.get_items_bounds(state.selected, state.padding, state.offset, list_height);
|
||||
state.offset = start;
|
||||
|
||||
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
layout::{Constraint, Rect},
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::Spans,
|
||||
|
@ -198,3 +198,168 @@ fn widgets_list_should_repeat_highlight_symbol() {
|
|||
}
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_list_should_respect_padding() {
|
||||
let backend = TestBackend::new(10, 4);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(4));
|
||||
|
||||
state.padding(None, Some(Constraint::Percentage(50)));
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
ListItem::new("Item 0"),
|
||||
ListItem::new("Item 1"),
|
||||
ListItem::new("Item 2"),
|
||||
ListItem::new("Item 3"),
|
||||
ListItem::new("Item 4"),
|
||||
ListItem::new("Item 5"),
|
||||
ListItem::new("Item 6"),
|
||||
ListItem::new("Item 7"),
|
||||
];
|
||||
let list = List::new(items)
|
||||
.highlight_symbol(">> ")
|
||||
.repeat_highlight_symbol(true);
|
||||
f.render_stateful_widget(list, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(vec![" Item 3 ", ">> Item 4 ", " Item 5 ", " Item 6 "]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
|
||||
state.padding(
|
||||
Some(Constraint::Percentage(50)),
|
||||
Some(Constraint::Percentage(50)),
|
||||
);
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
ListItem::new("Item 0"),
|
||||
ListItem::new("Item 1"),
|
||||
ListItem::new("Item 2"),
|
||||
ListItem::new("Item 3"),
|
||||
ListItem::new("Item 4"),
|
||||
ListItem::new("Item 5"),
|
||||
ListItem::new("Item 6"),
|
||||
ListItem::new("Item 7"),
|
||||
];
|
||||
let list = List::new(items)
|
||||
.highlight_symbol(">> ")
|
||||
.repeat_highlight_symbol(true);
|
||||
f.render_stateful_widget(list, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(vec![" Item 2 ", " Item 3 ", ">> Item 4 ", " Item 5 "]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
|
||||
state.padding(Some(Constraint::Max(1)), None);
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
ListItem::new("Item 0"),
|
||||
ListItem::new("Item 1"),
|
||||
ListItem::new("Item 2"),
|
||||
ListItem::new("Item 3"),
|
||||
ListItem::new("Item 4"),
|
||||
ListItem::new("Item 5"),
|
||||
ListItem::new("Item 6"),
|
||||
ListItem::new("Item 7"),
|
||||
];
|
||||
let list = List::new(items)
|
||||
.highlight_symbol(">> ")
|
||||
.repeat_highlight_symbol(true);
|
||||
f.render_stateful_widget(list, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(vec![" Item 3 ", ">> Item 4 ", " Item 5 ", " Item 6 "]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
|
||||
//Prefers top padding to bottom padding
|
||||
state.padding(Some(Constraint::Length(3)), Some(Constraint::Length(3)));
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
ListItem::new("Item 0"),
|
||||
ListItem::new("Item 1"),
|
||||
ListItem::new("Item 2"),
|
||||
ListItem::new("Item 3"),
|
||||
ListItem::new("Item 4"),
|
||||
ListItem::new("Item 5"),
|
||||
ListItem::new("Item 6"),
|
||||
ListItem::new("Item 7"),
|
||||
];
|
||||
let list = List::new(items)
|
||||
.highlight_symbol(">> ")
|
||||
.repeat_highlight_symbol(true);
|
||||
f.render_stateful_widget(list, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(vec![" Item 1 ", " Item 2 ", " Item 3 ", ">> Item 4 "]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_list_padding_doesnt_panic_for_offscreen_offset() {
|
||||
let backend = TestBackend::new(10, 1);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(7));
|
||||
|
||||
state.padding(
|
||||
Some(Constraint::Length(0)),
|
||||
Some(Constraint::Percentage(100)),
|
||||
);
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
ListItem::new("Item 0"),
|
||||
ListItem::new("Item 1"),
|
||||
ListItem::new("Item 2"),
|
||||
ListItem::new("Item 3"),
|
||||
ListItem::new("Item 4"),
|
||||
ListItem::new("Item 5"),
|
||||
ListItem::new("Item 6"),
|
||||
ListItem::new("Item 7"),
|
||||
];
|
||||
let list = List::new(items)
|
||||
.highlight_symbol(">> ")
|
||||
.repeat_highlight_symbol(true);
|
||||
f.render_stateful_widget(list, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(vec![">> Item 7 "]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
|
||||
// now the state.offset is set to 7
|
||||
// we set state.selected to 1
|
||||
// and we just check that it doesnt cause a panic in the padding code
|
||||
|
||||
state.select(Some(1));
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
ListItem::new("Item 0"),
|
||||
ListItem::new("Item 1"),
|
||||
ListItem::new("Item 2"),
|
||||
ListItem::new("Item 3"),
|
||||
ListItem::new("Item 4"),
|
||||
ListItem::new("Item 5"),
|
||||
ListItem::new("Item 6"),
|
||||
ListItem::new("Item 7"),
|
||||
];
|
||||
let list = List::new(items)
|
||||
.highlight_symbol(">> ")
|
||||
.repeat_highlight_symbol(true);
|
||||
f.render_stateful_widget(list, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(vec![">> Item 1 "]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue