Add List AsyncWidget
This commit is contained in:
parent
07960142e0
commit
267ef2bee9
2 changed files with 330 additions and 0 deletions
|
|
@ -1,3 +1,5 @@
|
||||||
|
mod list;
|
||||||
mod popup;
|
mod popup;
|
||||||
|
|
||||||
|
pub use self::list::*;
|
||||||
pub use self::popup::*;
|
pub use self::popup::*;
|
||||||
|
|
|
||||||
328
src/ui/widgets2/list.rs
Normal file
328
src/ui/widgets2/list.rs
Normal file
|
|
@ -0,0 +1,328 @@
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use toss::{AsyncWidget, Frame, Pos, Size, WidthDb};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Cursor<Id> {
|
||||||
|
/// Id of the element the cursor is pointing to.
|
||||||
|
///
|
||||||
|
/// If the rows change (e.g. reorder) but there is still a row with this id,
|
||||||
|
/// the cursor is moved to this row.
|
||||||
|
id: Id,
|
||||||
|
|
||||||
|
/// Index of the row the cursor is pointing to.
|
||||||
|
///
|
||||||
|
/// If the rows change and there is no longer a row with the cursor's id,
|
||||||
|
/// the cursor is moved up or down to the next selectable row. This way, it
|
||||||
|
/// stays close to its previous position.
|
||||||
|
idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Id> Cursor<Id> {
|
||||||
|
pub fn new(id: Id, idx: usize) -> Self {
|
||||||
|
Self { id, idx }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ListState<Id> {
|
||||||
|
/// Amount of lines that the list is scrolled, i.e. offset from the top.
|
||||||
|
offset: usize,
|
||||||
|
|
||||||
|
/// A cursor within the list.
|
||||||
|
///
|
||||||
|
/// Set to `None` if the list contains no selectable rows.
|
||||||
|
cursor: Option<Cursor<Id>>,
|
||||||
|
|
||||||
|
/// Height of the list when it was last rendered.
|
||||||
|
last_height: u16,
|
||||||
|
|
||||||
|
/// Rows when the list was last rendered.
|
||||||
|
last_rows: Vec<Option<Id>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Id> ListState<Id> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
offset: 0,
|
||||||
|
cursor: None,
|
||||||
|
last_height: 0,
|
||||||
|
last_rows: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected(&self) -> Option<&Id> {
|
||||||
|
self.cursor.as_ref().map(|cursor| &cursor.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Id: Clone> ListState<Id> {
|
||||||
|
fn first_selectable(&self) -> Option<Cursor<Id>> {
|
||||||
|
self.last_rows
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find_map(|(i, row)| row.as_ref().map(|id| Cursor::new(id.clone(), i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn last_selectable(&self) -> Option<Cursor<Id>> {
|
||||||
|
self.last_rows
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.rev()
|
||||||
|
.find_map(|(i, row)| row.as_ref().map(|id| Cursor::new(id.clone(), i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selectable_at_or_before_index(&self, i: usize) -> Option<Cursor<Id>> {
|
||||||
|
self.last_rows
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.take(i + 1)
|
||||||
|
.rev()
|
||||||
|
.find_map(|(i, row)| row.as_ref().map(|id| Cursor::new(id.clone(), i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selectable_at_or_after_index(&self, i: usize) -> Option<Cursor<Id>> {
|
||||||
|
self.last_rows
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.skip(i)
|
||||||
|
.find_map(|(i, row)| row.as_ref().map(|id| Cursor::new(id.clone(), i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selectable_before_index(&self, i: usize) -> Option<Cursor<Id>> {
|
||||||
|
self.last_rows
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.take(i)
|
||||||
|
.rev()
|
||||||
|
.find_map(|(i, row)| row.as_ref().map(|id| Cursor::new(id.clone(), i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selectable_after_index(&self, i: usize) -> Option<Cursor<Id>> {
|
||||||
|
self.last_rows
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.skip(i + 1)
|
||||||
|
.find_map(|(i, row)| row.as_ref().map(|id| Cursor::new(id.clone(), i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_cursor_to_make_it_visible(&mut self) {
|
||||||
|
if let Some(cursor) = &self.cursor {
|
||||||
|
let first_visible_line_idx = self.offset;
|
||||||
|
let last_visible_line_idx = self
|
||||||
|
.offset
|
||||||
|
.saturating_add(self.last_height.into())
|
||||||
|
.saturating_sub(1);
|
||||||
|
|
||||||
|
let new_cursor = if cursor.idx < first_visible_line_idx {
|
||||||
|
self.selectable_at_or_after_index(first_visible_line_idx)
|
||||||
|
} else if cursor.idx > last_visible_line_idx {
|
||||||
|
self.selectable_at_or_before_index(last_visible_line_idx)
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(new_cursor) = new_cursor {
|
||||||
|
self.cursor = Some(new_cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_so_cursor_is_visible(&mut self) {
|
||||||
|
if self.last_height == 0 {
|
||||||
|
// Cursor can't be visible because nothing is visible
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cursor) = &self.cursor {
|
||||||
|
// As long as height > 0, min <= max is true
|
||||||
|
let min = (cursor.idx + 1).saturating_sub(self.last_height.into());
|
||||||
|
let max = cursor.idx; // Rows have a height of 1
|
||||||
|
self.offset = self.offset.clamp(min, max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clamp_scrolling(&mut self) {
|
||||||
|
let min = 0;
|
||||||
|
let max = self.last_rows.len().saturating_sub(self.last_height.into());
|
||||||
|
self.offset = self.offset.clamp(min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_to(&mut self, new_offset: usize) {
|
||||||
|
self.offset = new_offset;
|
||||||
|
self.clamp_scrolling();
|
||||||
|
self.move_cursor_to_make_it_visible();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_cursor_to(&mut self, new_cursor: Cursor<Id>) {
|
||||||
|
self.cursor = Some(new_cursor);
|
||||||
|
self.scroll_so_cursor_is_visible();
|
||||||
|
self.clamp_scrolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll the list up by an amount of lines.
|
||||||
|
pub fn scroll_up(&mut self, lines: usize) {
|
||||||
|
self.scroll_to(self.offset.saturating_sub(lines));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll the list down by an amount of lines.
|
||||||
|
pub fn scroll_down(&mut self, lines: usize) {
|
||||||
|
self.scroll_to(self.offset.saturating_add(lines));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scroll so that the cursor is in the center of the widget, or at least as
|
||||||
|
/// close as possible.
|
||||||
|
pub fn center_cursor(&mut self) {
|
||||||
|
if let Some(cursor) = &self.cursor {
|
||||||
|
let height: usize = self.last_height.into();
|
||||||
|
self.scroll_to(cursor.idx.saturating_sub(height / 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor up to the next selectable row.
|
||||||
|
pub fn move_cursor_up(&mut self) {
|
||||||
|
if let Some(cursor) = &self.cursor {
|
||||||
|
if let Some(new_cursor) = self.selectable_before_index(cursor.idx) {
|
||||||
|
self.move_cursor_to(new_cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor down to the next selectable row.
|
||||||
|
pub fn move_cursor_down(&mut self) {
|
||||||
|
if let Some(cursor) = &self.cursor {
|
||||||
|
if let Some(new_cursor) = self.selectable_after_index(cursor.idx) {
|
||||||
|
self.move_cursor_to(new_cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to the first selectable row.
|
||||||
|
pub fn move_cursor_to_top(&mut self) {
|
||||||
|
if let Some(new_cursor) = self.first_selectable() {
|
||||||
|
self.move_cursor_to(new_cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor to the last selectable row.
|
||||||
|
pub fn move_cursor_to_bottom(&mut self) {
|
||||||
|
if let Some(new_cursor) = self.last_selectable() {
|
||||||
|
self.move_cursor_to(new_cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn widget<W>(&mut self) -> List<'_, Id, W> {
|
||||||
|
List {
|
||||||
|
state: self,
|
||||||
|
rows: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Id: Clone + Eq> ListState<Id> {
|
||||||
|
fn selectable_of_id(&self, id: &Id) -> Option<Cursor<Id>> {
|
||||||
|
self.last_rows
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find_map(|(i, row)| match row {
|
||||||
|
Some(rid) if rid == id => Some(Cursor::new(rid.clone(), i)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fix_cursor(&mut self) {
|
||||||
|
let new_cursor = if let Some(cursor) = &self.cursor {
|
||||||
|
self.selectable_of_id(&cursor.id)
|
||||||
|
.or_else(|| self.selectable_at_or_before_index(cursor.idx))
|
||||||
|
.or_else(|| self.selectable_at_or_after_index(cursor.idx))
|
||||||
|
} else {
|
||||||
|
self.first_selectable()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(new_cursor) = new_cursor {
|
||||||
|
self.move_cursor_to(new_cursor);
|
||||||
|
} else {
|
||||||
|
self.cursor = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Row<Id, W> {
|
||||||
|
id: Option<Id>,
|
||||||
|
widget: W,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct List<'a, Id, W> {
|
||||||
|
state: &'a mut ListState<Id>,
|
||||||
|
rows: Vec<Row<Id, W>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Id, W> List<'_, Id, W> {
|
||||||
|
pub fn state(&self) -> &ListState<Id> {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state_mut(&mut self) -> &mut ListState<Id> {
|
||||||
|
&mut self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.rows.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_unsel(&mut self, widget: W) {
|
||||||
|
self.rows.push(Row { id: None, widget });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_sel(&mut self, id: Id, widget: W) {
|
||||||
|
self.rows.push(Row {
|
||||||
|
id: Some(id),
|
||||||
|
widget,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<Id, E, W> AsyncWidget<E> for List<'_, Id, W>
|
||||||
|
where
|
||||||
|
Id: Clone + Eq + Send + Sync,
|
||||||
|
W: AsyncWidget<E> + Send + Sync,
|
||||||
|
{
|
||||||
|
async fn size(
|
||||||
|
&self,
|
||||||
|
widthdb: &mut WidthDb,
|
||||||
|
max_width: Option<u16>,
|
||||||
|
_max_height: Option<u16>,
|
||||||
|
) -> Result<Size, E> {
|
||||||
|
let mut width = 0;
|
||||||
|
for row in &self.rows {
|
||||||
|
let size = row.widget.size(widthdb, max_width, Some(1)).await?;
|
||||||
|
width = width.max(size.width);
|
||||||
|
}
|
||||||
|
let height = self.rows.len().try_into().unwrap_or(u16::MAX);
|
||||||
|
Ok(Size::new(width, height))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||||
|
let size = frame.size();
|
||||||
|
|
||||||
|
self.state.last_rows = self.rows.iter().map(|row| row.id.clone()).collect();
|
||||||
|
self.state.last_height = size.height;
|
||||||
|
self.state.fix_cursor();
|
||||||
|
|
||||||
|
for (y, row) in self
|
||||||
|
.rows
|
||||||
|
.into_iter()
|
||||||
|
.skip(self.state.offset)
|
||||||
|
.take(size.height.into())
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
frame.push(Pos::new(0, y as i32), Size::new(size.width, 1));
|
||||||
|
row.widget.draw(frame).await?;
|
||||||
|
frame.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue