From 0c74d2645649a88799a894ed684a728d135043fa Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 25 Apr 2024 01:39:34 +0200 Subject: Implement `Stack` widget It can be used to stack elements on top of each other! --- widget/src/stack.rs | 327 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 widget/src/stack.rs (limited to 'widget/src/stack.rs') diff --git a/widget/src/stack.rs b/widget/src/stack.rs new file mode 100644 index 00000000..84f1f7ff --- /dev/null +++ b/widget/src/stack.rs @@ -0,0 +1,327 @@ +//! 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::{Operation, Tree}; +use crate::core::{ + Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget, +}; + +/// A container that displays children on top of each other. +/// +/// The first [`Element`] dictates the intrinsic [`Size`] of a [`Stack`] and +/// will be displayed as the base layer. Every consecutive [`Element`] will be +/// renderer on top; on its own layer. +/// +/// Keep in mind that too much layering will normally produce bad UX as well as +/// introduce certain rendering overhead. Use this widget sparingly! +#[allow(missing_debug_implementations)] +pub struct Stack<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> +{ + width: Length, + height: Length, + children: Vec>, +} + +impl<'a, Message, Theme, Renderer> Stack<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, +{ + /// Creates an empty [`Stack`]. + pub fn new() -> Self { + Self::from_vec(Vec::new()) + } + + /// Creates a [`Stack`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + + /// Creates a [`Stack`] with the given elements. + pub fn with_children( + children: impl IntoIterator>, + ) -> Self { + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) + } + + /// Creates a [`Stack`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`Stack`] will not inspect the [`Vec`], which means + /// it won't automatically adapt to the sizing strategy of its contents. + /// + /// If any of the children have a [`Length::Fill`] strategy, you will need to + /// call [`Stack::width`] or [`Stack::height`] accordingly. + pub fn from_vec( + children: Vec>, + ) -> Self { + Self { + width: Length::Shrink, + height: Length::Shrink, + children, + } + } + + /// Sets the width of the [`Stack`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Stack`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Adds an element to the [`Stack`]. + pub fn push( + mut self, + child: impl Into>, + ) -> Self { + let child = child.into(); + + if self.children.is_empty() { + let child_size = child.as_widget().size_hint(); + + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); + } + + self.children.push(child); + self + } + + /// Adds an element to the [`Stack`], if `Some`. + pub fn push_maybe( + self, + child: Option>>, + ) -> Self { + if let Some(child) = child { + self.push(child) + } else { + self + } + } + + /// Extends the [`Stack`] with the given children. + pub fn extend( + self, + children: impl IntoIterator>, + ) -> Self { + children.into_iter().fold(self, Self::push) + } +} + +impl<'a, Message, Renderer> Default for Stack<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message, Theme, Renderer> Widget + for Stack<'a, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, +{ + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&self.children); + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + if self.children.is_empty() { + return layout::Node::new(Size::ZERO); + } + + let base = self.children[0].as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ); + + let size = base.size(); + let limits = layout::Limits::new(Size::ZERO, size); + + let nodes = std::iter::once(base) + .chain(self.children[1..].iter().zip(&mut tree.children[1..]).map( + |(layer, tree)| { + let node = + layer.as_widget().layout(tree, renderer, &limits); + + node + }, + )) + .collect(); + + layout::Node::with_children(size, nodes) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + 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() + .rev() + .zip(tree.children.iter_mut().rev()) + .zip(layout.children().rev()) + .map(|((child, state), layout)| { + child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + }) + .find(|&status| status == event::Status::Captured) + .unwrap_or(event::Status::Ignored) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .rev() + .zip(tree.children.iter().rev()) + .zip(layout.children().rev()) + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, + ) + }) + .find(|&interaction| interaction != mouse::Interaction::Idle) + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + for (i, ((layer, state), layout)) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .enumerate() + { + if i > 0 { + renderer.with_layer(clipped_viewport, |renderer| { + layer.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor, + &clipped_viewport, + ); + }); + } else { + layer.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor, + &clipped_viewport, + ); + } + } + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + translation, + ) + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: crate::core::Renderer + 'a, +{ + fn from(stack: Stack<'a, Message, Theme, Renderer>) -> Self { + Self::new(stack) + } +} -- cgit From 99434b3ecf7874d2382e7f739bc69a55768fd60b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 25 Apr 2024 01:47:07 +0200 Subject: Fix documentation of `stack` module --- widget/src/stack.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'widget/src/stack.rs') diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 84f1f7ff..d6a1538e 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -1,4 +1,4 @@ -//! Distribute content vertically. +//! Display content on top of other content. use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; -- cgit From 9492da11d90893b396e8dfdae47cc54c6ab42411 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 25 Apr 2024 02:25:36 +0200 Subject: Use `Limits::resolve` in `Stack` widget --- widget/src/stack.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'widget/src/stack.rs') diff --git a/widget/src/stack.rs b/widget/src/stack.rs index d6a1538e..6e5dacd2 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -155,13 +155,14 @@ where return layout::Node::new(Size::ZERO); } + let limits = limits.width(self.width).height(self.height); let base = self.children[0].as_widget().layout( &mut tree.children[0], renderer, - limits, + &limits, ); - let size = base.size(); + let size = limits.resolve(self.width, self.height, base.size()); let limits = layout::Limits::new(Size::ZERO, size); let nodes = std::iter::once(base) -- cgit From 4cd45643d7d2aa83212162f17a9b28ddae4a9340 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 25 Apr 2024 06:05:00 +0200 Subject: Introduce `opaque` widget helper --- widget/src/stack.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'widget/src/stack.rs') diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 6e5dacd2..8a0ea2eb 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -249,7 +249,7 @@ where state, layout, cursor, viewport, renderer, ) }) - .find(|&interaction| interaction != mouse::Interaction::Idle) + .find(|&interaction| interaction != mouse::Interaction::None) .unwrap_or_default() } -- cgit From bb9244107c8fee78f9b77f5ab0e4c2c31456d6c4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 27 Apr 2024 06:08:30 +0200 Subject: Respect `width` and `height` properties when `Stack` is empty --- widget/src/stack.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'widget/src/stack.rs') diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 8a0ea2eb..2774d035 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -152,7 +152,11 @@ where limits: &layout::Limits, ) -> layout::Node { if self.children.is_empty() { - return layout::Node::new(Size::ZERO); + return layout::Node::new(limits.resolve( + self.width, + self.height, + Size::ZERO, + )); } let limits = limits.width(self.width).height(self.height); -- cgit From c58155a971987529515570c0d137230e9bd8f4b3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 27 Apr 2024 06:20:41 +0200 Subject: Set proper size boundaries for `limits` in `Stack::layout` --- widget/src/stack.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'widget/src/stack.rs') diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 2774d035..5035541b 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -151,6 +151,8 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + if self.children.is_empty() { return layout::Node::new(limits.resolve( self.width, @@ -159,7 +161,6 @@ where )); } - let limits = limits.width(self.width).height(self.height); let base = self.children[0].as_widget().layout( &mut tree.children[0], renderer, -- cgit