Purify Animation API and introduce application::timed

This commit is contained in:
Héctor Ramón Jiménez 2025-04-29 03:03:32 +02:00
parent 4334923add
commit 29a19fcde1
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
9 changed files with 281 additions and 104 deletions

View file

@ -90,15 +90,17 @@ where
self self
} }
/// Transitions the [`Animation`] from its current state to the given new state. /// Transitions the [`Animation`] from its current state to the given new state
pub fn go(mut self, new_state: T) -> Self { /// at the given time.
self.go_mut(new_state); pub fn go(mut self, new_state: T, at: Instant) -> Self {
self.go_mut(new_state, at);
self self
} }
/// Transitions the [`Animation`] from its current state to the given new state, by reference. /// Transitions the [`Animation`] from its current state to the given new state
pub fn go_mut(&mut self, new_state: T) { /// at the given time, by reference.
self.raw.transition(new_state, Instant::now()); 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. /// Returns true if the [`Animation`] is currently in progress.

View file

@ -8,7 +8,7 @@ use iced::window;
use iced::{Element, Fill, Point, Rectangle, Renderer, Subscription, Theme}; use iced::{Element, Fill, Point, Rectangle, Renderer, Subscription, Theme};
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
iced::application(Arc::default, Arc::update, Arc::view) iced::application(Arc::new, Arc::update, Arc::view)
.subscription(Arc::subscription) .subscription(Arc::subscription)
.theme(|_| Theme::Dark) .theme(|_| Theme::Dark)
.run() .run()
@ -25,6 +25,13 @@ enum Message {
} }
impl Arc { impl Arc {
fn new() -> Self {
Arc {
start: Instant::now(),
cache: Cache::default(),
}
}
fn update(&mut self, _: Message) { fn update(&mut self, _: Message) {
self.cache.clear(); self.cache.clear();
} }
@ -38,15 +45,6 @@ impl Arc {
} }
} }
impl Default for Arc {
fn default() -> Self {
Arc {
start: Instant::now(),
cache: Cache::default(),
}
}
}
impl<Message> canvas::Program<Message> for Arc { impl<Message> canvas::Program<Message> for Arc {
type State = (); type State = ();

View file

@ -11,7 +11,7 @@ use iced::{
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
iced::application(Clock::default, Clock::update, Clock::view) iced::application(Clock::new, Clock::update, Clock::view)
.subscription(Clock::subscription) .subscription(Clock::subscription)
.theme(Clock::theme) .theme(Clock::theme)
.run() .run()
@ -28,6 +28,13 @@ enum Message {
} }
impl Clock { impl Clock {
fn new() -> Self {
Self {
now: chrono::offset::Local::now(),
clock: Cache::default(),
}
}
fn update(&mut self, message: Message) { fn update(&mut self, message: Message) {
match message { match message {
Message::Tick(local_time) => { 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<Message> canvas::Program<Message> for Clock { impl<Message> canvas::Program<Message> for Clock {
type State = (); type State = ();

View file

@ -21,14 +21,15 @@ use iced::{
use std::collections::HashMap; use std::collections::HashMap;
fn main() -> iced::Result { fn main() -> iced::Result {
iced::application(Gallery::new, Gallery::update, Gallery::view) iced::application::timed(
.window_size(( Gallery::new,
Preview::WIDTH as f32 * 4.0, Gallery::update,
Preview::HEIGHT as f32 * 2.5, Gallery::subscription,
)) Gallery::view,
.subscription(Gallery::subscription) )
.theme(Gallery::theme) .window_size((Preview::WIDTH as f32 * 4.0, Preview::HEIGHT as f32 * 2.5))
.run() .theme(Gallery::theme)
.run()
} }
struct Gallery { struct Gallery {
@ -48,7 +49,7 @@ enum Message {
BlurhashDecoded(Id, civitai::Blurhash), BlurhashDecoded(Id, civitai::Blurhash),
Open(Id), Open(Id),
Close, Close,
Animate(Instant), Animate,
} }
impl Gallery { impl Gallery {
@ -76,13 +77,15 @@ impl Gallery {
|| self.viewer.is_animating(self.now); || self.viewer.is_animating(self.now);
if is_animating { if is_animating {
window::frames().map(Message::Animate) window::frames().map(|_| Message::Animate)
} else { } else {
Subscription::none() Subscription::none()
} }
} }
pub fn update(&mut self, message: Message) -> Task<Message> { pub fn update(&mut self, message: Message, now: Instant) -> Task<Message> {
self.now = now;
match message { match message {
Message::ImagesListed(Ok(images)) => { Message::ImagesListed(Ok(images)) => {
self.images = images; self.images = images;
@ -109,16 +112,16 @@ impl Gallery {
) )
} }
Message::ImageDownloaded(Ok(rgba)) => { Message::ImageDownloaded(Ok(rgba)) => {
self.viewer.show(rgba); self.viewer.show(rgba, self.now);
Task::none() Task::none()
} }
Message::ThumbnailDownloaded(id, Ok(rgba)) => { Message::ThumbnailDownloaded(id, Ok(rgba)) => {
let thumbnail = if let Some(preview) = self.previews.remove(&id) let thumbnail = if let Some(preview) = self.previews.remove(&id)
{ {
preview.load(rgba) preview.load(rgba, self.now)
} else { } else {
Preview::ready(rgba) Preview::ready(rgba, self.now)
}; };
let _ = self.previews.insert(id, thumbnail); let _ = self.previews.insert(id, thumbnail);
@ -127,7 +130,7 @@ impl Gallery {
} }
Message::ThumbnailHovered(id, is_hovered) => { Message::ThumbnailHovered(id, is_hovered) => {
if let Some(preview) = self.previews.get_mut(&id) { if let Some(preview) = self.previews.get_mut(&id) {
preview.toggle_zoom(is_hovered); preview.toggle_zoom(is_hovered, self.now);
} }
Task::none() Task::none()
@ -136,7 +139,7 @@ impl Gallery {
if !self.previews.contains_key(&id) { if !self.previews.contains_key(&id) {
let _ = self let _ = self
.previews .previews
.insert(id, Preview::loading(blurhash.rgba)); .insert(id, Preview::loading(blurhash.rgba, self.now));
} }
Task::none() Task::none()
@ -151,7 +154,7 @@ impl Gallery {
return Task::none(); return Task::none();
}; };
self.viewer.open(); self.viewer.open(self.now);
Task::perform( Task::perform(
image.download(Size::Original), image.download(Size::Original),
@ -159,15 +162,11 @@ impl Gallery {
) )
} }
Message::Close => { Message::Close => {
self.viewer.close(); self.viewer.close(self.now);
Task::none()
}
Message::Animate(now) => {
self.now = now;
Task::none() Task::none()
} }
Message::Animate => Task::none(),
Message::ImagesListed(Err(error)) Message::ImagesListed(Err(error))
| Message::ImageDownloaded(Err(error)) | Message::ImageDownloaded(Err(error))
| Message::ThumbnailDownloaded(_, Err(error)) => { | Message::ThumbnailDownloaded(_, Err(error)) => {
@ -293,13 +292,13 @@ impl Preview {
const WIDTH: u32 = 320; const WIDTH: u32 = 320;
const HEIGHT: u32 = 410; const HEIGHT: u32 = 410;
fn loading(rgba: Rgba) -> Self { fn loading(rgba: Rgba, now: Instant) -> Self {
Self::Loading { Self::Loading {
blurhash: Blurhash { blurhash: Blurhash {
fade_in: Animation::new(false) fade_in: Animation::new(false)
.duration(milliseconds(700)) .duration(milliseconds(700))
.easing(animation::Easing::EaseIn) .easing(animation::Easing::EaseIn)
.go(true), .go(true, now),
handle: image::Handle::from_rgba( handle: image::Handle::from_rgba(
rgba.width, rgba.width,
rgba.height, rgba.height,
@ -309,27 +308,27 @@ impl Preview {
} }
} }
fn ready(rgba: Rgba) -> Self { fn ready(rgba: Rgba, now: Instant) -> Self {
Self::Ready { Self::Ready {
blurhash: None, 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 { let Self::Loading { blurhash } = self else {
return self; return self;
}; };
Self::Ready { Self::Ready {
blurhash: Some(blurhash), 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 { 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 { impl Thumbnail {
pub fn new(rgba: Rgba) -> Self { pub fn new(rgba: Rgba, now: Instant) -> Self {
Self { Self {
handle: image::Handle::from_rgba( handle: image::Handle::from_rgba(
rgba.width, rgba.width,
rgba.height, rgba.height,
rgba.pixels, rgba.pixels,
), ),
fade_in: Animation::new(false).slow().go(true), fade_in: Animation::new(false).slow().go(true, now),
zoom: Animation::new(false) zoom: Animation::new(false)
.quick() .quick()
.easing(animation::Easing::EaseInOut), .easing(animation::Easing::EaseInOut),
@ -391,24 +390,24 @@ impl Viewer {
} }
} }
fn open(&mut self) { fn open(&mut self, now: Instant) {
self.image = None; 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( self.image = Some(image::Handle::from_rgba(
rgba.width, rgba.width,
rgba.height, rgba.height,
rgba.pixels, rgba.pixels,
)); ));
self.background_fade_in.go_mut(true); self.background_fade_in.go_mut(true, now);
self.image_fade_in.go_mut(true); self.image_fade_in.go_mut(true, now);
} }
fn close(&mut self) { fn close(&mut self, now: Instant) {
self.background_fade_in.go_mut(false); self.background_fade_in.go_mut(false, now);
self.image_fade_in.go_mut(false); self.image_fade_in.go_mut(false, now);
} }
fn is_animating(&self, now: Instant) -> bool { fn is_animating(&self, now: Instant) -> bool {

View file

@ -18,11 +18,15 @@ use std::io;
use std::sync::Arc; use std::sync::Arc;
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
iced::application(Markdown::new, Markdown::update, Markdown::view) iced::application::timed(
.font(icon::FONT) Markdown::new,
.subscription(Markdown::subscription) Markdown::update,
.theme(Markdown::theme) Markdown::subscription,
.run() Markdown::view,
)
.font(icon::FONT)
.theme(Markdown::theme)
.run()
} }
struct Markdown { struct Markdown {
@ -58,7 +62,7 @@ enum Message {
ImageDownloaded(markdown::Url, Result<image::Handle, Error>), ImageDownloaded(markdown::Url, Result<image::Handle, Error>),
ToggleStream(bool), ToggleStream(bool),
NextToken, NextToken,
Animate(Instant), Tick,
} }
impl Markdown { impl Markdown {
@ -78,7 +82,9 @@ impl Markdown {
) )
} }
fn update(&mut self, message: Message) -> Task<Message> { fn update(&mut self, message: Message, now: Instant) -> Task<Message> {
self.now = now;
match message { match message {
Message::Edit(action) => { Message::Edit(action) => {
let is_edit = action.is_edit(); let is_edit = action.is_edit();
@ -119,7 +125,7 @@ impl Markdown {
fade_in: Animation::new(false) fade_in: Animation::new(false)
.quick() .quick()
.easing(animation::Easing::EaseInOut) .easing(animation::Easing::EaseInOut)
.go(true), .go(true, self.now),
}) })
.unwrap_or_else(Image::Errored), .unwrap_or_else(Image::Errored),
); );
@ -164,11 +170,7 @@ impl Markdown {
Task::none() Task::none()
} }
Message::Animate(now) => { Message::Tick => Task::none(),
self.now = now;
Task::none()
}
} }
} }
@ -230,7 +232,7 @@ impl Markdown {
}); });
if is_animating { if is_animating {
window::frames().map(Message::Animate) window::frames().map(|_| Message::Tick)
} else { } else {
Subscription::none() Subscription::none()
} }

View file

@ -21,31 +21,36 @@ use std::time::Instant;
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
iced::application( iced::application::timed(
SolarSystem::default, SolarSystem::new,
SolarSystem::update, SolarSystem::update,
SolarSystem::subscription,
SolarSystem::view, SolarSystem::view,
) )
.subscription(SolarSystem::subscription)
.theme(SolarSystem::theme) .theme(SolarSystem::theme)
.run() .run()
} }
#[derive(Default)]
struct SolarSystem { struct SolarSystem {
state: State, state: State,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum Message { enum Message {
Tick(Instant), Tick,
} }
impl SolarSystem { 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 { match message {
Message::Tick(instant) => { Message::Tick => {
self.state.update(instant); self.state.update(now);
} }
} }
} }
@ -59,7 +64,7 @@ impl SolarSystem {
} }
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
window::frames().map(Message::Tick) window::frames().map(|_| Message::Tick)
} }
} }
@ -105,10 +110,7 @@ impl State {
} }
pub fn update(&mut self, now: Instant) { pub fn update(&mut self, now: Instant) {
if self.start > now { self.start = self.start.min(now);
self.start = now;
}
self.now = now; self.now = now;
self.system_cache.clear(); self.system_cache.clear();
} }
@ -206,9 +208,3 @@ impl<Message> canvas::Program<Message> for State {
vec![background, system] vec![background, system]
} }
} }
impl Default for State {
fn default() -> Self {
Self::new()
}
}

View file

@ -1,5 +1,5 @@
use iced::mouse; use iced::mouse;
use iced::time::{self, Instant, milliseconds}; use iced::time::{self, milliseconds};
use iced::widget::canvas; use iced::widget::canvas;
use iced::{ use iced::{
Color, Element, Fill, Font, Point, Rectangle, Renderer, Subscription, Theme, Color, Element, Fill, Font, Point, Rectangle, Renderer, Subscription, Theme,
@ -22,13 +22,13 @@ struct TheMatrix {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum Message { enum Message {
Tick(Instant), Tick,
} }
impl TheMatrix { impl TheMatrix {
fn update(&mut self, message: Message) { fn update(&mut self, message: Message) {
match message { match message {
Message::Tick(_now) => { Message::Tick => {
self.tick += 1; self.tick += 1;
} }
} }
@ -39,7 +39,7 @@ impl TheMatrix {
} }
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
time::every(milliseconds(50)).map(Message::Tick) time::every(milliseconds(50)).map(|_| Message::Tick)
} }
} }

View file

@ -40,6 +40,10 @@ use crate::{
use std::borrow::Cow; use std::borrow::Cow;
pub mod timed;
pub use timed::timed;
/// Creates an iced [`Application`] given its boot, update, and view logic. /// Creates an iced [`Application`] given its boot, update, and view logic.
/// ///
/// # Example /// # Example

178
src/application/timed.rs Normal file
View file

@ -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<State, Message, Theme, Renderer>(
boot: impl Boot<State, Message>,
update: impl Update<State, Message>,
subscription: impl Fn(&State) -> Subscription<Message>,
view: impl for<'a> View<'a, State, Message, Theme, Renderer>,
) -> Application<
impl Program<State = State, Message = (Message, Instant), Theme = Theme>,
>
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<State>,
_message: PhantomData<Message>,
_theme: PhantomData<Theme>,
_renderer: PhantomData<Renderer>,
}
impl<State, Message, Theme, Renderer, Boot, Update, Subscription, View>
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<State, Message>,
Update: self::Update<State, Message>,
Subscription: Fn(&State) -> self::Subscription<Message>,
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::<State>();
name.split("::").next().unwrap_or("a_cool_application")
}
fn boot(&self) -> (State, Task<Self::Message>) {
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::Message> {
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::Message> {
(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<State, Message> {
/// Processes the message and updates the state of the [`Application`].
fn update(
&self,
state: &mut State,
message: Message,
now: Instant,
) -> impl Into<Task<Message>>;
}
impl<State, Message> Update<State, Message> for () {
fn update(
&self,
_state: &mut State,
_message: Message,
_now: Instant,
) -> impl Into<Task<Message>> {
}
}
impl<T, State, Message, C> Update<State, Message> for T
where
T: Fn(&mut State, Message, Instant) -> C,
C: Into<Task<Message>>,
{
fn update(
&self,
state: &mut State,
message: Message,
now: Instant,
) -> impl Into<Task<Message>> {
self(state, message, now)
}
}