From 29a19fcde1d097ec9f786b46c3c2090078ae5c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 29 Apr 2025 03:03:32 +0200 Subject: [PATCH] Purify `Animation` API and introduce `application::timed` --- core/src/animation.rs | 14 ++- examples/arc/src/main.rs | 18 ++- examples/clock/src/main.rs | 18 ++- examples/gallery/src/main.rs | 81 +++++++------- examples/markdown/src/main.rs | 30 ++--- examples/solar_system/src/main.rs | 34 +++--- examples/the_matrix/src/main.rs | 8 +- src/application.rs | 4 + src/application/timed.rs | 178 ++++++++++++++++++++++++++++++ 9 files changed, 281 insertions(+), 104 deletions(-) create mode 100644 src/application/timed.rs diff --git a/core/src/animation.rs b/core/src/animation.rs index 14cbb5c3..fa82c1dc 100644 --- a/core/src/animation.rs +++ b/core/src/animation.rs @@ -90,15 +90,17 @@ where self } - /// Transitions the [`Animation`] from its current state to the given new state. - pub fn go(mut self, new_state: T) -> Self { - self.go_mut(new_state); + /// Transitions the [`Animation`] from its current state to the given new state + /// at the given time. + pub fn go(mut self, new_state: T, at: Instant) -> Self { + self.go_mut(new_state, at); self } - /// Transitions the [`Animation`] from its current state to the given new state, by reference. - pub fn go_mut(&mut self, new_state: T) { - self.raw.transition(new_state, Instant::now()); + /// Transitions the [`Animation`] from its current state to the given new state + /// at the given time, by reference. + pub fn go_mut(&mut self, new_state: T, at: Instant) { + self.raw.transition(new_state, at); } /// Returns true if the [`Animation`] is currently in progress. diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index b83a36d8..3e80a5db 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -8,7 +8,7 @@ use iced::window; use iced::{Element, Fill, Point, Rectangle, Renderer, Subscription, Theme}; pub fn main() -> iced::Result { - iced::application(Arc::default, Arc::update, Arc::view) + iced::application(Arc::new, Arc::update, Arc::view) .subscription(Arc::subscription) .theme(|_| Theme::Dark) .run() @@ -25,6 +25,13 @@ enum Message { } impl Arc { + fn new() -> Self { + Arc { + start: Instant::now(), + cache: Cache::default(), + } + } + fn update(&mut self, _: Message) { self.cache.clear(); } @@ -38,15 +45,6 @@ impl Arc { } } -impl Default for Arc { - fn default() -> Self { - Arc { - start: Instant::now(), - cache: Cache::default(), - } - } -} - impl canvas::Program for Arc { type State = (); diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index f5f68eb4..b7811653 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -11,7 +11,7 @@ use iced::{ pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::application(Clock::default, Clock::update, Clock::view) + iced::application(Clock::new, Clock::update, Clock::view) .subscription(Clock::subscription) .theme(Clock::theme) .run() @@ -28,6 +28,13 @@ enum Message { } impl Clock { + fn new() -> Self { + Self { + now: chrono::offset::Local::now(), + clock: Cache::default(), + } + } + fn update(&mut self, message: Message) { match message { Message::Tick(local_time) => { @@ -58,15 +65,6 @@ impl Clock { } } -impl Default for Clock { - fn default() -> Self { - Self { - now: chrono::offset::Local::now(), - clock: Cache::default(), - } - } -} - impl canvas::Program for Clock { type State = (); diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 709771c1..bbf4ac7d 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -21,14 +21,15 @@ use iced::{ use std::collections::HashMap; fn main() -> iced::Result { - iced::application(Gallery::new, Gallery::update, Gallery::view) - .window_size(( - Preview::WIDTH as f32 * 4.0, - Preview::HEIGHT as f32 * 2.5, - )) - .subscription(Gallery::subscription) - .theme(Gallery::theme) - .run() + iced::application::timed( + Gallery::new, + Gallery::update, + Gallery::subscription, + Gallery::view, + ) + .window_size((Preview::WIDTH as f32 * 4.0, Preview::HEIGHT as f32 * 2.5)) + .theme(Gallery::theme) + .run() } struct Gallery { @@ -48,7 +49,7 @@ enum Message { BlurhashDecoded(Id, civitai::Blurhash), Open(Id), Close, - Animate(Instant), + Animate, } impl Gallery { @@ -76,13 +77,15 @@ impl Gallery { || self.viewer.is_animating(self.now); if is_animating { - window::frames().map(Message::Animate) + window::frames().map(|_| Message::Animate) } else { Subscription::none() } } - pub fn update(&mut self, message: Message) -> Task { + pub fn update(&mut self, message: Message, now: Instant) -> Task { + self.now = now; + match message { Message::ImagesListed(Ok(images)) => { self.images = images; @@ -109,16 +112,16 @@ impl Gallery { ) } Message::ImageDownloaded(Ok(rgba)) => { - self.viewer.show(rgba); + self.viewer.show(rgba, self.now); Task::none() } Message::ThumbnailDownloaded(id, Ok(rgba)) => { let thumbnail = if let Some(preview) = self.previews.remove(&id) { - preview.load(rgba) + preview.load(rgba, self.now) } else { - Preview::ready(rgba) + Preview::ready(rgba, self.now) }; let _ = self.previews.insert(id, thumbnail); @@ -127,7 +130,7 @@ impl Gallery { } Message::ThumbnailHovered(id, is_hovered) => { if let Some(preview) = self.previews.get_mut(&id) { - preview.toggle_zoom(is_hovered); + preview.toggle_zoom(is_hovered, self.now); } Task::none() @@ -136,7 +139,7 @@ impl Gallery { if !self.previews.contains_key(&id) { let _ = self .previews - .insert(id, Preview::loading(blurhash.rgba)); + .insert(id, Preview::loading(blurhash.rgba, self.now)); } Task::none() @@ -151,7 +154,7 @@ impl Gallery { return Task::none(); }; - self.viewer.open(); + self.viewer.open(self.now); Task::perform( image.download(Size::Original), @@ -159,15 +162,11 @@ impl Gallery { ) } Message::Close => { - self.viewer.close(); - - Task::none() - } - Message::Animate(now) => { - self.now = now; + self.viewer.close(self.now); Task::none() } + Message::Animate => Task::none(), Message::ImagesListed(Err(error)) | Message::ImageDownloaded(Err(error)) | Message::ThumbnailDownloaded(_, Err(error)) => { @@ -293,13 +292,13 @@ impl Preview { const WIDTH: u32 = 320; const HEIGHT: u32 = 410; - fn loading(rgba: Rgba) -> Self { + fn loading(rgba: Rgba, now: Instant) -> Self { Self::Loading { blurhash: Blurhash { fade_in: Animation::new(false) .duration(milliseconds(700)) .easing(animation::Easing::EaseIn) - .go(true), + .go(true, now), handle: image::Handle::from_rgba( rgba.width, rgba.height, @@ -309,27 +308,27 @@ impl Preview { } } - fn ready(rgba: Rgba) -> Self { + fn ready(rgba: Rgba, now: Instant) -> Self { Self::Ready { blurhash: None, - thumbnail: Thumbnail::new(rgba), + thumbnail: Thumbnail::new(rgba, now), } } - fn load(self, rgba: Rgba) -> Self { + fn load(self, rgba: Rgba, now: Instant) -> Self { let Self::Loading { blurhash } = self else { return self; }; Self::Ready { blurhash: Some(blurhash), - thumbnail: Thumbnail::new(rgba), + thumbnail: Thumbnail::new(rgba, now), } } - fn toggle_zoom(&mut self, enabled: bool) { + fn toggle_zoom(&mut self, enabled: bool, now: Instant) { if let Self::Ready { thumbnail, .. } = self { - thumbnail.zoom.go_mut(enabled); + thumbnail.zoom.go_mut(enabled, now); } } @@ -357,14 +356,14 @@ impl Preview { } impl Thumbnail { - pub fn new(rgba: Rgba) -> Self { + pub fn new(rgba: Rgba, now: Instant) -> Self { Self { handle: image::Handle::from_rgba( rgba.width, rgba.height, rgba.pixels, ), - fade_in: Animation::new(false).slow().go(true), + fade_in: Animation::new(false).slow().go(true, now), zoom: Animation::new(false) .quick() .easing(animation::Easing::EaseInOut), @@ -391,24 +390,24 @@ impl Viewer { } } - fn open(&mut self) { + fn open(&mut self, now: Instant) { self.image = None; - self.background_fade_in.go_mut(true); + self.background_fade_in.go_mut(true, now); } - fn show(&mut self, rgba: Rgba) { + fn show(&mut self, rgba: Rgba, now: Instant) { self.image = Some(image::Handle::from_rgba( rgba.width, rgba.height, rgba.pixels, )); - self.background_fade_in.go_mut(true); - self.image_fade_in.go_mut(true); + self.background_fade_in.go_mut(true, now); + self.image_fade_in.go_mut(true, now); } - fn close(&mut self) { - self.background_fade_in.go_mut(false); - self.image_fade_in.go_mut(false); + fn close(&mut self, now: Instant) { + self.background_fade_in.go_mut(false, now); + self.image_fade_in.go_mut(false, now); } fn is_animating(&self, now: Instant) -> bool { diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 99eb9ef8..d9a23b1b 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -18,11 +18,15 @@ use std::io; use std::sync::Arc; pub fn main() -> iced::Result { - iced::application(Markdown::new, Markdown::update, Markdown::view) - .font(icon::FONT) - .subscription(Markdown::subscription) - .theme(Markdown::theme) - .run() + iced::application::timed( + Markdown::new, + Markdown::update, + Markdown::subscription, + Markdown::view, + ) + .font(icon::FONT) + .theme(Markdown::theme) + .run() } struct Markdown { @@ -58,7 +62,7 @@ enum Message { ImageDownloaded(markdown::Url, Result), ToggleStream(bool), NextToken, - Animate(Instant), + Tick, } impl Markdown { @@ -78,7 +82,9 @@ impl Markdown { ) } - fn update(&mut self, message: Message) -> Task { + fn update(&mut self, message: Message, now: Instant) -> Task { + self.now = now; + match message { Message::Edit(action) => { let is_edit = action.is_edit(); @@ -119,7 +125,7 @@ impl Markdown { fade_in: Animation::new(false) .quick() .easing(animation::Easing::EaseInOut) - .go(true), + .go(true, self.now), }) .unwrap_or_else(Image::Errored), ); @@ -164,11 +170,7 @@ impl Markdown { Task::none() } - Message::Animate(now) => { - self.now = now; - - Task::none() - } + Message::Tick => Task::none(), } } @@ -230,7 +232,7 @@ impl Markdown { }); if is_animating { - window::frames().map(Message::Animate) + window::frames().map(|_| Message::Tick) } else { Subscription::none() } diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index a0b5c402..90545e66 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -21,31 +21,36 @@ use std::time::Instant; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::application( - SolarSystem::default, + iced::application::timed( + SolarSystem::new, SolarSystem::update, + SolarSystem::subscription, SolarSystem::view, ) - .subscription(SolarSystem::subscription) .theme(SolarSystem::theme) .run() } -#[derive(Default)] struct SolarSystem { state: State, } #[derive(Debug, Clone, Copy)] enum Message { - Tick(Instant), + Tick, } impl SolarSystem { - fn update(&mut self, message: Message) { + fn new() -> Self { + Self { + state: State::new(), + } + } + + fn update(&mut self, message: Message, now: Instant) { match message { - Message::Tick(instant) => { - self.state.update(instant); + Message::Tick => { + self.state.update(now); } } } @@ -59,7 +64,7 @@ impl SolarSystem { } fn subscription(&self) -> Subscription { - window::frames().map(Message::Tick) + window::frames().map(|_| Message::Tick) } } @@ -105,10 +110,7 @@ impl State { } pub fn update(&mut self, now: Instant) { - if self.start > now { - self.start = now; - } - + self.start = self.start.min(now); self.now = now; self.system_cache.clear(); } @@ -206,9 +208,3 @@ impl canvas::Program for State { vec![background, system] } } - -impl Default for State { - fn default() -> Self { - Self::new() - } -} diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs index ee10b0bc..2f520c27 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -1,5 +1,5 @@ use iced::mouse; -use iced::time::{self, Instant, milliseconds}; +use iced::time::{self, milliseconds}; use iced::widget::canvas; use iced::{ Color, Element, Fill, Font, Point, Rectangle, Renderer, Subscription, Theme, @@ -22,13 +22,13 @@ struct TheMatrix { #[derive(Debug, Clone, Copy)] enum Message { - Tick(Instant), + Tick, } impl TheMatrix { fn update(&mut self, message: Message) { match message { - Message::Tick(_now) => { + Message::Tick => { self.tick += 1; } } @@ -39,7 +39,7 @@ impl TheMatrix { } fn subscription(&self) -> Subscription { - time::every(milliseconds(50)).map(Message::Tick) + time::every(milliseconds(50)).map(|_| Message::Tick) } } diff --git a/src/application.rs b/src/application.rs index 5438e97d..a1846333 100644 --- a/src/application.rs +++ b/src/application.rs @@ -40,6 +40,10 @@ use crate::{ use std::borrow::Cow; +pub mod timed; + +pub use timed::timed; + /// Creates an iced [`Application`] given its boot, update, and view logic. /// /// # Example diff --git a/src/application/timed.rs b/src/application/timed.rs new file mode 100644 index 00000000..e4a1d78f --- /dev/null +++ b/src/application/timed.rs @@ -0,0 +1,178 @@ +//! An [`Application`] that receives an [`Instant`] in update logic. +use crate::application::{Application, Boot, View}; +use crate::program; +use crate::theme; +use crate::time::Instant; +use crate::window; +use crate::{Element, Program, Settings, Subscription, Task}; + +/// Creates an [`Application`] with an `update` function that also +/// takes the [`Instant`] of each `Message`. +/// +/// This constructor is useful to create animated applications that +/// are _pure_ (e.g. without relying on side-effect calls like [`Instant::now`]). +/// +/// Purity is needed when you want your application to end up in the +/// same exact state given the same history of messages. This property +/// enables proper time traveling debugging with [`comet`]. +/// +/// [`comet`]: https://github.com/iced-rs/comet +pub fn timed( + boot: impl Boot, + update: impl Update, + subscription: impl Fn(&State) -> Subscription, + view: impl for<'a> View<'a, State, Message, Theme, Renderer>, +) -> Application< + impl Program, +> +where + State: 'static, + Message: program::Message + 'static, + Theme: Default + theme::Base + 'static, + Renderer: program::Renderer + 'static, +{ + use std::marker::PhantomData; + + struct Instance< + State, + Message, + Theme, + Renderer, + Boot, + Update, + Subscription, + View, + > { + boot: Boot, + update: Update, + subscription: Subscription, + view: View, + _state: PhantomData, + _message: PhantomData, + _theme: PhantomData, + _renderer: PhantomData, + } + + impl + Program + for Instance< + State, + Message, + Theme, + Renderer, + Boot, + Update, + Subscription, + View, + > + where + Message: program::Message + 'static, + Theme: Default + theme::Base + 'static, + Renderer: program::Renderer + 'static, + Boot: self::Boot, + Update: self::Update, + Subscription: Fn(&State) -> self::Subscription, + View: for<'a> self::View<'a, State, Message, Theme, Renderer>, + { + type State = State; + type Message = (Message, Instant); + type Theme = Theme; + type Renderer = Renderer; + type Executor = iced_futures::backend::default::Executor; + + fn name() -> &'static str { + let name = std::any::type_name::(); + + name.split("::").next().unwrap_or("a_cool_application") + } + + fn boot(&self) -> (State, Task) { + let (state, task) = self.boot.boot(); + + (state, task.map(|message| (message, Instant::now()))) + } + + fn update( + &self, + state: &mut Self::State, + (message, now): Self::Message, + ) -> Task { + self.update + .update(state, message, now) + .into() + .map(|message| (message, Instant::now())) + } + + fn view<'a>( + &self, + state: &'a Self::State, + _window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.view + .view(state) + .into() + .map(|message| (message, Instant::now())) + } + + fn subscription( + &self, + state: &Self::State, + ) -> self::Subscription { + (self.subscription)(state).map(|message| (message, Instant::now())) + } + } + + Application { + raw: Instance { + boot, + update, + subscription, + view, + _state: PhantomData, + _message: PhantomData, + _theme: PhantomData, + _renderer: PhantomData, + }, + settings: Settings::default(), + window: window::Settings::default(), + } +} + +/// The update logic of some timed [`Application`]. +/// +/// This is like [`application::Update`](super::Update), +/// but it also takes an [`Instant`]. +pub trait Update { + /// Processes the message and updates the state of the [`Application`]. + fn update( + &self, + state: &mut State, + message: Message, + now: Instant, + ) -> impl Into>; +} + +impl Update for () { + fn update( + &self, + _state: &mut State, + _message: Message, + _now: Instant, + ) -> impl Into> { + } +} + +impl Update for T +where + T: Fn(&mut State, Message, Instant) -> C, + C: Into>, +{ + fn update( + &self, + state: &mut State, + message: Message, + now: Instant, + ) -> impl Into> { + self(state, message, now) + } +}