From d5d4479a53a12a2268d7c09741e52ab54acef57a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 17 Apr 2025 03:24:17 +0200 Subject: [PATCH 01/10] Draft `time-travel` debugging feature --- Cargo.lock | 1 + Cargo.toml | 2 + beacon/src/client.rs | 97 +++++++++++++++++++++++++------ beacon/src/error.rs | 9 +++ beacon/src/lib.rs | 89 +++++++++++++++++++++++----- beacon/src/span.rs | 1 + debug/Cargo.toml | 1 + debug/src/lib.rs | 51 ++++++++++++++-- devtools/Cargo.toml | 3 + devtools/src/lib.rs | 81 +++++++++++++++++++++++--- examples/multitouch/src/main.rs | 2 +- examples/solar_system/src/main.rs | 4 ++ examples/todos/Cargo.toml | 2 +- futures/src/subscription.rs | 5 ++ program/Cargo.toml | 9 ++- program/src/lib.rs | 16 ++++- src/application.rs | 4 +- src/daemon.rs | 4 +- src/lib.rs | 8 +-- winit/src/lib.rs | 4 -- 20 files changed, 330 insertions(+), 63 deletions(-) create mode 100644 beacon/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index d1e610ea..5318d98d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2453,6 +2453,7 @@ version = "0.14.0-dev" dependencies = [ "iced_beacon", "iced_core", + "iced_futures", "log", ] diff --git a/Cargo.toml b/Cargo.toml index b6638e66..133c2362 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,8 @@ markdown = ["iced_widget/markdown"] lazy = ["iced_widget/lazy"] # Enables a debug view in native platforms (press F12) debug = ["iced_winit/debug", "iced_devtools"] +# Enables time-travel debugging (very experimental!) +time-travel = ["debug", "iced_devtools/time-travel"] # Enables the `thread-pool` futures executor as the `executor::Default` on native platforms thread-pool = ["iced_futures/thread-pool"] # Enables `tokio` as the `executor::Default` on native platforms diff --git a/beacon/src/client.rs b/beacon/src/client.rs index 1ca5bc8c..b7444617 100644 --- a/beacon/src/client.rs +++ b/beacon/src/client.rs @@ -1,10 +1,12 @@ +use crate::Error; use crate::core::time::{Duration, SystemTime}; use crate::span; use crate::theme; +use futures::{FutureExt, select}; use semver::Version; use serde::{Deserialize, Serialize}; -use tokio::io::{self, AsyncWriteExt}; +use tokio::io::{self, AsyncReadExt, AsyncWriteExt}; use tokio::net; use tokio::sync::mpsc; use tokio::time; @@ -17,7 +19,7 @@ pub const SERVER_ADDRESS: &str = "127.0.0.1:9167"; #[derive(Debug, Clone)] pub struct Client { - sender: mpsc::Sender, + sender: mpsc::Sender, is_connected: Arc, _handle: Arc>, } @@ -43,17 +45,17 @@ pub enum Event { ThemeChanged(theme::Palette), SpanStarted(span::Stage), SpanFinished(span::Stage, Duration), - MessageLogged(String), + MessageLogged { number: usize, message: String }, CommandsSpawned(usize), SubscriptionsTracked(usize), } impl Client { pub fn log(&self, event: Event) { - let _ = self.sender.try_send(Message::EventLogged { + let _ = self.sender.try_send(Action::Send(Message::EventLogged { at: SystemTime::now(), event, - }); + })); } pub fn is_connected(&self) -> bool { @@ -61,21 +63,28 @@ impl Client { } pub fn quit(&self) { - let _ = self.sender.try_send(Message::Quit { + let _ = self.sender.try_send(Action::Send(Message::Quit { at: SystemTime::now(), - }); + })); + } + + pub fn subscribe(&self) -> mpsc::Receiver { + let (sender, receiver) = mpsc::channel(100); + let _ = self.sender.try_send(Action::Forward(sender)); + + receiver } } #[must_use] pub fn connect(name: String) -> Client { - let (sender, receiver) = mpsc::channel(100); + let (sender, receiver) = mpsc::channel(10_000); let is_connected = Arc::new(AtomicBool::new(false)); let handle = { let is_connected = is_connected.clone(); - std::thread::spawn(move || run(name, is_connected.clone(), receiver)) + std::thread::spawn(move || run(name, is_connected, receiver)) }; Client { @@ -85,16 +94,30 @@ pub fn connect(name: String) -> Client { } } +enum Action { + Send(Message), + Forward(mpsc::Sender), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Command { + RewindTo { message: usize }, +} + #[tokio::main] async fn run( name: String, is_connected: Arc, - mut receiver: mpsc::Receiver, + mut receiver: mpsc::Receiver, ) { let version = semver::Version::parse(env!("CARGO_PKG_VERSION")) .expect("Parse package version"); + let mut buffer = Vec::new(); + loop { + let mut command_sender = None; + match _connect().await { Ok(mut stream) => { is_connected.store(true, atomic::Ordering::Relaxed); @@ -109,16 +132,37 @@ async fn run( ) .await; - while let Some(output) = receiver.recv().await { - match send(&mut stream, output).await { - Ok(()) => {} - Err(error) => { - if error.kind() != io::ErrorKind::BrokenPipe { - log::warn!( - "Error sending message to server: {error}" - ); + loop { + select! { + action = receiver.recv().fuse() => { + let Some(action) = action else { break; }; + + match action { + Action::Send(message) => { + match send(&mut stream, message).await { + Ok(()) => {} + Err(error) => { + if error.kind() != io::ErrorKind::BrokenPipe + { + log::warn!( + "Error sending message to server: {error}" + ); + } + break; + } + } + } + Action::Forward(sender) => { + command_sender = Some(sender); + } + } + } + command = receive(&mut stream, &mut buffer).fuse() => { + let Ok(command) = command else { continue; }; + + if let Some(sender) = command_sender.as_mut() { + let _ = sender.send(command).await; } - break; } } } @@ -154,3 +198,18 @@ async fn send( Ok(()) } + +async fn receive( + stream: &mut net::TcpStream, + buffer: &mut Vec, +) -> Result { + let size = stream.read_u64().await? as usize; + + if buffer.len() < size { + buffer.resize(size, 0); + } + + let _n = stream.read_exact(&mut buffer[..size]).await?; + + Ok(bincode::deserialize(buffer)?) +} diff --git a/beacon/src/error.rs b/beacon/src/error.rs new file mode 100644 index 00000000..032e75d5 --- /dev/null +++ b/beacon/src/error.rs @@ -0,0 +1,9 @@ +use std::io; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("input/output operation failed: {0}")] + IOFailed(#[from] io::Error), + #[error("decoding failed: {0}")] + DecodingFailed(#[from] Box), +} diff --git a/beacon/src/lib.rs b/beacon/src/lib.rs index 76df883a..8d1c31f5 100644 --- a/beacon/src/lib.rs +++ b/beacon/src/lib.rs @@ -4,6 +4,7 @@ pub use semver::Version; pub mod client; pub mod span; +mod error; mod stream; pub use client::Client; @@ -11,14 +12,36 @@ pub use span::Span; use crate::core::theme; use crate::core::time::{Duration, SystemTime}; +use crate::error::Error; use futures::{SinkExt, Stream}; -use tokio::io::{self, AsyncReadExt}; +use tokio::io::{self, AsyncReadExt, AsyncWriteExt}; use tokio::net; +use tokio::sync::mpsc; +use tokio::task; + +#[derive(Debug, Clone)] +pub struct Connection { + commands: mpsc::Sender, +} + +impl Connection { + pub fn rewind_to<'a>( + &self, + message: usize, + ) -> impl Future + 'a { + let commands = self.commands.clone(); + + async move { + let _ = commands.send(client::Command::RewindTo { message }).await; + } + } +} #[derive(Debug, Clone)] pub enum Event { Connected { + connection: Connection, at: SystemTime, name: String, version: Version, @@ -86,18 +109,42 @@ pub fn run() -> impl Stream { }; loop { - let Ok((mut stream, _)) = server.accept().await else { + let Ok((stream, _)) = server.accept().await else { continue; }; - let _ = stream.set_nodelay(true); + let (mut reader, mut writer) = { + let _ = stream.set_nodelay(true); + stream.into_split() + }; + let (command_sender, mut command_receiver) = mpsc::channel(1); let mut last_message = String::new(); + let mut last_update_number = 0; let mut last_commands_spawned = 0; let mut last_present_window = None; + drop(task::spawn(async move { + let mut last_message_number = None; + + while let Some(command) = command_receiver.recv().await { + let client::Command::RewindTo { message } = command; + + if Some(message) == last_message_number { + continue; + } + + last_message_number = Some(message); + + let _ = + send(&mut writer, command).await.inspect_err(|error| { + log::error!("Error when sending command: {error}") + }); + } + })); + loop { - match receive(&mut stream, &mut buffer).await { + match receive(&mut reader, &mut buffer).await { Ok(message) => { match message { client::Message::Connected { @@ -107,6 +154,9 @@ pub fn run() -> impl Stream { } => { let _ = output .send(Event::Connected { + connection: Connection { + commands: command_sender.clone(), + }, at, name, version, @@ -133,7 +183,11 @@ pub fn run() -> impl Stream { }) .await; } - client::Event::MessageLogged(message) => { + client::Event::MessageLogged { + number, + message, + } => { + last_update_number = number; last_message = message; } client::Event::CommandsSpawned( @@ -161,6 +215,7 @@ pub fn run() -> impl Stream { span::Stage::Boot => Span::Boot, span::Stage::Update => { Span::Update { + number: last_update_number, message: last_message .clone(), commands_spawned: @@ -246,7 +301,7 @@ pub fn run() -> impl Stream { } async fn receive( - stream: &mut net::TcpStream, + stream: &mut net::tcp::OwnedReadHalf, buffer: &mut Vec, ) -> Result { let size = stream.read_u64().await? as usize; @@ -260,14 +315,20 @@ async fn receive( Ok(bincode::deserialize(buffer)?) } +async fn send( + stream: &mut net::tcp::OwnedWriteHalf, + command: client::Command, +) -> Result<(), io::Error> { + let bytes = bincode::serialize(&command).expect("Encode input message"); + let size = bytes.len() as u64; + + stream.write_all(&size.to_be_bytes()).await?; + stream.write_all(&bytes).await?; + stream.flush().await?; + + Ok(()) +} + async fn delay() { tokio::time::sleep(Duration::from_secs(2)).await; } - -#[derive(Debug, thiserror::Error)] -enum Error { - #[error("input/output operation failed: {0}")] - IOFailed(#[from] io::Error), - #[error("decoding failed: {0}")] - DecodingFailed(#[from] Box), -} diff --git a/beacon/src/span.rs b/beacon/src/span.rs index d35f7b54..453ef1bd 100644 --- a/beacon/src/span.rs +++ b/beacon/src/span.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; pub enum Span { Boot, Update { + number: usize, message: String, commands_spawned: usize, }, diff --git a/debug/Cargo.toml b/debug/Cargo.toml index 1c5e7324..f6c7c843 100644 --- a/debug/Cargo.toml +++ b/debug/Cargo.toml @@ -15,6 +15,7 @@ enable = ["dep:iced_beacon"] [dependencies] iced_core.workspace = true +iced_futures.workspace = true log.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/debug/src/lib.rs b/debug/src/lib.rs index f3335c70..8f5a0096 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -1,7 +1,9 @@ pub use iced_core as core; +pub use iced_futures as futures; use crate::core::theme; use crate::core::window; +use crate::futures::Subscription; pub use internal::Span; @@ -16,6 +18,11 @@ pub enum Primitive { Text, } +#[derive(Debug, Clone, Copy)] +pub enum Command { + RewindTo { message: usize }, +} + pub fn init(name: &str) { internal::init(name); } @@ -88,12 +95,18 @@ pub fn skip_next_timing() { internal::skip_next_timing(); } +pub fn commands() -> Subscription { + internal::commands() +} + #[cfg(all(feature = "enable", not(target_arch = "wasm32")))] mod internal { - use crate::Primitive; use crate::core::theme; use crate::core::time::Instant; use crate::core::window; + use crate::futures::Subscription; + use crate::futures::futures::Stream; + use crate::{Command, Primitive}; use iced_beacon as beacon; @@ -102,7 +115,7 @@ mod internal { use std::io; use std::process; - use std::sync::atomic::{self, AtomicBool}; + use std::sync::atomic::{self, AtomicBool, AtomicUsize}; use std::sync::{LazyLock, RwLock}; pub fn init(name: &str) { @@ -162,6 +175,8 @@ mod internal { pub fn update(message: &impl std::fmt::Debug) -> Span { let span = span(span::Stage::Update); + let number = LAST_UPDATE.fetch_add(1, atomic::Ordering::Relaxed); + let start = Instant::now(); let message = format!("{message:?}"); let elapsed = start.elapsed(); @@ -172,11 +187,13 @@ mod internal { ); } - BEACON.log(client::Event::MessageLogged(if message.len() > 49 { + let message = if message.len() > 49 { format!("{}...", &message[..49]) } else { message - })); + }; + + BEACON.log(client::Event::MessageLogged { number, message }); span } @@ -217,6 +234,24 @@ mod internal { SKIP_NEXT_SPAN.store(true, atomic::Ordering::Relaxed); } + pub fn commands() -> Subscription { + fn listen_for_commands() -> impl Stream { + use crate::futures::futures::stream; + + stream::unfold(BEACON.subscribe(), async move |mut receiver| { + let command = match receiver.recv().await? { + client::Command::RewindTo { message } => { + Command::RewindTo { message } + } + }; + + Some((command, receiver)) + }) + } + + Subscription::run(listen_for_commands) + } + fn span(span: span::Stage) -> Span { BEACON.log(client::Event::SpanStarted(span.clone())); @@ -260,15 +295,17 @@ mod internal { }); static NAME: RwLock = RwLock::new(String::new()); + static LAST_UPDATE: AtomicUsize = AtomicUsize::new(0); static LAST_PALETTE: RwLock> = RwLock::new(None); static SKIP_NEXT_SPAN: AtomicBool = AtomicBool::new(false); } #[cfg(any(not(feature = "enable"), target_arch = "wasm32"))] mod internal { - use crate::Primitive; use crate::core::theme; use crate::core::window; + use crate::futures::Subscription; + use crate::{Command, Primitive}; use std::io; @@ -326,6 +363,10 @@ mod internal { pub fn skip_next_timing() {} + pub fn commands() -> Subscription { + Subscription::none() + } + #[derive(Debug)] pub struct Span; diff --git a/devtools/Cargo.toml b/devtools/Cargo.toml index df7e5012..3034f9c3 100644 --- a/devtools/Cargo.toml +++ b/devtools/Cargo.toml @@ -13,6 +13,9 @@ rust-version.workspace = true [lints] workspace = true +[features] +time-travel = ["iced_program/time-travel"] + [dependencies] iced_program.workspace = true iced_widget.workspace = true diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 8d19140f..7bcf100a 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -115,6 +115,8 @@ where state: P::State, mode: Mode, show_notification: bool, + rewind: Option, + log: Vec, } #[derive(Debug, Clone)] @@ -147,6 +149,8 @@ where state, mode: Mode::None, show_notification: true, + rewind: None, + log: Vec::new(), }, executor::spawn_blocking(|mut sender| { thread::sleep(seconds(2)); @@ -250,7 +254,46 @@ where } }, Event::Program(message) => { - program.update(&mut self.state, message).map(Event::Program) + if self.rewind.is_some() { + return Task::none(); + } + + #[cfg(feature = "time-travel")] + { + self.log.push(message.clone()); + } + + let span = debug::update(&message); + let task = program.update(&mut self.state, message); + debug::tasks_spawned(task.units()); + span.finish(); + + task.map(Event::Program) + } + Event::Command(command) => { + match command { + debug::Command::RewindTo { message } => { + #[cfg(feature = "time-travel")] + { + let (mut state, _) = program.boot(); + + if message < self.log.len() { + // TODO: Run concurrently (?) + for message in &self.log[0..message] { + let _ = program + .update(&mut state, message.clone()); + } + } + + self.rewind = Some(state); + } + + #[cfg(not(feature = "time-travel"))] + let _ = message; + } + } + + Task::none() } } } @@ -260,8 +303,10 @@ where program: &P, window: window::Id, ) -> Element<'_, Event

, P::Theme, P::Renderer> { - let view = program.view(&self.state, window).map(Event::Program); - let theme = program.theme(&self.state, window); + let state = self.rewind.as_ref().unwrap_or(&self.state); + + let view = program.view(state, window).map(Event::Program); + let theme = program.theme(state, window); let derive_theme = move || { theme @@ -363,6 +408,7 @@ where }); stack![view] + .height(Fill) .push_maybe(mode.map(opaque)) .push_maybe(notification) .into() @@ -372,6 +418,8 @@ where let subscription = program.subscription(&self.state).map(Event::Program); + debug::subscriptions_tracked(subscription.units()); + let hotkeys = futures::keyboard::on_key_press(|key, _modifiers| match key { keyboard::Key::Named(keyboard::key::Named::F12) => { @@ -381,19 +429,22 @@ where }) .map(Event::Message); - Subscription::batch([subscription, hotkeys]) + let commands = debug::commands().map(Event::Command); + + Subscription::batch([subscription, hotkeys, commands]) } pub fn theme(&self, program: &P, window: window::Id) -> P::Theme { - program.theme(&self.state, window) + program.theme(self.rewind.as_ref().unwrap_or(&self.state), window) } pub fn style(&self, program: &P, theme: &P::Theme) -> theme::Style { - program.style(&self.state, theme) + program.style(self.rewind.as_ref().unwrap_or(&self.state), theme) } pub fn scale_factor(&self, program: &P, window: window::Id) -> f64 { - program.scale_factor(&self.state, window) + program + .scale_factor(self.rewind.as_ref().unwrap_or(&self.state), window) } } @@ -403,6 +454,7 @@ where { Message(Message), Program(P::Message), + Command(debug::Command), } impl

fmt::Debug for Event

@@ -413,6 +465,21 @@ where match self { Self::Message(message) => message.fmt(f), Self::Program(message) => message.fmt(f), + Self::Command(command) => command.fmt(f), + } + } +} + +#[cfg(feature = "time-travel")] +impl

Clone for Event

+where + P: Program, +{ + fn clone(&self) -> Self { + match self { + Event::Message(message) => Event::Message(message.clone()), + Event::Program(message) => Event::Program(message.clone()), + Event::Command(command) => Event::Command(*command), } } } diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index 4f22f552..0db1c09a 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -23,7 +23,7 @@ struct Multitouch { fingers: HashMap, } -#[derive(Debug)] +#[derive(Debug, Clone)] enum Message { FingerPressed { id: touch::Finger, position: Point }, FingerLifted { id: touch::Finger }, diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 07450309..a0b5c402 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -105,6 +105,10 @@ impl State { } pub fn update(&mut self, now: Instant) { + if self.start > now { + self.start = now; + } + self.now = now; self.system_cache.clear(); } diff --git a/examples/todos/Cargo.toml b/examples/todos/Cargo.toml index fd3433e6..5e16a2ac 100644 --- a/examples/todos/Cargo.toml +++ b/examples/todos/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["tokio", "debug"] +iced.features = ["tokio", "debug", "time-travel"] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index f799d5f8..e347e81f 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -285,6 +285,11 @@ impl Subscription { .collect(), } } + + /// Returns the amount of recipe units in this [`Subscription`]. + pub fn units(&self) -> usize { + self.recipes.len() + } } /// Creates a [`Subscription`] from a [`Recipe`] describing it. diff --git a/program/Cargo.toml b/program/Cargo.toml index 07880705..7aa6414d 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -10,9 +10,12 @@ categories.workspace = true keywords.workspace = true rust-version.workspace = true +[lints] +workspace = true + +[features] +time-travel = [] + [dependencies] iced_graphics.workspace = true iced_runtime.workspace = true - -[lints] -workspace = true diff --git a/program/src/lib.rs b/program/src/lib.rs index 7e5757de..e25cdb22 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -22,7 +22,7 @@ pub trait Program: Sized { type State; /// The message of the program. - type Message: Send + std::fmt::Debug + 'static; + type Message: Message + 'static; /// The theme of the program. type Theme: Default + theme::Base; @@ -642,3 +642,17 @@ impl Instance

