From 0c74d2645649a88799a894ed684a728d135043fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 25 Apr 2024 01:39:34 +0200 Subject: [PATCH 1/6] Implement `Stack` widget It can be used to stack elements on top of each other! --- core/src/layout.rs | 2 +- widget/src/helpers.rs | 27 +++- widget/src/lib.rs | 3 + widget/src/stack.rs | 327 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 widget/src/stack.rs diff --git a/core/src/layout.rs b/core/src/layout.rs index 95720aba16..98d056029f 100644 --- a/core/src/layout.rs +++ b/core/src/layout.rs @@ -54,7 +54,7 @@ impl<'a> Layout<'a> { } /// Returns an iterator over the [`Layout`] of the children of a [`Node`]. - pub fn children(self) -> impl Iterator> { + pub fn children(self) -> impl DoubleEndedIterator> { self.node.children().iter().map(move |node| { Layout::with_offset( Vector::new(self.position.x, self.position.y), diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 61789c193b..2afed3e63c 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -21,7 +21,7 @@ use crate::text_input::{self, TextInput}; use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; use crate::vertical_slider::{self, VerticalSlider}; -use crate::{Column, MouseArea, Row, Space, Themer}; +use crate::{Column, MouseArea, Row, Space, Stack, Themer}; use std::borrow::Borrow; use std::ops::RangeInclusive; @@ -52,6 +52,19 @@ macro_rules! row { ); } +/// Creates a [`Stack`] with the given children. +/// +/// [`Stack`]: crate::Stack +#[macro_export] +macro_rules! stack { + () => ( + $crate::Stack::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::Stack::with_children([$($crate::core::Element::from($x)),+]) + ); +} + /// Creates a new [`Container`] with the provided content. /// /// [`Container`]: crate::Container @@ -98,6 +111,18 @@ where Row::with_children(children) } +/// Creates a new [`Stack`] with the given children. +/// +/// [`Stack`]: crate::Stack +pub fn stack<'a, Message, Theme, Renderer>( + children: impl IntoIterator>, +) -> Stack<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + Stack::with_children(children) +} + /// Creates a new [`Scrollable`] with the provided content. /// /// [`Scrollable`]: crate::Scrollable diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 1eeacbae1b..00e9aaa49d 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -12,6 +12,7 @@ mod column; mod mouse_area; mod row; mod space; +mod stack; mod themer; pub mod button; @@ -78,6 +79,8 @@ pub use slider::Slider; #[doc(no_inline)] pub use space::Space; #[doc(no_inline)] +pub use stack::Stack; +#[doc(no_inline)] pub use text::Text; #[doc(no_inline)] pub use text_editor::TextEditor; diff --git a/widget/src/stack.rs b/widget/src/stack.rs new file mode 100644 index 0000000000..84f1f7ff75 --- /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) + } +} From 8b1c514c3816795437da2ec25c2cfc559ebe5b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 25 Apr 2024 01:40:06 +0200 Subject: [PATCH 2/6] Showcase new `Stack` widget in `bezier_tool` example --- examples/bezier_tool/src/main.rs | 53 +++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 289c919b45..ba51a00edc 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,9 +1,11 @@ //! This example showcases an interactive `Canvas` for drawing Bézier curves. -use iced::widget::{button, column, text}; -use iced::{Alignment, Element, Length}; +use iced::alignment; +use iced::widget::{button, container, stack}; +use iced::{Element, Length, Theme}; pub fn main() -> iced::Result { iced::program("Bezier Tool - Iced", Example::update, Example::view) + .theme(|_| Theme::CatppuccinMocha) .antialiasing(true) .run() } @@ -35,16 +37,18 @@ impl Example { } fn view(&self) -> Element { - column![ - text("Bezier tool example").width(Length::Shrink).size(50), + container(stack![ self.bezier.view(&self.curves).map(Message::AddCurve), - button("Clear") - .style(button::danger) - .on_press(Message::Clear), - ] + container( + button("Clear") + .style(button::danger) + .on_press(Message::Clear) + ) + .padding(10) + .width(Length::Fill) + .align_x(alignment::Horizontal::Right), + ]) .padding(20) - .spacing(20) - .align_items(Alignment::Center) .into() } } @@ -139,22 +143,24 @@ mod bezier { &self, state: &Self::State, renderer: &Renderer, - _theme: &Theme, + theme: &Theme, bounds: Rectangle, cursor: mouse::Cursor, ) -> Vec { let content = self.state.cache.draw(renderer, bounds.size(), |frame| { - Curve::draw_all(self.curves, frame); + Curve::draw_all(self.curves, frame, theme); frame.stroke( &Path::rectangle(Point::ORIGIN, frame.size()), - Stroke::default().with_width(2.0), + Stroke::default() + .with_width(2.0) + .with_color(theme.palette().text), ); }); if let Some(pending) = state { - vec![content, pending.draw(renderer, bounds, cursor)] + vec![content, pending.draw(renderer, theme, bounds, cursor)] } else { vec![content] } @@ -182,7 +188,7 @@ mod bezier { } impl Curve { - fn draw_all(curves: &[Curve], frame: &mut Frame) { + fn draw_all(curves: &[Curve], frame: &mut Frame, theme: &Theme) { let curves = Path::new(|p| { for curve in curves { p.move_to(curve.from); @@ -190,7 +196,12 @@ mod bezier { } }); - frame.stroke(&curves, Stroke::default().with_width(2.0)); + frame.stroke( + &curves, + Stroke::default() + .with_width(2.0) + .with_color(theme.palette().text), + ); } } @@ -204,6 +215,7 @@ mod bezier { fn draw( &self, renderer: &Renderer, + theme: &Theme, bounds: Rectangle, cursor: mouse::Cursor, ) -> Geometry { @@ -213,7 +225,12 @@ mod bezier { match *self { Pending::One { from } => { let line = Path::line(from, cursor_position); - frame.stroke(&line, Stroke::default().with_width(2.0)); + frame.stroke( + &line, + Stroke::default() + .with_width(2.0) + .with_color(theme.palette().text), + ); } Pending::Two { from, to } => { let curve = Curve { @@ -222,7 +239,7 @@ mod bezier { control: cursor_position, }; - Curve::draw_all(&[curve], &mut frame); + Curve::draw_all(&[curve], &mut frame, theme); } }; } From 99434b3ecf7874d2382e7f739bc69a55768fd60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 25 Apr 2024 01:47:07 +0200 Subject: [PATCH 3/6] Fix documentation of `stack` module --- widget/src/stack.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 84f1f7ff75..d6a1538eec 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; From 9492da11d90893b396e8dfdae47cc54c6ab42411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 25 Apr 2024 02:25:36 +0200 Subject: [PATCH 4/6] Use `Limits::resolve` in `Stack` widget --- widget/src/stack.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/widget/src/stack.rs b/widget/src/stack.rs index d6a1538eec..6e5dacd238 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) From 4cd45643d7d2aa83212162f17a9b28ddae4a9340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 25 Apr 2024 06:05:00 +0200 Subject: [PATCH 5/6] Introduce `opaque` widget helper --- core/src/mouse/interaction.rs | 1 + core/src/overlay.rs | 2 +- core/src/widget.rs | 2 +- examples/loupe/src/main.rs | 2 +- runtime/src/multi_window/state.rs | 2 +- runtime/src/program/state.rs | 2 +- widget/src/helpers.rs | 169 ++++++++++++++++++++++- widget/src/image/viewer.rs | 2 +- widget/src/mouse_area.rs | 2 +- widget/src/scrollable.rs | 2 +- widget/src/stack.rs | 2 +- winit/src/conversion.rs | 4 +- winit/src/multi_window/window_manager.rs | 2 +- 13 files changed, 182 insertions(+), 12 deletions(-) diff --git a/core/src/mouse/interaction.rs b/core/src/mouse/interaction.rs index 6ad6622926..065eb8e725 100644 --- a/core/src/mouse/interaction.rs +++ b/core/src/mouse/interaction.rs @@ -3,6 +3,7 @@ #[allow(missing_docs)] pub enum Interaction { #[default] + None, Idle, Pointer, Grab, diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 03076a30b2..3a57fe1641 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -79,7 +79,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse::Interaction::Idle + mouse::Interaction::None } /// Returns true if the cursor is over the [`Overlay`]. diff --git a/core/src/widget.rs b/core/src/widget.rs index 58a9f19be4..b02e3a4f8f 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -137,7 +137,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse::Interaction::Idle + mouse::Interaction::None } /// Returns the overlay of the [`Widget`], if there is any. diff --git a/examples/loupe/src/main.rs b/examples/loupe/src/main.rs index 6a5ff123ab..42b7f47148 100644 --- a/examples/loupe/src/main.rs +++ b/examples/loupe/src/main.rs @@ -159,7 +159,7 @@ mod loupe { if cursor.is_over(layout.bounds()) { mouse::Interaction::ZoomIn } else { - mouse::Interaction::Idle + mouse::Interaction::None } } } diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs index afd0451965..10366ec05b 100644 --- a/runtime/src/multi_window/state.rs +++ b/runtime/src/multi_window/state.rs @@ -48,7 +48,7 @@ where caches, queued_events: Vec::new(), queued_messages: Vec::new(), - mouse_interaction: mouse::Interaction::Idle, + mouse_interaction: mouse::Interaction::None, } } diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index d685b07c86..c6589c22be 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -47,7 +47,7 @@ where cache, queued_events: Vec::new(), queued_messages: Vec::new(), - mouse_interaction: mouse::Interaction::Idle, + mouse_interaction: mouse::Interaction::None, } } diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 2afed3e63c..fd34525195 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -5,7 +5,7 @@ use crate::combo_box::{self, ComboBox}; use crate::container::{self, Container}; use crate::core; use crate::core::widget::operation; -use crate::core::{Element, Length, Pixels}; +use crate::core::{Element, Length, Pixels, Widget}; use crate::keyed; use crate::overlay; use crate::pick_list::{self, PickList}; @@ -123,6 +123,173 @@ where Stack::with_children(children) } +/// Wraps the given widget and captures any mouse button presses inside the bounds of +/// the widget—therefore making it _opaque_. +/// +/// This helper is meant to be used to mark elements in a [`Stack`] to avoid mouse +/// events from passing through layers. +/// +/// [`Stack`]: crate::Stack +pub fn opaque<'a, Message, Theme, Renderer>( + content: impl Into>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + use crate::core::event::{self, Event}; + use crate::core::layout::{self, Layout}; + use crate::core::mouse; + use crate::core::renderer; + use crate::core::widget::tree::{self, Tree}; + use crate::core::{Rectangle, Shell, Size}; + + struct Opaque<'a, Message, Theme, Renderer> { + content: Element<'a, Message, Theme, Renderer>, + } + + impl<'a, Message, Theme, Renderer> Widget + for Opaque<'a, Message, Theme, Renderer> + where + Renderer: core::Renderer, + { + fn tag(&self) -> tree::Tag { + self.content.as_widget().tag() + } + + fn state(&self) -> tree::State { + self.content.as_widget().state() + } + + fn children(&self) -> Vec { + self.content.as_widget().children() + } + + fn diff(&self, tree: &mut Tree) { + self.content.as_widget().diff(tree); + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } + + fn size_hint(&self) -> Size { + self.content.as_widget().size_hint() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.as_widget().layout(tree, renderer, limits) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content + .as_widget() + .draw(tree, renderer, theme, style, layout, cursor, viewport); + } + + fn operate( + &self, + state: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn operation::Operation, + ) { + self.content + .as_widget() + .operate(state, layout, renderer, operation); + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn core::Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let is_mouse_press = matches!( + event, + core::Event::Mouse(mouse::Event::ButtonPressed(_)) + ); + + if let core::event::Status::Captured = + self.content.as_widget_mut().on_event( + state, event, layout, cursor, renderer, clipboard, shell, + viewport, + ) + { + return event::Status::Captured; + } + + if is_mouse_press && cursor.is_over(layout.bounds()) { + event::Status::Captured + } else { + event::Status::Ignored + } + } + + fn mouse_interaction( + &self, + state: &core::widget::Tree, + layout: core::Layout<'_>, + cursor: core::mouse::Cursor, + viewport: &core::Rectangle, + renderer: &Renderer, + ) -> core::mouse::Interaction { + let interaction = self + .content + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer); + + if interaction == mouse::Interaction::None + && cursor.is_over(layout.bounds()) + { + mouse::Interaction::Idle + } else { + interaction + } + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut core::widget::Tree, + layout: core::Layout<'_>, + renderer: &Renderer, + translation: core::Vector, + ) -> Option> + { + self.content.as_widget_mut().overlay( + state, + layout, + renderer, + translation, + ) + } + } + + Element::new(Opaque { + content: content.into(), + }) +} + /// Creates a new [`Scrollable`] with the provided content. /// /// [`Scrollable`]: crate::Scrollable diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 5f7bb345c7..75d73b1927 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -304,7 +304,7 @@ where } else if is_mouse_over { mouse::Interaction::Grab } else { - mouse::Interaction::Idle + mouse::Interaction::None } } diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 9634e477d0..d7235cf602 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -232,7 +232,7 @@ where ); match (self.interaction, content_interaction) { - (Some(interaction), mouse::Interaction::Idle) + (Some(interaction), mouse::Interaction::None) if cursor.is_over(layout.bounds()) => { interaction diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index cdc143a2b2..10e81cee1a 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -857,7 +857,7 @@ where if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) || state.scrollers_grabbed() { - mouse::Interaction::Idle + mouse::Interaction::None } else { let translation = state.translation(self.direction, bounds, content_bounds); diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 6e5dacd238..8a0ea2ebc4 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() } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index fc3d1c086b..0f83dac3cb 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -396,7 +396,9 @@ pub fn mouse_interaction( use mouse::Interaction; match interaction { - Interaction::Idle => winit::window::CursorIcon::Default, + Interaction::None | Interaction::Idle => { + winit::window::CursorIcon::Default + } Interaction::Pointer => winit::window::CursorIcon::Pointer, Interaction::Working => winit::window::CursorIcon::Progress, Interaction::Grab => winit::window::CursorIcon::Grab, diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs index 71c1688b80..3a0c8556a9 100644 --- a/winit/src/multi_window/window_manager.rs +++ b/winit/src/multi_window/window_manager.rs @@ -60,7 +60,7 @@ where exit_on_close_request, surface, renderer, - mouse_interaction: mouse::Interaction::Idle, + mouse_interaction: mouse::Interaction::None, }, ); From 4fc342c97963a7a7e7983604b9399fccf017aa53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 25 Apr 2024 06:05:40 +0200 Subject: [PATCH 6/6] Simplify `modal` example :tada: --- examples/modal/src/main.rs | 363 ++++--------------------------------- 1 file changed, 35 insertions(+), 328 deletions(-) diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 398728e0a5..a345924d09 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -2,12 +2,11 @@ use iced::event::{self, Event}; use iced::keyboard; use iced::keyboard::key; use iced::widget::{ - self, button, column, container, horizontal_space, pick_list, row, text, - text_input, + self, button, column, container, horizontal_space, mouse_area, opaque, + pick_list, row, stack, text, text_input, }; -use iced::{Alignment, Command, Element, Length, Subscription}; +use iced::{Alignment, Color, Command, Element, Length, Subscription}; -use modal::Modal; use std::fmt; pub fn main() -> iced::Result { @@ -121,7 +120,7 @@ impl App { .height(Length::Fill); if self.show_modal { - let modal = container( + let signup = container( column![ text("Sign Up").size(24), column![ @@ -162,9 +161,7 @@ impl App { .padding(10) .style(container::rounded_box); - Modal::new(content, modal) - .on_blur(Message::HideModal) - .into() + modal(content, signup, Message::HideModal) } else { content.into() } @@ -203,326 +200,36 @@ impl fmt::Display for Plan { } } -mod modal { - use iced::advanced::layout::{self, Layout}; - use iced::advanced::overlay; - use iced::advanced::renderer; - use iced::advanced::widget::{self, Widget}; - use iced::advanced::{self, Clipboard, Shell}; - use iced::alignment::Alignment; - use iced::event; - use iced::mouse; - use iced::{Color, Element, Event, Length, Point, Rectangle, Size, Vector}; - - /// A widget that centers a modal element over some base element - pub struct Modal<'a, Message, Theme, Renderer> { - base: Element<'a, Message, Theme, Renderer>, - modal: Element<'a, Message, Theme, Renderer>, - on_blur: Option, - } - - impl<'a, Message, Theme, Renderer> Modal<'a, Message, Theme, Renderer> { - /// Returns a new [`Modal`] - pub fn new( - base: impl Into>, - modal: impl Into>, - ) -> Self { - Self { - base: base.into(), - modal: modal.into(), - on_blur: None, - } - } - - /// Sets the message that will be produces when the background - /// of the [`Modal`] is pressed - pub fn on_blur(self, on_blur: Message) -> Self { - Self { - on_blur: Some(on_blur), - ..self - } - } - } - - impl<'a, Message, Theme, Renderer> Widget - for Modal<'a, Message, Theme, Renderer> - where - Renderer: advanced::Renderer, - Message: Clone, - { - fn children(&self) -> Vec { - vec![ - widget::Tree::new(&self.base), - widget::Tree::new(&self.modal), - ] - } - - fn diff(&self, tree: &mut widget::Tree) { - tree.diff_children(&[&self.base, &self.modal]); - } - - fn size(&self) -> Size { - self.base.as_widget().size() - } - - fn layout( - &self, - tree: &mut widget::Tree, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.base.as_widget().layout( - &mut tree.children[0], - renderer, - limits, - ) - } - - fn on_event( - &mut self, - state: &mut widget::Tree, - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - viewport: &Rectangle, - ) -> event::Status { - self.base.as_widget_mut().on_event( - &mut state.children[0], - event, - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) - } - - fn draw( - &self, - state: &widget::Tree, - renderer: &mut Renderer, - theme: &Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - self.base.as_widget().draw( - &state.children[0], - renderer, - theme, - style, - layout, - cursor, - viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - state: &'b mut widget::Tree, - layout: Layout<'_>, - _renderer: &Renderer, - translation: Vector, - ) -> Option> { - Some(overlay::Element::new(Box::new(Overlay { - position: layout.position() + translation, - content: &mut self.modal, - tree: &mut state.children[1], - size: layout.bounds().size(), - on_blur: self.on_blur.clone(), - }))) - } - - fn mouse_interaction( - &self, - state: &widget::Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.base.as_widget().mouse_interaction( - &state.children[0], - layout, - cursor, - viewport, - renderer, - ) - } - - fn operate( - &self, - state: &mut widget::Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.base.as_widget().operate( - &mut state.children[0], - layout, - renderer, - operation, - ); - } - } - - struct Overlay<'a, 'b, Message, Theme, Renderer> { - position: Point, - content: &'b mut Element<'a, Message, Theme, Renderer>, - tree: &'b mut widget::Tree, - size: Size, - on_blur: Option, - } - - impl<'a, 'b, Message, Theme, Renderer> - overlay::Overlay - for Overlay<'a, 'b, Message, Theme, Renderer> - where - Renderer: advanced::Renderer, - Message: Clone, - { - fn layout( - &mut self, - renderer: &Renderer, - _bounds: Size, - ) -> layout::Node { - let limits = layout::Limits::new(Size::ZERO, self.size) +fn modal<'a, Message>( + base: impl Into>, + content: impl Into>, + on_blur: Message, +) -> Element<'a, Message> +where + Message: Clone + 'a, +{ + stack![ + base.into(), + mouse_area( + container(opaque(content)) .width(Length::Fill) - .height(Length::Fill); - - let child = self - .content - .as_widget() - .layout(self.tree, renderer, &limits) - .align(Alignment::Center, Alignment::Center, limits.max()); - - layout::Node::with_children(self.size, vec![child]) - .move_to(self.position) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let content_bounds = layout.children().next().unwrap().bounds(); - - if let Some(message) = self.on_blur.as_ref() { - if let Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) = &event - { - if !cursor.is_over(content_bounds) { - shell.publish(message.clone()); - return event::Status::Captured; + .height(Length::Fill) + .center_x() + .center_y() + .style(|_theme| { + container::Style { + background: Some( + Color { + a: 0.8, + ..Color::BLACK + } + .into(), + ), + ..container::Style::default() } - } - } - - self.content.as_widget_mut().on_event( - self.tree, - event, - layout.children().next().unwrap(), - cursor, - renderer, - clipboard, - shell, - &layout.bounds(), - ) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - ) { - renderer.fill_quad( - renderer::Quad { - bounds: layout.bounds(), - ..renderer::Quad::default() - }, - Color { - a: 0.80, - ..Color::BLACK - }, - ); - - self.content.as_widget().draw( - self.tree, - renderer, - theme, - style, - layout.children().next().unwrap(), - cursor, - &layout.bounds(), - ); - } - - fn operate( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.content.as_widget().operate( - self.tree, - layout.children().next().unwrap(), - renderer, - operation, - ); - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.content.as_widget().mouse_interaction( - self.tree, - layout.children().next().unwrap(), - cursor, - viewport, - renderer, - ) - } - - fn overlay<'c>( - &'c mut self, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.content.as_widget_mut().overlay( - self.tree, - layout.children().next().unwrap(), - renderer, - Vector::ZERO, - ) - } - } - - impl<'a, Message, Theme, Renderer> From> - for Element<'a, Message, Theme, Renderer> - where - Theme: 'a, - Message: 'a + Clone, - Renderer: 'a + advanced::Renderer, - { - fn from(modal: Modal<'a, Message, Theme, Renderer>) -> Self { - Element::new(modal) - } - } + }) + ) + .on_press(on_blur) + ] + .into() }