diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 949bfad7a0..5e4da0c2cd 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -57,8 +57,9 @@ impl Example { mod bezier { use iced::mouse; - use iced::widget::canvas::event::{self, Event}; - use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke}; + use iced::widget::canvas::{ + self, Canvas, Event, Frame, Geometry, Path, Stroke, + }; use iced::{Element, Fill, Point, Rectangle, Renderer, Theme}; #[derive(Default)] @@ -96,48 +97,47 @@ mod bezier { event: Event, bounds: Rectangle, cursor: mouse::Cursor, - ) -> (event::Status, Option) { - let Some(cursor_position) = cursor.position_in(bounds) else { - return (event::Status::Ignored, None); - }; + ) -> Option> { + let cursor_position = cursor.position_in(bounds)?; match event { - Event::Mouse(mouse_event) => { - let message = match mouse_event { - mouse::Event::ButtonPressed(mouse::Button::Left) => { - match *state { - None => { - *state = Some(Pending::One { - from: cursor_position, - }); - - None - } - Some(Pending::One { from }) => { - *state = Some(Pending::Two { - from, - to: cursor_position, - }); - - None - } - Some(Pending::Two { from, to }) => { - *state = None; - - Some(Curve { - from, - to, - control: cursor_position, - }) - } - } + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) => Some( + match *state { + None => { + *state = Some(Pending::One { + from: cursor_position, + }); + + canvas::Action::request_redraw() } - _ => None, - }; + Some(Pending::One { from }) => { + *state = Some(Pending::Two { + from, + to: cursor_position, + }); - (event::Status::Captured, message) + canvas::Action::request_redraw() + } + Some(Pending::Two { from, to }) => { + *state = None; + + canvas::Action::publish(Curve { + from, + to, + control: cursor_position, + }) + } + } + .and_capture(), + ), + Event::Mouse(mouse::Event::CursorMoved { .. }) + if state.is_some() => + { + Some(canvas::Action::request_redraw()) } - _ => (event::Status::Ignored, None), + _ => None, } } diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 9dcebecc4b..7a7224d520 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -193,8 +193,9 @@ mod grid { use iced::mouse; use iced::touch; use iced::widget::canvas; - use iced::widget::canvas::event::{self, Event}; - use iced::widget::canvas::{Cache, Canvas, Frame, Geometry, Path, Text}; + use iced::widget::canvas::{ + Cache, Canvas, Event, Frame, Geometry, Path, Text, + }; use iced::{ Color, Element, Fill, Point, Rectangle, Renderer, Size, Theme, Vector, }; @@ -383,14 +384,12 @@ mod grid { event: Event, bounds: Rectangle, cursor: mouse::Cursor, - ) -> (event::Status, Option) { + ) -> Option> { if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { *interaction = Interaction::None; } - let Some(cursor_position) = cursor.position_in(bounds) else { - return (event::Status::Ignored, None); - }; + let cursor_position = cursor.position_in(bounds)?; let cell = Cell::at(self.project(cursor_position, bounds.size())); let is_populated = self.state.contains(&cell); @@ -413,7 +412,12 @@ mod grid { populate.or(unpopulate) }; - (event::Status::Captured, message) + Some( + message + .map(canvas::Action::publish) + .unwrap_or(canvas::Action::request_redraw()) + .and_capture(), + ) } Event::Mouse(mouse_event) => match mouse_event { mouse::Event::ButtonPressed(button) => { @@ -438,7 +442,12 @@ mod grid { _ => None, }; - (event::Status::Captured, message) + Some( + message + .map(canvas::Action::publish) + .unwrap_or(canvas::Action::request_redraw()) + .and_capture(), + ) } mouse::Event::CursorMoved { .. } => { let message = match *interaction { @@ -454,12 +463,14 @@ mod grid { Interaction::None => None, }; - let event_status = match interaction { - Interaction::None => event::Status::Ignored, - _ => event::Status::Captured, - }; + let action = message + .map(canvas::Action::publish) + .unwrap_or(canvas::Action::request_redraw()); - (event_status, message) + Some(match interaction { + Interaction::None => action, + _ => action.and_capture(), + }) } mouse::Event::WheelScrolled { delta } => match delta { mouse::ScrollDelta::Lines { y, .. } @@ -496,18 +507,21 @@ mod grid { None }; - ( - event::Status::Captured, - Some(Message::Scaled(scaling, translation)), + Some( + canvas::Action::publish(Message::Scaled( + scaling, + translation, + )) + .and_capture(), ) } else { - (event::Status::Captured, None) + Some(canvas::Action::capture()) } } }, - _ => (event::Status::Ignored, None), + _ => None, }, - _ => (event::Status::Ignored, None), + _ => None, } } diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index d5e5dffa76..5f4a5c90ec 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -3,9 +3,8 @@ //! computers like Microsoft Surface. use iced::mouse; use iced::touch; -use iced::widget::canvas::event; use iced::widget::canvas::stroke::{self, Stroke}; -use iced::widget::canvas::{self, Canvas, Geometry}; +use iced::widget::canvas::{self, Canvas, Event, Geometry}; use iced::{Color, Element, Fill, Point, Rectangle, Renderer, Theme}; use std::collections::HashMap; @@ -56,25 +55,25 @@ impl canvas::Program for Multitouch { fn update( &self, _state: &mut Self::State, - event: event::Event, + event: Event, _bounds: Rectangle, _cursor: mouse::Cursor, - ) -> (event::Status, Option) { - match event { - event::Event::Touch(touch_event) => match touch_event { + ) -> Option> { + let message = match event { + Event::Touch( touch::Event::FingerPressed { id, position } - | touch::Event::FingerMoved { id, position } => ( - event::Status::Captured, - Some(Message::FingerPressed { id, position }), - ), + | touch::Event::FingerMoved { id, position }, + ) => Some(Message::FingerPressed { id, position }), + Event::Touch( touch::Event::FingerLifted { id, .. } - | touch::Event::FingerLost { id, .. } => ( - event::Status::Captured, - Some(Message::FingerLifted { id }), - ), - }, - _ => (event::Status::Ignored, None), - } + | touch::Event::FingerLost { id, .. }, + ) => Some(Message::FingerLifted { id }), + _ => None, + }; + + message + .map(canvas::Action::publish) + .map(canvas::Action::and_capture) } fn draw( diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 99e7900a51..d4d483f5d4 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -1,6 +1,5 @@ use iced::mouse; -use iced::widget::canvas::event::{self, Event}; -use iced::widget::canvas::{self, Canvas, Geometry}; +use iced::widget::canvas::{self, Canvas, Event, Geometry}; use iced::widget::{column, row, slider, text}; use iced::{Center, Color, Fill, Point, Rectangle, Renderer, Size, Theme}; @@ -80,26 +79,22 @@ impl canvas::Program for SierpinskiGraph { event: Event, bounds: Rectangle, cursor: mouse::Cursor, - ) -> (event::Status, Option) { - let Some(cursor_position) = cursor.position_in(bounds) else { - return (event::Status::Ignored, None); - }; + ) -> Option> { + let cursor_position = cursor.position_in(bounds)?; match event { - Event::Mouse(mouse_event) => { - let message = match mouse_event { - iced::mouse::Event::ButtonPressed( - iced::mouse::Button::Left, - ) => Some(Message::PointAdded(cursor_position)), - iced::mouse::Event::ButtonPressed( - iced::mouse::Button::Right, - ) => Some(Message::PointRemoved), - _ => None, - }; - (event::Status::Captured, message) - } - _ => (event::Status::Ignored, None), + Event::Mouse(mouse::Event::ButtonPressed(button)) => match button { + mouse::Button::Left => Some(canvas::Action::publish( + Message::PointAdded(cursor_position), + )), + mouse::Button::Right => { + Some(canvas::Action::publish(Message::PointRemoved)) + } + _ => None, + }, + _ => None, } + .map(canvas::Action::and_capture) } fn draw( diff --git a/widget/src/action.rs b/widget/src/action.rs new file mode 100644 index 0000000000..1dd3a787a5 --- /dev/null +++ b/widget/src/action.rs @@ -0,0 +1,89 @@ +use crate::core::event; +use crate::core::time::Instant; +use crate::core::window; + +/// A runtime action that can be performed by some widgets. +#[derive(Debug, Clone)] +pub struct Action { + message_to_publish: Option, + redraw_request: Option, + event_status: event::Status, +} + +impl Action { + fn new() -> Self { + Self { + message_to_publish: None, + redraw_request: None, + event_status: event::Status::Ignored, + } + } + + /// Creates a new "capturing" [`Action`]. A capturing [`Action`] + /// will make other widgets consider it final and prevent further + /// processing. + /// + /// Prevents "event bubbling". + pub fn capture() -> Self { + Self { + event_status: event::Status::Captured, + ..Self::new() + } + } + + /// Creates a new [`Action`] that publishes the given `Message` for + /// the application to handle. + /// + /// Publishing a `Message` always produces a redraw. + pub fn publish(message: Message) -> Self { + Self { + message_to_publish: Some(message), + ..Self::new() + } + } + + /// Creates a new [`Action`] that requests a redraw to happen as + /// soon as possible; without publishing any `Message`. + pub fn request_redraw() -> Self { + Self { + redraw_request: Some(window::RedrawRequest::NextFrame), + ..Self::new() + } + } + + /// Creates a new [`Action`] that requests a redraw to happen at + /// the given [`Instant`]; without publishing any `Message`. + /// + /// This can be useful to efficiently animate content, like a + /// blinking caret on a text input. + pub fn request_redraw_at(at: Instant) -> Self { + Self { + redraw_request: Some(window::RedrawRequest::At(at)), + ..Self::new() + } + } + + /// Marks the [`Action`] as "capturing". See [`Self::capture`]. + pub fn and_capture(mut self) -> Self { + self.event_status = event::Status::Captured; + self + } + + /// Converts the [`Action`] into its internal parts. + /// + /// This method is meant to be used by runtimes, libraries, or internal + /// widget implementations. + pub fn into_inner( + self, + ) -> ( + Option, + Option, + event::Status, + ) { + ( + self.message_to_publish, + self.redraw_request, + self.event_status, + ) + } +} diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 63a2506433..23cc3f2b36 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -48,24 +48,24 @@ //! canvas(Circle { radius: 50.0 }).into() //! } //! ``` -pub mod event; - mod program; -pub use event::Event; pub use program::Program; +pub use crate::core::event::Event; pub use crate::graphics::cache::Group; pub use crate::graphics::geometry::{ fill, gradient, path, stroke, Fill, Gradient, Image, LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, }; +pub use crate::Action; -use crate::core; +use crate::core::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::window; use crate::core::{ Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget, }; @@ -148,6 +148,7 @@ where message_: PhantomData, theme_: PhantomData, renderer_: PhantomData, + last_mouse_interaction: Option, } impl Canvas @@ -166,6 +167,7 @@ where message_: PhantomData, theme_: PhantomData, renderer_: PhantomData, + last_mouse_interaction: None, } } @@ -216,39 +218,60 @@ where fn update( &mut self, tree: &mut Tree, - event: core::Event, + event: Event, layout: Layout<'_>, cursor: mouse::Cursor, - _renderer: &Renderer, + renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let bounds = layout.bounds(); - let canvas_event = match event { - core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), - core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), - core::Event::Keyboard(keyboard_event) => { - Some(Event::Keyboard(keyboard_event)) - } - core::Event::Window(_) => None, - }; - - if let Some(canvas_event) = canvas_event { - let state = tree.state.downcast_mut::(); + let state = tree.state.downcast_mut::(); + let is_redraw_request = matches!( + event, + Event::Window(window::Event::RedrawRequested(_now)), + ); - let (event_status, message) = - self.program.update(state, canvas_event, bounds, cursor); + if let Some(action) = self.program.update(state, event, bounds, cursor) + { + let (message, redraw_request, event_status) = action.into_inner(); if let Some(message) = message { shell.publish(message); } + if let Some(redraw_request) = redraw_request { + match redraw_request { + window::RedrawRequest::NextFrame => { + shell.request_redraw(); + } + window::RedrawRequest::At(at) => { + shell.request_redraw_at(at); + } + } + } + if event_status == event::Status::Captured { shell.capture_event(); } } + + if shell.redraw_request() != Some(window::RedrawRequest::NextFrame) { + let mouse_interaction = self + .mouse_interaction(tree, layout, cursor, viewport, renderer); + + if is_redraw_request { + self.last_mouse_interaction = Some(mouse_interaction); + } else if self.last_mouse_interaction.is_some_and( + |last_mouse_interaction| { + last_mouse_interaction != mouse_interaction + }, + ) { + shell.request_redraw(); + } + } } fn mouse_interaction( diff --git a/widget/src/canvas/event.rs b/widget/src/canvas/event.rs deleted file mode 100644 index a8eb47f7e0..0000000000 --- a/widget/src/canvas/event.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Handle events of a canvas. -use crate::core::keyboard; -use crate::core::mouse; -use crate::core::touch; - -pub use crate::core::event::Status; - -/// A [`Canvas`] event. -/// -/// [`Canvas`]: crate::Canvas -#[derive(Debug, Clone, PartialEq)] -pub enum Event { - /// A mouse event. - Mouse(mouse::Event), - - /// A touch event. - Touch(touch::Event), - - /// A keyboard event. - Keyboard(keyboard::Event), -} diff --git a/widget/src/canvas/program.rs b/widget/src/canvas/program.rs index a7ded0f47c..c68b28308a 100644 --- a/widget/src/canvas/program.rs +++ b/widget/src/canvas/program.rs @@ -1,8 +1,8 @@ -use crate::canvas::event::{self, Event}; use crate::canvas::mouse; -use crate::canvas::Geometry; +use crate::canvas::{Event, Geometry}; use crate::core::Rectangle; use crate::graphics::geometry; +use crate::Action; /// The state and logic of a [`Canvas`]. /// @@ -22,8 +22,9 @@ where /// When a [`Program`] is used in a [`Canvas`], the runtime will call this /// method for each [`Event`]. /// - /// This method can optionally return a `Message` to notify an application - /// of any meaningful interactions. + /// This method can optionally return an [`Action`] to either notify an + /// application of any meaningful interactions, capture the event, or + /// request a redraw. /// /// By default, this method does and returns nothing. /// @@ -34,8 +35,8 @@ where _event: Event, _bounds: Rectangle, _cursor: mouse::Cursor, - ) -> (event::Status, Option) { - (event::Status::Ignored, None) + ) -> Option> { + None } /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. @@ -84,7 +85,7 @@ where event: Event, bounds: Rectangle, cursor: mouse::Cursor, - ) -> (event::Status, Option) { + ) -> Option> { T::update(self, state, event, bounds, cursor) } diff --git a/widget/src/lib.rs b/widget/src/lib.rs index a68720d692..776a04a0fb 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -8,6 +8,7 @@ pub use iced_renderer::graphics; pub use iced_runtime as runtime; pub use iced_runtime::core; +mod action; mod column; mod mouse_area; mod row; @@ -131,4 +132,5 @@ pub use qr_code::QRCode; pub mod markdown; pub use crate::core::theme::{self, Theme}; +pub use action::Action; pub use renderer::Renderer;