{ self.program.scale_factor(&self.state, window) } } + +/// A trait alias for the [`Message`](Program::Message) of a [`Program`]. +#[cfg(feature = "time-travel")] +pub trait Message: Send + std::fmt::Debug + Clone {} + +#[cfg(feature = "time-travel")] +impl Message for T {} + +/// A trait alias for the [`Message`](Program::Message) of a [`Program`]. +#[cfg(not(feature = "time-travel"))] +pub trait Message: Send + std::fmt::Debug {} + +#[cfg(not(feature = "time-travel"))] +impl Message for T {} diff --git a/src/application.rs b/src/application.rs index 09dd2647..5438e97d 100644 --- a/src/application.rs +++ b/src/application.rs @@ -75,7 +75,7 @@ pub fn application( ) -> Application> where State: 'static, - Message: Send + std::fmt::Debug + 'static, + Message: program::Message + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, { @@ -94,7 +94,7 @@ where impl Program for Instance where - Message: Send + std::fmt::Debug + 'static, + Message: program::Message + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, Boot: self::Boot, diff --git a/src/daemon.rs b/src/daemon.rs index 8a356356..80271e73 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -25,7 +25,7 @@ pub fn daemon( ) -> Daemon> where State: 'static, - Message: Send + std::fmt::Debug + 'static, + Message: program::Message + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, { @@ -44,7 +44,7 @@ where impl Program for Instance where - Message: Send + std::fmt::Debug + 'static, + Message: program::Message + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, Boot: application::Boot, diff --git a/src/lib.rs b/src/lib.rs index b98dc4b9..c1c9fa38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -343,7 +343,7 @@ //! use iced::window; //! use iced::{Size, Subscription}; //! -//! #[derive(Debug)] +//! #[derive(Debug, Clone)] //! enum Message { //! WindowResized(Size), //! } @@ -387,7 +387,7 @@ //! # pub fn update(&mut self, message: Message) -> Action { unimplemented!() } //! # pub fn view(&self) -> Element { unimplemented!() } //! # } -//! # #[derive(Debug)] +//! # #[derive(Debug, Clone)] //! # pub enum Message {} //! # pub enum Action { None, Run(Task), Chat(()) } //! # } @@ -399,7 +399,7 @@ //! # pub fn update(&mut self, message: Message) -> Task { unimplemented!() } //! # pub fn view(&self) -> Element { unimplemented!() } //! # } -//! # #[derive(Debug)] +//! # #[derive(Debug, Clone)] //! # pub enum Message {} //! # } //! use contacts::Contacts; @@ -697,7 +697,7 @@ pub fn run( ) -> Result where State: Default + 'static, - Message: std::fmt::Debug + Send + 'static, + Message: program::Message + 'static, Theme: Default + theme::Base + 'static, Renderer: program::Renderer + 'static, { diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 23bcc091..814d4f58 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -1069,10 +1069,7 @@ fn update( P::Theme: theme::Base, { for message in messages.drain(..) { - let update_span = debug::update(&message); let task = runtime.enter(|| program.update(message)); - debug::tasks_spawned(task.units()); - update_span.finish(); if let Some(stream) = runtime::task::into_stream(task) { runtime.run(stream); @@ -1082,7 +1079,6 @@ fn update( let subscription = runtime.enter(|| program.subscription()); let recipes = subscription::into_recipes(subscription.map(Action::Output)); - debug::subscriptions_tracked(recipes.len()); runtime.track(recipes); } From 41d7487ab0f59d92a801c0dbc8debac81e89fb8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 20 Apr 2025 19:50:08 +0200 Subject: [PATCH 02/10] Replace `select!` with `into_split` in `beacon::client` --- beacon/src/client.rs | 48 ++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/beacon/src/client.rs b/beacon/src/client.rs index b7444617..271788ef 100644 --- a/beacon/src/client.rs +++ b/beacon/src/client.rs @@ -3,12 +3,12 @@ use crate::core::time::{Duration, SystemTime}; use crate::span; use crate::theme; -use futures::{FutureExt, select}; use semver::Version; use serde::{Deserialize, Serialize}; use tokio::io::{self, AsyncReadExt, AsyncWriteExt}; use tokio::net; -use tokio::sync::mpsc; +use tokio::sync::{RwLock, mpsc}; +use tokio::task; use tokio::time; use std::sync::Arc; @@ -116,14 +116,16 @@ async fn run( let mut buffer = Vec::new(); loop { - let mut command_sender = None; + let command_sender = Arc::new(RwLock::new(None)); match _connect().await { - Ok(mut stream) => { + Ok(stream) => { is_connected.store(true, atomic::Ordering::Relaxed); + let (mut reader, mut writer) = stream.into_split(); + let _ = send( - &mut stream, + &mut writer, Message::Connected { at: SystemTime::now(), name: name.clone(), @@ -132,17 +134,18 @@ async fn run( ) .await; - loop { - select! { - action = receiver.recv().fuse() => { - let Some(action) = action else { break; }; + { + let command_sender = command_sender.clone(); + drop(task::spawn(async move { + while let Some(action) = receiver.recv().await { match action { Action::Send(message) => { - match send(&mut stream, message).await { + match send(&mut writer, message).await { Ok(()) => {} Err(error) => { - if error.kind() != io::ErrorKind::BrokenPipe + if error.kind() + != io::ErrorKind::BrokenPipe { log::warn!( "Error sending message to server: {error}" @@ -153,17 +156,22 @@ async fn run( } } Action::Forward(sender) => { - command_sender = Some(sender); + *command_sender.write().await = + Some(sender); } } } - command = receive(&mut stream, &mut buffer).fuse() => { - let Ok(command) = command else { continue; }; + })) + }; - if let Some(sender) = command_sender.as_mut() { - let _ = sender.send(command).await; - } - } + loop { + let Ok(command) = receive(&mut reader, &mut buffer).await + else { + continue; + }; + + if let Some(sender) = command_sender.read().await.as_ref() { + let _ = sender.send(command).await; } } } @@ -186,7 +194,7 @@ async fn _connect() -> Result { } async fn send( - stream: &mut net::TcpStream, + stream: &mut net::tcp::OwnedWriteHalf, message: Message, ) -> Result<(), io::Error> { let bytes = bincode::serialize(&message).expect("Encode input message"); @@ -200,7 +208,7 @@ async fn send( } async fn receive( - stream: &mut net::TcpStream, + stream: &mut net::tcp::OwnedReadHalf, buffer: &mut Vec, ) -> Result { let size = stream.read_u64().await? as usize; From 162f8c0c29606eb49d9f2704eafc5451c8cee073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 20 Apr 2025 19:56:25 +0200 Subject: [PATCH 03/10] Replace `RwLock` with `Mutex` in `beacon::client` ... since there are never multiple readers. --- beacon/src/client.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/beacon/src/client.rs b/beacon/src/client.rs index 271788ef..d32dc589 100644 --- a/beacon/src/client.rs +++ b/beacon/src/client.rs @@ -7,7 +7,7 @@ use semver::Version; use serde::{Deserialize, Serialize}; use tokio::io::{self, AsyncReadExt, AsyncWriteExt}; use tokio::net; -use tokio::sync::{RwLock, mpsc}; +use tokio::sync::{Mutex, mpsc}; use tokio::task; use tokio::time; @@ -116,7 +116,7 @@ async fn run( let mut buffer = Vec::new(); loop { - let command_sender = Arc::new(RwLock::new(None)); + let command_sender = Arc::new(Mutex::new(None)); match _connect().await { Ok(stream) => { @@ -156,8 +156,7 @@ async fn run( } } Action::Forward(sender) => { - *command_sender.write().await = - Some(sender); + *command_sender.lock().await = Some(sender); } } } @@ -170,7 +169,7 @@ async fn run( continue; }; - if let Some(sender) = command_sender.read().await.as_ref() { + if let Some(sender) = command_sender.lock().await.as_ref() { let _ = sender.send(command).await; } } From 5b649541b6d3e94d2dc6ad60f70076c00f7d142c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 20 Apr 2025 20:20:03 +0200 Subject: [PATCH 04/10] Fix disconnection logic in `beacon::client` --- beacon/src/client.rs | 67 +++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/beacon/src/client.rs b/beacon/src/client.rs index d32dc589..bfd945d2 100644 --- a/beacon/src/client.rs +++ b/beacon/src/client.rs @@ -113,11 +113,14 @@ async fn run( let version = semver::Version::parse(env!("CARGO_PKG_VERSION")) .expect("Parse package version"); - let mut buffer = Vec::new(); + let command_sender = { + // Discard by default + let (sender, _receiver) = mpsc::channel(1); + + Arc::new(Mutex::new(sender)) + }; loop { - let command_sender = Arc::new(Mutex::new(None)); - match _connect().await { Ok(stream) => { is_connected.store(true, atomic::Ordering::Relaxed); @@ -138,39 +141,45 @@ async fn run( let command_sender = command_sender.clone(); drop(task::spawn(async move { - while let Some(action) = receiver.recv().await { - match action { - Action::Send(message) => { - match send(&mut writer, message).await { - Ok(()) => {} - Err(error) => { - if error.kind() - != io::ErrorKind::BrokenPipe - { - log::warn!( - "Error sending message to server: {error}" - ); - } - break; - } - } - } - Action::Forward(sender) => { - *command_sender.lock().await = Some(sender); + let mut buffer = Vec::new(); + + loop { + match receive(&mut reader, &mut buffer).await { + Ok(command) => { + let sender = command_sender.lock().await; + let _ = sender.send(command).await; } + Err(Error::DecodingFailed(_)) => {} + Err(Error::IOFailed(_)) => break, } } })) }; - loop { - let Ok(command) = receive(&mut reader, &mut buffer).await - else { - continue; - }; + while let Some(action) = receiver.recv().await { + match action { + Action::Send(message) => { + match send(&mut writer, message).await { + Ok(()) => {} + Err(error) => { + if error.kind() != io::ErrorKind::BrokenPipe + { + log::warn!( + "Error sending message to server: {error}" + ); + } - if let Some(sender) = command_sender.lock().await.as_ref() { - let _ = sender.send(command).await; + is_connected.store( + false, + atomic::Ordering::Relaxed, + ); + break; + } + } + } + Action::Forward(sender) => { + *command_sender.lock().await = sender; + } } } } From 7c6155242c9d292fd24316dc1ee733296b17c6ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 20 Apr 2025 21:50:12 +0200 Subject: [PATCH 05/10] Avoid redundant metrics when rewinding --- beacon/src/client.rs | 3 ++- beacon/src/lib.rs | 23 ++++++++++++++---- debug/src/lib.rs | 56 ++++++++++++++++++++++++++------------------ devtools/Cargo.toml | 2 +- devtools/src/lib.rs | 51 ++++++++++++++++++++++++++++++---------- src/lib.rs | 2 +- 6 files changed, 93 insertions(+), 44 deletions(-) diff --git a/beacon/src/client.rs b/beacon/src/client.rs index bfd945d2..85f44eb8 100644 --- a/beacon/src/client.rs +++ b/beacon/src/client.rs @@ -99,9 +99,10 @@ enum Action { Forward(mpsc::Sender), } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum Command { RewindTo { message: usize }, + GoLive, } #[tokio::main] diff --git a/beacon/src/lib.rs b/beacon/src/lib.rs index 8d1c31f5..e7fe9d75 100644 --- a/beacon/src/lib.rs +++ b/beacon/src/lib.rs @@ -36,6 +36,14 @@ impl Connection { let _ = commands.send(client::Command::RewindTo { message }).await; } } + + pub fn go_live<'a>(&self) -> impl Future + 'a { + let commands = self.commands.clone(); + + async move { + let _ = commands.send(client::Command::GoLive).await; + } + } } #[derive(Debug, Clone)] @@ -128,14 +136,19 @@ pub fn run() -> impl Stream { let mut last_message_number = None; while let Some(command) = command_receiver.recv().await { - let client::Command::RewindTo { message } = command; + match command { + client::Command::RewindTo { message } => { + if Some(message) == last_message_number { + continue; + } - if Some(message) == last_message_number { - continue; + last_message_number = Some(message); + } + client::Command::GoLive => { + last_message_number = None; + } } - last_message_number = Some(message); - let _ = send(&mut writer, command).await.inspect_err(|error| { log::error!("Error when sending command: {error}") diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 8f5a0096..5329c1a7 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -21,6 +21,15 @@ pub enum Primitive { #[derive(Debug, Clone, Copy)] pub enum Command { RewindTo { message: usize }, + GoLive, +} + +pub fn enable() { + internal::enable(); +} + +pub fn disable() { + internal::disable(); } pub fn init(name: &str) { @@ -91,10 +100,6 @@ pub fn time_with(name: impl Into, f: impl FnOnce() -> T) -> T { result } -pub fn skip_next_timing() { - internal::skip_next_timing(); -} - pub fn commands() -> Subscription { internal::commands() } @@ -139,7 +144,7 @@ mod internal { if let Some(palette) = LAST_PALETTE.read().expect("Read last palette").as_ref() { - BEACON.log(client::Event::ThemeChanged(*palette)); + log(client::Event::ThemeChanged(*palette)); } Ok(()) @@ -154,18 +159,18 @@ mod internal { if LAST_PALETTE.read().expect("Read last palette").as_ref() != Some(&palette) { - BEACON.log(client::Event::ThemeChanged(palette)); + log(client::Event::ThemeChanged(palette)); *LAST_PALETTE.write().expect("Write last palette") = Some(palette); } } pub fn tasks_spawned(amount: usize) { - BEACON.log(client::Event::CommandsSpawned(amount)); + log(client::Event::CommandsSpawned(amount)); } pub fn subscriptions_tracked(amount: usize) { - BEACON.log(client::Event::SubscriptionsTracked(amount)); + log(client::Event::SubscriptionsTracked(amount)); } pub fn boot() -> Span { @@ -193,7 +198,7 @@ mod internal { message }; - BEACON.log(client::Event::MessageLogged { number, message }); + log(client::Event::MessageLogged { number, message }); span } @@ -230,8 +235,12 @@ mod internal { span(span::Stage::Custom(name.into())) } - pub fn skip_next_timing() { - SKIP_NEXT_SPAN.store(true, atomic::Ordering::Relaxed); + pub fn enable() { + ENABLED.store(true, atomic::Ordering::Relaxed); + } + + pub fn disable() { + ENABLED.store(false, atomic::Ordering::Relaxed); } pub fn commands() -> Subscription { @@ -243,6 +252,7 @@ mod internal { client::Command::RewindTo { message } => { Command::RewindTo { message } } + client::Command::GoLive => Command::GoLive, }; Some((command, receiver)) @@ -253,7 +263,7 @@ mod internal { } fn span(span: span::Stage) -> Span { - BEACON.log(client::Event::SpanStarted(span.clone())); + log(client::Event::SpanStarted(span.clone())); Span { span, @@ -271,6 +281,12 @@ mod internal { } } + fn log(event: client::Event) { + if ENABLED.load(atomic::Ordering::Relaxed) { + BEACON.log(event); + } + } + #[derive(Debug)] pub struct Span { span: span::Stage, @@ -279,14 +295,7 @@ mod internal { impl Span { pub fn finish(self) { - if SKIP_NEXT_SPAN.fetch_and(false, atomic::Ordering::Relaxed) { - return; - } - - BEACON.log(client::Event::SpanFinished( - self.span, - self.start.elapsed(), - )); + log(client::Event::SpanFinished(self.span, self.start.elapsed())); } } @@ -297,7 +306,7 @@ mod internal { static NAME: RwLock = RwLock::new(String::new()); static LAST_UPDATE: AtomicUsize = AtomicUsize::new(0); static LAST_PALETTE: RwLock> = RwLock::new(None); - static SKIP_NEXT_SPAN: AtomicBool = AtomicBool::new(false); + static ENABLED: AtomicBool = AtomicBool::new(true); } #[cfg(any(not(feature = "enable"), target_arch = "wasm32"))] @@ -309,6 +318,9 @@ mod internal { use std::io; + pub fn enable() {} + pub fn disable() {} + pub fn init(_name: &str) {} pub fn toggle_comet() -> Result<(), io::Error> { @@ -361,8 +373,6 @@ mod internal { Span } - pub fn skip_next_timing() {} - pub fn commands() -> Subscription { Subscription::none() } diff --git a/devtools/Cargo.toml b/devtools/Cargo.toml index 3034f9c3..44fa9fb4 100644 --- a/devtools/Cargo.toml +++ b/devtools/Cargo.toml @@ -17,6 +17,6 @@ workspace = true time-travel = ["iced_program/time-travel"] [dependencies] +iced_debug.workspace = true iced_program.workspace = true iced_widget.workspace = true -iced_debug.workspace = true diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 7bcf100a..6c3004d8 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -116,7 +116,7 @@ where mode: Mode, show_notification: bool, rewind: Option, - log: Vec, + messages: Vec, } #[derive(Debug, Clone)] @@ -150,7 +150,7 @@ where mode: Mode::None, show_notification: true, rewind: None, - log: Vec::new(), + messages: Vec::new(), }, executor::spawn_blocking(|mut sender| { thread::sleep(seconds(2)); @@ -254,13 +254,13 @@ where } }, Event::Program(message) => { - if self.rewind.is_some() { - return Task::none(); - } - #[cfg(feature = "time-travel")] { - self.log.push(message.clone()); + self.messages.push(message.clone()); + } + + if self.rewind.is_some() { + debug::enable(); } let span = debug::update(&message); @@ -268,6 +268,10 @@ where debug::tasks_spawned(task.units()); span.finish(); + if self.rewind.is_some() { + debug::disable(); + } + task.map(Event::Program) } Event::Command(command) => { @@ -277,24 +281,33 @@ where { let (mut state, _) = program.boot(); - if message < self.log.len() { + if message < self.messages.len() { // TODO: Run concurrently (?) - for message in &self.log[0..message] { + for message in &self.messages[0..message] { let _ = program .update(&mut state, message.clone()); } } self.rewind = Some(state); + debug::disable(); } #[cfg(not(feature = "time-travel"))] let _ = message; } + debug::Command::GoLive => { + #[cfg(feature = "time-travel")] + { + self.rewind = None; + debug::enable(); + } + } } Task::none() } + Event::Discard => Task::none(), } } @@ -305,7 +318,16 @@ where ) -> Element<'_, Event

