diff options
author | 2023-09-04 02:55:09 +0200 | |
---|---|---|
committer | 2023-09-04 02:55:09 +0200 | |
commit | 34495bba1c1ffaa4ea2bab46103b5d66e333c51e (patch) | |
tree | 09c2393621ae1e83c25ca8e7d69dc7b951931fbe /widget/src | |
parent | 1a1da6a1f0ee58d5cdc7365681e0ad5edd0117af (diff) | |
download | iced-34495bba1c1ffaa4ea2bab46103b5d66e333c51e.tar.gz iced-34495bba1c1ffaa4ea2bab46103b5d66e333c51e.tar.bz2 iced-34495bba1c1ffaa4ea2bab46103b5d66e333c51e.zip |
Introduce `keyed::Column` widget
Diffstat (limited to 'widget/src')
-rw-r--r-- | widget/src/helpers.rs | 13 | ||||
-rw-r--r-- | widget/src/keyed.rs | 55 | ||||
-rw-r--r-- | widget/src/keyed/column.rs | 320 | ||||
-rw-r--r-- | widget/src/lib.rs | 1 |
4 files changed, 387 insertions, 2 deletions
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 9c3c83a9..c885d724 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -6,6 +6,7 @@ use crate::container::{self, Container}; use crate::core; use crate::core::widget::operation; use crate::core::{Element, Length, Pixels}; +use crate::keyed; use crate::overlay; use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; @@ -63,14 +64,22 @@ where } /// Creates a new [`Column`] with the given children. -/// -/// [`Column`]: widget::Column pub fn column<Message, Renderer>( children: Vec<Element<'_, Message, Renderer>>, ) -> Column<'_, Message, Renderer> { Column::with_children(children) } +/// Creates a new [`keyed::Column`] with the given children. +pub fn keyed_column<'a, Key, Message, Renderer>( + children: impl IntoIterator<Item = (Key, Element<'a, Message, Renderer>)>, +) -> keyed::Column<'a, Key, Message, Renderer> +where + Key: Copy + PartialEq, +{ + keyed::Column::with_children(children) +} + /// Creates a new [`Row`] with the given children. /// /// [`Row`]: widget::Row diff --git a/widget/src/keyed.rs b/widget/src/keyed.rs new file mode 100644 index 00000000..55019535 --- /dev/null +++ b/widget/src/keyed.rs @@ -0,0 +1,55 @@ +//! Use widgets that can provide hints to ensure continuity. +//! +//! # What is continuity? +//! Continuity is the feeling of persistence of state. +//! +//! In a graphical user interface, users expect widgets to have a +//! certain degree of continuous state. For instance, a text input +//! that is focused should stay focused even if the widget tree +//! changes slightly. +//! +//! Continuity is tricky in `iced` and the Elm Architecture because +//! the whole widget tree is rebuilt during every `view` call. This is +//! very convenient from a developer perspective because you can build +//! extremely dynamic interfaces without worrying about changing state. +//! +//! However, the tradeoff is that determining what changed becomes hard +//! for `iced`. If you have a list of things, adding an element at the +//! top may cause a loss of continuity on every element on the list! +//! +//! # How can we keep continuity? +//! The good news is that user interfaces generally have a static widget +//! structure. This structure can be relied on to ensure some degree of +//! continuity. `iced` already does this. +//! +//! However, sometimes you have a certain part of your interface that is +//! quite dynamic. For instance, a list of things where items may be added +//! or removed at any place. +//! +//! There are different ways to mitigate this during the reconciliation +//! stage, but they involve comparing trees at certain depths and +//! backtracking... Quite computationally expensive. +//! +//! One approach that is cheaper consists in letting the user provide some hints +//! about the identities of the different widgets so that they can be compared +//! directly without going deeper. +//! +//! The widgets in this module will all ask for a "hint" of some sort. In order +//! to help them keep continuity, you need to make sure the hint stays the same +//! for the same items in your user interface between `view` calls. +pub mod column; + +pub use column::Column; + +/// Creates a [`Column`] with the given children. +/// +/// [`Column`]: widget::Column +#[macro_export] +macro_rules! keyed_column { + () => ( + $crate::Column::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::keyed::Column::with_children(vec![$($crate::core::Element::from($x)),+]) + ); +} diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs new file mode 100644 index 00000000..19016679 --- /dev/null +++ b/widget/src/keyed/column.rs @@ -0,0 +1,320 @@ +//! Distribute content vertically. +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::tree::{self, Tree}; +use crate::core::widget::Operation; +use crate::core::{ + Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, + Shell, Widget, +}; + +/// A container that distributes its contents vertically. +#[allow(missing_debug_implementations)] +pub struct Column<'a, Key, Message, Renderer = crate::Renderer> +where + Key: Copy + PartialEq, +{ + spacing: f32, + padding: Padding, + width: Length, + height: Length, + max_width: f32, + align_items: Alignment, + keys: Vec<Key>, + children: Vec<Element<'a, Message, Renderer>>, +} + +impl<'a, Key, Message, Renderer> Column<'a, Key, Message, Renderer> +where + Key: Copy + PartialEq, +{ + /// Creates an empty [`Column`]. + pub fn new() -> Self { + Self::with_children(Vec::new()) + } + + /// Creates a [`Column`] with the given elements. + pub fn with_children( + children: impl IntoIterator<Item = (Key, Element<'a, Message, Renderer>)>, + ) -> Self { + let (keys, children) = children.into_iter().fold( + (Vec::new(), Vec::new()), + |(mut keys, mut children), (key, child)| { + keys.push(key); + children.push(child); + + (keys, children) + }, + ); + + Column { + spacing: 0.0, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + max_width: f32::INFINITY, + align_items: Alignment::Start, + keys, + children, + } + } + + /// 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<Pixels>) -> Self { + self.spacing = amount.into().0; + self + } + + /// Sets the [`Padding`] of the [`Column`]. + pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`Column`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Column`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Sets the maximum width of the [`Column`]. + pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self { + self.max_width = max_width.into().0; + self + } + + /// Sets the horizontal alignment of the contents of the [`Column`] . + pub fn align_items(mut self, align: Alignment) -> Self { + self.align_items = align; + self + } + + /// Adds an element to the [`Column`]. + pub fn push( + mut self, + key: Key, + child: impl Into<Element<'a, Message, Renderer>>, + ) -> Self { + self.keys.push(key); + self.children.push(child.into()); + self + } +} + +impl<'a, Key, Message, Renderer> Default for Column<'a, Key, Message, Renderer> +where + Key: Copy + PartialEq, +{ + fn default() -> Self { + Self::new() + } +} + +struct State<Key> +where + Key: Copy + PartialEq, +{ + keys: Vec<Key>, +} + +impl<'a, Key, Message, Renderer> Widget<Message, Renderer> + for Column<'a, Key, Message, Renderer> +where + Renderer: crate::core::Renderer, + Key: Copy + PartialEq + 'static, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State<Key>>() + } + + fn state(&self) -> tree::State { + tree::State::new(State { + keys: self.keys.clone(), + }) + } + + fn children(&self) -> Vec<Tree> { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + let Tree { + state, children, .. + } = tree; + + let state = state.downcast_mut::<State<Key>>(); + + tree::diff_children_custom_with_search( + children, + &self.children, + |tree, child| child.as_widget().diff(tree), + |index| { + self.keys.get(index).or_else(|| self.keys.last()).copied() + != Some(state.keys[index]) + }, + |child| Tree::new(child.as_widget()), + ); + + if state.keys != self.keys { + state.keys = self.keys.clone(); + } + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits + .max_width(self.max_width) + .width(self.width) + .height(self.height); + + layout::flex::resolve( + layout::flex::Axis::Vertical, + renderer, + &limits, + self.padding, + self.spacing, + self.align_items, + &self.children, + &mut tree.children, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<Message>, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child + .as_widget() + .operate(state, layout, renderer, operation); + }) + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + self.children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child + .as_widget() + .draw(state, renderer, theme, style, layout, cursor, viewport); + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + overlay::from_children(&mut self.children, tree, layout, renderer) + } +} + +impl<'a, Key, Message, Renderer> From<Column<'a, Key, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Key: Copy + PartialEq + 'static, + Message: 'a, + Renderer: crate::core::Renderer + 'a, +{ + fn from(column: Column<'a, Key, Message, Renderer>) -> Self { + Self::new(column) + } +} diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 316e8829..707fec04 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -29,6 +29,7 @@ pub mod button; pub mod checkbox; pub mod combo_box; pub mod container; +pub mod keyed; pub mod overlay; pub mod pane_grid; pub mod pick_list; |