Add List AsyncWidget

This commit is contained in:
Joscha 2023-04-12 00:15:30 +02:00
parent 07960142e0
commit 267ef2bee9
2 changed files with 330 additions and 0 deletions

View file

@ -1,3 +1,5 @@
mod list;
mod popup;
pub use self::list::*;
pub use self::popup::*;

328
src/ui/widgets2/list.rs Normal file
View 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(())
}
}