, P::Theme, P::Renderer> { let state = self.rewind.as_ref().unwrap_or(&self.state); - let view = program.view(state, window).map(Event::Program); + let view = { + let view = program.view(state, window); + + if self.rewind.is_some() { + view.map(|_| Event::Discard) + } else { + view.map(Event::Program) + } + }; + let theme = program.theme(state, window); let derive_theme = move || { @@ -455,6 +477,7 @@ where Message(Message), Program(P::Message), Command(debug::Command), + Discard, } impl

fmt::Debug for Event

@@ -466,6 +489,7 @@ where Self::Message(message) => message.fmt(f), Self::Program(message) => message.fmt(f), Self::Command(command) => command.fmt(f), + Self::Discard => f.write_str("Discard"), } } } @@ -477,9 +501,10 @@ where { fn clone(&self) -> Self { match self { - Event::Message(message) => Event::Message(message.clone()), - Event::Program(message) => Event::Program(message.clone()), - Event::Command(command) => Event::Command(*command), + Self::Message(message) => Self::Message(message.clone()), + Self::Program(message) => Self::Program(message.clone()), + Self::Command(command) => Self::Command(*command), + Self::Discard => Self::Discard, } } } diff --git a/src/lib.rs b/src/lib.rs index c1c9fa38..e9ca3a04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -535,7 +535,7 @@ pub use alignment::Vertical::{Bottom, Top}; pub mod debug { //! Debug your applications. - pub use iced_debug::{Span, skip_next_timing, time, time_with}; + pub use iced_debug::{Span, time, time_with}; } pub mod task { From 5ce3892a1e32a8c4ae0d248b3fdab494d2d0dcae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 20 Apr 2025 22:11:24 +0200 Subject: [PATCH 06/10] Abstract `time-travel` feature in `TimeMachine` struct --- devtools/src/lib.rs | 76 ++++++++++++------------------- devtools/src/time_machine.rs | 88 ++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 48 deletions(-) create mode 100644 devtools/src/time_machine.rs diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 6c3004d8..1af99b22 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -7,6 +7,7 @@ use iced_widget::runtime; use iced_widget::runtime::futures; mod executor; +mod time_machine; use crate::core::keyboard; use crate::core::theme::{self, Base, Theme}; @@ -16,6 +17,7 @@ use crate::core::{Color, Element, Length::Fill}; use crate::futures::Subscription; use crate::program::Program; use crate::runtime::Task; +use crate::time_machine::TimeMachine; use crate::widget::{ bottom_right, button, center, column, container, horizontal_space, opaque, row, scrollable, stack, text, themer, @@ -115,8 +117,7 @@ where state: P::State, mode: Mode, show_notification: bool, - rewind: Option, - messages: Vec, + time_machine: TimeMachine

, } #[derive(Debug, Clone)] @@ -143,14 +144,13 @@ impl

