Implement reactive-rendering for canvas

This commit is contained in:
Héctor Ramón Jiménez 2024-10-28 16:58:00 +01:00
parent a84b328dcc
commit 920596ed6f
No known key found for this signature in database
GPG key ID: 4C07CEC81AFA161F
9 changed files with 243 additions and 141 deletions

View file

@ -57,8 +57,9 @@ impl Example {
mod bezier { mod bezier {
use iced::mouse; use iced::mouse;
use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::{
use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke}; self, Canvas, Event, Frame, Geometry, Path, Stroke,
};
use iced::{Element, Fill, Point, Rectangle, Renderer, Theme}; use iced::{Element, Fill, Point, Rectangle, Renderer, Theme};
#[derive(Default)] #[derive(Default)]
@ -96,48 +97,47 @@ mod bezier {
event: Event, event: Event,
bounds: Rectangle, bounds: Rectangle,
cursor: mouse::Cursor, cursor: mouse::Cursor,
) -> (event::Status, Option<Curve>) { ) -> Option<canvas::Action<Curve>> {
let Some(cursor_position) = cursor.position_in(bounds) else { let cursor_position = cursor.position_in(bounds)?;
return (event::Status::Ignored, None);
};
match event { match event {
Event::Mouse(mouse_event) => { Event::Mouse(mouse::Event::ButtonPressed(
let message = match mouse_event { mouse::Button::Left,
mouse::Event::ButtonPressed(mouse::Button::Left) => { )) => Some(
match *state { match *state {
None => { None => {
*state = Some(Pending::One { *state = Some(Pending::One {
from: cursor_position, from: cursor_position,
}); });
None canvas::Action::request_redraw()
}
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,
})
}
}
} }
_ => 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,
} }
} }

View file

@ -193,8 +193,9 @@ mod grid {
use iced::mouse; use iced::mouse;
use iced::touch; use iced::touch;
use iced::widget::canvas; use iced::widget::canvas;
use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::{
use iced::widget::canvas::{Cache, Canvas, Frame, Geometry, Path, Text}; Cache, Canvas, Event, Frame, Geometry, Path, Text,
};
use iced::{ use iced::{
Color, Element, Fill, Point, Rectangle, Renderer, Size, Theme, Vector, Color, Element, Fill, Point, Rectangle, Renderer, Size, Theme, Vector,
}; };
@ -383,14 +384,12 @@ mod grid {
event: Event, event: Event,
bounds: Rectangle, bounds: Rectangle,
cursor: mouse::Cursor, cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) { ) -> Option<canvas::Action<Message>> {
if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event {
*interaction = Interaction::None; *interaction = Interaction::None;
} }
let Some(cursor_position) = cursor.position_in(bounds) else { let cursor_position = cursor.position_in(bounds)?;
return (event::Status::Ignored, None);
};
let cell = Cell::at(self.project(cursor_position, bounds.size())); let cell = Cell::at(self.project(cursor_position, bounds.size()));
let is_populated = self.state.contains(&cell); let is_populated = self.state.contains(&cell);
@ -413,7 +412,12 @@ mod grid {
populate.or(unpopulate) 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 { Event::Mouse(mouse_event) => match mouse_event {
mouse::Event::ButtonPressed(button) => { mouse::Event::ButtonPressed(button) => {
@ -438,7 +442,12 @@ mod grid {
_ => None, _ => None,
}; };
(event::Status::Captured, message) Some(
message
.map(canvas::Action::publish)
.unwrap_or(canvas::Action::request_redraw())
.and_capture(),
)
} }
mouse::Event::CursorMoved { .. } => { mouse::Event::CursorMoved { .. } => {
let message = match *interaction { let message = match *interaction {
@ -454,12 +463,14 @@ mod grid {
Interaction::None => None, Interaction::None => None,
}; };
let event_status = match interaction { let action = message
Interaction::None => event::Status::Ignored, .map(canvas::Action::publish)
_ => event::Status::Captured, .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::Event::WheelScrolled { delta } => match delta {
mouse::ScrollDelta::Lines { y, .. } mouse::ScrollDelta::Lines { y, .. }
@ -496,18 +507,21 @@ mod grid {
None None
}; };
( Some(
event::Status::Captured, canvas::Action::publish(Message::Scaled(
Some(Message::Scaled(scaling, translation)), scaling,
translation,
))
.and_capture(),
) )
} else { } else {
(event::Status::Captured, None) Some(canvas::Action::capture())
} }
} }
}, },
_ => (event::Status::Ignored, None), _ => None,
}, },
_ => (event::Status::Ignored, None), _ => None,
} }
} }

