From 0b068e486e5c4519ed1404cc601bdd9bfdc2e901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 4 May 2024 13:34:41 +0200 Subject: [PATCH] Infinite `List` widget from upstream feature/list-widget-reloaded branch --- Cargo.lock | 7 + examples/list/Cargo.toml | 10 + examples/list/src/main.rs | 107 +++++ widget/src/helpers.rs | 13 + widget/src/lib.rs | 3 + widget/src/list.rs | 792 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 932 insertions(+) create mode 100644 examples/list/Cargo.toml create mode 100644 examples/list/src/main.rs create mode 100644 widget/src/list.rs diff --git a/Cargo.lock b/Cargo.lock index d63e6186..42ac27f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3741,6 +3741,13 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "list" +version = "0.1.0" +dependencies = [ + "iced", +] + [[package]] name = "litemap" version = "0.8.1" diff --git a/examples/list/Cargo.toml b/examples/list/Cargo.toml new file mode 100644 index 00000000..42d476e2 --- /dev/null +++ b/examples/list/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "list" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["debug", "winit", "tokio", "wayland"] diff --git a/examples/list/src/main.rs b/examples/list/src/main.rs new file mode 100644 index 00000000..5f60f5c5 --- /dev/null +++ b/examples/list/src/main.rs @@ -0,0 +1,107 @@ +use iced::widget::{ + button, center, column, container, list, row, scrollable, + space::horizontal, text, +}; +use iced::{Alignment, Element, Length, Task, Theme}; + +pub fn main() -> iced::Result { + iced::application(List::new, List::update, List::view) + .title(List::title) + .window_size((500.0, 800.0)) + .theme(List::theme) + .run() +} + +struct List { + content: list::Content<(usize, State)>, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + Update(usize), + Remove(usize), +} + +impl List { + fn new() -> (Self, Task) { + (Self::default(), Task::none()) + } + + fn title(&self) -> String { + "List - Iced".to_string() + } + + fn theme(&self) -> Theme { + Theme::TokyoNight + } + + fn update(&mut self, message: Message) -> Task { + match message { + Message::Update(index) => { + if let Some((_id, state)) = self.content.get_mut(index) { + *state = State::Updated; + } + } + Message::Remove(index) => { + let _ = self.content.remove(index); + } + } + Task::none() + } + + fn view(&self) -> Element<'_, Message> { + center( + scrollable( + container(list(&self.content, |index, (id, state)| { + row![ + match state { + State::Idle => + Element::from(text(format!("I am item {id}!"))), + State::Updated => center( + column![ + text(format!("I am item {id}!")), + text("... but different!") + ] + .spacing(20) + ) + .height(300) + .into(), + }, + horizontal(), + button("Update").on_press_maybe( + matches!(state, State::Idle) + .then_some(Message::Update(index)) + ), + button("Remove") + .on_press(Message::Remove(index)) + .style(button::danger) + ] + .spacing(10) + .padding(5) + .align_y(Alignment::Center) + .into() + })) + .padding(10), + ) + .width(Length::Fill), + ) + .padding(10) + .into() + } +} + +impl Default for List { + fn default() -> Self { + Self { + content: list::Content::from_iter( + (0..1_000).map(|id| (id, State::Idle)), + ), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum State { + Idle, + Updated, +} diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index a4f81b6d..1cad36d9 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -10,6 +10,7 @@ use crate::core::window; use crate::core::{Element, Length, Size, Widget}; use crate::float::{self, Float}; use crate::keyed; +use crate::list::{self, List}; use crate::overlay; use crate::pane_grid::{self, PaneGrid}; use crate::pick_list::{self, PickList}; @@ -1054,6 +1055,18 @@ where Scrollable::new(content) } +/// Creates a new [`List`] with the provided [`Content`] and +/// closure to view an item of the [`List`]. +/// +/// [`List`]: crate::List +/// [`Content`]: crate::list::Content +pub fn list<'a, T, Message, Theme, Renderer>( + content: &'a list::Content, + view_item: impl Fn(usize, &'a T) -> Element<'a, Message, Theme, Renderer> + 'a, +) -> List<'a, T, Message, Theme, Renderer> { + List::new(content, view_item) +} + /// Creates a new [`Button`] with the provided content. /// /// # Example diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 3164db2d..d6034101 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -24,6 +24,7 @@ pub mod container; pub mod float; pub mod grid; pub mod keyed; +pub mod list; pub mod overlay; pub mod pane_grid; pub mod pick_list; @@ -68,6 +69,8 @@ pub use float::Float; #[doc(no_inline)] pub use grid::Grid; #[doc(no_inline)] +pub use list::List; +#[doc(no_inline)] pub use mouse_area::MouseArea; #[doc(no_inline)] pub use pane_grid::PaneGrid; diff --git a/widget/src/list.rs b/widget/src/list.rs new file mode 100644 index 00000000..de9fe9b8 --- /dev/null +++ b/widget/src/list.rs @@ -0,0 +1,792 @@ +#![allow(missing_docs)] +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget; +use crate::core::widget::tree::{self, Tree}; +use crate::core::window; +use crate::core::{ + self, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, + Size, Vector, Widget, +}; + +use std::cell::RefCell; +use std::cmp::Ordering; +use std::collections::VecDeque; + +#[allow(missing_debug_implementations)] +pub struct List<'a, T, Message, Theme, Renderer> { + content: &'a Content, + spacing: f32, + view_item: + Box Element<'a, Message, Theme, Renderer> + 'a>, + visible_elements: Vec>, +} + +impl<'a, T, Message, Theme, Renderer> List<'a, T, Message, Theme, Renderer> { + pub fn new( + content: &'a Content, + view_item: impl Fn(usize, &'a T) -> Element<'a, Message, Theme, Renderer> + + 'a, + ) -> Self { + Self { + content, + spacing: 0.0, + view_item: Box::new(view_item), + visible_elements: Vec::new(), + } + } + + /// Sets the vertical spacing _between_ elements. + /// + /// Custom margins per element do not exist in iced. You should use this + /// method instead! While less flexible, it helps you keep spacing between + /// elements consistent. + pub fn spacing(mut self, amount: impl Into) -> Self { + self.spacing = amount.into().0; + self + } +} + +struct State { + last_limits: layout::Limits, + visible_layouts: Vec<(usize, layout::Node, Tree)>, + size: Size, + offsets: Vec, + widths: Vec, + task: Task, + visible_outdated: bool, +} + +enum Task { + Idle, + Computing { + current: usize, + offsets: Vec, + widths: Vec, + size: Size, + }, +} + +impl State { + fn recompute(&mut self, size: usize) { + let mut offsets = Vec::with_capacity(size + 1); + offsets.push(0.0); + + self.task = Task::Computing { + current: 0, + offsets, + widths: Vec::with_capacity(size), + size: Size::ZERO, + }; + self.visible_layouts.clear(); + } +} + +impl<'a, T, Message, Theme, Renderer> Widget + for List<'a, T, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State { + last_limits: layout::Limits::NONE, + visible_layouts: Vec::new(), + size: Size::ZERO, + offsets: vec![0.0], + widths: Vec::new(), + task: Task::Idle, + visible_outdated: false, + }) + } + + fn size(&self) -> Size { + Size { + width: Length::Shrink, + height: Length::Shrink, + } + } + + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let state = tree.state.downcast_mut::(); + let loose_limits = limits.loose(); + + if state.last_limits != loose_limits { + state.last_limits = loose_limits; + state.recompute(self.content.len()); + } + + let mut changes = self.content.changes.borrow_mut(); + + match state.task { + Task::Idle => { + while let Some(change) = changes.pop_front() { + match change { + Change::Updated { original, current } => { + let mut new_element = (self.view_item)( + current, + &self.content.items[current], + ); + + let visible_index = state + .visible_layouts + .iter_mut() + .position(|(i, _, _)| *i == original); + + let mut new_tree; + + // Update if visible + let tree = + if let Some(visible_index) = visible_index { + let (_i, _layout, tree) = &mut state + .visible_layouts[visible_index]; + + tree.diff(&mut new_element); + state.visible_outdated = true; + + tree + } else { + new_tree = Tree::new(&new_element); + + &mut new_tree + }; + + let new_layout = new_element + .as_widget_mut() + .layout(tree, renderer, &state.last_limits); + + let new_size = new_layout.size(); + + let height_difference = new_size.height + - (state.offsets[original + 1] + - state.offsets[original]); + + for offset in &mut state.offsets[original + 1..] { + *offset += height_difference; + } + + let original_width = state.widths[original]; + state.widths[original] = new_size.width; + + if let Some(visible_index) = visible_index { + state.visible_layouts[visible_index].1 = + new_layout; + + for (i, layout, _) in + &mut state.visible_layouts[visible_index..] + { + layout + .move_to_mut((0.0, state.offsets[*i])); + } + } else if let Some(first_visible) = + state.visible_layouts.first() + { + let first_visible_index = first_visible.0; + if original < first_visible_index { + for (i, layout, _) in + &mut state.visible_layouts[..] + { + layout.move_to_mut(( + 0.0, + state.offsets[*i], + )); + } + } + } + + state.size.height += height_difference; + + if original_width == state.size.width { + state.size.width = state.widths.iter().fold( + 0.0, + |current, candidate| { + current.max(*candidate) + }, + ); + } + } + Change::Removed { original, .. } => { + let height = state.offsets[original + 1] + - state.offsets[original]; + + let original_width = state.widths.remove(original); + let _ = state.offsets.remove(original + 1); + + for offset in &mut state.offsets[original + 1..] { + *offset -= height; + } + + // TODO: Smarter visible layout partial updates + state.visible_layouts.clear(); + + state.size.height -= height; + + if original_width == state.size.width { + state.size.width = state.widths.iter().fold( + 0.0, + |current, candidate| { + current.max(*candidate) + }, + ); + } + } + Change::Pushed { current, .. } => { + let mut new_element = (self.view_item)( + current, + &self.content.items[current], + ); + + let mut tree = Tree::new(&new_element); + + let layout = new_element.as_widget_mut().layout( + &mut tree, + renderer, + &state.last_limits, + ); + + let size = layout.size(); + + state.widths.push(size.width); + state.offsets.push( + state.offsets.last().unwrap() + size.height, + ); + + state.size.width = state.size.width.max(size.width); + state.size.height += size.height; + } + } + } + } + Task::Computing { .. } => { + if !changes.is_empty() { + // If changes happen during layout computation, + // we simply restart the computation + changes.clear(); + state.recompute(self.content.len()); + } + } + } + + // Recompute if new + { + let mut is_new = self.content.is_new.borrow_mut(); + + if *is_new { + state.recompute(self.content.len()); + *is_new = false; + } + } + + match &mut state.task { + Task::Idle => {} + Task::Computing { + current, + size, + widths, + offsets, + } => { + const MAX_BATCH_SIZE: usize = 50; + + let end = (*current + MAX_BATCH_SIZE).min(self.content.len()); + + let batch = &self.content.items[*current..end]; + + let mut max_width = size.width; + let mut accumulated_height = + offsets.last().copied().unwrap_or(0.0); + + for (i, item) in batch.iter().enumerate() { + let mut element = (self.view_item)(*current + i, item); + let mut tree = Tree::new(&element); + + let layout = element + .as_widget_mut() + .layout(&mut tree, renderer, &state.last_limits) + .move_to((0.0, accumulated_height)); + + let bounds = layout.bounds(); + + max_width = max_width.max(bounds.width); + accumulated_height += bounds.height; + + offsets.push(accumulated_height); + widths.push(bounds.width); + } + + *size = Size::new(max_width, accumulated_height); + + if end < self.content.len() { + *current = end; + } else { + state.offsets = std::mem::take(offsets); + state.widths = std::mem::take(widths); + state.size = std::mem::take(size); + state.task = Task::Idle; + } + } + } + + let intrinsic_size = Size::new( + state.size.width, + state.size.height + + self.content.len().saturating_sub(1) as f32 * self.spacing, + ); + + let size = + limits.resolve(Length::Shrink, Length::Shrink, intrinsic_size); + + layout::Node::new(size) + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + let state = tree.state.downcast_mut::(); + let offset = layout.position() - Point::ORIGIN; + + self.visible_elements + .iter_mut() + .zip(&mut state.visible_layouts) + .map(|(element, (index, layout, tree))| { + element.as_widget_mut().update( + tree, + event, + Layout::with_offset( + offset + Vector::new(0.0, self.spacing * *index as f32), + layout, + ), + cursor, + renderer, + clipboard, + shell, + viewport, + ) + }); + + if let Event::Window(window::Event::RedrawRequested(_)) = event { + match &mut state.task { + Task::Idle => {} + Task::Computing { .. } => { + shell.invalidate_layout(); + shell.request_redraw(); + } + } + + let offsets = &state.offsets; + + let start = + match binary_search_with_index_by(offsets, |i, height| { + (*height + i.saturating_sub(1) as f32 * self.spacing) + .partial_cmp(&(viewport.y - offset.y)) + .unwrap_or(Ordering::Equal) + }) { + Ok(i) => i, + Err(i) => i.saturating_sub(1), + } + .min(self.content.len()); + + let end = match binary_search_with_index_by(offsets, |i, height| { + (*height + i.saturating_sub(1) as f32 * self.spacing) + .partial_cmp(&(viewport.y + viewport.height - offset.y)) + .unwrap_or(Ordering::Equal) + }) { + Ok(i) => i, + Err(i) => i, + } + .min(self.content.len()); + + if state.visible_outdated + || state.visible_layouts.len() != self.visible_elements.len() + { + self.visible_elements.clear(); + state.visible_outdated = false; + } + + // If view was recreated, we repopulate the visible elements + // out of the internal visible layouts + if self.visible_elements.is_empty() { + self.visible_elements = state + .visible_layouts + .iter() + .map(|(i, _, _)| { + (self.view_item)(*i, &self.content.items[*i]) + }) + .collect(); + } + + // Clear no longer visible elements + let top = state + .visible_layouts + .iter() + .take_while(|(i, _, _)| *i < start) + .count(); + + let bottom = state + .visible_layouts + .iter() + .rev() + .take_while(|(i, _, _)| *i >= end) + .count(); + + let _ = self.visible_elements.splice(..top, []); + let _ = state.visible_layouts.splice(..top, []); + + let _ = self + .visible_elements + .splice(self.visible_elements.len() - bottom.., []); + let _ = state + .visible_layouts + .splice(state.visible_layouts.len() - bottom.., []); + + // Prepend new visible elements + if let Some(first_visible) = + state.visible_layouts.first().map(|(i, _, _)| *i) + { + if start < first_visible { + for (i, item) in self.content.items[start..first_visible] + .iter() + .enumerate() + { + let mut element = (self.view_item)(start + i, item); + let mut tree = Tree::new(&element); + + let layout = element + .as_widget_mut() + .layout(&mut tree, renderer, &state.last_limits) + .move_to(( + 0.0, + offsets[start + i] + + (start + i) as f32 * self.spacing, + )); + + state + .visible_layouts + .insert(i, (start + i, layout, tree)); + self.visible_elements.insert(i, element); + } + } + } + + // Append new visible elements + let last_visible = state + .visible_layouts + .last() + .map(|(i, _, _)| *i + 1) + .unwrap_or(start); + + if last_visible < end { + for (i, item) in + self.content.items[last_visible..end].iter().enumerate() + { + let mut element = (self.view_item)(last_visible + i, item); + let mut tree = Tree::new(&element); + + let layout = element + .as_widget_mut() + .layout(&mut tree, renderer, &state.last_limits) + .move_to(( + 0.0, + offsets[last_visible + i] + + (last_visible + i) as f32 * self.spacing, + )); + + state.visible_layouts.push(( + last_visible + i, + layout, + tree, + )); + self.visible_elements.push(element); + } + } + } + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::(); + let offset = layout.position() - Point::ORIGIN; + + for (element, (_item, layout, tree)) in + self.visible_elements.iter().zip(&state.visible_layouts) + { + element.as_widget().draw( + tree, + renderer, + theme, + style, + Layout::with_offset(offset, layout), + cursor, + viewport, + ); + } + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let state = tree.state.downcast_ref::(); + let offset = layout.position() - Point::ORIGIN; + + self.visible_elements + .iter() + .zip(&state.visible_layouts) + .map(|(element, (_item, layout, tree))| { + element.as_widget().mouse_interaction( + tree, + Layout::with_offset(offset, layout), + cursor, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + let state = tree.state.downcast_mut::(); + let offset = layout.position() - Point::ORIGIN; + + for (element, (_item, layout, tree)) in self + .visible_elements + .iter_mut() + .zip(&mut state.visible_layouts) + { + element.as_widget_mut().operate( + tree, + Layout::with_offset(offset, layout), + renderer, + operation, + ); + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + let state = tree.state.downcast_mut::(); + let offset = layout.position() - Point::ORIGIN; + + let children = self + .visible_elements + .iter_mut() + .zip(&mut state.visible_layouts) + .filter_map(|(child, (_item, layout, tree))| { + child.as_widget_mut().overlay( + tree, + Layout::with_offset(offset, layout), + renderer, + viewport, + translation, + ) + }) + .collect::>(); + + (!children.is_empty()) + .then(|| overlay::Group::with_children(children).overlay()) + } +} + +impl<'a, T, Message, Theme, Renderer> + From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + fn from(list: List<'a, T, Message, Theme, Renderer>) -> Self { + Self::new(list) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Content { + items: Vec, + is_new: RefCell, + changes: RefCell>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Change { + Updated { original: usize, current: usize }, + Removed { original: usize, current: usize }, + Pushed { original: usize, current: usize }, +} + +impl Content { + pub fn new() -> Self { + Self { + items: Vec::new(), + is_new: RefCell::new(true), + changes: RefCell::new(VecDeque::new()), + } + } + + pub fn with_items(items: Vec) -> Self { + Self { + items, + is_new: RefCell::new(true), + changes: RefCell::new(VecDeque::new()), + } + } + + pub fn get(&self, index: usize) -> Option<&T> { + self.items.get(index) + } + + pub fn get_mut(&mut self, index: usize) -> Option<&mut T> { + self.changes.borrow_mut().push_back(Change::Updated { + original: index, + current: index, + }); + self.items.get_mut(index) + } + + pub fn push(&mut self, item: T) { + let index = self.items.len(); + + self.changes.borrow_mut().push_back(Change::Pushed { + original: index, + current: index, + }); + + self.items.push(item); + } + + pub fn remove(&mut self, index: usize) -> T { + let mut changes = self.changes.borrow_mut(); + + // Update pending changes after removal + changes.retain_mut(|change| match change { + Change::Updated { current, .. } + | Change::Removed { current, .. } + | Change::Pushed { current, .. } + if *current > index => + { + // Decrement index of later changes + *current -= 1; + + true + } + _ => true, + }); + + changes.push_back(Change::Removed { + original: index, + current: index, + }); + + self.items.remove(index) + } + + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } + + pub fn len(&self) -> usize { + self.items.len() + } + + pub fn into_vec(self) -> Vec { + self.items + } +} + +impl Default for Content { + fn default() -> Self { + Self::new() + } +} + +impl FromIterator for Content { + fn from_iter>(iter: I) -> Self { + Self::with_items(iter.into_iter().collect()) + } +} + +/// SAFETY: Copied from the `std` library. +#[allow(unsafe_code)] +fn binary_search_with_index_by<'a, T, F>( + slice: &'a [T], + mut f: F, +) -> Result +where + F: FnMut(usize, &'a T) -> Ordering, +{ + use std::cmp::Ordering::*; + + // INVARIANTS: + // - 0 <= left <= left + size = right <= self.len() + // - f returns Less for everything in self[..left] + // - f returns Greater for everything in self[right..] + let mut size = slice.len(); + let mut left = 0; + let mut right = size; + while left < right { + let mid = left + size / 2; + + // SAFETY: the while condition means `size` is strictly positive, so + // `size/2 < size`. Thus `left + size/2 < left + size`, which + // coupled with the `left + size <= self.len()` invariant means + // we have `left + size/2 < self.len()`, and this is in-bounds. + let cmp = f(mid, unsafe { slice.get_unchecked(mid) }); + + // This control flow produces conditional moves, which results in + // fewer branches and instructions than if/else or matching on + // cmp::Ordering. + // This is x86 asm for u8: https://rust.godbolt.org/z/698eYffTx. + left = if cmp == Less { mid + 1 } else { left }; + right = if cmp == Greater { mid } else { right }; + if cmp == Equal { + return Ok(mid); + } + + size = right - left; + } + + Err(left) +}