DevTools

where P: Program + 'static, { - pub fn new(state: P::State) -> (Self, Task) { + fn new(state: P::State) -> (Self, Task) { ( Self { state, mode: Mode::None, show_notification: true, - rewind: None, - messages: Vec::new(), + time_machine: TimeMachine::new(), }, executor::spawn_blocking(|mut sender| { thread::sleep(seconds(2)); @@ -160,11 +160,11 @@ where ) } - pub fn title(&self, program: &P, window: window::Id) -> String { + fn title(&self, program: &P, window: window::Id) -> String { program.title(&self.state, window) } - pub fn update(&mut self, program: &P, event: Event

) -> Task> { + fn update(&mut self, program: &P, event: Event

) -> Task> { match event { Event::Message(message) => match message { Message::HideNotification => { @@ -254,13 +254,12 @@ where } }, Event::Program(message) => { - #[cfg(feature = "time-travel")] { - self.messages.push(message.clone()); - } + self.time_machine.push(&message); - if self.rewind.is_some() { - debug::enable(); + if self.time_machine.is_rewinding() { + debug::enable(); + } } let span = debug::update(&message); @@ -268,7 +267,7 @@ where debug::tasks_spawned(task.units()); span.finish(); - if self.rewind.is_some() { + if self.time_machine.is_rewinding() { debug::disable(); } @@ -277,31 +276,10 @@ where Event::Command(command) => { match command { debug::Command::RewindTo { message } => { - #[cfg(feature = "time-travel")] - { - let (mut state, _) = program.boot(); - - if message < self.messages.len() { - // TODO: Run concurrently (?) - for message in &self.messages[0..message] { - let _ = program - .update(&mut state, message.clone()); - } - } - - self.rewind = Some(state); - debug::disable(); - } - - #[cfg(not(feature = "time-travel"))] - let _ = message; + self.time_machine.rewind(program, message); } debug::Command::GoLive => { - #[cfg(feature = "time-travel")] - { - self.rewind = None; - debug::enable(); - } + self.time_machine.go_to_present(); } } @@ -311,17 +289,17 @@ where } } - pub fn view( + fn view( &self, program: &P, window: window::Id, ) -> Element<'_, Event

, P::Theme, P::Renderer> { - let state = self.rewind.as_ref().unwrap_or(&self.state); + let state = self.state(); let view = { let view = program.view(state, window); - if self.rewind.is_some() { + if self.time_machine.is_rewinding() { view.map(|_| Event::Discard) } else { view.map(Event::Program) @@ -436,10 +414,9 @@ where .into() } - pub fn subscription(&self, program: &P) -> Subscription> { + fn subscription(&self, program: &P) -> Subscription> { let subscription = program.subscription(&self.state).map(Event::Program); - debug::subscriptions_tracked(subscription.units()); let hotkeys = @@ -456,17 +433,20 @@ where Subscription::batch([subscription, hotkeys, commands]) } - pub fn theme(&self, program: &P, window: window::Id) -> P::Theme { - program.theme(self.rewind.as_ref().unwrap_or(&self.state), window) + fn theme(&self, program: &P, window: window::Id) -> P::Theme { + program.theme(self.state(), window) } - pub fn style(&self, program: &P, theme: &P::Theme) -> theme::Style { - program.style(self.rewind.as_ref().unwrap_or(&self.state), theme) + fn style(&self, program: &P, theme: &P::Theme) -> theme::Style { + program.style(self.state(), theme) } - pub fn scale_factor(&self, program: &P, window: window::Id) -> f64 { - program - .scale_factor(self.rewind.as_ref().unwrap_or(&self.state), window) + fn scale_factor(&self, program: &P, window: window::Id) -> f64 { + program.scale_factor(self.state(), window) + } + + fn state(&self) -> &P::State { + self.time_machine.state().unwrap_or(&self.state) } } diff --git a/devtools/src/time_machine.rs b/devtools/src/time_machine.rs new file mode 100644 index 00000000..6d8b80ae --- /dev/null +++ b/devtools/src/time_machine.rs @@ -0,0 +1,88 @@ +use crate::Program; + +#[cfg(feature = "time-travel")] +pub struct TimeMachine

+where + P: Program, +{ + state: Option, + messages: Vec, +} + +#[cfg(feature = "time-travel")] +impl

TimeMachine

+where + P: Program, +{ + pub fn new() -> Self { + Self { + state: None, + messages: Vec::new(), + } + } + + pub fn is_rewinding(&self) -> bool { + self.state.is_some() + } + + pub fn push(&mut self, message: &P::Message) { + self.messages.push(message.clone()); + } + + pub fn rewind(&mut self, program: &P, message: usize) { + let (mut state, _) = program.boot(); + + if message < self.messages.len() { + // TODO: Run concurrently (?) + for message in &self.messages[0..message] { + let _ = program.update(&mut state, message.clone()); + } + } + + self.state = Some(state); + crate::debug::disable(); + } + + pub fn go_to_present(&mut self) { + self.state = None; + crate::debug::enable(); + } + + pub fn state(&self) -> Option<&P::State> { + self.state.as_ref() + } +} + +#[cfg(not(feature = "time-travel"))] +pub struct TimeMachine

+where + P: Program, +{ + _program: std::marker::PhantomData

, +} + +#[cfg(not(feature = "time-travel"))] +impl

TimeMachine

+where + P: Program, +{ + pub fn new() -> Self { + Self { + _program: std::marker::PhantomData, + } + } + + pub fn is_rewinding(&self) -> bool { + false + } + + pub fn push(&mut self, _message: &P::Message) {} + + pub fn rewind(&mut self, _program: &P, _message: usize) {} + + pub fn go_to_present(&mut self) {} + + pub fn state(&self) -> Option<&P::State> { + None + } +} From a105ad4f9f9442312402c56c26555e587562b0b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 21 Apr 2025 05:12:08 +0200 Subject: [PATCH 07/10] Unify `SubscriptionsTracked` with `Span::Update` --- beacon/src/lib.rs | 24 ++++++++---------------- beacon/src/span.rs | 3 ++- devtools/src/lib.rs | 8 +++----- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/beacon/src/lib.rs b/beacon/src/lib.rs index e7fe9d75..a5291e32 100644 --- a/beacon/src/lib.rs +++ b/beacon/src/lib.rs @@ -61,10 +61,6 @@ pub enum Event { at: SystemTime, palette: theme::Palette, }, - SubscriptionsTracked { - at: SystemTime, - amount_alive: usize, - }, SpanFinished { at: SystemTime, duration: Duration, @@ -84,7 +80,6 @@ impl Event { Self::Connected { at, .. } | Self::Disconnected { at, .. } | Self::ThemeChanged { at, .. } - | Self::SubscriptionsTracked { at, .. } | Self::SpanFinished { at, .. } | Self::QuitRequested { at } | Self::AlreadyRunning { at } => *at, @@ -129,7 +124,8 @@ pub fn run() -> impl Stream { let (command_sender, mut command_receiver) = mpsc::channel(1); let mut last_message = String::new(); let mut last_update_number = 0; - let mut last_commands_spawned = 0; + let mut last_tasks = 0; + let mut last_subscriptions = 0; let mut last_present_window = None; drop(task::spawn(async move { @@ -189,12 +185,7 @@ pub fn run() -> impl Stream { client::Event::SubscriptionsTracked( amount_alive, ) => { - let _ = output - .send(Event::SubscriptionsTracked { - at, - amount_alive, - }) - .await; + last_subscriptions = amount_alive; } client::Event::MessageLogged { number, @@ -206,13 +197,13 @@ pub fn run() -> impl Stream { client::Event::CommandsSpawned( commands, ) => { - last_commands_spawned = commands; + last_tasks = commands; } client::Event::SpanStarted( span::Stage::Update, ) => { last_message.clear(); - last_commands_spawned = 0; + last_tasks = 0; } client::Event::SpanStarted( span::Stage::Present(window), @@ -231,8 +222,9 @@ pub fn run() -> impl Stream { number: last_update_number, message: last_message .clone(), - commands_spawned: - last_commands_spawned, + tasks: last_tasks, + subscriptions: + last_subscriptions, } } span::Stage::View(window) => { diff --git a/beacon/src/span.rs b/beacon/src/span.rs index 453ef1bd..ff869e1e 100644 --- a/beacon/src/span.rs +++ b/beacon/src/span.rs @@ -8,7 +8,8 @@ pub enum Span { Update { number: usize, message: String, - commands_spawned: usize, + tasks: usize, + subscriptions: usize, }, View { window: window::Id, diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 1af99b22..1126bcf9 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -254,12 +254,10 @@ where } }, Event::Program(message) => { - { - self.time_machine.push(&message); + self.time_machine.push(&message); - if self.time_machine.is_rewinding() { - debug::enable(); - } + if self.time_machine.is_rewinding() { + debug::enable(); } let span = debug::update(&message); From 267583c2a9ce271e102399cfd098a7f0be49f146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 28 Apr 2025 09:48:55 +0200 Subject: [PATCH 08/10] Draft auto-update process for `comet` in `devtools` --- Cargo.lock | 1 + debug/src/lib.rs | 30 ++---- devtools/Cargo.toml | 2 + devtools/src/comet.rs | 108 ++++++++++++++++++++++ devtools/src/executor.rs | 24 +++++ devtools/src/lib.rs | 192 ++++++++++++++++++++++----------------- 6 files changed, 250 insertions(+), 107 deletions(-) create mode 100644 devtools/src/comet.rs diff --git a/Cargo.lock b/Cargo.lock index 5318d98d..920dcf1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2464,6 +2464,7 @@ dependencies = [ "iced_debug", "iced_program", "iced_widget", + "log", ] [[package]] diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 5329c1a7..83a424d2 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -7,8 +7,6 @@ use crate::futures::Subscription; pub use internal::Span; -use std::io; - #[derive(Debug, Clone, Copy)] pub enum Primitive { Quad, @@ -36,8 +34,8 @@ pub fn init(name: &str) { internal::init(name); } -pub fn toggle_comet() -> Result<(), io::Error> { - internal::toggle_comet() +pub fn quit() -> bool { + internal::quit() } pub fn theme_changed(f: impl FnOnce() -> Option) { @@ -118,8 +116,6 @@ mod internal { use beacon::client::{self, Client}; use beacon::span; - use std::io; - use std::process; use std::sync::atomic::{self, AtomicBool, AtomicUsize}; use std::sync::{LazyLock, RwLock}; @@ -129,25 +125,13 @@ mod internal { name.clone_into(&mut NAME.write().expect("Write application name")); } - pub fn toggle_comet() -> Result<(), io::Error> { + pub fn quit() -> bool { if BEACON.is_connected() { BEACON.quit(); - Ok(()) + true } else { - let _ = process::Command::new("iced_comet") - .stdin(process::Stdio::null()) - .stdout(process::Stdio::null()) - .stderr(process::Stdio::null()) - .spawn()?; - - if let Some(palette) = - LAST_PALETTE.read().expect("Read last palette").as_ref() - { - log(client::Event::ThemeChanged(*palette)); - } - - Ok(()) + false } } @@ -323,8 +307,8 @@ mod internal { pub fn init(_name: &str) {} - pub fn toggle_comet() -> Result<(), io::Error> { - Ok(()) + pub fn quit() -> bool { + false } pub fn theme_changed(_f: impl FnOnce() -> Option) {} diff --git a/devtools/Cargo.toml b/devtools/Cargo.toml index 44fa9fb4..04c3792b 100644 --- a/devtools/Cargo.toml +++ b/devtools/Cargo.toml @@ -20,3 +20,5 @@ time-travel = ["iced_program/time-travel"] iced_debug.workspace = true iced_program.workspace = true iced_widget.workspace = true + +log.workspace = true diff --git a/devtools/src/comet.rs b/devtools/src/comet.rs new file mode 100644 index 00000000..941e4f61 --- /dev/null +++ b/devtools/src/comet.rs @@ -0,0 +1,108 @@ +use crate::executor; +use crate::runtime::Task; + +use std::io; +use std::process; +use std::sync::Arc; + +pub const COMPATIBLE_REVISION: &str = + "69dd2283886dccdaa1ee6e1c274af62f7250bc38"; + +pub fn launch() -> Task> { + executor::try_spawn_blocking(|mut sender| { + let cargo_install = process::Command::new("cargo") + .args(["install", "--list"]) + .output()?; + + let installed_packages = String::from_utf8_lossy(&cargo_install.stdout); + + for line in installed_packages.lines() { + if !line.starts_with("iced_comet ") { + continue; + } + + let Some((_, revision)) = line.rsplit_once("?rev=") else { + return Err(Error::Outdated { revision: None }); + }; + + let Some((revision, _)) = revision.rsplit_once("#") else { + return Err(Error::Outdated { revision: None }); + }; + + if revision != COMPATIBLE_REVISION { + return Err(Error::Outdated { + revision: Some(revision.to_owned()), + }); + } + + let _ = process::Command::new("iced_comet") + .stdin(process::Stdio::null()) + .stdout(process::Stdio::null()) + .stderr(process::Stdio::null()) + .spawn()?; + + let _ = sender.try_send(()); + return Ok(()); + } + + Err(Error::NotFound) + }) +} + +pub fn install() -> Task> { + executor::try_spawn_blocking(|mut sender| { + use std::io::{BufRead, BufReader}; + use std::process::{Command, Stdio}; + + let install = Command::new("cargo") + .args([ + "install", + "--locked", + "--git", + "https://github.com/iced-rs/comet.git", + "--rev", + COMPATIBLE_REVISION, + ]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn()?; + + let mut stderr = + BufReader::new(install.stderr.expect("stderr must be piped")); + + let mut log = String::new(); + + while let Ok(n) = stderr.read_line(&mut log) { + if n == 0 { + break; + } + + let _ = sender.try_send(Installation::Logged(log.clone())); + log.clear(); + } + + let _ = sender.try_send(Installation::Finished); + + Ok(()) + }) +} + +#[derive(Debug, Clone)] +pub enum Installation { + Logged(String), + Finished, +} + +#[derive(Debug, Clone)] +pub enum Error { + NotFound, + Outdated { revision: Option }, + IoFailed(Arc), +} + +impl From for Error { + fn from(error: io::Error) -> Self { + Self::IoFailed(Arc::new(error)) + } +} diff --git a/devtools/src/executor.rs b/devtools/src/executor.rs index 5d7d5397..1e7317a2 100644 --- a/devtools/src/executor.rs +++ b/devtools/src/executor.rs @@ -1,4 +1,6 @@ use crate::futures::futures::channel::mpsc; +use crate::futures::futures::channel::oneshot; +use crate::futures::futures::stream::{self, StreamExt}; use crate::runtime::Task; use std::thread; @@ -17,3 +19,25 @@ where Task::stream(receiver) } + +pub fn try_spawn_blocking( + f: impl FnOnce(mpsc::Sender) -> Result<(), E> + Send + 'static, +) -> Task> +where + T: Send + 'static, + E: Send + 'static, +{ + let (sender, receiver) = mpsc::channel(1); + let (error_sender, error_receiver) = oneshot::channel(); + + let _ = thread::spawn(move || { + if let Err(error) = f(sender) { + let _ = error_sender.send(Err(error)); + } + }); + + Task::stream(stream::select( + receiver.map(Ok), + stream::once(error_receiver).filter_map(async |result| result.ok()), + )) +} diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 1126bcf9..18c118bf 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -6,6 +6,7 @@ use iced_widget::core; use iced_widget::runtime; use iced_widget::runtime::futures; +mod comet; mod executor; mod time_machine; @@ -24,7 +25,6 @@ use crate::widget::{ }; use std::fmt; -use std::io; use std::thread; pub fn attach(program: impl Program + 'static) -> impl Program { @@ -124,9 +124,9 @@ where enum Message { HideNotification, ToggleComet, + CometLaunched(Result<(), comet::Error>), InstallComet, - InstallationLogged(String), - InstallationFinished, + InstallationProgressed(Result), CancelSetup, } @@ -136,10 +136,15 @@ enum Mode { } enum Setup { - Idle, + Idle { goal: Goal }, Running { logs: Vec }, } +enum Goal { + Installation, + Update { revision: Option }, +} + impl

DevTools

where P: Program + 'static, @@ -174,12 +179,34 @@ where } Message::ToggleComet => { if let Mode::Setup(setup) = &self.mode { - if matches!(setup, Setup::Idle) { + if matches!(setup, Setup::Idle { .. }) { self.mode = Mode::None; } - } else if let Err(error) = debug::toggle_comet() { - if error.kind() == io::ErrorKind::NotFound { - self.mode = Mode::Setup(Setup::Idle); + + Task::none() + } else if debug::quit() { + Task::none() + } else { + comet::launch() + .map(Message::CometLaunched) + .map(Event::Message) + } + } + Message::CometLaunched(Ok(())) => Task::none(), + Message::CometLaunched(Err(error)) => { + match error { + comet::Error::NotFound => { + self.mode = Mode::Setup(Setup::Idle { + goal: Goal::Installation, + }); + } + comet::Error::Outdated { revision } => { + self.mode = Mode::Setup(Setup::Idle { + goal: Goal::Update { revision }, + }); + } + comet::Error::IoFailed(error) => { + log::error!("comet failed to run: {error}"); } } @@ -189,62 +216,30 @@ where self.mode = Mode::Setup(Setup::Running { logs: Vec::new() }); - executor::spawn_blocking(|mut sender| { - use std::io::{BufRead, BufReader}; - use std::process::{Command, Stdio}; + comet::install() + .map(Message::InstallationProgressed) + .map(Event::Message) + } - let Ok(install) = Command::new("cargo") - .args([ - "install", - "--locked", - "--git", - "https://github.com/iced-rs/comet.git", - "--rev", - "eb114ba564a872acbd95e337d13e55f5f667b2f3", - ]) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::piped()) - .spawn() - else { - return; - }; + Message::InstallationProgressed(Ok(installation)) => { + let Mode::Setup(Setup::Running { logs }) = &mut self.mode + else { + return Task::none(); + }; - let mut stderr = BufReader::new( - install.stderr.expect("stderr must be piped"), - ); - - let mut log = String::new(); - - while let Ok(n) = stderr.read_line(&mut log) { - if n == 0 { - break; - } - - let _ = sender.try_send( - Message::InstallationLogged(log.clone()), - ); - - log.clear(); + match installation { + comet::Installation::Logged(log) => { + logs.push(log); + Task::none() + } + comet::Installation::Finished => { + self.mode = Mode::None; + comet::launch().discard() } - - let _ = sender.try_send(Message::InstallationFinished); - }) - .map(Event::Message) - } - Message::InstallationLogged(log) => { - if let Mode::Setup(Setup::Running { logs }) = &mut self.mode - { - logs.push(log); } - - Task::none() } - Message::InstallationFinished => { - self.mode = Mode::None; - - let _ = debug::toggle_comet(); - + Message::InstallationProgressed(_error) => { + // TODO Task::none() } Message::CancelSetup => { @@ -317,40 +312,69 @@ where Mode::None => None, Mode::Setup(setup) => { let stage: Element<'_, _, Theme, P::Renderer> = match setup { - Setup::Idle => { + Setup::Idle { goal } => { let controls = row![ button(text("Cancel").center().width(Fill)) .width(100) .on_press(Message::CancelSetup) .style(button::danger), horizontal_space(), - button(text("Install").center().width(Fill)) - .width(100) - .on_press(Message::InstallComet) - .style(button::success), + button( + text(match goal { + Goal::Installation => "Install", + Goal::Update { .. } => "Update", + }) + .center() + .width(Fill) + ) + .width(100) + .on_press(Message::InstallComet) + .style(button::success), ]; - column![ - text("comet is not installed!").size(20), - "In order to display performance metrics, the \ - comet debugger must be installed in your system.", - "The comet debugger is an official companion tool \ - that helps you debug your iced applications.", - "Do you wish to install it with the following \ - command?", - container( - text( - "cargo install --locked \ - --git https://github.com/iced-rs/comet.git" - ) - .size(14) + let command = container( + text( + "cargo install --locked \ + --git https://github.com/iced-rs/comet.git", ) - .width(Fill) - .padding(5) - .style(container::dark), - controls, - ] - .spacing(20) + .size(14), + ) + .width(Fill) + .padding(5) + .style(container::dark); + + match goal { + Goal::Installation => column![ + text("comet is not installed!").size(20), + "In order to display performance \ + metrics, the comet debugger must \ + be installed in your system.", + "The comet debugger is an official \ + companion tool that helps you debug \ + your iced applications.", + "Do you wish to install it with the \ + following command?", + command, + controls, + ] + .spacing(20), + Goal::Update { revision } => column![ + text("comet is out of date!").size(20), + text!( + "The installed revision is \"{current}\", \ + but the latest compatible is \"{compatible}\".", + current = revision + .as_deref() + .unwrap_or("Unknown"), + compatible = comet::COMPATIBLE_REVISION, + ), + "Do you wish to update it with the following \ + command?", + command, + controls, + ] + .spacing(20), + } .into() } Setup::Running { logs } => column![ From ef16ea3b2ac11a1a45ce92497e047b0a81cbd006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 28 Apr 2025 22:31:13 +0200 Subject: [PATCH 09/10] Tweak and organize `devtools` crate --- core/src/renderer/null.rs | 1 + core/src/text.rs | 5 + core/src/theme.rs | 14 +- devtools/src/comet.rs | 88 +++++++++---- devtools/src/lib.rs | 260 +++++++++++++++++++++++--------------- renderer/src/fallback.rs | 1 + tiny_skia/src/lib.rs | 1 + wgpu/src/lib.rs | 1 + 8 files changed, 239 insertions(+), 132 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index bf474b58..92fdd660 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -31,6 +31,7 @@ impl text::Renderer for () { type Paragraph = (); type Editor = (); + const MONOSPACE_FONT: Font = Font::MONOSPACE; const ICON_FONT: Font = Font::DEFAULT; const CHECKMARK_ICON: char = '0'; const ARROW_DOWN_ICON: char = '0'; diff --git a/core/src/text.rs b/core/src/text.rs index 79911b62..e2e75e58 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -232,6 +232,11 @@ pub trait Renderer: crate::Renderer { /// The [`Editor`] of this [`Renderer`]. type Editor: Editor + 'static; + /// A monospace font. + /// + /// It may be used by devtools. + const MONOSPACE_FONT: Self::Font; + /// The icon font of the backend. const ICON_FONT: Self::Font; diff --git a/core/src/theme.rs b/core/src/theme.rs index a0ec538b..0adf9ab2 100644 --- a/core/src/theme.rs +++ b/core/src/theme.rs @@ -5,6 +5,7 @@ pub use palette::Palette; use crate::Color; +use std::borrow::Cow; use std::fmt; use std::sync::Arc; @@ -87,14 +88,17 @@ impl Theme { ]; /// Creates a new custom [`Theme`] from the given [`Palette`]. - pub fn custom(name: String, palette: Palette) -> Self { + pub fn custom( + name: impl Into>, + palette: Palette, + ) -> Self { Self::custom_with_fn(name, palette, palette::Extended::generate) } /// Creates a new custom [`Theme`] from the given [`Palette`], with /// a custom generator of a [`palette::Extended`]. pub fn custom_with_fn( - name: String, + name: impl Into>, palette: Palette, generate: impl FnOnce(Palette) -> palette::Extended, ) -> Self { @@ -220,7 +224,7 @@ impl fmt::Display for Theme { /// A [`Theme`] with a customized [`Palette`]. #[derive(Debug, Clone, PartialEq)] pub struct Custom { - name: String, + name: Cow<'static, str>, palette: Palette, extended: palette::Extended, } @@ -234,12 +238,12 @@ impl Custom { /// Creates a [`Custom`] theme from the given [`Palette`] with /// a custom generator of a [`palette::Extended`]. pub fn with_fn( - name: String, + name: impl Into>, palette: Palette, generate: impl FnOnce(Palette) -> palette::Extended, ) -> Self { Self { - name, + name: name.into(), palette, extended: generate(palette), } diff --git a/devtools/src/comet.rs b/devtools/src/comet.rs index 941e4f61..2372899f 100644 --- a/devtools/src/comet.rs +++ b/devtools/src/comet.rs @@ -1,14 +1,12 @@ use crate::executor; use crate::runtime::Task; -use std::io; use std::process; -use std::sync::Arc; pub const COMPATIBLE_REVISION: &str = "69dd2283886dccdaa1ee6e1c274af62f7250bc38"; -pub fn launch() -> Task> { +pub fn launch() -> Task { executor::try_spawn_blocking(|mut sender| { let cargo_install = process::Command::new("cargo") .args(["install", "--list"]) @@ -22,15 +20,15 @@ pub fn launch() -> Task> { } let Some((_, revision)) = line.rsplit_once("?rev=") else { - return Err(Error::Outdated { revision: None }); + return Err(launch::Error::Outdated { revision: None }); }; let Some((revision, _)) = revision.rsplit_once("#") else { - return Err(Error::Outdated { revision: None }); + return Err(launch::Error::Outdated { revision: None }); }; if revision != COMPATIBLE_REVISION { - return Err(Error::Outdated { + return Err(launch::Error::Outdated { revision: Some(revision.to_owned()), }); } @@ -45,16 +43,16 @@ pub fn launch() -> Task> { return Ok(()); } - Err(Error::NotFound) + Err(launch::Error::NotFound) }) } -pub fn install() -> Task> { +pub fn install() -> Task { executor::try_spawn_blocking(|mut sender| { use std::io::{BufRead, BufReader}; use std::process::{Command, Stdio}; - let install = Command::new("cargo") + let mut install = Command::new("cargo") .args([ "install", "--locked", @@ -68,41 +66,75 @@ pub fn install() -> Task> { .stderr(Stdio::piped()) .spawn()?; - let mut stderr = - BufReader::new(install.stderr.expect("stderr must be piped")); + let mut stderr = BufReader::new( + install.stderr.take().expect("stderr must be piped"), + ); let mut log = String::new(); while let Ok(n) = stderr.read_line(&mut log) { if n == 0 { - break; + let status = install.wait()?; + + if status.success() { + break; + } else { + return Err(install::Error::ProcessFailed(status)); + } } - let _ = sender.try_send(Installation::Logged(log.clone())); + let _ = sender.try_send(install::Event::Logged(log.clone())); log.clear(); } - let _ = sender.try_send(Installation::Finished); + let _ = sender.try_send(install::Event::Finished); Ok(()) }) } -#[derive(Debug, Clone)] -pub enum Installation { - Logged(String), - Finished, -} +pub mod launch { + use std::io; + use std::sync::Arc; -#[derive(Debug, Clone)] -pub enum Error { - NotFound, - Outdated { revision: Option }, - IoFailed(Arc), -} + pub type Result = std::result::Result<(), Error>; -impl From for Error { - fn from(error: io::Error) -> Self { - Self::IoFailed(Arc::new(error)) + #[derive(Debug, Clone)] + pub enum Error { + NotFound, + Outdated { revision: Option }, + IoFailed(Arc), + } + + impl From for Error { + fn from(error: io::Error) -> Self { + Self::IoFailed(Arc::new(error)) + } + } +} + +pub mod install { + use std::io; + use std::process; + use std::sync::Arc; + + pub type Result = std::result::Result; + + #[derive(Debug, Clone)] + pub enum Event { + Logged(String), + Finished, + } + + #[derive(Debug, Clone)] + pub enum Error { + ProcessFailed(process::ExitStatus), + IoFailed(Arc), + } + + impl From for Error { + fn from(error: io::Error) -> Self { + Self::IoFailed(Arc::new(error)) + } } } diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 18c118bf..95ac3626 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -10,11 +10,12 @@ mod comet; mod executor; mod time_machine; +use crate::core::border; use crate::core::keyboard; use crate::core::theme::{self, Base, Theme}; use crate::core::time::seconds; use crate::core::window; -use crate::core::{Color, Element, Length::Fill}; +use crate::core::{Alignment::Center, Color, Element, Length::Fill}; use crate::futures::Subscription; use crate::program::Program; use crate::runtime::Task; @@ -124,9 +125,9 @@ where enum Message { HideNotification, ToggleComet, - CometLaunched(Result<(), comet::Error>), + CometLaunched(comet::launch::Result), InstallComet, - InstallationProgressed(Result), + Installing(comet::install::Result), CancelSetup, } @@ -195,17 +196,17 @@ where Message::CometLaunched(Ok(())) => Task::none(), Message::CometLaunched(Err(error)) => { match error { - comet::Error::NotFound => { + comet::launch::Error::NotFound => { self.mode = Mode::Setup(Setup::Idle { goal: Goal::Installation, }); } - comet::Error::Outdated { revision } => { + comet::launch::Error::Outdated { revision } => { self.mode = Mode::Setup(Setup::Idle { goal: Goal::Update { revision }, }); } - comet::Error::IoFailed(error) => { + comet::launch::Error::IoFailed(error) => { log::error!("comet failed to run: {error}"); } } @@ -217,29 +218,42 @@ where Mode::Setup(Setup::Running { logs: Vec::new() }); comet::install() - .map(Message::InstallationProgressed) + .map(Message::Installing) .map(Event::Message) } - Message::InstallationProgressed(Ok(installation)) => { + Message::Installing(Ok(installation)) => { let Mode::Setup(Setup::Running { logs }) = &mut self.mode else { return Task::none(); }; match installation { - comet::Installation::Logged(log) => { + comet::install::Event::Logged(log) => { logs.push(log); Task::none() } - comet::Installation::Finished => { + comet::install::Event::Finished => { self.mode = Mode::None; comet::launch().discard() } } } - Message::InstallationProgressed(_error) => { - // TODO + Message::Installing(Err(error)) => { + let Mode::Setup(Setup::Running { logs }) = &mut self.mode + else { + return Task::none(); + }; + + match error { + comet::install::Error::ProcessFailed(status) => { + logs.push(format!("process failed with {status}")); + } + comet::install::Error::IoFailed(error) => { + logs.push(error.to_string()); + } + } + Task::none() } Message::CancelSetup => { @@ -304,7 +318,7 @@ where let derive_theme = move || { theme .palette() - .map(|palette| Theme::custom("DevTools".to_owned(), palette)) + .map(|palette| Theme::custom("iced devtools", palette)) .unwrap_or_default() }; @@ -312,97 +326,14 @@ where Mode::None => None, Mode::Setup(setup) => { let stage: Element<'_, _, Theme, P::Renderer> = match setup { - Setup::Idle { goal } => { - let controls = row![ - button(text("Cancel").center().width(Fill)) - .width(100) - .on_press(Message::CancelSetup) - .style(button::danger), - horizontal_space(), - button( - text(match goal { - Goal::Installation => "Install", - Goal::Update { .. } => "Update", - }) - .center() - .width(Fill) - ) - .width(100) - .on_press(Message::InstallComet) - .style(button::success), - ]; - - let command = container( - text( - "cargo install --locked \ - --git https://github.com/iced-rs/comet.git", - ) - .size(14), - ) - .width(Fill) - .padding(5) - .style(container::dark); - - match goal { - Goal::Installation => column![ - text("comet is not installed!").size(20), - "In order to display performance \ - metrics, the comet debugger must \ - be installed in your system.", - "The comet debugger is an official \ - companion tool that helps you debug \ - your iced applications.", - "Do you wish to install it with the \ - following command?", - command, - controls, - ] - .spacing(20), - Goal::Update { revision } => column![ - text("comet is out of date!").size(20), - text!( - "The installed revision is \"{current}\", \ - but the latest compatible is \"{compatible}\".", - current = revision - .as_deref() - .unwrap_or("Unknown"), - compatible = comet::COMPATIBLE_REVISION, - ), - "Do you wish to update it with the following \ - command?", - command, - controls, - ] - .spacing(20), - } - .into() - } - Setup::Running { logs } => column![ - text("Installing comet...").size(20), - container( - scrollable( - column( - logs.iter() - .map(|log| text(log).size(12).into()), - ) - .spacing(3), - ) - .spacing(10) - .width(Fill) - .height(300) - .anchor_bottom(), - ) - .padding(10) - .style(container::dark) - ] - .spacing(20) - .into(), + Setup::Idle { goal } => self::setup(goal), + Setup::Running { logs } => installation(logs), }; let setup = center( container(stage) .padding(20) - .width(500) + .max_width(500) .style(container::bordered_box), ) .padding(10) @@ -510,3 +441,134 @@ where } } } + +fn setup(goal: &Goal) -> Element<'_, Message, Theme, Renderer> +where + Renderer: core::text::Renderer + 'static, +{ + let controls = row![ + button(text("Cancel").center().width(Fill)) + .width(100) + .on_press(Message::CancelSetup) + .style(button::danger), + horizontal_space(), + button( + text(match goal { + Goal::Installation => "Install", + Goal::Update { .. } => "Update", + }) + .center() + .width(Fill) + ) + .width(100) + .on_press(Message::InstallComet) + .style(button::success), + ]; + + let command = container( + text!( + "cargo install --locked \\ + --git https://github.com/iced-rs/comet.git \\ + --rev {}", + comet::COMPATIBLE_REVISION + ) + .size(14) + .font(Renderer::MONOSPACE_FONT), + ) + .width(Fill) + .padding(5) + .style(container::dark); + + Element::from(match goal { + Goal::Installation => column![ + text("comet is not installed!").size(20), + "In order to display performance \ + metrics, the comet debugger must \ + be installed in your system.", + "The comet debugger is an official \ + companion tool that helps you debug \ + your iced applications.", + column![ + "Do you wish to install it with the \ + following command?", + command + ] + .spacing(10), + controls, + ] + .spacing(20), + Goal::Update { revision } => { + let comparison = column![ + row![ + "Installed revision:", + horizontal_space(), + inline_code(revision.as_deref().unwrap_or("Unknown")) + ] + .align_y(Center), + row![ + "Compatible revision:", + horizontal_space(), + inline_code(comet::COMPATIBLE_REVISION), + ] + .align_y(Center) + ] + .spacing(5); + + column![ + text("comet is out of date!").size(20), + comparison, + column![ + "Do you wish to update it with the following \ + command?", + command + ] + .spacing(10), + controls, + ] + .spacing(20) + } + }) +} + +fn installation<'a, Renderer>( + logs: &'a [String], +) -> Element<'a, Message, Theme, Renderer> +where + Renderer: core::text::Renderer + 'a, +{ + column![ + text("Installing comet...").size(20), + container( + scrollable( + column(logs.iter().map(|log| { + text(log).size(12).font(Renderer::MONOSPACE_FONT).into() + }),) + .spacing(3), + ) + .spacing(10) + .width(Fill) + .height(300) + .anchor_bottom(), + ) + .padding(10) + .style(container::dark) + ] + .spacing(20) + .into() +} + +fn inline_code<'a, Renderer>( + code: impl text::IntoFragment<'a>, +) -> Element<'a, Message, Theme, Renderer> +where + Renderer: core::text::Renderer + 'a, +{ + container(text(code).font(Renderer::MONOSPACE_FONT).size(12)) + .style(|_theme| { + container::Style::default() + .background(Color::BLACK) + .border(border::rounded(2)) + }) + .padding([2, 4]) + .into() +} diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 4cea1a15..7223f06f 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -84,6 +84,7 @@ where type Paragraph = A::Paragraph; type Editor = A::Editor; + const MONOSPACE_FONT: Self::Font = A::MONOSPACE_FONT; const ICON_FONT: Self::Font = A::ICON_FONT; const CHECKMARK_ICON: char = A::CHECKMARK_ICON; const ARROW_DOWN_ICON: char = A::ARROW_DOWN_ICON; diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index a222e23c..8f343277 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -235,6 +235,7 @@ impl core::text::Renderer for Renderer { type Paragraph = Paragraph; type Editor = Editor; + const MONOSPACE_FONT: Font = Font::MONOSPACE; const ICON_FONT: Font = Font::with_name("Iced-Icons"); const CHECKMARK_ICON: char = '\u{f00c}'; const ARROW_DOWN_ICON: char = '\u{e800}'; diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 3fe2fbbe..94ab81c4 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -643,6 +643,7 @@ impl core::text::Renderer for Renderer { type Paragraph = Paragraph; type Editor = Editor; + const MONOSPACE_FONT: Font = Font::MONOSPACE; const ICON_FONT: Font = Font::with_name("Iced-Icons"); const CHECKMARK_ICON: char = '\u{f00c}'; const ARROW_DOWN_ICON: char = '\u{e800}'; 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 10/10] 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) + } +}