View file

@ -3,9 +3,8 @@
//! computers like Microsoft Surface. //! computers like Microsoft Surface.
use iced::mouse; use iced::mouse;
use iced::touch; use iced::touch;
use iced::widget::canvas::event;
use iced::widget::canvas::stroke::{self, Stroke}; 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 iced::{Color, Element, Fill, Point, Rectangle, Renderer, Theme};
use std::collections::HashMap; use std::collections::HashMap;
@ -56,25 +55,25 @@ impl canvas::Program<Message> for Multitouch {
fn update( fn update(
&self, &self,
_state: &mut Self::State, _state: &mut Self::State,
event: event::Event, event: Event,
_bounds: Rectangle, _bounds: Rectangle,
_cursor: mouse::Cursor, _cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) { ) -> Option<canvas::Action<Message>> {
match event { let message = match event {
event::Event::Touch(touch_event) => match touch_event { Event::Touch(
touch::Event::FingerPressed { id, position } touch::Event::FingerPressed { id, position }
| touch::Event::FingerMoved { id, position } => ( | touch::Event::FingerMoved { id, position },
event::Status::Captured, ) => Some(Message::FingerPressed { id, position }),
Some(Message::FingerPressed { id, position }), Event::Touch(
),
touch::Event::FingerLifted { id, .. } touch::Event::FingerLifted { id, .. }
| touch::Event::FingerLost { id, .. } => ( | touch::Event::FingerLost { id, .. },
event::Status::Captured, ) => Some(Message::FingerLifted { id }),
Some(Message::FingerLifted { id }), _ => None,
), };
},
_ => (event::Status::Ignored, None), message
} .map(canvas::Action::publish)
.map(canvas::Action::and_capture)
} }
fn draw( fn draw(

View file

@ -1,6 +1,5 @@
use iced::mouse; use iced::mouse;
use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::{self, Canvas, Event, Geometry};
use iced::widget::canvas::{self, Canvas, Geometry};
use iced::widget::{column, row, slider, text}; use iced::widget::{column, row, slider, text};
use iced::{Center, Color, Fill, Point, Rectangle, Renderer, Size, Theme}; use iced::{Center, Color, Fill, Point, Rectangle, Renderer, Size, Theme};
@ -80,26 +79,22 @@ impl canvas::Program<Message> for SierpinskiGraph {
event: Event, event: Event,
bounds: Rectangle, bounds: Rectangle,
cursor: mouse::Cursor, cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) { ) -> Option<canvas::Action<Message>> {
let Some(cursor_position) = cursor.position_in(bounds) else { let cursor_position = cursor.position_in(bounds)?;
return (event::Status::Ignored, None);
};
match event { match event {
Event::Mouse(mouse_event) => { Event::Mouse(mouse::Event::ButtonPressed(button)) => match button {
let message = match mouse_event { mouse::Button::Left => Some(canvas::Action::publish(
iced::mouse::Event::ButtonPressed( Message::PointAdded(cursor_position),
iced::mouse::Button::Left, )),
) => Some(Message::PointAdded(cursor_position)), mouse::Button::Right => {
iced::mouse::Event::ButtonPressed( Some(canvas::Action::publish(Message::PointRemoved))
iced::mouse::Button::Right, }
) => Some(Message::PointRemoved), _ => None,
_ => None, },
}; _ => None,
(event::Status::Captured, message)
}
_ => (event::Status::Ignored, None),
} }
.map(canvas::Action::and_capture)
} }
fn draw( fn draw(

89
widget/src/action.rs Normal file
View file

@ -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> {
message_to_publish: Option<Message>,
redraw_request: Option<window::RedrawRequest>,
event_status: event::Status,
}
impl<Message> Action<Message> {
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<Message>,
Option<window::RedrawRequest>,
event::Status,
) {
(
self.message_to_publish,
self.redraw_request,
self.event_status,
)
}
}

View file

@ -48,24 +48,24 @@
//! canvas(Circle { radius: 50.0 }).into() //! canvas(Circle { radius: 50.0 }).into()
//! } //! }
//! ``` //! ```
pub mod event;
mod program; mod program;
pub use event::Event;
pub use program::Program; pub use program::Program;
pub use crate::core::event::Event;
pub use crate::graphics::cache::Group; pub use crate::graphics::cache::Group;
pub use crate::graphics::geometry::{ pub use crate::graphics::geometry::{
fill, gradient, path, stroke, Fill, Gradient, Image, LineCap, LineDash, fill, gradient, path, stroke, Fill, Gradient, Image, LineCap, LineDash,
LineJoin, Path, Stroke, Style, Text, 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::layout::{self, Layout};
use crate::core::mouse; use crate::core::mouse;
use crate::core::renderer; use crate::core::renderer;
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::window;
use crate::core::{ use crate::core::{
Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget, Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget,
}; };
@ -148,6 +148,7 @@ where
message_: PhantomData<Message>, message_: PhantomData<Message>,
theme_: PhantomData<Theme>, theme_: PhantomData<Theme>,
renderer_: PhantomData<Renderer>, renderer_: PhantomData<Renderer>,
last_mouse_interaction: Option<mouse::Interaction>,
} }
impl<P, Message, Theme, Renderer> Canvas<P, Message, Theme, Renderer> impl<P, Message, Theme, Renderer> Canvas<P, Message, Theme, Renderer>
@ -166,6 +167,7 @@ where
message_: PhantomData, message_: PhantomData,
theme_: PhantomData, theme_: PhantomData,
renderer_: PhantomData, renderer_: PhantomData,
last_mouse_interaction: None,
} }
} }
@ -216,39 +218,60 @@ where
fn update( fn update(
&mut self, &mut self,
tree: &mut Tree, tree: &mut Tree,
event: core::Event, event: Event,
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
_renderer: &Renderer, renderer: &Renderer,
_clipboard: &mut dyn Clipboard, _clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
_viewport: &Rectangle, viewport: &Rectangle,
) { ) {
let bounds = layout.bounds(); let bounds = layout.bounds();
let canvas_event = match event { let state = tree.state.downcast_mut::<P::State>();
core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), let is_redraw_request = matches!(
core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), event,
core::Event::Keyboard(keyboard_event) => { Event::Window(window::Event::RedrawRequested(_now)),
Some(Event::Keyboard(keyboard_event)) );
}
core::Event::Window(_) => None,
};
if let Some(canvas_event) = canvas_event { if let Some(action) = self.program.update(state, event, bounds, cursor)
let state = tree.state.downcast_mut::<P::State>(); {
let (message, redraw_request, event_status) = action.into_inner();
let (event_status, message) =
self.program.update(state, canvas_event, bounds, cursor);
if let Some(message) = message { if let Some(message) = message {
shell.publish(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 { if event_status == event::Status::Captured {
shell.capture_event(); 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( fn mouse_interaction(

View file

@ -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),
}

View file

@ -1,8 +1,8 @@
use crate::canvas::event::{self, Event};
use crate::canvas::mouse; use crate::canvas::mouse;
use crate::canvas::Geometry; use crate::canvas::{Event, Geometry};
use crate::core::Rectangle; use crate::core::Rectangle;
use crate::graphics::geometry; use crate::graphics::geometry;
use crate::Action;
/// The state and logic of a [`Canvas`]. /// 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 /// When a [`Program`] is used in a [`Canvas`], the runtime will call this
/// method for each [`Event`]. /// method for each [`Event`].
/// ///
/// This method can optionally return a `Message` to notify an application /// This method can optionally return an [`Action`] to either notify an
/// of any meaningful interactions. /// application of any meaningful interactions, capture the event, or
/// request a redraw.
/// ///
/// By default, this method does and returns nothing. /// By default, this method does and returns nothing.
/// ///
@ -34,8 +35,8 @@ where
_event: Event, _event: Event,
_bounds: Rectangle, _bounds: Rectangle,
_cursor: mouse::Cursor, _cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) { ) -> Option<Action<Message>> {
(event::Status::Ignored, None) None
} }
/// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. /// Draws the state of the [`Program`], producing a bunch of [`Geometry`].
@ -84,7 +85,7 @@ where
event: Event, event: Event,
bounds: Rectangle, bounds: Rectangle,
cursor: mouse::Cursor, cursor: mouse::Cursor,
) -> (event::Status, Option<Message>) { ) -> Option<Action<Message>> {
T::update(self, state, event, bounds, cursor) T::update(self, state, event, bounds, cursor)
} }

View file

@ -8,6 +8,7 @@ pub use iced_renderer::graphics;
pub use iced_runtime as runtime; pub use iced_runtime as runtime;
pub use iced_runtime::core; pub use iced_runtime::core;
mod action;
mod column; mod column;
mod mouse_area; mod mouse_area;
mod row; mod row;
@ -131,4 +132,5 @@ pub use qr_code::QRCode;
pub mod markdown; pub mod markdown;
pub use crate::core::theme::{self, Theme}; pub use crate::core::theme::{self, Theme};
pub use action::Action;
pub use renderer::Renderer; pub use renderer::Renderer;