From 3c866c15aa1db944a2056f01449a2fbdda2f5abb Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Tue, 17 Jan 2023 10:01:17 -0800 Subject: [PATCH 1/6] Add group overlay element --- native/src/overlay.rs | 2 + native/src/overlay/group.rs | 165 ++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 native/src/overlay/group.rs diff --git a/native/src/overlay.rs b/native/src/overlay.rs index 22f8b6ec80..0b1e8dafa9 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -1,9 +1,11 @@ //! Display interactive elements on top of other widgets. mod element; +mod group; pub mod menu; pub use element::Element; +pub use group::Group; pub use menu::Menu; use crate::event::{self, Event}; diff --git a/native/src/overlay/group.rs b/native/src/overlay/group.rs new file mode 100644 index 0000000000..f894f9117f --- /dev/null +++ b/native/src/overlay/group.rs @@ -0,0 +1,165 @@ +use iced_core::{Point, Rectangle, Size}; + +use crate::event; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::renderer; +use crate::widget; +use crate::{Clipboard, Event, Layout, Overlay, Shell}; + +/// An [`Overlay`] container that displays multiple overlay +/// [`overlay::Element`] children +#[allow(missing_debug_implementations)] +pub struct Group<'a, Message, Renderer> { + children: Vec>, +} + +impl<'a, Message, Renderer> Group<'a, Message, Renderer> +where + Renderer: 'a + crate::Renderer, + Message: 'a, +{ + /// Creates an empty [`Group`]. + pub fn new() -> Self { + Self::default() + } + + /// Creates a [`Group`] with the given elements. + pub fn with_children( + children: Vec>, + ) -> Self { + Group { children } + } + + /// Adds an [`overlay::Element`] to the [`Group`]. + pub fn push( + mut self, + child: impl Into>, + ) -> Self { + self.children.push(child.into()); + self + } + + /// Turns the [`Group`] into an overlay [`overlay::Element`] + pub fn overlay(self) -> overlay::Element<'a, Message, Renderer> { + overlay::Element::new(Point::ORIGIN, Box::new(self)) + } +} + +impl<'a, Message, Renderer> Default for Group<'a, Message, Renderer> +where + Renderer: 'a + crate::Renderer, + Message: 'a, +{ + fn default() -> Self { + Self::with_children(Vec::new()) + } +} + +impl<'a, Message, Renderer> Overlay + for Group<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + _position: Point, + ) -> layout::Node { + layout::Node::with_children( + bounds, + self.children + .iter() + .map(|child| child.layout(renderer, bounds)) + .collect(), + ) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.children + .iter_mut() + .zip(layout.children()) + .map(|(child, layout)| { + child.on_event( + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + for (child, layout) in self.children.iter().zip(layout.children()) { + child.draw(renderer, theme, style, layout, cursor_position); + } + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(layout.children()) + .map(|(child, layout)| { + child.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + operation.container(None, &mut |operation| { + self.children.iter_mut().zip(layout.children()).for_each( + |(child, layout)| { + child.operate(layout, renderer, operation); + }, + ) + }); + } +} + +impl<'a, Message, Renderer> From> + for overlay::Element<'a, Message, Renderer> +where + Renderer: 'a + crate::Renderer, + Message: 'a, +{ + fn from(group: Group<'a, Message, Renderer>) -> Self { + group.overlay() + } +} From b2a3a85acb2a0722e90c46b70d574f1d676da9d1 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Tue, 17 Jan 2023 10:12:51 -0800 Subject: [PATCH 2/6] Use group overlay for containers w/ children --- native/src/overlay.rs | 8 +++++--- native/src/widget/pane_grid.rs | 13 ++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/native/src/overlay.rs b/native/src/overlay.rs index 0b1e8dafa9..e739449489 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -91,7 +91,7 @@ where } } -/// Obtains the first overlay [`Element`] found in the given children. +/// Returns a [`Group`] of overlay [`Element`] children. /// /// This method will generally only be used by advanced users that are /// implementing the [`Widget`](crate::Widget) trait. @@ -104,12 +104,14 @@ pub fn from_children<'a, Message, Renderer>( where Renderer: crate::Renderer, { - children + let children = children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .filter_map(|((child, state), layout)| { child.as_widget_mut().overlay(state, layout, renderer) }) - .next() + .collect::>(); + + (!children.is_empty()).then(|| Group::with_children(children).overlay()) } diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 8dbd182569..eb04c0bac1 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -35,7 +35,7 @@ pub use iced_style::pane_grid::{Line, StyleSheet}; use crate::event::{self, Event}; use crate::layout; use crate::mouse; -use crate::overlay; +use crate::overlay::{self, Group}; use crate::renderer; use crate::touch; use crate::widget; @@ -450,14 +450,17 @@ where layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - self.contents + let children = self + .contents .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .filter_map(|(((_, pane), tree), layout)| { - pane.overlay(tree, layout, renderer) + .filter_map(|(((_, content), state), layout)| { + content.overlay(state, layout, renderer) }) - .next() + .collect::>(); + + (!children.is_empty()).then(|| Group::with_children(children).overlay()) } } From 3ab679725526bd095cc1a160705312b16c408b92 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Tue, 17 Jan 2023 11:12:10 -0800 Subject: [PATCH 3/6] New method to determine if overlay contains cursor This is needed for "container" overlay's such as `Group` which should only consider it's childrens layouts and not it's own when determining if the cursor is captured by the overlay. --- native/src/overlay.rs | 9 +++++++++ native/src/overlay/element.rs | 17 +++++++++++++++++ native/src/overlay/group.rs | 13 +++++++++++++ native/src/user_interface.rs | 11 +++++++++-- 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/native/src/overlay.rs b/native/src/overlay.rs index e739449489..16d8bb31a3 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -89,6 +89,15 @@ where ) -> mouse::Interaction { mouse::Interaction::Idle } + + /// Whether the [`Overlay`] contains the cursor + fn contains_cursor( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + layout.bounds().contains(cursor_position) + } } /// Returns a [`Group`] of overlay [`Element`] children. diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs index 41a8a59720..125258c551 100644 --- a/native/src/overlay/element.rs +++ b/native/src/overlay/element.rs @@ -115,6 +115,15 @@ where ) { self.overlay.operate(layout, renderer, operation); } + + /// Whether the [`Overlay`] contains the cursor + pub fn contains_cursor( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + self.overlay.contains_cursor(layout, cursor_position) + } } struct Map<'a, A, B, Renderer> { @@ -252,4 +261,12 @@ where self.content .draw(renderer, theme, style, layout, cursor_position) } + + fn contains_cursor( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + self.content.contains_cursor(layout, cursor_position) + } } diff --git a/native/src/overlay/group.rs b/native/src/overlay/group.rs index f894f9117f..96d10c19ca 100644 --- a/native/src/overlay/group.rs +++ b/native/src/overlay/group.rs @@ -151,6 +151,19 @@ where ) }); } + + fn contains_cursor( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + self.children + .iter() + .zip(layout.children()) + .any(|(child, layout)| { + child.contains_cursor(layout, cursor_position) + }) + } } impl<'a, Message, Renderer> From> diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 29cc3472e4..8659caa319 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -261,7 +261,11 @@ where } } - let base_cursor = if layout.bounds().contains(cursor_position) { + let base_cursor = if manual_overlay + .as_ref() + .unwrap() + .contains_cursor(Layout::new(&layout), cursor_position) + { // TODO: Type-safe cursor availability Point::new(-1.0, -1.0) } else { @@ -504,7 +508,10 @@ where ); }); - if overlay_bounds.contains(cursor_position) { + if overlay.contains_cursor( + Layout::new(layout), + cursor_position, + ) { overlay_interaction } else { base_interaction From d470467718ecad0f37599a811bef846846dbb2b9 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Tue, 17 Jan 2023 17:10:58 -0800 Subject: [PATCH 4/6] Add toast example --- examples/toast/Cargo.toml | 10 + examples/toast/src/main.rs | 674 +++++++++++++++++++++++++++++++++++++ native/src/shell.rs | 5 + 3 files changed, 689 insertions(+) create mode 100644 examples/toast/Cargo.toml create mode 100644 examples/toast/src/main.rs diff --git a/examples/toast/Cargo.toml b/examples/toast/Cargo.toml new file mode 100644 index 0000000000..f1f986aabc --- /dev/null +++ b/examples/toast/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "toast" +version = "0.1.0" +authors = ["tarkah "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = [] } +iced_native = { path = "../../native" } diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs new file mode 100644 index 0000000000..11b5589562 --- /dev/null +++ b/examples/toast/src/main.rs @@ -0,0 +1,674 @@ +use iced::widget::{ + self, button, column, container, pick_list, row, slider, text, text_input, +}; +use iced::{ + executor, keyboard, subscription, Alignment, Application, Command, Element, + Event, Length, Settings, Subscription, +}; + +use toast::{Status, Toast}; + +pub fn main() -> iced::Result { + App::run(Settings::default()) +} + +#[derive(Default)] +struct App { + toasts: Vec, + editing: Toast, + timeout_secs: u64, +} + +#[derive(Debug, Clone)] +#[allow(clippy::enum_variant_names)] +enum Message { + Add, + Close(usize), + Title(String), + Body(String), + Status(Status), + Timeout(f64), + Event(Event), +} + +impl Application for App { + type Executor = executor::Default; + type Message = Message; + type Theme = iced::Theme; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command) { + ( + App { + toasts: vec![Toast { + title: "Example Toast".into(), + body: "Add more toasts in the form below!".into(), + status: Status::Primary, + }], + timeout_secs: toast::DEFAULT_TIMEOUT, + ..Default::default() + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Toast - Iced") + } + + fn subscription(&self) -> Subscription { + subscription::events().map(Message::Event) + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::Add => { + if !self.editing.title.is_empty() + && !self.editing.body.is_empty() + { + self.toasts.push(std::mem::take(&mut self.editing)); + } + Command::none() + } + Message::Close(index) => { + self.toasts.remove(index); + Command::none() + } + Message::Title(title) => { + self.editing.title = title; + Command::none() + } + Message::Body(body) => { + self.editing.body = body; + Command::none() + } + Message::Status(status) => { + self.editing.status = status; + Command::none() + } + Message::Timeout(timeout) => { + self.timeout_secs = timeout as u64; + Command::none() + } + Message::Event(Event::Keyboard(keyboard::Event::KeyPressed { + key_code: keyboard::KeyCode::Tab, + modifiers, + })) if modifiers.shift() => widget::focus_previous(), + Message::Event(Event::Keyboard(keyboard::Event::KeyPressed { + key_code: keyboard::KeyCode::Tab, + .. + })) => widget::focus_next(), + Message::Event(_) => Command::none(), + } + } + + fn view<'a>(&'a self) -> Element<'a, Message> { + let subtitle = |title, content: Element<'a, Message>| { + column![text(title).size(14), content] + .width(Length::Fill) + .spacing(5) + }; + + let mut add_toast = button("Add Toast"); + + if !self.editing.body.is_empty() && !self.editing.title.is_empty() { + add_toast = add_toast.on_press(Message::Add); + } + + let content = container( + column![ + subtitle( + "Title", + text_input("", &self.editing.title, Message::Title) + .on_submit(Message::Add) + .into() + ), + subtitle( + "Message", + text_input("", &self.editing.body, Message::Body) + .on_submit(Message::Add) + .into() + ), + subtitle( + "Status", + pick_list( + toast::Status::ALL, + Some(self.editing.status), + Message::Status + ) + .width(Length::Fill) + .into() + ), + subtitle( + "Timeout", + row![ + text(format!("{:0>2} sec", self.timeout_secs)), + slider( + 1.0..=30.0, + self.timeout_secs as f64, + Message::Timeout + ) + .step(1.0) + .width(Length::Fill) + ] + .spacing(5) + .into() + ), + column![add_toast] + .width(Length::Fill) + .align_items(Alignment::End) + ] + .spacing(10) + .max_width(200), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y(); + + toast::Manager::new(content, &self.toasts, Message::Close) + .timeout(self.timeout_secs) + .into() + } +} + +mod toast { + use std::fmt; + use std::time::{Duration, Instant}; + + use iced::theme; + use iced::widget::{ + button, column, container, horizontal_rule, horizontal_space, row, text, + }; + use iced::{ + Alignment, Element, Length, Point, Rectangle, Renderer, Size, Theme, + Vector, + }; + use iced_native::widget::{tree, Operation, Tree}; + use iced_native::{event, layout, mouse, overlay, renderer, window}; + use iced_native::{Clipboard, Event, Layout, Shell, Widget}; + + pub const DEFAULT_TIMEOUT: u64 = 5; + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] + pub enum Status { + #[default] + Primary, + Secondary, + Success, + Danger, + } + + impl Status { + pub const ALL: &[Self] = + &[Self::Primary, Self::Secondary, Self::Success, Self::Danger]; + } + + impl container::StyleSheet for Status { + type Style = Theme; + + fn appearance(&self, theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); + + let pair = match self { + Status::Primary => palette.primary.weak, + Status::Secondary => palette.secondary.weak, + Status::Success => palette.success.weak, + Status::Danger => palette.danger.weak, + }; + + container::Appearance { + background: pair.color.into(), + text_color: pair.text.into(), + ..Default::default() + } + } + } + + impl fmt::Display for Status { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Status::Primary => "Primary", + Status::Secondary => "Secondary", + Status::Success => "Success", + Status::Danger => "Danger", + } + .fmt(f) + } + } + + #[derive(Debug, Clone, Default)] + pub struct Toast { + pub title: String, + pub body: String, + pub status: Status, + } + + pub struct Manager<'a, Message> { + content: Element<'a, Message>, + toasts: Vec>, + timeout_secs: u64, + on_close: Box Message + 'a>, + } + + impl<'a, Message> Manager<'a, Message> + where + Message: 'a + Clone, + { + pub fn new( + content: impl Into>, + toasts: &'a [Toast], + on_close: impl Fn(usize) -> Message + 'a, + ) -> Self { + let toasts = toasts + .iter() + .enumerate() + .map(|(index, toast)| { + container(column![ + container( + row![ + text(toast.title.as_str()), + horizontal_space(Length::Fill), + button("X") + .on_press((on_close)(index)) + .padding(3), + ] + .align_items(Alignment::Center) + ) + .width(Length::Fill) + .padding(5) + .style( + theme::Container::Custom(Box::new(toast.status)) + ), + horizontal_rule(1), + container(text(toast.body.as_str())) + .width(Length::Fill) + .padding(5) + .style(theme::Container::Box), + ]) + .max_width(200) + .into() + }) + .collect(); + + Self { + content: content.into(), + toasts, + timeout_secs: DEFAULT_TIMEOUT, + on_close: Box::new(on_close), + } + } + + pub fn timeout(self, seconds: u64) -> Self { + Self { + timeout_secs: seconds, + ..self + } + } + } + + impl<'a, Message> Widget for Manager<'a, Message> { + fn width(&self) -> Length { + self.content.as_widget().width() + } + + fn height(&self) -> Length { + self.content.as_widget().height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.as_widget().layout(renderer, limits) + } + + fn tag(&self) -> tree::Tag { + struct Marker(Vec); + iced_native::widget::tree::Tag::of::() + } + + fn state(&self) -> tree::State { + iced_native::widget::tree::State::new(Vec::>::new()) + } + + fn children(&self) -> Vec { + std::iter::once(Tree::new(&self.content)) + .chain(self.toasts.iter().map(Tree::new)) + .collect() + } + + fn diff(&self, tree: &mut Tree) { + let instants = tree.state.downcast_mut::>>(); + + // Invalidating removed instants to None allows us to remove + // them here so that diffing for removed / new toast instants + // is accurate + instants.retain(Option::is_some); + + match (instants.len(), self.toasts.len()) { + (old, new) if old > new => { + instants.truncate(new); + } + (old, new) if old < new => { + instants.extend( + std::iter::repeat(Some(Instant::now())).take(new - old), + ); + } + _ => {} + } + + tree.diff_children( + &std::iter::once(&self.content) + .chain(self.toasts.iter()) + .collect::>(), + ); + } + + fn operate( + &self, + state: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + operation.container(None, &mut |operation| { + self.content.as_widget().operate( + &mut state.children[0], + layout, + renderer, + operation, + ); + }); + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.content.as_widget_mut().on_event( + &mut state.children[0], + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &state.children[0], + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ); + } + + fn mouse_interaction( + &self, + state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &state.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + let instants = state.state.downcast_mut::>>(); + + let (content_state, toasts_state) = state.children.split_at_mut(1); + + let content = self.content.as_widget_mut().overlay( + &mut content_state[0], + layout, + renderer, + ); + + let toasts = (!self.toasts.is_empty()).then(|| { + overlay::Element::new( + layout.bounds().position(), + Box::new(Overlay { + toasts: &mut self.toasts, + state: toasts_state, + instants, + on_close: &self.on_close, + timeout_secs: self.timeout_secs, + }), + ) + }); + let overlays = + content.into_iter().chain(toasts).collect::>(); + + (!overlays.is_empty()) + .then(|| overlay::Group::with_children(overlays).overlay()) + } + } + + struct Overlay<'a, 'b, Message> { + toasts: &'b mut [Element<'a, Message>], + state: &'b mut [Tree], + instants: &'b mut [Option], + on_close: &'b dyn Fn(usize) -> Message, + timeout_secs: u64, + } + + impl<'a, 'b, Message> overlay::Overlay + for Overlay<'a, 'b, Message> + { + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + let limits = layout::Limits::new(Size::ZERO, bounds) + .width(Length::Fill) + .height(Length::Fill); + + layout::flex::resolve( + layout::flex::Axis::Vertical, + renderer, + &limits, + 10.into(), + 10.0, + Alignment::End, + self.toasts, + ) + .translate(Vector::new(position.x, position.y)) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + if let Event::Window(window::Event::RedrawRequested(now)) = &event { + let mut next_redraw: Option = None; + + self.instants.iter_mut().enumerate().for_each( + |(index, maybe_instant)| { + if let Some(instant) = maybe_instant.as_mut() { + let remaining = + Duration::from_secs(self.timeout_secs) + .saturating_sub(instant.elapsed()); + + if remaining == Duration::ZERO { + maybe_instant.take(); + shell.publish((self.on_close)(index)); + next_redraw = + Some(window::RedrawRequest::NextFrame); + } else { + let redraw_at = + window::RedrawRequest::At(*now + remaining); + next_redraw = next_redraw + .map(|redraw| redraw.min(redraw_at)) + .or(Some(redraw_at)); + } + } + }, + ); + + if let Some(redraw) = next_redraw { + shell.request_redraw(redraw); + } + } + + self.toasts + .iter_mut() + .zip(self.state.iter_mut()) + .zip(layout.children()) + .zip(self.instants.iter_mut()) + .map(|(((child, state), layout), instant)| { + let mut local_messages = vec![]; + let mut local_shell = Shell::new(&mut local_messages); + + let status = child.as_widget_mut().on_event( + state, + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + &mut local_shell, + ); + + if !local_shell.is_empty() { + instant.take(); + } + + shell.merge(local_shell, std::convert::identity); + + status + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + let viewport = layout.bounds(); + + for ((child, state), layout) in self + .toasts + .iter() + .zip(self.state.iter()) + .zip(layout.children()) + { + child.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor_position, + &viewport, + ); + } + } + + fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn iced_native::widget::Operation, + ) { + operation.container(None, &mut |operation| { + self.toasts + .iter() + .zip(self.state.iter_mut()) + .zip(layout.children()) + .for_each(|((child, state), layout)| { + child + .as_widget() + .operate(state, layout, renderer, operation); + }) + }); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.toasts + .iter() + .zip(self.state.iter()) + .zip(layout.children()) + .map(|((child, state), layout)| { + child.as_widget().mouse_interaction( + state, + layout, + cursor_position, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn contains_cursor( + &self, + layout: Layout<'_>, + cursor_position: Point, + ) -> bool { + layout + .children() + .any(|layout| layout.bounds().contains(cursor_position)) + } + } + + impl<'a, Message> From> for Element<'a, Message> + where + Message: 'a, + { + fn from(manager: Manager<'a, Message>) -> Self { + Element::new(manager) + } + } +} diff --git a/native/src/shell.rs b/native/src/shell.rs index f1ddb48e61..74a5c616de 100644 --- a/native/src/shell.rs +++ b/native/src/shell.rs @@ -25,6 +25,11 @@ impl<'a, Message> Shell<'a, Message> { } } + /// Returns true if the [`Shell`] contains no published messages + pub fn is_empty(&self) -> bool { + self.messages.is_empty() + } + /// Publish the given `Message` for an application to process it. pub fn publish(&mut self, message: Message) { self.messages.push(message); From be860508a9deed1f4583e045790eb9ddd74d07d5 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Tue, 17 Jan 2023 17:20:53 -0800 Subject: [PATCH 5/6] Rename method to is_over --- examples/toast/src/main.rs | 6 +----- native/src/overlay.rs | 8 ++------ native/src/overlay/element.rs | 18 +++++------------- native/src/overlay/group.rs | 10 ++-------- native/src/user_interface.rs | 8 +++----- 5 files changed, 13 insertions(+), 37 deletions(-) diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 11b5589562..e74b3ee62c 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -652,11 +652,7 @@ mod toast { .unwrap_or_default() } - fn contains_cursor( - &self, - layout: Layout<'_>, - cursor_position: Point, - ) -> bool { + fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { layout .children() .any(|layout| layout.bounds().contains(cursor_position)) diff --git a/native/src/overlay.rs b/native/src/overlay.rs index 16d8bb31a3..1c3d0fb91f 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -90,12 +90,8 @@ where mouse::Interaction::Idle } - /// Whether the [`Overlay`] contains the cursor - fn contains_cursor( - &self, - layout: Layout<'_>, - cursor_position: Point, - ) -> bool { + /// Returns true if the cursor is over the [`Overlay`] + fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { layout.bounds().contains(cursor_position) } } diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs index 125258c551..edeb7dbfd5 100644 --- a/native/src/overlay/element.rs +++ b/native/src/overlay/element.rs @@ -116,13 +116,9 @@ where self.overlay.operate(layout, renderer, operation); } - /// Whether the [`Overlay`] contains the cursor - pub fn contains_cursor( - &self, - layout: Layout<'_>, - cursor_position: Point, - ) -> bool { - self.overlay.contains_cursor(layout, cursor_position) + /// Returns true if the cursor is over the [`Element`] + pub fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + self.overlay.is_over(layout, cursor_position) } } @@ -262,11 +258,7 @@ where .draw(renderer, theme, style, layout, cursor_position) } - fn contains_cursor( - &self, - layout: Layout<'_>, - cursor_position: Point, - ) -> bool { - self.content.contains_cursor(layout, cursor_position) + fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + self.content.is_over(layout, cursor_position) } } diff --git a/native/src/overlay/group.rs b/native/src/overlay/group.rs index 96d10c19ca..fa3c7396e3 100644 --- a/native/src/overlay/group.rs +++ b/native/src/overlay/group.rs @@ -152,17 +152,11 @@ where }); } - fn contains_cursor( - &self, - layout: Layout<'_>, - cursor_position: Point, - ) -> bool { + fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { self.children .iter() .zip(layout.children()) - .any(|(child, layout)| { - child.contains_cursor(layout, cursor_position) - }) + .any(|(child, layout)| child.is_over(layout, cursor_position)) } } diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 8659caa319..0def730cdd 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -264,7 +264,7 @@ where let base_cursor = if manual_overlay .as_ref() .unwrap() - .contains_cursor(Layout::new(&layout), cursor_position) + .is_over(Layout::new(&layout), cursor_position) { // TODO: Type-safe cursor availability Point::new(-1.0, -1.0) @@ -508,10 +508,8 @@ where ); }); - if overlay.contains_cursor( - Layout::new(layout), - cursor_position, - ) { + if overlay.is_over(Layout::new(layout), cursor_position) + { overlay_interaction } else { base_interaction From 01c484245be54c1aeb6605659fb0f222856c28da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 24 Jan 2023 01:59:34 +0100 Subject: [PATCH 6/6] Fix some minor documentation inconsistencies --- native/src/overlay.rs | 5 ++++- native/src/overlay/element.rs | 2 +- native/src/overlay/group.rs | 6 +++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/native/src/overlay.rs b/native/src/overlay.rs index 1c3d0fb91f..6cada41655 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -90,7 +90,10 @@ where mouse::Interaction::Idle } - /// Returns true if the cursor is over the [`Overlay`] + /// Returns true if the cursor is over the [`Overlay`]. + /// + /// By default, it returns true if the bounds of the `layout` contain + /// the `cursor_position`. fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { layout.bounds().contains(cursor_position) } diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs index edeb7dbfd5..bdf7766e7f 100644 --- a/native/src/overlay/element.rs +++ b/native/src/overlay/element.rs @@ -116,7 +116,7 @@ where self.overlay.operate(layout, renderer, operation); } - /// Returns true if the cursor is over the [`Element`] + /// Returns true if the cursor is over the [`Element`]. pub fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { self.overlay.is_over(layout, cursor_position) } diff --git a/native/src/overlay/group.rs b/native/src/overlay/group.rs index fa3c7396e3..5c9cf809b3 100644 --- a/native/src/overlay/group.rs +++ b/native/src/overlay/group.rs @@ -8,8 +8,8 @@ use crate::renderer; use crate::widget; use crate::{Clipboard, Event, Layout, Overlay, Shell}; -/// An [`Overlay`] container that displays multiple overlay -/// [`overlay::Element`] children +/// An [`Overlay`] container that displays multiple overlay [`overlay::Element`] +/// children. #[allow(missing_debug_implementations)] pub struct Group<'a, Message, Renderer> { children: Vec>, @@ -41,7 +41,7 @@ where self } - /// Turns the [`Group`] into an overlay [`overlay::Element`] + /// Turns the [`Group`] into an overlay [`overlay::Element`]. pub fn overlay(self) -> overlay::Element<'a, Message, Renderer> { overlay::Element::new(Point::ORIGIN, Box::new(self)) }