use std::sync::Arc; use async_trait::async_trait; use parking_lot::Mutex; use toss::frame::{Frame, Pos, Size}; use super::Widget; /////////// // State // /////////// #[derive(Debug, Clone)] struct Cursor { /// 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 Cursor { pub fn new(id: Id, idx: usize) -> Self { Self { id, idx } } } #[derive(Debug)] struct InnerListState { rows: Vec>, offset: usize, cursor: Option>, make_cursor_visible: bool, } impl InnerListState { fn new() -> Self { Self { rows: vec![], offset: 0, cursor: None, make_cursor_visible: false, } } } impl InnerListState { fn first_selectable(&self) -> Option> { self.rows .iter() .enumerate() .find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i))) } fn last_selectable(&self) -> Option> { self.rows .iter() .enumerate() .rev() .find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i))) } fn selectable_at_or_before_index(&self, i: usize) -> Option> { self.rows .iter() .enumerate() .take(i + 1) .rev() .find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i))) } fn selectable_at_or_after_index(&self, i: usize) -> Option> { self.rows .iter() .enumerate() .skip(i) .find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i))) } fn selectable_before_index(&self, i: usize) -> Option> { self.rows .iter() .enumerate() .take(i) .rev() .find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i))) } fn selectable_after_index(&self, i: usize) -> Option> { self.rows .iter() .enumerate() .skip(i + 1) .find_map(|(i, r)| r.as_ref().map(|c| Cursor::new(c.clone(), i))) } fn make_cursor_visible(&mut self, height: usize) { if 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(height); let max = cursor.idx; self.offset = self.offset.clamp(min, max); } } fn clamp_scrolling(&mut self, height: usize) { let min = 0; let max = self.rows.len().saturating_sub(height); self.offset = self.offset.clamp(min, max); } } impl InnerListState { fn selectable_of_id(&self, id: &Id) -> Option> { self.rows.iter().enumerate().find_map(|(i, r)| match r { Some(rid) if rid == id => Some(Cursor::new(id.clone(), i)), _ => None, }) } fn fix_cursor(&mut self) { self.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() } } /// Bring the list into a state consistent with the current rows and height. fn stabilize(&mut self, rows: &[Row], height: usize) { self.rows = rows.iter().map(|r| r.id().cloned()).collect(); self.fix_cursor(); if self.make_cursor_visible { self.make_cursor_visible(height); self.make_cursor_visible = false; } self.clamp_scrolling(height); } } pub struct ListState(Arc>>); impl ListState { pub fn new() -> Self { Self(Arc::new(Mutex::new(InnerListState::new()))) } pub fn list(&self) -> List { List::new(self.0.clone()) } pub fn scroll_up(&mut self) { let mut guard = self.0.lock(); guard.offset = guard.offset.saturating_sub(1); } pub fn scroll_down(&mut self) { let mut guard = self.0.lock(); guard.offset = guard.offset.saturating_add(1); } } impl ListState { pub fn cursor(&self) -> Option { self.0.lock().cursor.as_ref().map(|c| c.id.clone()) } pub fn move_cursor_up(&mut self) { let mut guard = self.0.lock(); if let Some(cursor) = &guard.cursor { if let Some(new_cursor) = guard.selectable_before_index(cursor.idx) { guard.cursor = Some(new_cursor); guard.make_cursor_visible = true; } } } pub fn move_cursor_down(&mut self) { let mut guard = self.0.lock(); if let Some(cursor) = &guard.cursor { if let Some(new_cursor) = guard.selectable_after_index(cursor.idx) { guard.cursor = Some(new_cursor); guard.make_cursor_visible = true; } } } } //////////// // Widget // //////////// enum Row { Unselectable { normal: Box, }, Selectable { id: Id, normal: Box, selected: Box, }, } impl Row { fn id(&self) -> Option<&Id> { match self { Row::Unselectable { .. } => None, Row::Selectable { id, .. } => Some(id), } } fn size(&self, frame: &mut Frame, max_width: Option, max_height: Option) -> Size { match self { Row::Unselectable { normal } => normal.size(frame, max_width, max_height), Row::Selectable { normal, selected, .. } => { let normal_size = normal.size(frame, max_width, max_height); let selected_size = selected.size(frame, max_width, max_height); Size::new( normal_size.width.max(selected_size.width), normal_size.height.max(selected_size.height), ) } } } } pub struct List { state: Arc>>, rows: Vec>, focus: bool, } impl List { fn new(state: Arc>>) -> Self { Self { state, rows: vec![], focus: false, } } pub fn focus(mut self, focus: bool) -> Self { self.focus = focus; self } pub fn is_empty(&self) -> bool { self.rows.is_empty() } pub fn add_unsel(&mut self, normal: W) { self.rows.push(Row::Unselectable { normal: Box::new(normal), }); } pub fn add_sel(&mut self, id: Id, normal: W1, selected: W2) where W1: 'static + Widget + Send, W2: 'static + Widget + Send, { self.rows.push(Row::Selectable { id, normal: Box::new(normal), selected: Box::new(selected), }); } } #[async_trait] impl Widget for List { fn size(&self, frame: &mut Frame, max_width: Option, _max_height: Option) -> Size { let width = self .rows .iter() .map(|r| r.size(frame, max_width, Some(1)).width) .max() .unwrap_or(0); let height = self.rows.len(); Size::new(width, height as u16) } async fn render(self: Box, frame: &mut Frame, pos: Pos, size: Size) { // Guard acquisition and dropping must be inside its own block or the // compiler complains that "future created by async block is not // `Send`", pointing to the function body. // // I assume this is because I'm using the parking lot mutex whose guard // is not Send, and even though I was explicitly dropping it with // drop(), rustc couldn't figure this out without some help. let (offset, cursor) = { let mut guard = self.state.lock(); guard.stabilize(&self.rows, size.height.into()); (guard.offset as i32, guard.cursor.clone()) }; let row_size = Size::new(size.width, 1); for (i, row) in self.rows.into_iter().enumerate() { let dy = i as i32 - offset; if dy < 0 || dy >= size.height as i32 { break; } let pos = pos + Pos::new(0, dy); match row { Row::Unselectable { normal } => normal.render(frame, pos, row_size).await, Row::Selectable { id, normal, selected, } => { let focusing = self.focus && if let Some(cursor) = &cursor { cursor.id == id } else { false }; let widget = if focusing { selected } else { normal }; widget.render(frame, pos, row_size).await; } } } } }