diff options
Diffstat (limited to '')
| -rw-r--r-- | widget/src/keyed.rs | 53 | ||||
| -rw-r--r-- | widget/src/keyed/column.rs | 320 | 
2 files changed, 373 insertions, 0 deletions
diff --git a/widget/src/keyed.rs b/widget/src/keyed.rs new file mode 100644 index 00000000..ad531e66 --- /dev/null +++ b/widget/src/keyed.rs @@ -0,0 +1,53 @@ +//! 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. +#[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) +    } +}  | 
