mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-10 07:04:17 +00:00
fix(list): Modify List and List example to support saving offsets. (#667)
The current `List` example will unselect and reset the position of a list. This PR will save the last selected item, and updates `List` to honor its offset, preventing the list from resetting when the user `unselect()`s a `StatefulList`.
This commit is contained in:
parent
2819eea82b
commit
b3a57f3dff
2 changed files with 83 additions and 29 deletions
|
@ -14,6 +14,7 @@ use ratatui::{prelude::*, widgets::*};
|
|||
struct StatefulList<T> {
|
||||
state: ListState,
|
||||
items: Vec<T>,
|
||||
last_selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
|
@ -21,6 +22,7 @@ impl<T> StatefulList<T> {
|
|||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
last_selected: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,7 +35,7 @@ impl<T> StatefulList<T> {
|
|||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
None => self.last_selected.unwrap_or(0),
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
@ -47,13 +49,16 @@ impl<T> StatefulList<T> {
|
|||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
None => self.last_selected.unwrap_or(0),
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn unselect(&mut self) {
|
||||
let offset = self.state.offset();
|
||||
self.last_selected = self.state.selected();
|
||||
self.state.select(None);
|
||||
*self.state.offset_mut() = offset;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -738,6 +738,7 @@ impl<'a> List<'a> {
|
|||
self.items.is_empty()
|
||||
}
|
||||
|
||||
/// Given an offset, calculate which items can fit in a given area
|
||||
fn get_items_bounds(
|
||||
&self,
|
||||
selected: Option<usize>,
|
||||
|
@ -745,35 +746,69 @@ impl<'a> List<'a> {
|
|||
max_height: usize,
|
||||
) -> (usize, usize) {
|
||||
let offset = offset.min(self.items.len().saturating_sub(1));
|
||||
let mut start = offset;
|
||||
let mut end = offset;
|
||||
let mut height = 0;
|
||||
|
||||
// Note: visible here implies visible in the given area
|
||||
let mut first_visible_index = offset;
|
||||
let mut last_visible_index = offset;
|
||||
|
||||
// Current height of all items in the list to render, beginning at the offset
|
||||
let mut height_from_offset = 0;
|
||||
|
||||
// Calculate the last visible index and total height of the items
|
||||
// that will fit in the available space
|
||||
for item in self.items.iter().skip(offset) {
|
||||
if height + item.height() > max_height {
|
||||
if height_from_offset + item.height() > max_height {
|
||||
break;
|
||||
}
|
||||
height += item.height();
|
||||
end += 1;
|
||||
|
||||
height_from_offset += item.height();
|
||||
|
||||
last_visible_index += 1;
|
||||
}
|
||||
|
||||
let selected = selected.unwrap_or(0).min(self.items.len() - 1);
|
||||
while selected >= end {
|
||||
height = height.saturating_add(self.items[end].height());
|
||||
end += 1;
|
||||
while height > max_height {
|
||||
height = height.saturating_sub(self.items[start].height());
|
||||
start += 1;
|
||||
// Get the selected index, but still honor the offset if nothing is selected
|
||||
// This allows for the list to stay at a position after select()ing None.
|
||||
let index_to_display = selected.unwrap_or(offset).min(self.items.len() - 1);
|
||||
|
||||
// Recall that last_visible_index is the index of what we
|
||||
// can render up to in the given space after the offset
|
||||
// If we have an item selected that is out of the viewable area (or
|
||||
// the offset is still set), we still need to show this item
|
||||
while index_to_display >= last_visible_index {
|
||||
height_from_offset =
|
||||
height_from_offset.saturating_add(self.items[last_visible_index].height());
|
||||
|
||||
last_visible_index += 1;
|
||||
|
||||
// Now we need to hide previous items since we didn't have space
|
||||
// for the selected/offset item
|
||||
while height_from_offset > max_height {
|
||||
height_from_offset =
|
||||
height_from_offset.saturating_sub(self.items[first_visible_index].height());
|
||||
|
||||
// Remove this item to view by starting at the next item index
|
||||
first_visible_index += 1;
|
||||
}
|
||||
}
|
||||
while selected < start {
|
||||
start -= 1;
|
||||
height = height.saturating_add(self.items[start].height());
|
||||
while height > max_height {
|
||||
end -= 1;
|
||||
height = height.saturating_sub(self.items[end].height());
|
||||
|
||||
// Here we're doing something similar to what we just did above
|
||||
// If the selected item index is not in the viewable area, let's try to show the item
|
||||
while index_to_display < first_visible_index {
|
||||
first_visible_index -= 1;
|
||||
|
||||
height_from_offset =
|
||||
height_from_offset.saturating_add(self.items[first_visible_index].height());
|
||||
|
||||
// Don't show an item if it is beyond our viewable height
|
||||
while height_from_offset > max_height {
|
||||
last_visible_index -= 1;
|
||||
|
||||
height_from_offset =
|
||||
height_from_offset.saturating_sub(self.items[last_visible_index].height());
|
||||
}
|
||||
}
|
||||
(start, end)
|
||||
|
||||
(first_visible_index, last_visible_index)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -791,18 +826,19 @@ impl<'a> StatefulWidget for List<'a> {
|
|||
None => area,
|
||||
};
|
||||
|
||||
if list_area.width < 1 || list_area.height < 1 {
|
||||
if self.items.is_empty() || list_area.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
let list_height = list_area.height as usize;
|
||||
|
||||
let (start, end) = self.get_items_bounds(state.selected, state.offset, list_height);
|
||||
state.offset = start;
|
||||
let (first_visible_index, last_visible_index) =
|
||||
self.get_items_bounds(state.selected, state.offset, list_height);
|
||||
|
||||
// Important: this changes the state's offset to be the beginning of the now viewable items
|
||||
state.offset = first_visible_index;
|
||||
|
||||
// Get our set highlighted symbol (if one was set)
|
||||
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
||||
let blank_symbol = " ".repeat(highlight_symbol.width());
|
||||
|
||||
|
@ -813,7 +849,7 @@ impl<'a> StatefulWidget for List<'a> {
|
|||
.iter_mut()
|
||||
.enumerate()
|
||||
.skip(state.offset)
|
||||
.take(end - start)
|
||||
.take(last_visible_index - first_visible_index)
|
||||
{
|
||||
let (x, y) = if self.direction == ListDirection::BottomToTop {
|
||||
current_height += item.height() as u16;
|
||||
|
@ -1649,6 +1685,19 @@ mod tests {
|
|||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_offset_renders_shifted() {
|
||||
let items = list_items(vec![
|
||||
"Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6",
|
||||
]);
|
||||
let list = List::new(items);
|
||||
let mut state = ListState::default().with_offset(3);
|
||||
let buffer = render_stateful_widget(list, &mut state, 6, 3);
|
||||
|
||||
let expected = Buffer::with_lines(vec!["Item 3", "Item 4", "Item 5"]);
|
||||
assert_buffer_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_long_lines() {
|
||||
let items = list_items(vec![
|
||||
|
|
Loading…
Reference in a new issue