From dd36893f7ae0cc2071763da2bb0ca17e3c6a59d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 26 Feb 2024 07:00:51 +0100 Subject: [PATCH 01/39] Introduce `iced_sentinel` and `iced_debug` crates --- Cargo.toml | 10 +- debug/Cargo.toml | 23 +++ debug/src/lib.rs | 162 +++++++++++++++++++++ examples/integration/src/main.rs | 12 +- graphics/src/compositor.rs | 6 +- renderer/src/compositor.rs | 10 +- runtime/Cargo.toml | 3 +- runtime/src/debug/basic.rs | 220 ----------------------------- runtime/src/debug/null.rs | 47 ------ runtime/src/lib.rs | 11 +- runtime/src/multi_window/state.rs | 50 +++---- runtime/src/program/state.rs | 37 ++--- sentinel/Cargo.toml | 26 ++++ sentinel/src/client.rs | 80 +++++++++++ sentinel/src/lib.rs | 105 ++++++++++++++ sentinel/src/timing.rs | 25 ++++ tiny_skia/src/backend.rs | 50 +------ tiny_skia/src/window/compositor.rs | 32 +---- wgpu/src/backend.rs | 9 +- wgpu/src/window/compositor.rs | 24 +--- winit/Cargo.toml | 1 - winit/src/application.rs | 63 +++------ winit/src/application/state.rs | 10 +- winit/src/lib.rs | 1 + winit/src/multi_window.rs | 86 +++++------ winit/src/multi_window/state.rs | 7 +- 26 files changed, 543 insertions(+), 567 deletions(-) create mode 100644 debug/Cargo.toml create mode 100644 debug/src/lib.rs delete mode 100644 runtime/src/debug/basic.rs delete mode 100644 runtime/src/debug/null.rs create mode 100644 sentinel/Cargo.toml create mode 100644 sentinel/src/client.rs create mode 100644 sentinel/src/lib.rs create mode 100644 sentinel/src/timing.rs diff --git a/Cargo.toml b/Cargo.toml index 7f55ce0e..aef0d9f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ qr_code = ["iced_widget/qr_code"] # Enables lazy widgets lazy = ["iced_widget/lazy"] # Enables a debug view in native platforms (press F12) -debug = ["iced_winit/debug"] +debug = ["iced_debug/enable"] # Enables `tokio` as the `executor::Default` on native platforms tokio = ["iced_futures/tokio"] # Enables `async-std` as the `executor::Default` on native platforms @@ -58,6 +58,7 @@ fira-sans = ["iced_renderer/fira-sans"] [dependencies] iced_core.workspace = true +iced_debug.workspace = true iced_futures.workspace = true iced_renderer.workspace = true iced_widget.workspace = true @@ -85,11 +86,13 @@ strip = "debuginfo" [workspace] members = [ "core", + "debug", "futures", "graphics", "highlighter", "renderer", "runtime", + "sentinel", "style", "tiny_skia", "wgpu", @@ -111,11 +114,13 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] [workspace.dependencies] iced = { version = "0.13.0-dev", path = "." } iced_core = { version = "0.13.0-dev", path = "core" } +iced_debug = { version = "0.13.0-dev", path = "debug" } iced_futures = { version = "0.13.0-dev", path = "futures" } iced_graphics = { version = "0.13.0-dev", path = "graphics" } iced_highlighter = { version = "0.13.0-dev", path = "highlighter" } iced_renderer = { version = "0.13.0-dev", path = "renderer" } iced_runtime = { version = "0.13.0-dev", path = "runtime" } +iced_sentinel = { version = "0.13.0-dev", path = "sentinel" } iced_style = { version = "0.13.0-dev", path = "style" } iced_tiny_skia = { version = "0.13.0-dev", path = "tiny_skia" } iced_wgpu = { version = "0.13.0-dev", path = "wgpu" } @@ -145,6 +150,9 @@ qrcode = { version = "0.13", default-features = false } raw-window-handle = "0.6" resvg = "0.36" rustc-hash = "1.0" +serde = "1.0" +serde_json = "1.0" +semver = "1.0" smol = "1.0" smol_str = "0.2" softbuffer = "0.4" diff --git a/debug/Cargo.toml b/debug/Cargo.toml new file mode 100644 index 00000000..77cabf6a --- /dev/null +++ b/debug/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "iced_debug" +description = "A pluggable API for debugging iced applications" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +categories.workspace = true +keywords.workspace = true + +[features] +enable = ["iced_sentinel", "once_cell"] + +[dependencies] +iced_core.workspace = true + +iced_sentinel.workspace = true +iced_sentinel.optional = true + +once_cell.workspace = true +once_cell.optional = true diff --git a/debug/src/lib.rs b/debug/src/lib.rs new file mode 100644 index 00000000..9edf0073 --- /dev/null +++ b/debug/src/lib.rs @@ -0,0 +1,162 @@ +pub use iced_core as core; + +pub use internal::Timer; + +pub fn open_axe() {} + +pub fn log_message(_message: &impl std::fmt::Debug) {} + +pub fn boot_time() -> Timer { + internal::boot_time() +} + +pub fn update_time() -> Timer { + internal::update_time() +} + +pub fn view_time() -> Timer { + internal::view_time() +} + +pub fn layout_time() -> Timer { + internal::layout_time() +} + +pub fn interact_time() -> Timer { + internal::interact_time() +} + +pub fn draw_time() -> Timer { + internal::draw_time() +} + +pub fn render_time() -> Timer { + internal::render_time() +} + +pub fn time(name: impl AsRef) -> Timer { + internal::time(name) +} + +#[cfg(feature = "enable")] +mod internal { + use crate::core::time::Instant; + + use iced_sentinel::client::{self, Client}; + use iced_sentinel::timing::{self, Timing}; + use iced_sentinel::Report; + + use once_cell::sync::Lazy; + use std::sync::{Mutex, MutexGuard}; + + pub fn boot_time() -> Timer { + timer(timing::Stage::Boot) + } + + pub fn update_time() -> Timer { + timer(timing::Stage::Update) + } + + pub fn view_time() -> Timer { + timer(timing::Stage::View) + } + + pub fn layout_time() -> Timer { + timer(timing::Stage::Layout) + } + + pub fn interact_time() -> Timer { + timer(timing::Stage::Interact) + } + + pub fn draw_time() -> Timer { + timer(timing::Stage::Draw) + } + + pub fn render_time() -> Timer { + timer(timing::Stage::Render) + } + + pub fn time(name: impl AsRef) -> Timer { + timer(timing::Stage::Custom(name.as_ref().to_owned())) + } + + fn timer(stage: timing::Stage) -> Timer { + Timer { + stage, + start: Instant::now(), + } + } + + #[derive(Debug)] + pub struct Timer { + stage: timing::Stage, + start: Instant, + } + + impl Timer { + pub fn finish(self) { + lock().sentinel.report(Report::Timing(Timing { + stage: self.stage, + duration: self.start.elapsed(), + })); + } + } + + #[derive(Debug)] + struct Debug { + sentinel: Client, + } + + fn lock() -> MutexGuard<'static, Debug> { + static DEBUG: Lazy> = Lazy::new(|| { + Mutex::new(Debug { + sentinel: client::connect(), + }) + }); + + DEBUG.lock().expect("Acquire debug lock") + } +} + +#[cfg(not(feature = "enable"))] +mod internal { + pub fn boot_time() -> Timer { + Timer + } + + pub fn update_time() -> Timer { + Timer + } + + pub fn view_time() -> Timer { + Timer + } + + pub fn layout_time() -> Timer { + Timer + } + + pub fn interact_time() -> Timer { + Timer + } + + pub fn draw_time() -> Timer { + Timer + } + + pub fn render_time() -> Timer { + Timer + } + + pub fn time(_name: impl AsRef) -> Timer { + Timer + } + + #[derive(Debug)] + pub struct Timer; + + impl Timer { + pub fn finish(self) {} + } +} diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 0e2e53ac..63efcbea 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -13,7 +13,6 @@ use iced_winit::core::window; use iced_winit::core::{Color, Font, Pixels, Size}; use iced_winit::futures; use iced_winit::runtime::program; -use iced_winit::runtime::Debug; use iced_winit::style::Theme; use iced_winit::winit; use iced_winit::Clipboard; @@ -155,19 +154,14 @@ pub fn main() -> Result<(), Box> { let controls = Controls::new(); // Initialize iced - let mut debug = Debug::new(); let mut renderer = Renderer::new( Backend::new(&adapter, &device, &queue, Settings::default(), format), Font::default(), Pixels(16.0), ); - let mut state = program::State::new( - controls, - viewport.logical_size(), - &mut renderer, - &mut debug, - ); + let mut state = + program::State::new(controls, viewport.logical_size(), &mut renderer); // Run event loop event_loop.run(move |event, window_target| { @@ -239,7 +233,6 @@ pub fn main() -> Result<(), Box> { &view, primitive, &viewport, - &debug.overlay(), ); }); @@ -315,7 +308,6 @@ pub fn main() -> Result<(), Box> { text_color: Color::WHITE, }, &mut clipboard, - &mut debug, ); // and request a redraw diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 0188f4d8..786fd8a4 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -55,26 +55,24 @@ pub trait Compositor: Sized { /// /// [`Renderer`]: Self::Renderer /// [`Surface`]: Self::Surface - fn present>( + fn present( &mut self, renderer: &mut Self::Renderer, surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Result<(), SurfaceError>; /// Screenshots the current [`Renderer`] primitives to an offscreen texture, and returns the bytes of /// the texture ordered as `RGBA` in the `sRGB` color space. /// /// [`Renderer`]: Self::Renderer - fn screenshot>( + fn screenshot( &mut self, renderer: &mut Self::Renderer, surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Vec; } diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index dc2c50ff..0777f6e4 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -101,13 +101,12 @@ impl crate::graphics::Compositor for Compositor { } } - fn present>( + fn present( &mut self, renderer: &mut Self::Renderer, surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Result<(), SurfaceError> { match (self, renderer, surface) { ( @@ -121,7 +120,6 @@ impl crate::graphics::Compositor for Compositor { primitives, viewport, background_color, - overlay, ) }), #[cfg(feature = "wgpu")] @@ -137,7 +135,6 @@ impl crate::graphics::Compositor for Compositor { primitives, viewport, background_color, - overlay, ) }), #[allow(unreachable_patterns)] @@ -148,13 +145,12 @@ impl crate::graphics::Compositor for Compositor { } } - fn screenshot>( + fn screenshot( &mut self, renderer: &mut Self::Renderer, surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Vec { match (self, renderer, surface) { ( @@ -168,7 +164,6 @@ impl crate::graphics::Compositor for Compositor { primitives, viewport, background_color, - overlay, ) }), #[cfg(feature = "wgpu")] @@ -183,7 +178,6 @@ impl crate::graphics::Compositor for Compositor { primitives, viewport, background_color, - overlay, ) }), #[allow(unreachable_patterns)] diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 3a47a971..6d49ef93 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -11,11 +11,12 @@ categories.workspace = true keywords.workspace = true [features] -debug = [] multi-window = [] [dependencies] iced_core.workspace = true +iced_debug.workspace = true + iced_futures.workspace = true iced_futures.features = ["thread-pool"] diff --git a/runtime/src/debug/basic.rs b/runtime/src/debug/basic.rs deleted file mode 100644 index 4c994a2f..00000000 --- a/runtime/src/debug/basic.rs +++ /dev/null @@ -1,220 +0,0 @@ -#![allow(missing_docs)] -use crate::core::time; - -use std::collections::VecDeque; - -/// A bunch of time measurements for debugging purposes. -#[derive(Debug)] -pub struct Debug { - is_enabled: bool, - - startup_start: time::Instant, - startup_duration: time::Duration, - - update_start: time::Instant, - update_durations: TimeBuffer, - - view_start: time::Instant, - view_durations: TimeBuffer, - - layout_start: time::Instant, - layout_durations: TimeBuffer, - - event_start: time::Instant, - event_durations: TimeBuffer, - - draw_start: time::Instant, - draw_durations: TimeBuffer, - - render_start: time::Instant, - render_durations: TimeBuffer, - - message_count: usize, - last_messages: VecDeque, -} - -impl Debug { - /// Creates a new [`struct@Debug`]. - pub fn new() -> Self { - let now = time::Instant::now(); - - Self { - is_enabled: false, - startup_start: now, - startup_duration: time::Duration::from_secs(0), - - update_start: now, - update_durations: TimeBuffer::new(200), - - view_start: now, - view_durations: TimeBuffer::new(200), - - layout_start: now, - layout_durations: TimeBuffer::new(200), - - event_start: now, - event_durations: TimeBuffer::new(200), - - draw_start: now, - draw_durations: TimeBuffer::new(200), - - render_start: now, - render_durations: TimeBuffer::new(50), - - message_count: 0, - last_messages: VecDeque::new(), - } - } - - pub fn toggle(&mut self) { - self.is_enabled = !self.is_enabled; - } - - pub fn startup_started(&mut self) { - self.startup_start = time::Instant::now(); - } - - pub fn startup_finished(&mut self) { - self.startup_duration = self.startup_start.elapsed(); - } - - pub fn update_started(&mut self) { - self.update_start = time::Instant::now(); - } - - pub fn update_finished(&mut self) { - self.update_durations.push(self.update_start.elapsed()); - } - - pub fn view_started(&mut self) { - self.view_start = time::Instant::now(); - } - - pub fn view_finished(&mut self) { - self.view_durations.push(self.view_start.elapsed()); - } - - pub fn layout_started(&mut self) { - self.layout_start = time::Instant::now(); - } - - pub fn layout_finished(&mut self) { - self.layout_durations.push(self.layout_start.elapsed()); - } - - pub fn event_processing_started(&mut self) { - self.event_start = time::Instant::now(); - } - - pub fn event_processing_finished(&mut self) { - self.event_durations.push(self.event_start.elapsed()); - } - - pub fn draw_started(&mut self) { - self.draw_start = time::Instant::now(); - } - - pub fn draw_finished(&mut self) { - self.draw_durations.push(self.draw_start.elapsed()); - } - - pub fn render_started(&mut self) { - self.render_start = time::Instant::now(); - } - - pub fn render_finished(&mut self) { - self.render_durations.push(self.render_start.elapsed()); - } - - pub fn log_message(&mut self, message: &Message) { - self.last_messages.push_back(format!("{message:?}")); - - if self.last_messages.len() > 10 { - let _ = self.last_messages.pop_front(); - } - - self.message_count += 1; - } - - pub fn overlay(&self) -> Vec { - if !self.is_enabled { - return Vec::new(); - } - - let mut lines = Vec::new(); - - fn key_value(key: &str, value: T) -> String { - format!("{key} {value:?}") - } - - lines.push(format!( - "{} {} - {}", - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_VERSION"), - env!("CARGO_PKG_REPOSITORY"), - )); - lines.push(key_value("Startup:", self.startup_duration)); - lines.push(key_value("Update:", self.update_durations.average())); - lines.push(key_value("View:", self.view_durations.average())); - lines.push(key_value("Layout:", self.layout_durations.average())); - lines.push(key_value( - "Event processing:", - self.event_durations.average(), - )); - lines.push(key_value( - "Primitive generation:", - self.draw_durations.average(), - )); - lines.push(key_value("Render:", self.render_durations.average())); - lines.push(key_value("Message count:", self.message_count)); - lines.push(String::from("Last messages:")); - lines.extend(self.last_messages.iter().map(|msg| { - if msg.len() <= 100 { - format!(" {msg}") - } else { - format!(" {msg:.100}...") - } - })); - - lines - } -} - -impl Default for Debug { - fn default() -> Self { - Self::new() - } -} - -#[derive(Debug)] -struct TimeBuffer { - head: usize, - size: usize, - contents: Vec, -} - -impl TimeBuffer { - fn new(capacity: usize) -> TimeBuffer { - TimeBuffer { - head: 0, - size: 0, - contents: vec![time::Duration::from_secs(0); capacity], - } - } - - fn push(&mut self, duration: time::Duration) { - self.head = (self.head + 1) % self.contents.len(); - self.contents[self.head] = duration; - self.size = (self.size + 1).min(self.contents.len()); - } - - fn average(&self) -> time::Duration { - let sum: time::Duration = if self.size == self.contents.len() { - self.contents[..].iter().sum() - } else { - self.contents[..self.size].iter().sum() - }; - - sum / self.size.max(1) as u32 - } -} diff --git a/runtime/src/debug/null.rs b/runtime/src/debug/null.rs deleted file mode 100644 index 2db0eebb..00000000 --- a/runtime/src/debug/null.rs +++ /dev/null @@ -1,47 +0,0 @@ -#![allow(missing_docs)] -#[derive(Debug, Default)] -pub struct Debug; - -impl Debug { - pub fn new() -> Self { - Self - } - - pub fn startup_started(&mut self) {} - - pub fn startup_finished(&mut self) {} - - pub fn update_started(&mut self) {} - - pub fn update_finished(&mut self) {} - - pub fn view_started(&mut self) {} - - pub fn view_finished(&mut self) {} - - pub fn layout_started(&mut self) {} - - pub fn layout_finished(&mut self) {} - - pub fn event_processing_started(&mut self) {} - - pub fn event_processing_finished(&mut self) {} - - pub fn draw_started(&mut self) {} - - pub fn draw_finished(&mut self) {} - - pub fn render_started(&mut self) {} - - pub fn render_finished(&mut self) {} - - pub fn log_message( - &mut self, - _message: &Message, - ) { - } - - pub fn overlay(&self) -> Vec { - Vec::new() - } -} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5c2836a5..18059ef5 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -29,20 +29,11 @@ pub mod window; #[cfg(feature = "multi-window")] pub mod multi_window; -// We disable debug capabilities on release builds unless the `debug` feature -// is explicitly enabled. -#[cfg(feature = "debug")] -#[path = "debug/basic.rs"] -mod debug; -#[cfg(not(feature = "debug"))] -#[path = "debug/null.rs"] -mod debug; - pub use iced_core as core; +pub use iced_debug as debug; pub use iced_futures as futures; pub use command::Command; -pub use debug::Debug; pub use font::Font; pub use program::Program; pub use user_interface::UserInterface; diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs index afd04519..215c87e1 100644 --- a/runtime/src/multi_window/state.rs +++ b/runtime/src/multi_window/state.rs @@ -4,8 +4,9 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::operation::{self, Operation}; use crate::core::{Clipboard, Size}; +use crate::debug; use crate::user_interface::{self, UserInterface}; -use crate::{Command, Debug, Program}; +use crate::{Command, Program}; /// The execution state of a multi-window [`Program`]. It leverages caching, event /// processing, and rendering primitive storage. @@ -27,18 +28,12 @@ where { /// Creates a new [`State`] with the provided [`Program`], initializing its /// primitive with the given logical bounds and renderer. - pub fn new( - program: P, - bounds: Size, - renderer: &mut P::Renderer, - debug: &mut Debug, - ) -> Self { + pub fn new(program: P, bounds: Size, renderer: &mut P::Renderer) -> Self { let user_interface = build_user_interface( &program, user_interface::Cache::default(), renderer, bounds, - debug, ); let caches = Some(vec![user_interface.into_cache()]); @@ -95,17 +90,15 @@ where theme: &P::Theme, style: &renderer::Style, clipboard: &mut dyn Clipboard, - debug: &mut Debug, ) -> (Vec, Option>) { let mut user_interfaces = build_user_interfaces( &self.program, self.caches.take().unwrap(), renderer, bounds, - debug, ); - debug.event_processing_started(); + let interact_timer = debug::interact_time(); let mut messages = Vec::new(); let uncaptured_events = user_interfaces.iter_mut().fold( @@ -135,17 +128,15 @@ where self.queued_events.clear(); messages.append(&mut self.queued_messages); - debug.event_processing_finished(); + drop(interact_timer); let commands = if messages.is_empty() { - debug.draw_started(); - + let draw_timer = debug::draw_time(); for ui in &mut user_interfaces { self.mouse_interaction = ui.draw(renderer, theme, style, cursor); } - - debug.draw_finished(); + drop(draw_timer); self.caches = Some( user_interfaces @@ -164,11 +155,11 @@ where drop(user_interfaces); let commands = Command::batch(messages.into_iter().map(|msg| { - debug.log_message(&msg); + debug::log_message(&msg); - debug.update_started(); + let update_timer = debug::update_time(); let command = self.program.update(msg); - debug.update_finished(); + drop(update_timer); command })); @@ -178,15 +169,14 @@ where temp_caches, renderer, bounds, - debug, ); - debug.draw_started(); + let draw_timer = debug::draw_time(); for ui in &mut user_interfaces { self.mouse_interaction = ui.draw(renderer, theme, style, cursor); } - debug.draw_finished(); + drop(draw_timer); self.caches = Some( user_interfaces @@ -207,14 +197,12 @@ where renderer: &mut P::Renderer, operations: impl Iterator>>, bounds: Size, - debug: &mut Debug, ) { let mut user_interfaces = build_user_interfaces( &self.program, self.caches.take().unwrap(), renderer, bounds, - debug, ); for operation in operations { @@ -251,13 +239,10 @@ fn build_user_interfaces<'a, P: Program>( mut caches: Vec, renderer: &mut P::Renderer, size: Size, - debug: &mut Debug, ) -> Vec> { caches .drain(..) - .map(|cache| { - build_user_interface(program, cache, renderer, size, debug) - }) + .map(|cache| build_user_interface(program, cache, renderer, size)) .collect() } @@ -266,15 +251,14 @@ fn build_user_interface<'a, P: Program>( cache: user_interface::Cache, renderer: &mut P::Renderer, size: Size, - debug: &mut Debug, ) -> UserInterface<'a, P::Message, P::Theme, P::Renderer> { - debug.view_started(); + let view_timer = debug::view_time(); let view = program.view(); - debug.view_finished(); + drop(view_timer); - debug.layout_started(); + let layout_timer = debug::layout_time(); let user_interface = UserInterface::build(view, size, cache, renderer); - debug.layout_finished(); + drop(layout_timer); user_interface } diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index d685b07c..0c9051d2 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -3,8 +3,9 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::operation::{self, Operation}; use crate::core::{Clipboard, Size}; +use crate::debug; use crate::user_interface::{self, UserInterface}; -use crate::{Command, Debug, Program}; +use crate::{Command, Program}; /// The execution state of a [`Program`]. It leverages caching, event /// processing, and rendering primitive storage. @@ -30,14 +31,12 @@ where mut program: P, bounds: Size, renderer: &mut P::Renderer, - debug: &mut Debug, ) -> Self { let user_interface = build_user_interface( &mut program, user_interface::Cache::default(), renderer, bounds, - debug, ); let cache = Some(user_interface.into_cache()); @@ -94,17 +93,15 @@ where theme: &P::Theme, style: &renderer::Style, clipboard: &mut dyn Clipboard, - debug: &mut Debug, ) -> (Vec, Option>) { let mut user_interface = build_user_interface( &mut self.program, self.cache.take().unwrap(), renderer, bounds, - debug, ); - debug.event_processing_started(); + let interact_timer = debug::interact_time(); let mut messages = Vec::new(); let (_, event_statuses) = user_interface.update( @@ -127,13 +124,13 @@ where self.queued_events.clear(); messages.append(&mut self.queued_messages); - debug.event_processing_finished(); + drop(interact_timer); let command = if messages.is_empty() { - debug.draw_started(); + let draw_timer = debug::draw_time(); self.mouse_interaction = user_interface.draw(renderer, theme, style, cursor); - debug.draw_finished(); + drop(draw_timer); self.cache = Some(user_interface.into_cache()); @@ -145,11 +142,11 @@ where let commands = Command::batch(messages.into_iter().map(|message| { - debug.log_message(&message); + debug::log_message(&message); - debug.update_started(); + let update_timer = debug::update_time(); let command = self.program.update(message); - debug.update_finished(); + drop(update_timer); command })); @@ -159,13 +156,12 @@ where temp_cache, renderer, bounds, - debug, ); - debug.draw_started(); + let draw_timer = debug::draw_time(); self.mouse_interaction = user_interface.draw(renderer, theme, style, cursor); - debug.draw_finished(); + drop(draw_timer); self.cache = Some(user_interface.into_cache()); @@ -181,14 +177,12 @@ where renderer: &mut P::Renderer, operations: impl Iterator>>, bounds: Size, - debug: &mut Debug, ) { let mut user_interface = build_user_interface( &mut self.program, self.cache.take().unwrap(), renderer, bounds, - debug, ); for operation in operations { @@ -218,15 +212,14 @@ fn build_user_interface<'a, P: Program>( cache: user_interface::Cache, renderer: &mut P::Renderer, size: Size, - debug: &mut Debug, ) -> UserInterface<'a, P::Message, P::Theme, P::Renderer> { - debug.view_started(); + let view_timer = debug::view_time(); let view = program.view(); - debug.view_finished(); + drop(view_timer); - debug.layout_started(); + let layout_timer = debug::layout_time(); let user_interface = UserInterface::build(view, size, cache, renderer); - debug.layout_finished(); + drop(layout_timer); user_interface } diff --git a/sentinel/Cargo.toml b/sentinel/Cargo.toml new file mode 100644 index 00000000..efb36659 --- /dev/null +++ b/sentinel/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "iced_sentinel" +description = "A client/server protocol to monitor and supervise iced applications" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +categories.workspace = true +keywords.workspace = true + +[dependencies] +iced_core.workspace = true +serde_json.workspace = true +futures.workspace = true +log.workspace = true + +tokio.workspace = true +tokio.features = ["rt", "rt-multi-thread", "net", "sync", "time", "io-util", "macros"] + +serde.workspace = true +serde.features = ["derive"] + +semver.workspace = true +semver.features = ["serde"] diff --git a/sentinel/src/client.rs b/sentinel/src/client.rs new file mode 100644 index 00000000..3e173ecf --- /dev/null +++ b/sentinel/src/client.rs @@ -0,0 +1,80 @@ +use crate::{Input, Report, SOCKET_ADDRESS}; + +use tokio::io::{self, AsyncWriteExt}; +use tokio::net; +use tokio::sync::mpsc; +use tokio::time; + +#[derive(Debug, Clone)] +pub struct Client { + sender: mpsc::Sender, +} + +impl Client { + pub fn report(&mut self, report: Report) { + let _ = self.sender.try_send(Input::Reported(report)); + } +} + +#[must_use] +pub fn connect() -> Client { + let (sender, receiver) = mpsc::channel(1_000); + + std::thread::spawn(move || run(receiver)); + + Client { sender } +} + +#[tokio::main] +async fn run(mut receiver: mpsc::Receiver) { + let version = semver::Version::parse(env!("CARGO_PKG_VERSION")) + .expect("Parse package version"); + + loop { + match _connect().await { + Ok(mut stream) => { + let _ = send(&mut stream, Input::Connected(version)).await; + + while let Some(input) = receiver.recv().await { + if send(&mut stream, input).await.is_err() { + break; + } + } + + break; + } + Err(_) => { + time::sleep(time::Duration::from_secs(2)).await; + } + } + } +} + +async fn _connect() -> Result, io::Error> { + log::debug!("Attempting to connect sentinel to server..."); + let stream = net::TcpStream::connect(SOCKET_ADDRESS).await?; + + stream.set_nodelay(true)?; + stream.writable().await?; + + Ok(io::BufStream::new(stream)) +} + +async fn send( + stream: &mut io::BufStream, + input: Input, +) -> Result<(), io::Error> { + stream + .write_all( + format!( + "{}\n", + serde_json::to_string(&input).expect("Serialize input message") + ) + .as_bytes(), + ) + .await?; + + stream.flush().await?; + + Ok(()) +} diff --git a/sentinel/src/lib.rs b/sentinel/src/lib.rs new file mode 100644 index 00000000..f18b9267 --- /dev/null +++ b/sentinel/src/lib.rs @@ -0,0 +1,105 @@ +pub use iced_core as core; + +pub mod client; +pub mod timing; + +use crate::timing::Timing; + +use futures::future; +use futures::stream::{self, Stream, StreamExt}; +use semver::Version; +use serde::{Deserialize, Serialize}; +use tokio::io::{self, AsyncBufReadExt, BufStream}; +use tokio::net; + +pub const SOCKET_ADDRESS: &str = "127.0.0.1:9167"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Input { + Connected(Version), + Reported(Report), +} + +#[derive(Debug, Clone)] +pub enum Event { + Connected(Version), + Disconnected, + Reported(Report), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Report { + Timing(Timing), +} + +pub fn run() -> impl Stream { + enum State { + Disconnected, + Connected(BufStream), + } + + stream::unfold(State::Disconnected, |state| async { + match state { + State::Disconnected => match connect().await { + Ok(stream) => { + let stream = BufStream::new(stream); + + Some((None, State::Connected(stream))) + } + Err(_error) => Some((None, State::Disconnected)), + }, + State::Connected(stream) => match receive(stream).await { + Ok((_, Event::Disconnected)) | Err(_) => { + Some((Some(Event::Disconnected), State::Disconnected)) + } + Ok((stream, message)) => { + Some((Some(message), State::Connected(stream))) + } + }, + } + }) + .filter_map(future::ready) +} + +async fn connect() -> Result { + let listener = net::TcpListener::bind(SOCKET_ADDRESS).await?; + + let (stream, _) = listener.accept().await?; + + stream.set_nodelay(true)?; + stream.readable().await?; + + Ok(stream) +} + +async fn receive( + mut stream: BufStream, +) -> Result<(BufStream, Event), io::Error> { + let mut input = String::new(); + + loop { + match stream.read_line(&mut input).await? { + 0 => return Ok((stream, Event::Disconnected)), + n => { + match serde_json::from_str(&input[..n]) { + Ok(input) => { + return Ok(( + stream, + match dbg!(input) { + Input::Connected(version) => { + Event::Connected(version) + } + Input::Reported(report) => { + Event::Reported(report) + } + }, + )) + } + Err(_) => { + // TODO: Log decoding error + } + } + } + } + } +} diff --git a/sentinel/src/timing.rs b/sentinel/src/timing.rs new file mode 100644 index 00000000..b4a588f2 --- /dev/null +++ b/sentinel/src/timing.rs @@ -0,0 +1,25 @@ +use crate::core::time::Duration; + +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +pub struct Timing { + pub stage: Stage, + pub duration: Duration, +} + +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +pub enum Stage { + Boot, + Update, + View, + Layout, + Interact, + Draw, + Render, + Custom(String), +} diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index b6487b38..d2811fe2 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -31,7 +31,7 @@ impl Backend { } } - pub fn draw>( + pub fn draw( &mut self, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: &mut tiny_skia::Mask, @@ -39,38 +39,9 @@ impl Backend { viewport: &Viewport, damage: &[Rectangle], background_color: Color, - overlay: &[T], ) { - let physical_size = viewport.physical_size(); let scale_factor = viewport.scale_factor() as f32; - if !overlay.is_empty() { - let path = tiny_skia::PathBuilder::from_rect( - tiny_skia::Rect::from_xywh( - 0.0, - 0.0, - physical_size.width as f32, - physical_size.height as f32, - ) - .expect("Create damage rectangle"), - ); - - pixels.fill_path( - &path, - &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor(into_color(Color { - a: 0.1, - ..background_color - })), - anti_alias: false, - ..Default::default() - }, - tiny_skia::FillRule::default(), - tiny_skia::Transform::identity(), - None, - ); - } - for ®ion in damage { let path = tiny_skia::PathBuilder::from_rect( tiny_skia::Rect::from_xywh( @@ -109,25 +80,6 @@ impl Backend { Transformation::IDENTITY, ); } - - if !overlay.is_empty() { - pixels.stroke_path( - &path, - &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor(into_color( - Color::from_rgb(1.0, 0.0, 0.0), - )), - anti_alias: false, - ..tiny_skia::Paint::default() - }, - &tiny_skia::Stroke { - width: 1.0, - ..tiny_skia::Stroke::default() - }, - tiny_skia::Transform::identity(), - None, - ); - } } self.text_pipeline.trim_cache(); diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 21ccf620..09531a5d 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -95,43 +95,27 @@ impl crate::graphics::Compositor for Compositor { } } - fn present>( + fn present( &mut self, renderer: &mut Self::Renderer, surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Result<(), compositor::SurfaceError> { renderer.with_primitives(|backend, primitives| { - present( - backend, - surface, - primitives, - viewport, - background_color, - overlay, - ) + present(backend, surface, primitives, viewport, background_color) }) } - fn screenshot>( + fn screenshot( &mut self, renderer: &mut Self::Renderer, surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Vec { renderer.with_primitives(|backend, primitives| { - screenshot( - surface, - backend, - primitives, - viewport, - background_color, - overlay, - ) + screenshot(surface, backend, primitives, viewport, background_color) }) } } @@ -147,13 +131,12 @@ pub fn new( Compositor { context, settings } } -pub fn present>( +pub fn present( backend: &mut Backend, surface: &mut Surface, primitives: &[Primitive], viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Result<(), compositor::SurfaceError> { let physical_size = viewport.physical_size(); let scale_factor = viewport.scale_factor() as f32; @@ -206,19 +189,17 @@ pub fn present>( viewport, &damage, background_color, - overlay, ); buffer.present().map_err(|_| compositor::SurfaceError::Lost) } -pub fn screenshot>( +pub fn screenshot( surface: &mut Surface, backend: &mut Backend, primitives: &[Primitive], viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Vec { let size = viewport.physical_size(); @@ -240,7 +221,6 @@ pub fn screenshot>( size.height as f32, ))], background_color, - overlay, ); offscreen_buffer.iter().fold( diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 09ddbe4d..8ce0b26d 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -67,7 +67,7 @@ impl Backend { /// /// The text provided as overlay will be rendered on top of the primitives. /// This is useful for rendering debug information. - pub fn present>( + pub fn present( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, @@ -77,7 +77,6 @@ impl Backend { frame: &wgpu::TextureView, primitives: &[Primitive], viewport: &Viewport, - overlay_text: &[T], ) { log::debug!("Drawing"); #[cfg(feature = "tracing")] @@ -87,11 +86,7 @@ impl Backend { let scale_factor = viewport.scale_factor() as f32; let transformation = viewport.projection(); - let mut layers = Layer::generate(primitives, viewport); - - if !overlay_text.is_empty() { - layers.push(Layer::overlay(overlay_text, viewport)); - } + let layers = Layer::generate(primitives, viewport); self.prepare( device, diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 0a5d2c8f..fdec1152 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -172,14 +172,13 @@ pub fn new( } /// Presents the given primitives with the given [`Compositor`] and [`Backend`]. -pub fn present>( +pub fn present( compositor: &mut Compositor, backend: &mut Backend, surface: &mut wgpu::Surface<'static>, primitives: &[Primitive], viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Result<(), compositor::SurfaceError> { match surface.get_current_texture() { Ok(frame) => { @@ -202,7 +201,6 @@ pub fn present>( view, primitives, viewport, - overlay, ); // Submit work @@ -294,13 +292,12 @@ impl graphics::Compositor for Compositor { } } - fn present>( + fn present( &mut self, renderer: &mut Self::Renderer, surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Result<(), compositor::SurfaceError> { renderer.with_primitives(|backend, primitives| { present( @@ -310,28 +307,19 @@ impl graphics::Compositor for Compositor { primitives, viewport, background_color, - overlay, ) }) } - fn screenshot>( + fn screenshot( &mut self, renderer: &mut Self::Renderer, _surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Vec { renderer.with_primitives(|backend, primitives| { - screenshot( - self, - backend, - primitives, - viewport, - background_color, - overlay, - ) + screenshot(self, backend, primitives, viewport, background_color) }) } } @@ -339,13 +327,12 @@ impl graphics::Compositor for Compositor { /// Renders the current surface to an offscreen buffer. /// /// Returns RGBA bytes of the texture data. -pub fn screenshot>( +pub fn screenshot( compositor: &Compositor, backend: &mut Backend, primitives: &[Primitive], viewport: &Viewport, background_color: Color, - overlay: &[T], ) -> Vec { let mut encoder = compositor.device.create_command_encoder( &wgpu::CommandEncoderDescriptor { @@ -385,7 +372,6 @@ pub fn screenshot>( &view, primitives, viewport, - overlay, ); let texture = crate::color::convert( diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 87e600ae..fca2919a 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -12,7 +12,6 @@ keywords.workspace = true [features] default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] -debug = ["iced_runtime/debug"] system = ["sysinfo"] application = [] x11 = ["winit/x11"] diff --git a/winit/src/application.rs b/winit/src/application.rs index 05a4f070..6a056d88 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -11,13 +11,14 @@ use crate::core::time::Instant; use crate::core::widget::operation; use crate::core::window; use crate::core::{Event, Point, Size}; +use crate::debug; use crate::futures::futures; use crate::futures::{Executor, Runtime, Subscription}; use crate::graphics::compositor::{self, Compositor}; use crate::runtime::clipboard; use crate::runtime::program::Program; use crate::runtime::user_interface::{self, UserInterface}; -use crate::runtime::{Command, Debug}; +use crate::runtime::Command; use crate::style::application::{Appearance, StyleSheet}; use crate::{Clipboard, Error, Proxy, Settings}; @@ -111,8 +112,7 @@ where use futures::Future; use winit::event_loop::EventLoopBuilder; - let mut debug = Debug::new(); - debug.startup_started(); + let boot_timer = debug::boot_time(); let event_loop = EventLoopBuilder::with_user_event() .build() @@ -206,13 +206,13 @@ where renderer, runtime, proxy, - debug, event_receiver, control_sender, init_command, window, should_be_visible, exit_on_close_request, + boot_timer, )); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -276,7 +276,6 @@ async fn run_instance( mut renderer: A::Renderer, mut runtime: Runtime, A::Message>, mut proxy: winit::event_loop::EventLoopProxy, - mut debug: Debug, mut event_receiver: mpsc::UnboundedReceiver< winit::event::Event, >, @@ -285,6 +284,7 @@ async fn run_instance( window: Arc, should_be_visible: bool, exit_on_close_request: bool, + boot_timer: debug::Timer, ) where A: Application + 'static, E: Executor + 'static, @@ -324,17 +324,16 @@ async fn run_instance( &mut clipboard, &mut should_exit, &mut proxy, - &mut debug, &window, ); runtime.track(application.subscription().into_recipes()); + boot_timer.finish(); let mut user_interface = ManuallyDrop::new(build_user_interface( &application, cache, &mut renderer, state.logical_size(), - &mut debug, )); let mut mouse_interaction = mouse::Interaction::default(); @@ -342,8 +341,6 @@ async fn run_instance( let mut messages = Vec::new(); let mut redraw_pending = false; - debug.startup_finished(); - while let Some(event) = event_receiver.next().await { match event { event::Event::NewEvents( @@ -382,12 +379,12 @@ async fn run_instance( if viewport_version != current_viewport_version { let logical_size = state.logical_size(); - debug.layout_started(); + let layout_timer = debug::layout_time(); user_interface = ManuallyDrop::new( ManuallyDrop::into_inner(user_interface) .relayout(logical_size, &mut renderer), ); - debug.layout_finished(); + layout_timer.finish(); compositor.configure_surface( &mut surface, @@ -434,7 +431,7 @@ async fn run_instance( runtime.broadcast(redraw_event, core::event::Status::Ignored); - debug.draw_started(); + let draw_timer = debug::draw_time(); let new_mouse_interaction = user_interface.draw( &mut renderer, state.theme(), @@ -444,7 +441,7 @@ async fn run_instance( state.cursor(), ); redraw_pending = false; - debug.draw_finished(); + draw_timer.finish(); if new_mouse_interaction != mouse_interaction { window.set_cursor_icon(conversion::mouse_interaction( @@ -454,19 +451,15 @@ async fn run_instance( mouse_interaction = new_mouse_interaction; } - debug.render_started(); + let render_timer = debug::render_time(); match compositor.present( &mut renderer, &mut surface, state.viewport(), state.background_color(), - &debug.overlay(), ) { Ok(()) => { - debug.render_finished(); - - // TODO: Handle animations! - // Maybe we can use `ControlFlow::WaitUntil` for this. + render_timer.finish(); } Err(error) => match error { // This is an unrecoverable error. @@ -474,8 +467,6 @@ async fn run_instance( panic!("{error:?}"); } _ => { - debug.render_finished(); - // Try rendering again next frame. window.request_redraw(); } @@ -492,7 +483,7 @@ async fn run_instance( break; } - state.update(&window, &window_event, &mut debug); + state.update(&window, &window_event); if let Some(event) = conversion::window_event( window::Id::MAIN, @@ -508,8 +499,7 @@ async fn run_instance( continue; } - debug.event_processing_started(); - + let interact_timer = debug::interact_time(); let (interface_state, statuses) = user_interface.update( &events, state.cursor(), @@ -517,8 +507,7 @@ async fn run_instance( &mut clipboard, &mut messages, ); - - debug.event_processing_finished(); + interact_timer.finish(); for (event, status) in events.drain(..).zip(statuses.into_iter()) @@ -547,7 +536,6 @@ async fn run_instance( &mut clipboard, &mut should_exit, &mut proxy, - &mut debug, &mut messages, &window, ); @@ -557,7 +545,6 @@ async fn run_instance( cache, &mut renderer, state.logical_size(), - &mut debug, )); if should_exit { @@ -609,18 +596,17 @@ pub fn build_user_interface<'a, A: Application>( cache: user_interface::Cache, renderer: &mut A::Renderer, size: Size, - debug: &mut Debug, ) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> where A::Theme: StyleSheet, { - debug.view_started(); + let view_timer = debug::view_time(); let view = application.view(); - debug.view_finished(); + view_timer.finish(); - debug.layout_started(); + let layout_timer = debug::layout_time(); let user_interface = UserInterface::build(view, size, cache, renderer); - debug.layout_finished(); + layout_timer.finish(); user_interface } @@ -638,7 +624,6 @@ pub fn update( clipboard: &mut Clipboard, should_exit: &mut bool, proxy: &mut winit::event_loop::EventLoopProxy, - debug: &mut Debug, messages: &mut Vec, window: &winit::window::Window, ) where @@ -646,11 +631,11 @@ pub fn update( A::Theme: StyleSheet, { for message in messages.drain(..) { - debug.log_message(&message); + debug::log_message(&message); - debug.update_started(); + let update_timer = debug::update_time(); let command = runtime.enter(|| application.update(message)); - debug.update_finished(); + update_timer.finish(); run_command( application, @@ -664,7 +649,6 @@ pub fn update( clipboard, should_exit, proxy, - debug, window, ); } @@ -688,7 +672,6 @@ pub fn run_command( clipboard: &mut Clipboard, should_exit: &mut bool, proxy: &mut winit::event_loop::EventLoopProxy, - debug: &mut Debug, window: &winit::window::Window, ) where A: Application, @@ -855,7 +838,6 @@ pub fn run_command( surface, state.viewport(), state.background_color(), - &debug.overlay(), ); proxy @@ -895,7 +877,6 @@ pub fn run_command( current_cache, renderer, state.logical_size(), - debug, ); while let Some(mut operation) = current_operation.take() { diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index c17a3bcc..14c4507f 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -3,7 +3,6 @@ use crate::conversion; use crate::core::mouse; use crate::core::{Color, Size}; use crate::graphics::Viewport; -use crate::runtime::Debug; use crate::Application; use std::marker::PhantomData; @@ -122,12 +121,7 @@ where /// Processes the provided window event and updates the [`State`] /// accordingly. - pub fn update( - &mut self, - window: &Window, - event: &WindowEvent, - _debug: &mut Debug, - ) { + pub fn update(&mut self, window: &Window, event: &WindowEvent) { match event { WindowEvent::Resized(new_size) => { let size = Size::new(new_size.width, new_size.height); @@ -176,7 +170,7 @@ where .. }, .. - } => _debug.toggle(), + } => crate::debug::open_axe(), _ => {} } } diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 3b1b0d3a..5936ded3 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -29,6 +29,7 @@ pub use iced_graphics as graphics; pub use iced_runtime as runtime; pub use iced_runtime::core; +pub use iced_runtime::debug; pub use iced_runtime::futures; pub use iced_style as style; pub use winit; diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 03066d6c..10278c77 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -11,6 +11,7 @@ use crate::core::renderer; use crate::core::widget::operation; use crate::core::window; use crate::core::{Point, Size}; +use crate::debug; use crate::futures::futures::channel::mpsc; use crate::futures::futures::{task, Future, StreamExt}; use crate::futures::{Executor, Runtime, Subscription}; @@ -19,7 +20,6 @@ use crate::multi_window::window_manager::WindowManager; use crate::runtime::command::{self, Command}; use crate::runtime::multi_window::Program; use crate::runtime::user_interface::{self, UserInterface}; -use crate::runtime::Debug; use crate::style::application::StyleSheet; use crate::{Clipboard, Error, Proxy, Settings}; @@ -112,8 +112,7 @@ where { use winit::event_loop::EventLoopBuilder; - let mut debug = Debug::new(); - debug.startup_started(); + let boot_timer = debug::boot_time(); let event_loop = EventLoopBuilder::with_user_event() .build() @@ -202,12 +201,12 @@ where compositor, runtime, proxy, - debug, event_receiver, control_sender, init_command, window_manager, should_main_be_visible, + boot_timer, )); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -339,12 +338,12 @@ async fn run_instance( mut compositor: C, mut runtime: Runtime, A::Message>, mut proxy: winit::event_loop::EventLoopProxy, - mut debug: Debug, mut event_receiver: mpsc::UnboundedReceiver>, mut control_sender: mpsc::UnboundedSender, init_command: Command, mut window_manager: WindowManager, should_main_window_be_visible: bool, + boot_timer: debug::Timer, ) where A: Application + 'static, E: Executor + 'static, @@ -377,15 +376,6 @@ async fn run_instance( }; let mut ui_caches = HashMap::new(); - let mut user_interfaces = ManuallyDrop::new(build_user_interfaces( - &application, - &mut debug, - &mut window_manager, - HashMap::from_iter([( - window::Id::MAIN, - user_interface::Cache::default(), - )]), - )); run_command( &application, @@ -395,17 +385,24 @@ async fn run_instance( &mut clipboard, &mut control_sender, &mut proxy, - &mut debug, &mut window_manager, &mut ui_caches, ); runtime.track(application.subscription().into_recipes()); + boot_timer.finish(); + + let mut user_interfaces = ManuallyDrop::new(build_user_interfaces( + &application, + &mut window_manager, + HashMap::from_iter([( + window::Id::MAIN, + user_interface::Cache::default(), + )]), + )); let mut messages = Vec::new(); - debug.startup_finished(); - 'main: while let Some(event) = event_receiver.next().await { match event { Event::WindowCreated { @@ -430,7 +427,6 @@ async fn run_instance( user_interface::Cache::default(), &mut window.renderer, logical_size, - &mut debug, id, ), ); @@ -513,7 +509,7 @@ async fn run_instance( &mut messages, ); - debug.draw_started(); + let draw_timer = debug::draw_time(); let new_mouse_interaction = ui.draw( &mut window.renderer, window.state.theme(), @@ -522,7 +518,7 @@ async fn run_instance( }, cursor, ); - debug.draw_finished(); + draw_timer.finish(); if new_mouse_interaction != window.mouse_interaction { window.raw.set_cursor_icon( @@ -569,7 +565,7 @@ async fn run_instance( { let logical_size = window.state.logical_size(); - debug.layout_started(); + let layout_time = debug::layout_time(); let ui = user_interfaces .remove(&id) .expect("Remove user interface"); @@ -578,9 +574,9 @@ async fn run_instance( id, ui.relayout(logical_size, &mut window.renderer), ); - debug.layout_finished(); + layout_time.finish(); - debug.draw_started(); + let draw_time = debug::draw_time(); let new_mouse_interaction = user_interfaces .get_mut(&id) .expect("Get user interface") @@ -592,7 +588,7 @@ async fn run_instance( }, window.state.cursor(), ); - debug.draw_finished(); + draw_time.finish(); if new_mouse_interaction != window.mouse_interaction { @@ -616,16 +612,15 @@ async fn run_instance( window.state.viewport_version(); } - debug.render_started(); + let render_time = debug::render_time(); match compositor.present( &mut window.renderer, &mut window.surface, window.state.viewport(), window.state.background_color(), - &debug.overlay(), ) { Ok(()) => { - debug.render_finished(); + render_time.finish(); // TODO: Handle animations! // Maybe we can use `ControlFlow::WaitUntil` for this. @@ -636,8 +631,6 @@ async fn run_instance( panic!("{:?}", error); } _ => { - debug.render_finished(); - log::error!( "Error {error:?} when \ presenting surface." @@ -681,11 +674,7 @@ async fn run_instance( break 'main; } } else { - window.state.update( - &window.raw, - &window_event, - &mut debug, - ); + window.state.update(&window.raw, &window_event); if let Some(event) = conversion::window_event( id, @@ -702,7 +691,7 @@ async fn run_instance( continue; } - debug.event_processing_started(); + let interact_time = debug::interact_time(); let mut uis_stale = false; for (id, window) in window_manager.iter_mut() { @@ -749,8 +738,7 @@ async fn run_instance( runtime.broadcast(event, status); } } - - debug.event_processing_finished(); + interact_time.finish(); // TODO mw application update returns which window IDs to update if !messages.is_empty() || uis_stale { @@ -770,7 +758,6 @@ async fn run_instance( &mut clipboard, &mut control_sender, &mut proxy, - &mut debug, &mut messages, &mut window_manager, &mut cached_interfaces, @@ -794,7 +781,6 @@ async fn run_instance( user_interfaces = ManuallyDrop::new(build_user_interfaces( &application, - &mut debug, &mut window_manager, cached_interfaces, )); @@ -815,19 +801,18 @@ fn build_user_interface<'a, A: Application>( cache: user_interface::Cache, renderer: &mut A::Renderer, size: Size, - debug: &mut Debug, id: window::Id, ) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> where A::Theme: StyleSheet, { - debug.view_started(); + let view_timer = debug::view_time(); let view = application.view(id); - debug.view_finished(); + view_timer.finish(); - debug.layout_started(); + let layout_timer = debug::layout_time(); let user_interface = UserInterface::build(view, size, cache, renderer); - debug.layout_finished(); + layout_timer.finish(); user_interface } @@ -841,7 +826,6 @@ fn update( clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender, proxy: &mut winit::event_loop::EventLoopProxy, - debug: &mut Debug, messages: &mut Vec, window_manager: &mut WindowManager, ui_caches: &mut HashMap, @@ -850,11 +834,11 @@ fn update( A::Theme: StyleSheet, { for message in messages.drain(..) { - debug.log_message(&message); - debug.update_started(); + debug::log_message(&message); + let update_timer = debug::update_time(); let command = runtime.enter(|| application.update(message)); - debug.update_finished(); + update_timer.finish(); run_command( application, @@ -864,7 +848,6 @@ fn update( clipboard, control_sender, proxy, - debug, window_manager, ui_caches, ); @@ -883,7 +866,6 @@ fn run_command( clipboard: &mut Clipboard, control_sender: &mut mpsc::UnboundedSender, proxy: &mut winit::event_loop::EventLoopProxy, - debug: &mut Debug, window_manager: &mut WindowManager, ui_caches: &mut HashMap, ) where @@ -1118,7 +1100,6 @@ fn run_command( &mut window.surface, window.state.viewport(), window.state.background_color(), - &debug.overlay(), ); proxy @@ -1155,7 +1136,6 @@ fn run_command( let mut uis = build_user_interfaces( application, - debug, window_manager, std::mem::take(ui_caches), ); @@ -1211,7 +1191,6 @@ fn run_command( /// Build the user interface for every window. pub fn build_user_interfaces<'a, A: Application, C: Compositor>( application: &'a A, - debug: &mut Debug, window_manager: &mut WindowManager, mut cached_user_interfaces: HashMap, ) -> HashMap> @@ -1231,7 +1210,6 @@ where cache, &mut window.renderer, window.state.logical_size(), - debug, id, ), )) diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index 2e97a13d..aeada137 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -138,12 +138,7 @@ where } /// Processes the provided window event and updates the [`State`] accordingly. - pub fn update( - &mut self, - window: &Window, - event: &WindowEvent, - _debug: &mut crate::runtime::Debug, - ) { + pub fn update(&mut self, window: &Window, event: &WindowEvent) { match event { WindowEvent::Resized(new_size) => { let size = Size::new(new_size.width, new_size.height); From 7f7c5ea337a23beff2fea810a4fd5fd29ebcfd91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 27 Feb 2024 14:46:57 +0100 Subject: [PATCH 02/39] Remove `Report` type in `iced_sentinel` --- debug/src/lib.rs | 5 ++--- sentinel/src/client.rs | 6 +++--- sentinel/src/lib.rs | 13 ++++--------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 9edf0073..c02540ec 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -44,7 +44,6 @@ mod internal { use iced_sentinel::client::{self, Client}; use iced_sentinel::timing::{self, Timing}; - use iced_sentinel::Report; use once_cell::sync::Lazy; use std::sync::{Mutex, MutexGuard}; @@ -96,10 +95,10 @@ mod internal { impl Timer { pub fn finish(self) { - lock().sentinel.report(Report::Timing(Timing { + lock().sentinel.report_timing(Timing { stage: self.stage, duration: self.start.elapsed(), - })); + }); } } diff --git a/sentinel/src/client.rs b/sentinel/src/client.rs index 3e173ecf..ad1feb5d 100644 --- a/sentinel/src/client.rs +++ b/sentinel/src/client.rs @@ -1,4 +1,4 @@ -use crate::{Input, Report, SOCKET_ADDRESS}; +use crate::{Input, Timing, SOCKET_ADDRESS}; use tokio::io::{self, AsyncWriteExt}; use tokio::net; @@ -11,8 +11,8 @@ pub struct Client { } impl Client { - pub fn report(&mut self, report: Report) { - let _ = self.sender.try_send(Input::Reported(report)); + pub fn report_timing(&mut self, timing: Timing) { + let _ = self.sender.try_send(Input::TimingMeasured(timing)); } } diff --git a/sentinel/src/lib.rs b/sentinel/src/lib.rs index f18b9267..3d13c53d 100644 --- a/sentinel/src/lib.rs +++ b/sentinel/src/lib.rs @@ -17,19 +17,14 @@ pub const SOCKET_ADDRESS: &str = "127.0.0.1:9167"; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Input { Connected(Version), - Reported(Report), + TimingMeasured(Timing), } #[derive(Debug, Clone)] pub enum Event { Connected(Version), Disconnected, - Reported(Report), -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum Report { - Timing(Timing), + TimingMeasured(Timing), } pub fn run() -> impl Stream { @@ -89,8 +84,8 @@ async fn receive( Input::Connected(version) => { Event::Connected(version) } - Input::Reported(report) => { - Event::Reported(report) + Input::TimingMeasured(timing) => { + Event::TimingMeasured(timing) } }, )) From c856d2b5130f1e1f3109e7bf54ac1894ac37837b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 27 Feb 2024 15:19:26 +0100 Subject: [PATCH 03/39] Add `ThemeChanged` variant to `Event` in `iced_sentinel` --- core/Cargo.toml | 4 ++++ core/src/color.rs | 1 + core/src/lib.rs | 3 ++- debug/Cargo.toml | 3 ++- debug/src/lib.rs | 16 ++++++++++++++++ sentinel/Cargo.toml | 4 ++++ sentinel/src/client.rs | 5 +++++ sentinel/src/lib.rs | 7 +++++++ style/Cargo.toml | 7 +++++++ style/src/application.rs | 14 +++++++++++++- style/src/lib.rs | 3 ++- style/src/theme/palette.rs | 1 + winit/src/application/state.rs | 5 +++++ 13 files changed, 69 insertions(+), 4 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index 2360e822..6e1f5ffb 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -23,6 +23,10 @@ xxhash-rust.workspace = true palette.workspace = true palette.optional = true +serde.workspace = true +serde.optional = true +serde.features = ["derive"] + [target.'cfg(windows)'.dependencies] raw-window-handle.workspace = true diff --git a/core/src/color.rs b/core/src/color.rs index b8db322f..a9a0aa55 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -3,6 +3,7 @@ use palette::rgb::{Srgb, Srgba}; /// A color in the `sRGB` color space. #[derive(Debug, Clone, Copy, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Color { /// Red component, 0.0 - 1.0 pub r: f32, diff --git a/core/src/lib.rs b/core/src/lib.rs index 002336ee..e46e8726 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -9,11 +9,12 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![forbid(unsafe_code, rust_2018_idioms)] +#![forbid(unsafe_code)] #![deny( missing_debug_implementations, missing_docs, unused_results, + rust_2018_idioms, rustdoc::broken_intra_doc_links )] pub mod alignment; diff --git a/debug/Cargo.toml b/debug/Cargo.toml index 77cabf6a..5f63fb90 100644 --- a/debug/Cargo.toml +++ b/debug/Cargo.toml @@ -11,10 +11,11 @@ categories.workspace = true keywords.workspace = true [features] -enable = ["iced_sentinel", "once_cell"] +enable = ["dep:iced_sentinel", "dep:once_cell"] [dependencies] iced_core.workspace = true +iced_style.workspace = true iced_sentinel.workspace = true iced_sentinel.optional = true diff --git a/debug/src/lib.rs b/debug/src/lib.rs index c02540ec..7bab52e9 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -1,4 +1,7 @@ pub use iced_core as core; +pub use iced_style as style; + +use crate::style::theme; pub use internal::Timer; @@ -6,6 +9,10 @@ pub fn open_axe() {} pub fn log_message(_message: &impl std::fmt::Debug) {} +pub fn theme_changed(palette: theme::Palette) { + internal::theme_changed(palette); +} + pub fn boot_time() -> Timer { internal::boot_time() } @@ -41,6 +48,7 @@ pub fn time(name: impl AsRef) -> Timer { #[cfg(feature = "enable")] mod internal { use crate::core::time::Instant; + use crate::style::theme; use iced_sentinel::client::{self, Client}; use iced_sentinel::timing::{self, Timing}; @@ -48,6 +56,10 @@ mod internal { use once_cell::sync::Lazy; use std::sync::{Mutex, MutexGuard}; + pub fn theme_changed(palette: theme::Palette) { + lock().sentinel.report_theme_change(palette); + } + pub fn boot_time() -> Timer { timer(timing::Stage::Boot) } @@ -120,6 +132,10 @@ mod internal { #[cfg(not(feature = "enable"))] mod internal { + use crate::style::theme; + + pub fn theme_changed(_palette: theme::Palette) {} + pub fn boot_time() -> Timer { Timer } diff --git a/sentinel/Cargo.toml b/sentinel/Cargo.toml index efb36659..c34c5048 100644 --- a/sentinel/Cargo.toml +++ b/sentinel/Cargo.toml @@ -12,6 +12,10 @@ keywords.workspace = true [dependencies] iced_core.workspace = true + +iced_style.workspace = true +iced_style.features = ["serde"] + serde_json.workspace = true futures.workspace = true log.workspace = true diff --git a/sentinel/src/client.rs b/sentinel/src/client.rs index ad1feb5d..5792dc85 100644 --- a/sentinel/src/client.rs +++ b/sentinel/src/client.rs @@ -1,3 +1,4 @@ +use crate::style::theme; use crate::{Input, Timing, SOCKET_ADDRESS}; use tokio::io::{self, AsyncWriteExt}; @@ -11,6 +12,10 @@ pub struct Client { } impl Client { + pub fn report_theme_change(&mut self, palette: theme::Palette) { + let _ = self.sender.try_send(Input::ThemeChanged(palette)); + } + pub fn report_timing(&mut self, timing: Timing) { let _ = self.sender.try_send(Input::TimingMeasured(timing)); } diff --git a/sentinel/src/lib.rs b/sentinel/src/lib.rs index 3d13c53d..c645ea49 100644 --- a/sentinel/src/lib.rs +++ b/sentinel/src/lib.rs @@ -1,8 +1,10 @@ pub use iced_core as core; +pub use iced_style as style; pub mod client; pub mod timing; +use crate::style::theme; use crate::timing::Timing; use futures::future; @@ -18,6 +20,7 @@ pub const SOCKET_ADDRESS: &str = "127.0.0.1:9167"; pub enum Input { Connected(Version), TimingMeasured(Timing), + ThemeChanged(theme::Palette), } #[derive(Debug, Clone)] @@ -25,6 +28,7 @@ pub enum Event { Connected(Version), Disconnected, TimingMeasured(Timing), + ThemeChanged(theme::Palette), } pub fn run() -> impl Stream { @@ -87,6 +91,9 @@ async fn receive( Input::TimingMeasured(timing) => { Event::TimingMeasured(timing) } + Input::ThemeChanged(palette) => { + Event::ThemeChanged(palette) + } }, )) } diff --git a/style/Cargo.toml b/style/Cargo.toml index 3f00e787..9f28c670 100644 --- a/style/Cargo.toml +++ b/style/Cargo.toml @@ -10,9 +10,16 @@ homepage.workspace = true categories.workspace = true keywords.workspace = true +[features] +serde = ["dep:serde", "iced_core/serde"] + [dependencies] iced_core.workspace = true iced_core.features = ["palette"] palette.workspace = true once_cell.workspace = true + +serde.workspace = true +serde.optional = true +serde.features = ["derive"] diff --git a/style/src/application.rs b/style/src/application.rs index e9a1f4ff..db9a673a 100644 --- a/style/src/application.rs +++ b/style/src/application.rs @@ -1,5 +1,6 @@ //! Change the appearance of an application. -use iced_core::Color; +use crate::core::Color; +use crate::theme; /// A set of rules that dictate the style of an application. pub trait StyleSheet { @@ -10,6 +11,17 @@ pub trait StyleSheet { /// /// [`Style`]: Self::Style fn appearance(&self, style: &Self::Style) -> Appearance; + + /// Returns the [`theme::Palette`] of the application, if any. + /// + /// This may be used by other parts of the `iced` runtime to + /// try to match the style of your application. + /// + /// For instance, the Iced Axe uses this [`theme::Palette`] to + /// automatically style itself using your application's colors. + fn palette(&self) -> Option { + None + } } /// The appearance of an application. diff --git a/style/src/lib.rs b/style/src/lib.rs index 3c2865eb..67fcad52 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -7,11 +7,12 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] -#![forbid(unsafe_code, rust_2018_idioms)] +#![forbid(unsafe_code)] #![deny( unused_results, missing_docs, unused_results, + rust_2018_idioms, rustdoc::broken_intra_doc_links )] pub use iced_core as core; diff --git a/style/src/theme/palette.rs b/style/src/theme/palette.rs index 15a964cd..74a8253a 100644 --- a/style/src/theme/palette.rs +++ b/style/src/theme/palette.rs @@ -8,6 +8,7 @@ use palette::{FromColor, Hsl, Mix}; /// A color palette. #[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Palette { /// The background [`Color`] of the [`Palette`]. pub background: Color, diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index 14c4507f..b70d8dc2 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -2,6 +2,7 @@ use crate::application::{self, StyleSheet as _}; use crate::conversion; use crate::core::mouse; use crate::core::{Color, Size}; +use crate::debug; use crate::graphics::Viewport; use crate::Application; @@ -37,6 +38,8 @@ where let theme = application.theme(); let appearance = theme.appearance(&application.style()); + let _ = theme.palette().map(debug::theme_changed); + let viewport = { let physical_size = window.inner_size(); @@ -211,5 +214,7 @@ where // Update theme and appearance self.theme = application.theme(); self.appearance = self.theme.appearance(&application.style()); + + let _ = self.theme.palette().map(debug::theme_changed); } } From 26d49be1b2c09aff99dd7ac467d8ebd93db7088e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 27 Feb 2024 15:38:47 +0100 Subject: [PATCH 04/39] Notify only `Palette` changes in `iced_debug` --- debug/src/lib.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 7bab52e9..a71eedb9 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -57,7 +57,13 @@ mod internal { use std::sync::{Mutex, MutexGuard}; pub fn theme_changed(palette: theme::Palette) { - lock().sentinel.report_theme_change(palette); + let mut debug = lock(); + + if debug.last_palette.as_ref() != Some(&palette) { + debug.sentinel.report_theme_change(palette); + + debug.last_palette = Some(palette); + } } pub fn boot_time() -> Timer { @@ -117,12 +123,14 @@ mod internal { #[derive(Debug)] struct Debug { sentinel: Client, + last_palette: Option, } fn lock() -> MutexGuard<'static, Debug> { static DEBUG: Lazy> = Lazy::new(|| { Mutex::new(Debug { sentinel: client::connect(), + last_palette: None, }) }); From 4f8ed7d6ee09c75fb5e9b3cd7bcf68086e602bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 27 Feb 2024 15:41:20 +0100 Subject: [PATCH 05/39] Implement `palette` method for `Theme` --- style/src/theme.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/style/src/theme.rs b/style/src/theme.rs index 0b56e101..6844039d 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -275,6 +275,10 @@ impl application::StyleSheet for Theme { Application::Custom(custom) => custom.appearance(self), } } + + fn palette(&self) -> Option { + Some(self.palette()) + } } impl application::Appearance> application::StyleSheet for T { From 0870b158d7a5b3ad70fc3a530e2e3d8f06eac774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 27 Feb 2024 16:16:52 +0100 Subject: [PATCH 06/39] Use `bincode` instead of `serde_json` in `iced_sentinel` --- Cargo.toml | 2 +- sentinel/Cargo.toml | 2 +- sentinel/src/client.rs | 13 ++++------- sentinel/src/lib.rs | 53 +++++++++++++++++++++--------------------- 4 files changed, 33 insertions(+), 37 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aef0d9f5..336133b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -128,6 +128,7 @@ iced_widget = { version = "0.13.0-dev", path = "widget" } iced_winit = { version = "0.13.0-dev", path = "winit" } async-std = "1.0" +bincode = "1.3" bitflags = "2.0" bytemuck = { version = "1.0", features = ["derive"] } cosmic-text = "0.10" @@ -151,7 +152,6 @@ raw-window-handle = "0.6" resvg = "0.36" rustc-hash = "1.0" serde = "1.0" -serde_json = "1.0" semver = "1.0" smol = "1.0" smol_str = "0.2" diff --git a/sentinel/Cargo.toml b/sentinel/Cargo.toml index c34c5048..0b22fbcd 100644 --- a/sentinel/Cargo.toml +++ b/sentinel/Cargo.toml @@ -16,7 +16,7 @@ iced_core.workspace = true iced_style.workspace = true iced_style.features = ["serde"] -serde_json.workspace = true +bincode.workspace = true futures.workspace = true log.workspace = true diff --git a/sentinel/src/client.rs b/sentinel/src/client.rs index 5792dc85..ec370088 100644 --- a/sentinel/src/client.rs +++ b/sentinel/src/client.rs @@ -69,16 +69,11 @@ async fn send( stream: &mut io::BufStream, input: Input, ) -> Result<(), io::Error> { - stream - .write_all( - format!( - "{}\n", - serde_json::to_string(&input).expect("Serialize input message") - ) - .as_bytes(), - ) - .await?; + let bytes = bincode::serialize(&input).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(()) diff --git a/sentinel/src/lib.rs b/sentinel/src/lib.rs index c645ea49..e1446951 100644 --- a/sentinel/src/lib.rs +++ b/sentinel/src/lib.rs @@ -11,7 +11,7 @@ use futures::future; use futures::stream::{self, Stream, StreamExt}; use semver::Version; use serde::{Deserialize, Serialize}; -use tokio::io::{self, AsyncBufReadExt, BufStream}; +use tokio::io::{self, AsyncReadExt, BufStream}; use tokio::net; pub const SOCKET_ADDRESS: &str = "127.0.0.1:9167"; @@ -74,33 +74,34 @@ async fn connect() -> Result { async fn receive( mut stream: BufStream, ) -> Result<(BufStream, Event), io::Error> { - let mut input = String::new(); + let mut bytes = Vec::new(); loop { - match stream.read_line(&mut input).await? { - 0 => return Ok((stream, Event::Disconnected)), - n => { - match serde_json::from_str(&input[..n]) { - Ok(input) => { - return Ok(( - stream, - match dbg!(input) { - Input::Connected(version) => { - Event::Connected(version) - } - Input::TimingMeasured(timing) => { - Event::TimingMeasured(timing) - } - Input::ThemeChanged(palette) => { - Event::ThemeChanged(palette) - } - }, - )) - } - Err(_) => { - // TODO: Log decoding error - } - } + let size = stream.read_u64().await? as usize; + + if bytes.len() < size { + bytes.resize(size, 0); + } + + let _n = stream.read_exact(&mut bytes[..size]).await?; + + match bincode::deserialize(&bytes) { + Ok(input) => { + return Ok(( + stream, + match dbg!(input) { + Input::Connected(version) => Event::Connected(version), + Input::TimingMeasured(timing) => { + Event::TimingMeasured(timing) + } + Input::ThemeChanged(palette) => { + Event::ThemeChanged(palette) + } + }, + )); + } + Err(_) => { + // TODO: Log decoding error } } } From 2b5b586e30e936ceb351222dcf72f880fc000d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 27 Feb 2024 16:21:22 +0100 Subject: [PATCH 07/39] Export `semver::Version` in `iced_sentinel` --- sentinel/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentinel/src/lib.rs b/sentinel/src/lib.rs index e1446951..012d54ee 100644 --- a/sentinel/src/lib.rs +++ b/sentinel/src/lib.rs @@ -1,5 +1,6 @@ pub use iced_core as core; pub use iced_style as style; +pub use semver::Version; pub mod client; pub mod timing; @@ -9,7 +10,6 @@ use crate::timing::Timing; use futures::future; use futures::stream::{self, Stream, StreamExt}; -use semver::Version; use serde::{Deserialize, Serialize}; use tokio::io::{self, AsyncReadExt, BufStream}; use tokio::net; From 8591e5a14895e8a602f863734add6922ed41fc38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 28 Feb 2024 15:25:45 +0100 Subject: [PATCH 08/39] Introduce `window::Id` to `timing::Stage` in `iced_sentinel` --- core/src/window/id.rs | 3 +- debug/src/lib.rs | 63 ++++++++++++++++--------------- runtime/src/multi_window.rs | 37 ++++++++++++++++-- runtime/src/multi_window/state.rs | 4 +- runtime/src/program/state.rs | 11 +++--- sentinel/src/timing.rs | 13 ++++--- winit/src/application.rs | 12 +++--- winit/src/multi_window.rs | 16 ++++---- 8 files changed, 97 insertions(+), 62 deletions(-) diff --git a/core/src/window/id.rs b/core/src/window/id.rs index 20474c8f..80ab8e98 100644 --- a/core/src/window/id.rs +++ b/core/src/window/id.rs @@ -2,10 +2,11 @@ use std::hash::Hash; use std::sync::atomic::{self, AtomicU64}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] /// The id of the window. /// /// Internally Iced reserves `window::Id::MAIN` for the first window spawned. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Id(u64); static COUNT: AtomicU64 = AtomicU64::new(1); diff --git a/debug/src/lib.rs b/debug/src/lib.rs index a71eedb9..8d7aeba8 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -1,6 +1,7 @@ pub use iced_core as core; pub use iced_style as style; +use crate::core::window; use crate::style::theme; pub use internal::Timer; @@ -21,33 +22,34 @@ pub fn update_time() -> Timer { internal::update_time() } -pub fn view_time() -> Timer { - internal::view_time() +pub fn view_time(window: window::Id) -> Timer { + internal::view_time(window) } -pub fn layout_time() -> Timer { - internal::layout_time() +pub fn layout_time(window: window::Id) -> Timer { + internal::layout_time(window) } -pub fn interact_time() -> Timer { - internal::interact_time() +pub fn interact_time(window: window::Id) -> Timer { + internal::interact_time(window) } -pub fn draw_time() -> Timer { - internal::draw_time() +pub fn draw_time(window: window::Id) -> Timer { + internal::draw_time(window) } -pub fn render_time() -> Timer { - internal::render_time() +pub fn render_time(window: window::Id) -> Timer { + internal::render_time(window) } -pub fn time(name: impl AsRef) -> Timer { - internal::time(name) +pub fn time(window: window::Id, name: impl AsRef) -> Timer { + internal::time(window, name) } #[cfg(feature = "enable")] mod internal { use crate::core::time::Instant; + use crate::core::window; use crate::style::theme; use iced_sentinel::client::{self, Client}; @@ -74,28 +76,28 @@ mod internal { timer(timing::Stage::Update) } - pub fn view_time() -> Timer { - timer(timing::Stage::View) + pub fn view_time(window: window::Id) -> Timer { + timer(timing::Stage::View(window)) } - pub fn layout_time() -> Timer { - timer(timing::Stage::Layout) + pub fn layout_time(window: window::Id) -> Timer { + timer(timing::Stage::Layout(window)) } - pub fn interact_time() -> Timer { - timer(timing::Stage::Interact) + pub fn interact_time(window: window::Id) -> Timer { + timer(timing::Stage::Interact(window)) } - pub fn draw_time() -> Timer { - timer(timing::Stage::Draw) + pub fn draw_time(window: window::Id) -> Timer { + timer(timing::Stage::Draw(window)) } - pub fn render_time() -> Timer { - timer(timing::Stage::Render) + pub fn render_time(window: window::Id) -> Timer { + timer(timing::Stage::Render(window)) } - pub fn time(name: impl AsRef) -> Timer { - timer(timing::Stage::Custom(name.as_ref().to_owned())) + pub fn time(window: window::Id, name: impl AsRef) -> Timer { + timer(timing::Stage::Custom(window, name.as_ref().to_owned())) } fn timer(stage: timing::Stage) -> Timer { @@ -140,6 +142,7 @@ mod internal { #[cfg(not(feature = "enable"))] mod internal { + use crate::core::window; use crate::style::theme; pub fn theme_changed(_palette: theme::Palette) {} @@ -152,27 +155,27 @@ mod internal { Timer } - pub fn view_time() -> Timer { + pub fn view_time(_window: window::Id) -> Timer { Timer } - pub fn layout_time() -> Timer { + pub fn layout_time(_window: window::Id) -> Timer { Timer } - pub fn interact_time() -> Timer { + pub fn interact_time(_window: window::Id) -> Timer { Timer } - pub fn draw_time() -> Timer { + pub fn draw_time(_window: window::Id) -> Timer { Timer } - pub fn render_time() -> Timer { + pub fn render_time(_window: window::Id) -> Timer { Timer } - pub fn time(_name: impl AsRef) -> Timer { + pub fn time(_window: window::Id, _name: impl AsRef) -> Timer { Timer } diff --git a/runtime/src/multi_window.rs b/runtime/src/multi_window.rs index cf778a20..34a2c9f4 100644 --- a/runtime/src/multi_window.rs +++ b/runtime/src/multi_window.rs @@ -1,6 +1,35 @@ //! A multi-window application. -pub mod program; -pub mod state; +use crate::core::text; +use crate::core::window; +use crate::core::{Element, Renderer}; +use crate::Command; -pub use program::Program; -pub use state::State; +/// The core of a user interface for a multi-window application following The Elm Architecture. +pub trait Program: Sized { + /// The graphics backend to use to draw the [`Program`]. + type Renderer: Renderer + text::Renderer; + + /// The type of __messages__ your [`Program`] will produce. + type Message: std::fmt::Debug + Send; + + /// The theme used to draw the [`Program`]. + type Theme; + + /// Handles a __message__ and updates the state of the [`Program`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Command`] returned will be executed immediately in the + /// background by shells. + fn update(&mut self, message: Self::Message) -> Command; + + /// Returns the widgets to display in the [`Program`] for the `window`. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; +} diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs index 215c87e1..2150de76 100644 --- a/runtime/src/multi_window/state.rs +++ b/runtime/src/multi_window/state.rs @@ -98,12 +98,12 @@ where bounds, ); - let interact_timer = debug::interact_time(); let mut messages = Vec::new(); let uncaptured_events = user_interfaces.iter_mut().fold( vec![], |mut uncaptured_events, ui| { + let interact_timer = debug::interact_time(); let (_, event_statuses) = ui.update( &self.queued_events, cursor, @@ -111,6 +111,7 @@ where clipboard, &mut messages, ); + interact_timer.finish(); uncaptured_events.extend( self.queued_events @@ -128,7 +129,6 @@ where self.queued_events.clear(); messages.append(&mut self.queued_messages); - drop(interact_timer); let commands = if messages.is_empty() { let draw_timer = debug::draw_time(); diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index 0c9051d2..cec3186b 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -2,6 +2,7 @@ use crate::core::event::{self, Event}; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::operation::{self, Operation}; +use crate::core::window; use crate::core::{Clipboard, Size}; use crate::debug; use crate::user_interface::{self, UserInterface}; @@ -101,7 +102,7 @@ where bounds, ); - let interact_timer = debug::interact_time(); + let interact_timer = debug::interact_time(window::Id::MAIN); let mut messages = Vec::new(); let (_, event_statuses) = user_interface.update( @@ -127,7 +128,7 @@ where drop(interact_timer); let command = if messages.is_empty() { - let draw_timer = debug::draw_time(); + let draw_timer = debug::draw_time(window::Id::MAIN); self.mouse_interaction = user_interface.draw(renderer, theme, style, cursor); drop(draw_timer); @@ -158,7 +159,7 @@ where bounds, ); - let draw_timer = debug::draw_time(); + let draw_timer = debug::draw_time(window::Id::MAIN); self.mouse_interaction = user_interface.draw(renderer, theme, style, cursor); drop(draw_timer); @@ -213,11 +214,11 @@ fn build_user_interface<'a, P: Program>( renderer: &mut P::Renderer, size: Size, ) -> UserInterface<'a, P::Message, P::Theme, P::Renderer> { - let view_timer = debug::view_time(); + let view_timer = debug::view_time(window::Id::MAIN); let view = program.view(); drop(view_timer); - let layout_timer = debug::layout_time(); + let layout_timer = debug::layout_time(window::Id::MAIN); let user_interface = UserInterface::build(view, size, cache, renderer); drop(layout_timer); diff --git a/sentinel/src/timing.rs b/sentinel/src/timing.rs index b4a588f2..64ef561e 100644 --- a/sentinel/src/timing.rs +++ b/sentinel/src/timing.rs @@ -1,4 +1,5 @@ use crate::core::time::Duration; +use crate::core::window; use serde::{Deserialize, Serialize}; @@ -16,10 +17,10 @@ pub struct Timing { pub enum Stage { Boot, Update, - View, - Layout, - Interact, - Draw, - Render, - Custom(String), + View(window::Id), + Layout(window::Id), + Interact(window::Id), + Draw(window::Id), + Render(window::Id), + Custom(window::Id, String), } diff --git a/winit/src/application.rs b/winit/src/application.rs index 6a056d88..752d9c8d 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -379,7 +379,7 @@ async fn run_instance( if viewport_version != current_viewport_version { let logical_size = state.logical_size(); - let layout_timer = debug::layout_time(); + let layout_timer = debug::layout_time(window::Id::MAIN); user_interface = ManuallyDrop::new( ManuallyDrop::into_inner(user_interface) .relayout(logical_size, &mut renderer), @@ -431,7 +431,7 @@ async fn run_instance( runtime.broadcast(redraw_event, core::event::Status::Ignored); - let draw_timer = debug::draw_time(); + let draw_timer = debug::draw_time(window::Id::MAIN); let new_mouse_interaction = user_interface.draw( &mut renderer, state.theme(), @@ -451,7 +451,7 @@ async fn run_instance( mouse_interaction = new_mouse_interaction; } - let render_timer = debug::render_time(); + let render_timer = debug::render_time(window::Id::MAIN); match compositor.present( &mut renderer, &mut surface, @@ -499,7 +499,7 @@ async fn run_instance( continue; } - let interact_timer = debug::interact_time(); + let interact_timer = debug::interact_time(window::Id::MAIN); let (interface_state, statuses) = user_interface.update( &events, state.cursor(), @@ -600,11 +600,11 @@ pub fn build_user_interface<'a, A: Application>( where A::Theme: StyleSheet, { - let view_timer = debug::view_time(); + let view_timer = debug::view_time(window::Id::MAIN); let view = application.view(); view_timer.finish(); - let layout_timer = debug::layout_time(); + let layout_timer = debug::layout_time(window::Id::MAIN); let user_interface = UserInterface::build(view, size, cache, renderer); layout_timer.finish(); diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 10278c77..7f361e0e 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -509,7 +509,7 @@ async fn run_instance( &mut messages, ); - let draw_timer = debug::draw_time(); + let draw_timer = debug::draw_time(id); let new_mouse_interaction = ui.draw( &mut window.renderer, window.state.theme(), @@ -565,7 +565,7 @@ async fn run_instance( { let logical_size = window.state.logical_size(); - let layout_time = debug::layout_time(); + let layout_time = debug::layout_time(id); let ui = user_interfaces .remove(&id) .expect("Remove user interface"); @@ -576,7 +576,7 @@ async fn run_instance( ); layout_time.finish(); - let draw_time = debug::draw_time(); + let draw_time = debug::draw_time(id); let new_mouse_interaction = user_interfaces .get_mut(&id) .expect("Get user interface") @@ -612,7 +612,7 @@ async fn run_instance( window.state.viewport_version(); } - let render_time = debug::render_time(); + let render_time = debug::render_time(id); match compositor.present( &mut window.renderer, &mut window.surface, @@ -691,10 +691,10 @@ async fn run_instance( continue; } - let interact_time = debug::interact_time(); let mut uis_stale = false; for (id, window) in window_manager.iter_mut() { + let interact_time = debug::interact_time(id); let mut window_events = vec![]; events.retain(|(window_id, event)| { @@ -737,8 +737,8 @@ async fn run_instance( { runtime.broadcast(event, status); } + interact_time.finish(); } - interact_time.finish(); // TODO mw application update returns which window IDs to update if !messages.is_empty() || uis_stale { @@ -806,11 +806,11 @@ fn build_user_interface<'a, A: Application>( where A::Theme: StyleSheet, { - let view_timer = debug::view_time(); + let view_timer = debug::view_time(id); let view = application.view(id); view_timer.finish(); - let layout_timer = debug::layout_time(); + let layout_timer = debug::layout_time(id); let user_interface = UserInterface::build(view, size, cache, renderer); layout_timer.finish(); From 3d90665f9d01759c53bb04ba05661ab9c5b8391f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 28 Feb 2024 16:06:55 +0100 Subject: [PATCH 09/39] Introduce `SystemTime` to `Event` in `iced_sentinel` --- core/src/time.rs | 1 + debug/src/lib.rs | 5 ++- sentinel/src/client.rs | 15 ++++++-- sentinel/src/lib.rs | 78 +++++++++++++++++++++++++++++------------- sentinel/src/timing.rs | 3 +- 5 files changed, 74 insertions(+), 28 deletions(-) diff --git a/core/src/time.rs b/core/src/time.rs index dcfe4e41..a57075b7 100644 --- a/core/src/time.rs +++ b/core/src/time.rs @@ -2,3 +2,4 @@ pub use web_time::Duration; pub use web_time::Instant; +pub use web_time::SystemTime; diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 8d7aeba8..d04a7ac2 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -48,7 +48,7 @@ pub fn time(window: window::Id, name: impl AsRef) -> Timer { #[cfg(feature = "enable")] mod internal { - use crate::core::time::Instant; + use crate::core::time::{Instant, SystemTime}; use crate::core::window; use crate::style::theme; @@ -104,6 +104,7 @@ mod internal { Timer { stage, start: Instant::now(), + start_system_time: SystemTime::now(), } } @@ -111,12 +112,14 @@ mod internal { pub struct Timer { stage: timing::Stage, start: Instant, + start_system_time: SystemTime, } impl Timer { pub fn finish(self) { lock().sentinel.report_timing(Timing { stage: self.stage, + start: self.start_system_time, duration: self.start.elapsed(), }); } diff --git a/sentinel/src/client.rs b/sentinel/src/client.rs index ec370088..8f7bfd65 100644 --- a/sentinel/src/client.rs +++ b/sentinel/src/client.rs @@ -1,3 +1,4 @@ +use crate::core::time::SystemTime; use crate::style::theme; use crate::{Input, Timing, SOCKET_ADDRESS}; @@ -13,7 +14,10 @@ pub struct Client { impl Client { pub fn report_theme_change(&mut self, palette: theme::Palette) { - let _ = self.sender.try_send(Input::ThemeChanged(palette)); + let _ = self.sender.try_send(Input::ThemeChanged { + at: SystemTime::now(), + palette, + }); } pub fn report_timing(&mut self, timing: Timing) { @@ -38,7 +42,14 @@ async fn run(mut receiver: mpsc::Receiver) { loop { match _connect().await { Ok(mut stream) => { - let _ = send(&mut stream, Input::Connected(version)).await; + let _ = send( + &mut stream, + Input::Connected { + at: SystemTime::now(), + version, + }, + ) + .await; while let Some(input) = receiver.recv().await { if send(&mut stream, input).await.is_err() { diff --git a/sentinel/src/lib.rs b/sentinel/src/lib.rs index 012d54ee..24fdc3c0 100644 --- a/sentinel/src/lib.rs +++ b/sentinel/src/lib.rs @@ -5,6 +5,7 @@ pub use semver::Version; pub mod client; pub mod timing; +use crate::core::time::SystemTime; use crate::style::theme; use crate::timing::Timing; @@ -18,17 +19,42 @@ pub const SOCKET_ADDRESS: &str = "127.0.0.1:9167"; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Input { - Connected(Version), + Connected { + at: SystemTime, + version: Version, + }, + ThemeChanged { + at: SystemTime, + palette: theme::Palette, + }, TimingMeasured(Timing), - ThemeChanged(theme::Palette), } #[derive(Debug, Clone)] pub enum Event { - Connected(Version), - Disconnected, + Connected { + at: SystemTime, + version: Version, + }, + Disconnected { + at: SystemTime, + }, + ThemeChanged { + at: SystemTime, + palette: theme::Palette, + }, TimingMeasured(Timing), - ThemeChanged(theme::Palette), +} + +impl Event { + pub fn at(&self) -> SystemTime { + match self { + Self::Connected { at, .. } + | Self::Disconnected { at } + | Self::ThemeChanged { at, .. } => *at, + Self::TimingMeasured(timing) => timing.start, + } + } } pub fn run() -> impl Stream { @@ -48,12 +74,27 @@ pub fn run() -> impl Stream { Err(_error) => Some((None, State::Disconnected)), }, State::Connected(stream) => match receive(stream).await { - Ok((_, Event::Disconnected)) | Err(_) => { - Some((Some(Event::Disconnected), State::Disconnected)) - } - Ok((stream, message)) => { - Some((Some(message), State::Connected(stream))) + Ok((stream, input)) => { + let event = match dbg!(input) { + Input::Connected { at, version } => { + Event::Connected { at, version } + } + Input::TimingMeasured(timing) => { + Event::TimingMeasured(timing) + } + Input::ThemeChanged { at, palette } => { + Event::ThemeChanged { at, palette } + } + }; + + Some((Some(event), State::Connected(stream))) } + Err(_) => Some(( + Some(Event::Disconnected { + at: SystemTime::now(), + }), + State::Disconnected, + )), }, } }) @@ -73,7 +114,7 @@ async fn connect() -> Result { async fn receive( mut stream: BufStream, -) -> Result<(BufStream, Event), io::Error> { +) -> Result<(BufStream, Input), io::Error> { let mut bytes = Vec::new(); loop { @@ -87,21 +128,10 @@ async fn receive( match bincode::deserialize(&bytes) { Ok(input) => { - return Ok(( - stream, - match dbg!(input) { - Input::Connected(version) => Event::Connected(version), - Input::TimingMeasured(timing) => { - Event::TimingMeasured(timing) - } - Input::ThemeChanged(palette) => { - Event::ThemeChanged(palette) - } - }, - )); + return Ok((stream, input)); } Err(_) => { - // TODO: Log decoding error + log::warn!("Error decoding sentinel message"); } } } diff --git a/sentinel/src/timing.rs b/sentinel/src/timing.rs index 64ef561e..e10ce678 100644 --- a/sentinel/src/timing.rs +++ b/sentinel/src/timing.rs @@ -1,4 +1,4 @@ -use crate::core::time::Duration; +use crate::core::time::{Duration, SystemTime}; use crate::core::window; use serde::{Deserialize, Serialize}; @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; )] pub struct Timing { pub stage: Stage, + pub start: SystemTime, pub duration: Duration, } From 54fae3c7283632e292a2ad99b314e5879bf02176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 28 Feb 2024 17:15:59 +0100 Subject: [PATCH 10/39] Re-export `SystemTime` in `time` module --- src/time.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/time.rs b/src/time.rs index e255d751..38aa54ab 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,5 +1,5 @@ //! Listen and react to time. -pub use iced_core::time::{Duration, Instant}; +pub use iced_core::time::{Duration, Instant, SystemTime}; #[allow(unused_imports)] #[cfg_attr( From 88a688e6abd596457c1f5d31ce4e75568f0a60e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 28 Feb 2024 17:16:14 +0100 Subject: [PATCH 11/39] Add `pane_grid` function helper to `widget` module --- widget/src/helpers.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index ed385ea5..f5cceff9 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -8,6 +8,7 @@ use crate::core::widget::operation; use crate::core::{Element, Length, Pixels}; use crate::keyed; use crate::overlay; +use crate::pane_grid::{self, PaneGrid}; use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; use crate::radio::{self, Radio}; @@ -429,7 +430,7 @@ where Command::widget(operation::focusable::focus_next()) } -/// A container intercepting mouse events. +/// Creates a new [`MouseArea`]. pub fn mouse_area<'a, Message, Theme, Renderer>( widget: impl Into>, ) -> MouseArea<'a, Message, Theme, Renderer> @@ -439,7 +440,7 @@ where MouseArea::new(widget) } -/// A widget that applies any `Theme` to its contents. +/// Creates a new [`Themer`]. pub fn themer<'a, Message, Theme, Renderer>( theme: Theme, content: impl Into>, @@ -450,3 +451,19 @@ where { Themer::new(theme, content) } + +/// Creates a new [`PaneGrid`]. +pub fn pane_grid<'a, T, Message, Theme, Renderer>( + state: &'a pane_grid::State, + view: impl Fn( + pane_grid::Pane, + &'a T, + bool, + ) -> pane_grid::Content<'a, Message, Theme, Renderer>, +) -> PaneGrid<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, + Theme: pane_grid::StyleSheet + container::StyleSheet, +{ + PaneGrid::new(state, view) +} From df62075040ff18859d9821c706a285818276c1ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 28 Feb 2024 17:16:37 +0100 Subject: [PATCH 12/39] Reconnect `Client` after disconnect in `iced_sentinel` --- sentinel/src/client.rs | 12 +++++++----- sentinel/src/lib.rs | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/sentinel/src/client.rs b/sentinel/src/client.rs index 8f7bfd65..3a7b38c8 100644 --- a/sentinel/src/client.rs +++ b/sentinel/src/client.rs @@ -46,18 +46,20 @@ async fn run(mut receiver: mpsc::Receiver) { &mut stream, Input::Connected { at: SystemTime::now(), - version, + version: version.clone(), }, ) .await; while let Some(input) = receiver.recv().await { - if send(&mut stream, input).await.is_err() { - break; + match send(&mut stream, input).await { + Ok(()) => {} + Err(error) => { + log::warn!("Error sending message to sentinel server: {error}"); + break; + } } } - - break; } Err(_) => { time::sleep(time::Duration::from_secs(2)).await; diff --git a/sentinel/src/lib.rs b/sentinel/src/lib.rs index 24fdc3c0..49b2f1b9 100644 --- a/sentinel/src/lib.rs +++ b/sentinel/src/lib.rs @@ -75,7 +75,7 @@ pub fn run() -> impl Stream { }, State::Connected(stream) => match receive(stream).await { Ok((stream, input)) => { - let event = match dbg!(input) { + let event = match input { Input::Connected { at, version } => { Event::Connected { at, version } } From 30e66056506f6f5557fa6f29a83f64e5faac8937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 28 Feb 2024 19:37:26 +0100 Subject: [PATCH 13/39] Run `UserInterface::update` only when events are present --- winit/src/application.rs | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/winit/src/application.rs b/winit/src/application.rs index 752d9c8d..b19a5517 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -499,21 +499,29 @@ async fn run_instance( continue; } - let interact_timer = debug::interact_time(window::Id::MAIN); - let (interface_state, statuses) = user_interface.update( - &events, - state.cursor(), - &mut renderer, - &mut clipboard, - &mut messages, - ); - interact_timer.finish(); + let interface_state = if events.is_empty() { + user_interface::State::Updated { + redraw_request: None, + } + } else { + let interact_timer = debug::interact_time(window::Id::MAIN); + let (interface_state, statuses) = user_interface.update( + &events, + state.cursor(), + &mut renderer, + &mut clipboard, + &mut messages, + ); - for (event, status) in - events.drain(..).zip(statuses.into_iter()) - { - runtime.broadcast(event, status); - } + for (event, status) in + events.drain(..).zip(statuses.into_iter()) + { + runtime.broadcast(event, status); + } + interact_timer.finish(); + + interface_state + }; if !messages.is_empty() || matches!( From 1fda2d151d97d80806bce85cf77bbd2b5c9f61a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 28 Feb 2024 19:38:08 +0100 Subject: [PATCH 14/39] Implement `Display` for `Timing` in `iced_sentinel` --- sentinel/src/timing.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/sentinel/src/timing.rs b/sentinel/src/timing.rs index e10ce678..ffbc7e46 100644 --- a/sentinel/src/timing.rs +++ b/sentinel/src/timing.rs @@ -2,6 +2,7 @@ use crate::core::time::{Duration, SystemTime}; use crate::core::window; use serde::{Deserialize, Serialize}; +use std::fmt; #[derive( Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, @@ -25,3 +26,18 @@ pub enum Stage { Render(window::Id), Custom(window::Id, String), } + +impl fmt::Display for Stage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Boot => write!(f, "Boot"), + Self::Update => write!(f, "Update"), + Self::View(_) => write!(f, "View"), + Self::Layout(_) => write!(f, "Layout"), + Self::Interact(_) => write!(f, "Interact"), + Self::Draw(_) => write!(f, "Draw"), + Self::Render(_) => write!(f, "Render"), + Self::Custom(_, name) => f.write_str(name), + } + } +} From 609738252022f89a8aae68ee8a34503cb19578df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 1 Mar 2024 22:04:46 +0100 Subject: [PATCH 15/39] Introduce `skip_next_timing` in `iced_debug` This is useful to avoid infinite recursion when implementing a `sentinel` client that can inspect itself. --- debug/src/lib.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/debug/src/lib.rs b/debug/src/lib.rs index d04a7ac2..cabe4440 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -46,6 +46,10 @@ pub fn time(window: window::Id, name: impl AsRef) -> Timer { internal::time(window, name) } +pub fn skip_next_timing() { + internal::skip_next_timing(); +} + #[cfg(feature = "enable")] mod internal { use crate::core::time::{Instant, SystemTime}; @@ -100,6 +104,10 @@ mod internal { timer(timing::Stage::Custom(window, name.as_ref().to_owned())) } + pub fn skip_next_timing() { + lock().skip_next_timing = true; + } + fn timer(stage: timing::Stage) -> Timer { Timer { stage, @@ -117,7 +125,14 @@ mod internal { impl Timer { pub fn finish(self) { - lock().sentinel.report_timing(Timing { + let mut debug = lock(); + + if debug.skip_next_timing { + debug.skip_next_timing = false; + return; + } + + debug.sentinel.report_timing(Timing { stage: self.stage, start: self.start_system_time, duration: self.start.elapsed(), @@ -129,6 +144,7 @@ mod internal { struct Debug { sentinel: Client, last_palette: Option, + skip_next_timing: bool, } fn lock() -> MutexGuard<'static, Debug> { @@ -136,6 +152,7 @@ mod internal { Mutex::new(Debug { sentinel: client::connect(), last_palette: None, + skip_next_timing: false, }) }); @@ -182,6 +199,8 @@ mod internal { Timer } + pub fn skip_next_timing() {} + #[derive(Debug)] pub struct Timer; From d7413a00135fe7dee5684f88329af091d6b824b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 1 Mar 2024 22:05:34 +0100 Subject: [PATCH 16/39] Export `iced_debug` in `iced::advanced` module --- src/advanced.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/advanced.rs b/src/advanced.rs index 8e026f84..52fb3f2b 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -10,6 +10,7 @@ pub use crate::core::text::{self, Text}; pub use crate::core::widget::{self, Widget}; pub use crate::core::{Hasher, Shell}; pub use crate::renderer::graphics; +pub use iced_debug as debug; pub mod subscription { //! Write your own subscriptions. From 57033dc4d04bb4ce4ab2fba5056b12422755301a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 10 May 2024 20:08:09 +0200 Subject: [PATCH 17/39] Rename `iced_sentinel` to `iced_beacon` and refactor its API --- Cargo.toml | 4 +- {sentinel => beacon}/Cargo.toml | 3 +- beacon/src/client.rs | 123 ++++++++++++++++++++ beacon/src/lib.rs | 184 +++++++++++++++++++++++++++++ beacon/src/span.rs | 61 ++++++++++ beacon/src/stream.rs | 15 +++ debug/Cargo.toml | 6 +- debug/src/lib.rs | 200 ++++++++++++++++---------------- docs/logo-no-shadow.svg | 2 + runtime/src/program/state.rs | 24 ++-- sentinel/src/client.rs | 93 --------------- sentinel/src/lib.rs | 137 ---------------------- sentinel/src/timing.rs | 43 ------- src/application.rs | 7 ++ src/program.rs | 36 +++++- wgpu/src/lib.rs | 8 +- winit/src/application.rs | 40 ++++--- winit/src/application/state.rs | 8 +- winit/src/multi_window.rs | 40 +++---- 19 files changed, 596 insertions(+), 438 deletions(-) rename {sentinel => beacon}/Cargo.toml (93%) create mode 100644 beacon/src/client.rs create mode 100644 beacon/src/lib.rs create mode 100644 beacon/src/span.rs create mode 100644 beacon/src/stream.rs create mode 100644 docs/logo-no-shadow.svg delete mode 100644 sentinel/src/client.rs delete mode 100644 sentinel/src/lib.rs delete mode 100644 sentinel/src/timing.rs diff --git a/Cargo.toml b/Cargo.toml index 2ea64300..3874b403 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,7 +106,7 @@ members = [ "highlighter", "renderer", "runtime", - "sentinel", + "beacon", "tiny_skia", "wgpu", "widget", @@ -126,6 +126,7 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] [workspace.dependencies] iced = { version = "0.13.0-dev", path = "." } +iced_beacon = { version = "0.13.0-dev", path = "beacon" } iced_core = { version = "0.13.0-dev", path = "core" } iced_debug = { version = "0.13.0-dev", path = "debug" } iced_futures = { version = "0.13.0-dev", path = "futures" } @@ -133,7 +134,6 @@ iced_graphics = { version = "0.13.0-dev", path = "graphics" } iced_highlighter = { version = "0.13.0-dev", path = "highlighter" } iced_renderer = { version = "0.13.0-dev", path = "renderer" } iced_runtime = { version = "0.13.0-dev", path = "runtime" } -iced_sentinel = { version = "0.13.0-dev", path = "sentinel" } iced_tiny_skia = { version = "0.13.0-dev", path = "tiny_skia" } iced_wgpu = { version = "0.13.0-dev", path = "wgpu" } iced_widget = { version = "0.13.0-dev", path = "widget" } diff --git a/sentinel/Cargo.toml b/beacon/Cargo.toml similarity index 93% rename from sentinel/Cargo.toml rename to beacon/Cargo.toml index d8ec8e64..f141fabe 100644 --- a/sentinel/Cargo.toml +++ b/beacon/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "iced_sentinel" +name = "iced_beacon" description = "A client/server protocol to monitor and supervise iced applications" version.workspace = true edition.workspace = true @@ -17,6 +17,7 @@ iced_core.features = ["serde"] bincode.workspace = true futures.workspace = true log.workspace = true +thiserror.workspace = true tokio.workspace = true tokio.features = ["rt", "rt-multi-thread", "net", "sync", "time", "io-util", "macros"] diff --git a/beacon/src/client.rs b/beacon/src/client.rs new file mode 100644 index 00000000..28cb2eeb --- /dev/null +++ b/beacon/src/client.rs @@ -0,0 +1,123 @@ +use crate::core::time::{Duration, SystemTime}; +use crate::span; +use crate::theme; + +use semver::Version; +use serde::{Deserialize, Serialize}; +use tokio::io::{self, AsyncWriteExt}; +use tokio::net; +use tokio::sync::mpsc; +use tokio::time; + +use std::sync::Arc; +use std::thread; + +pub const SERVER_ADDRESS: &str = "127.0.0.1:9167"; + +#[derive(Debug, Clone)] +pub struct Client { + sender: mpsc::Sender, + _handle: Arc>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Message { + Connected { + at: SystemTime, + name: String, + version: Version, + }, + EventLogged { + at: SystemTime, + event: Event, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Event { + ThemeChanged(theme::Palette), + SpanStarted(span::Stage), + SpanFinished(span::Stage, Duration), +} + +impl Client { + pub fn log(&self, event: Event) { + let _ = self.sender.try_send(Message::EventLogged { + at: SystemTime::now(), + event, + }); + } +} + +#[must_use] +pub fn connect(name: String) -> Client { + let (sender, receiver) = mpsc::channel(100); + + let handle = std::thread::spawn(move || run(name, receiver)); + + Client { + sender, + _handle: Arc::new(handle), + } +} + +#[tokio::main] +async fn run(name: String, mut receiver: mpsc::Receiver) { + let version = semver::Version::parse(env!("CARGO_PKG_VERSION")) + .expect("Parse package version"); + + loop { + match _connect().await { + Ok(mut stream) => { + let _ = send( + &mut stream, + Message::Connected { + at: SystemTime::now(), + name: name.clone(), + version: version.clone(), + }, + ) + .await; + + while let Some(output) = receiver.recv().await { + match send(&mut stream, output).await { + Ok(()) => {} + Err(error) => { + log::warn!( + "Error sending message to server: {error}" + ); + break; + } + } + } + } + Err(_) => { + time::sleep(time::Duration::from_secs(2)).await; + } + } + } +} + +async fn _connect() -> Result { + log::debug!("Attempting to connect to server..."); + let stream = net::TcpStream::connect(SERVER_ADDRESS).await?; + + stream.set_nodelay(true)?; + stream.writable().await?; + + Ok(stream) +} + +async fn send( + stream: &mut net::TcpStream, + message: Message, +) -> Result<(), io::Error> { + let bytes = bincode::serialize(&message).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(()) +} diff --git a/beacon/src/lib.rs b/beacon/src/lib.rs new file mode 100644 index 00000000..3149d8b5 --- /dev/null +++ b/beacon/src/lib.rs @@ -0,0 +1,184 @@ +pub use iced_core as core; +pub use semver::Version; + +pub mod client; +pub mod span; + +mod stream; + +pub use client::Client; +pub use span::Span; + +use crate::core::theme; +use crate::core::time::{Duration, SystemTime}; + +use futures::{SinkExt, Stream}; +use tokio::io::{self, AsyncReadExt}; +use tokio::net; + +#[derive(Debug, Clone)] +pub enum Event { + Connected { + at: SystemTime, + name: String, + version: Version, + }, + Disconnected { + at: SystemTime, + }, + ThemeChanged { + at: SystemTime, + palette: theme::Palette, + }, + SpanFinished { + at: SystemTime, + duration: Duration, + span: Span, + }, +} + +impl Event { + pub fn at(&self) -> SystemTime { + match self { + Self::Connected { at, .. } + | Self::Disconnected { at, .. } + | Self::ThemeChanged { at, .. } + | Self::SpanFinished { at, .. } => *at, + } + } +} + +pub fn run() -> impl Stream { + stream::channel(|mut output| async move { + let mut buffer = Vec::new(); + + loop { + let Ok(mut stream) = connect().await else { + delay().await; + continue; + }; + + loop { + match receive(&mut stream, &mut buffer).await { + Ok(message) => { + match message { + client::Message::Connected { + at, + name, + version, + } => { + let _ = output + .send(Event::Connected { + at, + name, + version, + }) + .await; + } + client::Message::EventLogged { at, event } => { + match event { + client::Event::ThemeChanged(palette) => { + let _ = output + .send(Event::ThemeChanged { + at, + palette, + }) + .await; + } + client::Event::SpanStarted(_) => {} + client::Event::SpanFinished( + stage, + duration, + ) => { + let span = match stage { + span::Stage::Boot => Span::Boot, + span::Stage::Update => Span::Update, + span::Stage::View(window) => { + Span::View { window } + } + span::Stage::Layout(window) => { + Span::Layout { window } + } + span::Stage::Interact(window) => { + Span::Interact { window } + } + span::Stage::Draw(window) => { + Span::Draw { window } + } + span::Stage::Present(window) => { + Span::Present { window } + } + span::Stage::Custom( + window, + name, + ) => Span::Custom { window, name }, + }; + + let _ = output + .send(Event::SpanFinished { + at, + duration, + span, + }) + .await; + } + } + } + }; + } + Err(Error::IOFailed(_)) => { + let _ = output + .send(Event::Disconnected { + at: SystemTime::now(), + }) + .await; + + delay().await; + break; + } + Err(Error::DecodingFailed(error)) => { + log::warn!("Error decoding beacon output: {error}") + } + } + } + } + }) +} + +async fn connect() -> Result { + let listener = net::TcpListener::bind(client::SERVER_ADDRESS).await?; + + let (stream, _) = listener.accept().await?; + + stream.set_nodelay(true)?; + stream.readable().await?; + + Ok(stream) +} + +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)?) +} + +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 new file mode 100644 index 00000000..7d673663 --- /dev/null +++ b/beacon/src/span.rs @@ -0,0 +1,61 @@ +use crate::core::window; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Span { + Boot, + Update, + View { window: window::Id }, + Layout { window: window::Id }, + Interact { window: window::Id }, + Draw { window: window::Id }, + Present { window: window::Id }, + Custom { window: window::Id, name: String }, +} + +impl Span { + pub fn stage(&self) -> Stage { + match self { + Span::Boot => Stage::Boot, + Span::Update => Stage::Update, + Span::View { window } => Stage::View(*window), + Span::Layout { window } => Stage::Layout(*window), + Span::Interact { window } => Stage::Interact(*window), + Span::Draw { window } => Stage::Draw(*window), + Span::Present { window } => Stage::Present(*window), + Span::Custom { window, name } => { + Stage::Custom(*window, name.clone()) + } + } + } +} + +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +pub enum Stage { + Boot, + Update, + View(window::Id), + Layout(window::Id), + Interact(window::Id), + Draw(window::Id), + Present(window::Id), + Custom(window::Id, String), +} + +impl std::fmt::Display for Stage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Stage::Boot => "Boot", + Stage::Update => "Update", + Stage::View(_) => "View", + Stage::Layout(_) => "Layout", + Stage::Interact(_) => "Interact", + Stage::Draw(_) => "Draw", + Stage::Present(_) => "Present", + Stage::Custom(_, name) => name, + }) + } +} diff --git a/beacon/src/stream.rs b/beacon/src/stream.rs new file mode 100644 index 00000000..855576e7 --- /dev/null +++ b/beacon/src/stream.rs @@ -0,0 +1,15 @@ +use futures::channel::mpsc; +use futures::stream::{self, Stream, StreamExt}; +use futures::Future; + +pub fn channel(f: impl Fn(mpsc::Sender) -> F) -> impl Stream +where + F: Future, +{ + let (sender, receiver) = mpsc::channel(1); + + stream::select( + receiver, + stream::once(f(sender)).filter_map(|_| async { None }), + ) +} diff --git a/debug/Cargo.toml b/debug/Cargo.toml index 4e3e0a61..99ee1ea1 100644 --- a/debug/Cargo.toml +++ b/debug/Cargo.toml @@ -11,13 +11,13 @@ categories.workspace = true keywords.workspace = true [features] -enable = ["dep:iced_sentinel", "dep:once_cell"] +enable = ["dep:iced_beacon", "dep:once_cell"] [dependencies] iced_core.workspace = true -iced_sentinel.workspace = true -iced_sentinel.optional = true +iced_beacon.workspace = true +iced_beacon.optional = true once_cell.workspace = true once_cell.optional = true diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 4fc9a9a1..779cbb2c 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -3,45 +3,49 @@ pub use iced_core as core; use crate::core::theme; use crate::core::window; -pub use internal::Timer; +pub use internal::Span; -pub fn open_axe() {} +pub fn init(name: &str) { + internal::init(name); +} + +pub fn open_comet() {} pub fn log_message(_message: &impl std::fmt::Debug) {} -pub fn theme_changed(palette: theme::Palette) { - internal::theme_changed(palette); +pub fn theme_changed(f: impl FnOnce() -> Option) { + internal::theme_changed(f); } -pub fn boot_time() -> Timer { - internal::boot_time() +pub fn boot() -> Span { + internal::boot() } -pub fn update_time() -> Timer { - internal::update_time() +pub fn update() -> Span { + internal::update() } -pub fn view_time(window: window::Id) -> Timer { - internal::view_time(window) +pub fn view(window: window::Id) -> Span { + internal::view(window) } -pub fn layout_time(window: window::Id) -> Timer { - internal::layout_time(window) +pub fn layout(window: window::Id) -> Span { + internal::layout(window) } -pub fn interact_time(window: window::Id) -> Timer { - internal::interact_time(window) +pub fn interact(window: window::Id) -> Span { + internal::interact(window) } -pub fn draw_time(window: window::Id) -> Timer { - internal::draw_time(window) +pub fn draw(window: window::Id) -> Span { + internal::draw(window) } -pub fn render_time(window: window::Id) -> Timer { - internal::render_time(window) +pub fn present(window: window::Id) -> Span { + internal::present(window) } -pub fn time(window: window::Id, name: impl AsRef) -> Timer { +pub fn time(window: window::Id, name: impl AsRef) -> Span { internal::time(window, name) } @@ -52,158 +56,156 @@ pub fn skip_next_timing() { #[cfg(feature = "enable")] mod internal { use crate::core::theme; - use crate::core::time::{Instant, SystemTime}; + use crate::core::time::Instant; use crate::core::window; - use iced_sentinel::client::{self, Client}; - use iced_sentinel::timing::{self, Timing}; + use iced_beacon as beacon; + + use beacon::client::{self, Client}; + use beacon::span; use once_cell::sync::Lazy; - use std::sync::{Mutex, MutexGuard}; + use std::sync::atomic::{self, AtomicBool}; + use std::sync::RwLock; - pub fn theme_changed(palette: theme::Palette) { - let mut debug = lock(); + pub fn init(name: &str) { + name.clone_into(&mut NAME.write().expect("Write application name")); + } - if debug.last_palette.as_ref() != Some(&palette) { - debug.sentinel.report_theme_change(palette); + pub fn theme_changed(f: impl FnOnce() -> Option) { + let Some(palette) = f() else { + return; + }; - debug.last_palette = Some(palette); + if LAST_PALETTE.read().expect("Read last palette").as_ref() + != Some(&palette) + { + BEACON.log(client::Event::ThemeChanged(palette)); + + *LAST_PALETTE.write().expect("Write last palette") = Some(palette); } } - pub fn boot_time() -> Timer { - timer(timing::Stage::Boot) + pub fn boot() -> Span { + span(span::Stage::Boot) } - pub fn update_time() -> Timer { - timer(timing::Stage::Update) + pub fn update() -> Span { + span(span::Stage::Update) } - pub fn view_time(window: window::Id) -> Timer { - timer(timing::Stage::View(window)) + pub fn view(window: window::Id) -> Span { + span(span::Stage::View(window)) } - pub fn layout_time(window: window::Id) -> Timer { - timer(timing::Stage::Layout(window)) + pub fn layout(window: window::Id) -> Span { + span(span::Stage::Layout(window)) } - pub fn interact_time(window: window::Id) -> Timer { - timer(timing::Stage::Interact(window)) + pub fn interact(window: window::Id) -> Span { + span(span::Stage::Interact(window)) } - pub fn draw_time(window: window::Id) -> Timer { - timer(timing::Stage::Draw(window)) + pub fn draw(window: window::Id) -> Span { + span(span::Stage::Draw(window)) } - pub fn render_time(window: window::Id) -> Timer { - timer(timing::Stage::Render(window)) + pub fn present(window: window::Id) -> Span { + span(span::Stage::Present(window)) } - pub fn time(window: window::Id, name: impl AsRef) -> Timer { - timer(timing::Stage::Custom(window, name.as_ref().to_owned())) + pub fn time(window: window::Id, name: impl AsRef) -> Span { + span(span::Stage::Custom(window, name.as_ref().to_owned())) } pub fn skip_next_timing() { - lock().skip_next_timing = true; + SKIP_NEXT_SPAN.store(true, atomic::Ordering::Relaxed); } - fn timer(stage: timing::Stage) -> Timer { - Timer { - stage, + fn span(span: span::Stage) -> Span { + BEACON.log(client::Event::SpanStarted(span.clone())); + + Span { + span, start: Instant::now(), - start_system_time: SystemTime::now(), } } #[derive(Debug)] - pub struct Timer { - stage: timing::Stage, + pub struct Span { + span: span::Stage, start: Instant, - start_system_time: SystemTime, } - impl Timer { + impl Span { pub fn finish(self) { - let mut debug = lock(); - - if debug.skip_next_timing { - debug.skip_next_timing = false; + if SKIP_NEXT_SPAN.fetch_and(false, atomic::Ordering::Relaxed) { return; } - debug.sentinel.report_timing(Timing { - stage: self.stage, - start: self.start_system_time, - duration: self.start.elapsed(), - }); + BEACON.log(client::Event::SpanFinished( + self.span, + self.start.elapsed(), + )); } } - #[derive(Debug)] - struct Debug { - sentinel: Client, - last_palette: Option, - skip_next_timing: bool, - } + static BEACON: Lazy = Lazy::new(|| { + client::connect(NAME.read().expect("Read application name").to_owned()) + }); - fn lock() -> MutexGuard<'static, Debug> { - static DEBUG: Lazy> = Lazy::new(|| { - Mutex::new(Debug { - sentinel: client::connect(), - last_palette: None, - skip_next_timing: false, - }) - }); - - DEBUG.lock().expect("Acquire debug lock") - } + static NAME: RwLock = RwLock::new(String::new()); + static LAST_PALETTE: RwLock> = RwLock::new(None); + static SKIP_NEXT_SPAN: AtomicBool = AtomicBool::new(false); } #[cfg(not(feature = "enable"))] mod internal { + use crate::core::theme; use crate::core::window; - use crate::style::theme; - pub fn theme_changed(_palette: theme::Palette) {} + pub fn init(_name: &str) {} - pub fn boot_time() -> Timer { - Timer + pub fn theme_changed(_f: impl FnOnce() -> Option) {} + + pub fn boot() -> Span { + Span } - pub fn update_time() -> Timer { - Timer + pub fn update() -> Span { + Span } - pub fn view_time(_window: window::Id) -> Timer { - Timer + pub fn view(_window: window::Id) -> Span { + Span } - pub fn layout_time(_window: window::Id) -> Timer { - Timer + pub fn layout(_window: window::Id) -> Span { + Span } - pub fn interact_time(_window: window::Id) -> Timer { - Timer + pub fn interact(_window: window::Id) -> Span { + Span } - pub fn draw_time(_window: window::Id) -> Timer { - Timer + pub fn draw(_window: window::Id) -> Span { + Span } - pub fn render_time(_window: window::Id) -> Timer { - Timer + pub fn present(_window: window::Id) -> Span { + Span } - pub fn time(_window: window::Id, _name: impl AsRef) -> Timer { - Timer + pub fn time(_window: window::Id, _name: impl AsRef) -> Span { + Span } pub fn skip_next_timing() {} #[derive(Debug)] - pub struct Timer; + pub struct Span; - impl Timer { + impl Span { pub fn finish(self) {} } } diff --git a/docs/logo-no-shadow.svg b/docs/logo-no-shadow.svg new file mode 100644 index 00000000..459b7fbb --- /dev/null +++ b/docs/logo-no-shadow.svg @@ -0,0 +1,2 @@ + + diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index 182169be..129f2449 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -102,7 +102,7 @@ where bounds, ); - let interact_timer = debug::interact_time(window::Id::MAIN); + let interact_span = debug::interact(window::Id::MAIN); let mut messages = Vec::new(); let (_, event_statuses) = user_interface.update( @@ -125,13 +125,13 @@ where self.queued_events.clear(); messages.append(&mut self.queued_messages); - drop(interact_timer); + interact_span.finish(); let command = if messages.is_empty() { - let draw_timer = debug::draw_time(window::Id::MAIN); + let draw_span = debug::draw(window::Id::MAIN); self.mouse_interaction = user_interface.draw(renderer, theme, style, cursor); - drop(draw_timer); + draw_span.finish(); self.cache = Some(user_interface.into_cache()); @@ -145,9 +145,9 @@ where Command::batch(messages.into_iter().map(|message| { debug::log_message(&message); - let update_timer = debug::update_time(); + let update_span = debug::update(); let command = self.program.update(message); - drop(update_timer); + update_span.finish(); command })); @@ -159,10 +159,10 @@ where bounds, ); - let draw_timer = debug::draw_time(window::Id::MAIN); + let draw_spawn = debug::draw(window::Id::MAIN); self.mouse_interaction = user_interface.draw(renderer, theme, style, cursor); - drop(draw_timer); + draw_spawn.finish(); self.cache = Some(user_interface.into_cache()); @@ -214,13 +214,13 @@ fn build_user_interface<'a, P: Program>( renderer: &mut P::Renderer, size: Size, ) -> UserInterface<'a, P::Message, P::Theme, P::Renderer> { - let view_timer = debug::view_time(window::Id::MAIN); + let view_span = debug::view(window::Id::MAIN); let view = program.view(); - drop(view_timer); + view_span.finish(); - let layout_timer = debug::layout_time(window::Id::MAIN); + let layout_span = debug::layout(window::Id::MAIN); let user_interface = UserInterface::build(view, size, cache, renderer); - drop(layout_timer); + layout_span.finish(); user_interface } diff --git a/sentinel/src/client.rs b/sentinel/src/client.rs deleted file mode 100644 index 79e3dca4..00000000 --- a/sentinel/src/client.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::core::time::SystemTime; -use crate::theme; -use crate::{Input, Timing, SOCKET_ADDRESS}; - -use tokio::io::{self, AsyncWriteExt}; -use tokio::net; -use tokio::sync::mpsc; -use tokio::time; - -#[derive(Debug, Clone)] -pub struct Client { - sender: mpsc::Sender, -} - -impl Client { - pub fn report_theme_change(&mut self, palette: theme::Palette) { - let _ = self.sender.try_send(Input::ThemeChanged { - at: SystemTime::now(), - palette, - }); - } - - pub fn report_timing(&mut self, timing: Timing) { - let _ = self.sender.try_send(Input::TimingMeasured(timing)); - } -} - -#[must_use] -pub fn connect() -> Client { - let (sender, receiver) = mpsc::channel(1_000); - - std::thread::spawn(move || run(receiver)); - - Client { sender } -} - -#[tokio::main] -async fn run(mut receiver: mpsc::Receiver) { - let version = semver::Version::parse(env!("CARGO_PKG_VERSION")) - .expect("Parse package version"); - - loop { - match _connect().await { - Ok(mut stream) => { - let _ = send( - &mut stream, - Input::Connected { - at: SystemTime::now(), - version: version.clone(), - }, - ) - .await; - - while let Some(input) = receiver.recv().await { - match send(&mut stream, input).await { - Ok(()) => {} - Err(error) => { - log::warn!("Error sending message to sentinel server: {error}"); - break; - } - } - } - } - Err(_) => { - time::sleep(time::Duration::from_secs(2)).await; - } - } - } -} - -async fn _connect() -> Result, io::Error> { - log::debug!("Attempting to connect sentinel to server..."); - let stream = net::TcpStream::connect(SOCKET_ADDRESS).await?; - - stream.set_nodelay(true)?; - stream.writable().await?; - - Ok(io::BufStream::new(stream)) -} - -async fn send( - stream: &mut io::BufStream, - input: Input, -) -> Result<(), io::Error> { - let bytes = bincode::serialize(&input).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(()) -} diff --git a/sentinel/src/lib.rs b/sentinel/src/lib.rs deleted file mode 100644 index e7377861..00000000 --- a/sentinel/src/lib.rs +++ /dev/null @@ -1,137 +0,0 @@ -pub use iced_core as core; -pub use semver::Version; - -pub mod client; -pub mod timing; - -use crate::core::theme; -use crate::core::time::SystemTime; -use crate::timing::Timing; - -use futures::future; -use futures::stream::{self, Stream, StreamExt}; -use serde::{Deserialize, Serialize}; -use tokio::io::{self, AsyncReadExt, BufStream}; -use tokio::net; - -pub const SOCKET_ADDRESS: &str = "127.0.0.1:9167"; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum Input { - Connected { - at: SystemTime, - version: Version, - }, - ThemeChanged { - at: SystemTime, - palette: theme::Palette, - }, - TimingMeasured(Timing), -} - -#[derive(Debug, Clone)] -pub enum Event { - Connected { - at: SystemTime, - version: Version, - }, - Disconnected { - at: SystemTime, - }, - ThemeChanged { - at: SystemTime, - palette: theme::Palette, - }, - TimingMeasured(Timing), -} - -impl Event { - pub fn at(&self) -> SystemTime { - match self { - Self::Connected { at, .. } - | Self::Disconnected { at } - | Self::ThemeChanged { at, .. } => *at, - Self::TimingMeasured(timing) => timing.start, - } - } -} - -pub fn run() -> impl Stream { - enum State { - Disconnected, - Connected(BufStream), - } - - stream::unfold(State::Disconnected, |state| async { - match state { - State::Disconnected => match connect().await { - Ok(stream) => { - let stream = BufStream::new(stream); - - Some((None, State::Connected(stream))) - } - Err(_error) => Some((None, State::Disconnected)), - }, - State::Connected(stream) => match receive(stream).await { - Ok((stream, input)) => { - let event = match input { - Input::Connected { at, version } => { - Event::Connected { at, version } - } - Input::TimingMeasured(timing) => { - Event::TimingMeasured(timing) - } - Input::ThemeChanged { at, palette } => { - Event::ThemeChanged { at, palette } - } - }; - - Some((Some(event), State::Connected(stream))) - } - Err(_) => Some(( - Some(Event::Disconnected { - at: SystemTime::now(), - }), - State::Disconnected, - )), - }, - } - }) - .filter_map(future::ready) -} - -async fn connect() -> Result { - let listener = net::TcpListener::bind(SOCKET_ADDRESS).await?; - - let (stream, _) = listener.accept().await?; - - stream.set_nodelay(true)?; - stream.readable().await?; - - Ok(stream) -} - -async fn receive( - mut stream: BufStream, -) -> Result<(BufStream, Input), io::Error> { - let mut bytes = Vec::new(); - - loop { - let size = stream.read_u64().await? as usize; - - if bytes.len() < size { - bytes.resize(size, 0); - } - - let _n = stream.read_exact(&mut bytes[..size]).await?; - - match bincode::deserialize(&bytes) { - Ok(input) => { - return Ok((stream, input)); - } - Err(_) => { - log::warn!("Error decoding sentinel message"); - } - } - } -} diff --git a/sentinel/src/timing.rs b/sentinel/src/timing.rs deleted file mode 100644 index ffbc7e46..00000000 --- a/sentinel/src/timing.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::core::time::{Duration, SystemTime}; -use crate::core::window; - -use serde::{Deserialize, Serialize}; -use std::fmt; - -#[derive( - Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, -)] -pub struct Timing { - pub stage: Stage, - pub start: SystemTime, - pub duration: Duration, -} - -#[derive( - Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, -)] -pub enum Stage { - Boot, - Update, - View(window::Id), - Layout(window::Id), - Interact(window::Id), - Draw(window::Id), - Render(window::Id), - Custom(window::Id, String), -} - -impl fmt::Display for Stage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Boot => write!(f, "Boot"), - Self::Update => write!(f, "Update"), - Self::View(_) => write!(f, "View"), - Self::Layout(_) => write!(f, "Layout"), - Self::Interact(_) => write!(f, "Interact"), - Self::Draw(_) => write!(f, "Draw"), - Self::Render(_) => write!(f, "Render"), - Self::Custom(_, name) => f.write_str(name), - } - } -} diff --git a/src/application.rs b/src/application.rs index d12ba73d..7bdef972 100644 --- a/src/application.rs +++ b/src/application.rs @@ -118,6 +118,9 @@ where /// The data needed to initialize your [`Application`]. type Flags; + /// Returns the unique name of the [`Application`]. + fn name() -> &'static str; + /// Initializes the [`Application`] with the flags provided to /// [`run`] as part of the [`Settings`]. /// @@ -250,6 +253,10 @@ where { type Flags = A::Flags; + fn name() -> &'static str { + A::name() + } + fn new(flags: Self::Flags) -> (Self, Command) { let (app, command) = A::new(flags); diff --git a/src/program.rs b/src/program.rs index d4c2a266..70d3bd51 100644 --- a/src/program.rs +++ b/src/program.rs @@ -106,6 +106,12 @@ where type Renderer = Renderer; type Executor = executor::Default; + fn name() -> &'static str { + let type_name = std::any::type_name::(); + + type_name.split("::").next().unwrap_or(type_name) + } + fn load(&self) -> Command { Command::none() } @@ -211,6 +217,10 @@ impl Program

{ ) } + fn name() -> &'static str { + P::name() + } + fn title(&self) -> String { self.program.title(&self.state) } @@ -431,6 +441,8 @@ pub trait Definition: Sized { /// The executor of the program. type Executor: Executor; + fn name() -> &'static str; + fn load(&self) -> Command; fn update( @@ -484,12 +496,16 @@ fn with_title( type Renderer = P::Renderer; type Executor = P::Executor; + fn title(&self, state: &Self::State) -> String { + self.title.title(state) + } + fn load(&self) -> Command { self.program.load() } - fn title(&self, state: &Self::State) -> String { - self.title.title(state) + fn name() -> &'static str { + P::name() } fn update( @@ -553,6 +569,10 @@ fn with_load( Command::batch([self.program.load(), (self.load)()]) } + fn name() -> &'static str { + P::name() + } + fn update( &self, state: &mut Self::State, @@ -621,6 +641,10 @@ fn with_subscription( (self.subscription)(state) } + fn name() -> &'static str { + P::name() + } + fn load(&self) -> Command { self.program.load() } @@ -686,6 +710,10 @@ fn with_theme( (self.theme)(state) } + fn name() -> &'static str { + P::name() + } + fn load(&self) -> Command { self.program.load() } @@ -755,6 +783,10 @@ fn with_style( (self.style)(state, theme) } + fn name() -> &'static str { + P::name() + } + fn load(&self) -> Command { self.program.load() } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 095e1f1b..9d6c09f5 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -61,7 +61,7 @@ pub use settings::Settings; pub use geometry::Geometry; use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, + Background, Color, Font, Pixels, Point, Rectangle, Transformation, }; use crate::graphics::text::{Editor, Paragraph}; use crate::graphics::Viewport; @@ -477,7 +477,7 @@ impl core::text::Renderer for Renderer { impl core::image::Renderer for Renderer { type Handle = core::image::Handle; - fn measure_image(&self, handle: &Self::Handle) -> Size { + fn measure_image(&self, handle: &Self::Handle) -> core::Size { self.image_cache.borrow_mut().measure_image(handle) } @@ -503,7 +503,7 @@ impl core::image::Renderer for Renderer { #[cfg(feature = "svg")] impl core::svg::Renderer for Renderer { - fn measure_svg(&self, handle: &core::svg::Handle) -> Size { + fn measure_svg(&self, handle: &core::svg::Handle) -> core::Size { self.image_cache.borrow_mut().measure_svg(handle) } @@ -539,7 +539,7 @@ impl graphics::geometry::Renderer for Renderer { type Geometry = Geometry; type Frame = geometry::Frame; - fn new_frame(&self, size: Size) -> Self::Frame { + fn new_frame(&self, size: core::Size) -> Self::Frame { geometry::Frame::new(size) } diff --git a/winit/src/application.rs b/winit/src/application.rs index 3d11bd4a..49ccb61c 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -48,6 +48,9 @@ where /// The data needed to initialize your [`Application`]. type Flags; + /// Returns the unique name of the [`Application`]. + fn name() -> &'static str; + /// Initializes the [`Application`] with the flags provided to /// [`run`] as part of the [`Settings`]. /// @@ -156,7 +159,8 @@ where use futures::Future; use winit::event_loop::EventLoop; - let boot_timer = debug::boot_time(); + debug::init(A::name()); + let boot_span = debug::boot(); let event_loop = EventLoop::with_user_event() .build() @@ -193,7 +197,7 @@ where control_sender, init_command, settings.fonts, - boot_timer, + boot_span, )); let context = task::Context::from_waker(task::noop_waker_ref()); @@ -498,7 +502,7 @@ async fn run_instance( mut control_sender: mpsc::UnboundedSender, init_command: Command, fonts: Vec>, - boot_timer: debug::Timer, + boot_span: debug::Span, ) where A: Application + 'static, E: Executor + 'static, @@ -554,7 +558,7 @@ async fn run_instance( &window, ); runtime.track(application.subscription().into_recipes()); - boot_timer.finish(); + boot_span.finish(); let mut user_interface = ManuallyDrop::new(build_user_interface( &application, @@ -608,12 +612,12 @@ async fn run_instance( if viewport_version != current_viewport_version { let logical_size = state.logical_size(); - let layout_timer = debug::layout_time(window::Id::MAIN); + let layout_span = debug::layout(window::Id::MAIN); user_interface = ManuallyDrop::new( ManuallyDrop::into_inner(user_interface) .relayout(logical_size, &mut renderer), ); - layout_timer.finish(); + layout_span.finish(); compositor.configure_surface( &mut surface, @@ -660,7 +664,7 @@ async fn run_instance( runtime.broadcast(redraw_event, core::event::Status::Ignored); - let draw_timer = debug::draw_time(window::Id::MAIN); + let draw_span = debug::draw(window::Id::MAIN); let new_mouse_interaction = user_interface.draw( &mut renderer, state.theme(), @@ -670,7 +674,7 @@ async fn run_instance( state.cursor(), ); redraw_pending = false; - draw_timer.finish(); + draw_span.finish(); if new_mouse_interaction != mouse_interaction { window.set_cursor(conversion::mouse_interaction( @@ -680,7 +684,7 @@ async fn run_instance( mouse_interaction = new_mouse_interaction; } - let render_timer = debug::render_time(window::Id::MAIN); + let present_span = debug::present(window::Id::MAIN); match compositor.present( &mut renderer, &mut surface, @@ -688,7 +692,7 @@ async fn run_instance( state.background_color(), ) { Ok(()) => { - render_timer.finish(); + present_span.finish(); } Err(error) => match error { // This is an unrecoverable error. @@ -733,7 +737,7 @@ async fn run_instance( redraw_request: None, } } else { - let interact_timer = debug::interact_time(window::Id::MAIN); + let interact_span = debug::interact(window::Id::MAIN); let (interface_state, statuses) = user_interface.update( &events, state.cursor(), @@ -747,7 +751,7 @@ async fn run_instance( { runtime.broadcast(event, status); } - interact_timer.finish(); + interact_span.finish(); interface_state }; @@ -842,13 +846,13 @@ pub fn build_user_interface<'a, A: Application>( where A::Theme: DefaultStyle, { - let view_timer = debug::view_time(window::Id::MAIN); + let view_span = debug::view(window::Id::MAIN); let view = application.view(); - view_timer.finish(); + view_span.finish(); - let layout_timer = debug::layout_time(window::Id::MAIN); + let layout_span = debug::layout(window::Id::MAIN); let user_interface = UserInterface::build(view, size, cache, renderer); - layout_timer.finish(); + layout_span.finish(); user_interface } @@ -875,9 +879,9 @@ pub fn update( for message in messages.drain(..) { debug::log_message(&message); - let update_timer = debug::update_time(); + let update_span = debug::update(); let command = runtime.enter(|| application.update(message)); - update_timer.finish(); + update_span.finish(); run_command( application, diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index eae9c3a8..da9519ee 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -38,8 +38,7 @@ where let theme = application.theme(); let appearance = application.style(&theme); - let _ = application::DefaultStyle::palette(&theme) - .map(debug::theme_changed); + debug::theme_changed(|| application::DefaultStyle::palette(&theme)); let viewport = { let physical_size = window.inner_size(); @@ -216,7 +215,8 @@ where self.theme = application.theme(); self.appearance = application.style(&self.theme); - let _ = application::DefaultStyle::palette(&self.theme) - .map(debug::theme_changed); + debug::theme_changed(|| { + application::DefaultStyle::palette(&self.theme) + }); } } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 61c0f736..473913bc 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -117,7 +117,7 @@ where { use winit::event_loop::EventLoop; - let boot_timer = debug::boot_time(); + let boot_span = debug::boot(); let event_loop = EventLoop::with_user_event() .build() @@ -153,7 +153,7 @@ where event_receiver, control_sender, init_command, - boot_timer, + boot_span, )); let context = task::Context::from_waker(task::noop_waker_ref()); @@ -452,7 +452,7 @@ async fn run_instance( mut event_receiver: mpsc::UnboundedReceiver>, mut control_sender: mpsc::UnboundedSender, init_command: Command, - boot_timer: debug::Timer, + boot_span: debug::Span, ) where A: Application + 'static, E: Executor + 'static, @@ -524,7 +524,7 @@ async fn run_instance( ); runtime.track(application.subscription().into_recipes()); - boot_timer.finish(); + boot_span.finish(); let mut messages = Vec::new(); let mut user_events = 0; @@ -636,7 +636,7 @@ async fn run_instance( &mut messages, ); - let draw_timer = debug::draw_time(id); + let draw_span = debug::draw(id); let new_mouse_interaction = ui.draw( &mut window.renderer, window.state.theme(), @@ -645,7 +645,7 @@ async fn run_instance( }, cursor, ); - draw_timer.finish(); + draw_span.finish(); if new_mouse_interaction != window.mouse_interaction { window.raw.set_cursor( @@ -692,7 +692,7 @@ async fn run_instance( { let logical_size = window.state.logical_size(); - let layout_time = debug::layout_time(id); + let layout = debug::layout(id); let ui = user_interfaces .remove(&id) .expect("Remove user interface"); @@ -701,9 +701,9 @@ async fn run_instance( id, ui.relayout(logical_size, &mut window.renderer), ); - layout_time.finish(); + layout.finish(); - let draw_time = debug::draw_time(id); + let draw = debug::draw(id); let new_mouse_interaction = user_interfaces .get_mut(&id) .expect("Get user interface") @@ -715,7 +715,7 @@ async fn run_instance( }, window.state.cursor(), ); - draw_time.finish(); + draw.finish(); if new_mouse_interaction != window.mouse_interaction { @@ -739,7 +739,7 @@ async fn run_instance( window.state.viewport_version(); } - let render_time = debug::render_time(id); + let present_span = debug::present(id); match compositor.present( &mut window.renderer, &mut window.surface, @@ -747,7 +747,7 @@ async fn run_instance( window.state.background_color(), ) { Ok(()) => { - render_time.finish(); + present_span.finish(); // TODO: Handle animations! // Maybe we can use `ControlFlow::WaitUntil` for this. @@ -821,7 +821,7 @@ async fn run_instance( let mut uis_stale = false; for (id, window) in window_manager.iter_mut() { - let interact_time = debug::interact_time(id); + let interact = debug::interact(id); let mut window_events = vec![]; events.retain(|(window_id, event)| { @@ -864,7 +864,7 @@ async fn run_instance( { runtime.broadcast(event, status); } - interact_time.finish(); + interact.finish(); } // TODO mw application update returns which window IDs to update @@ -938,13 +938,13 @@ fn build_user_interface<'a, A: Application>( where A::Theme: DefaultStyle, { - let view_timer = debug::view_time(id); + let view_span = debug::view(id); let view = application.view(id); - view_timer.finish(); + view_span.finish(); - let layout_timer = debug::layout_time(id); + let layout_span = debug::layout(id); let user_interface = UserInterface::build(view, size, cache, renderer); - layout_timer.finish(); + layout_span.finish(); user_interface } @@ -968,9 +968,9 @@ fn update( for message in messages.drain(..) { debug::log_message(&message); - let update_timer = debug::update_time(); + let update_span = debug::update(); let command = runtime.enter(|| application.update(message)); - update_timer.finish(); + update_span.finish(); run_command( application, From b7c65c877df0180b96579a1ba395212de78057bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 11 May 2024 12:25:44 +0200 Subject: [PATCH 18/39] Toggle the Comet when pressing `F12` --- Cargo.toml | 4 ++-- beacon/src/client.rs | 32 +++++++++++++++++++++++++++-- beacon/src/lib.rs | 36 +++++++++++++++++++++++++++++---- debug/src/lib.rs | 19 ++++++++++++++++- winit/Cargo.toml | 2 ++ winit/src/application.rs | 20 ++++++++++++++++++ winit/src/application/state.rs | 13 ------------ winit/src/multi_window.rs | 21 +++++++++++++++++++ winit/src/multi_window/state.rs | 13 ------------ 9 files changed, 125 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3874b403..046e92ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ qr_code = ["iced_widget/qr_code"] # Enables lazy widgets lazy = ["iced_widget/lazy"] # Enables a debug view in native platforms (press F12) -debug = ["iced_debug/enable"] +debug = ["iced_winit/debug"] # Enables `tokio` as the `executor::Default` on native platforms tokio = ["iced_futures/tokio"] # Enables `async-std` as the `executor::Default` on native platforms @@ -62,8 +62,8 @@ fira-sans = ["iced_renderer/fira-sans"] auto-detect-theme = ["iced_core/auto-detect-theme"] [dependencies] -iced_core.workspace = true iced_debug.workspace = true +iced_core.workspace = true iced_futures.workspace = true iced_renderer.workspace = true iced_widget.workspace = true diff --git a/beacon/src/client.rs b/beacon/src/client.rs index 28cb2eeb..44f6af52 100644 --- a/beacon/src/client.rs +++ b/beacon/src/client.rs @@ -9,6 +9,7 @@ use tokio::net; use tokio::sync::mpsc; use tokio::time; +use std::sync::atomic::{self, AtomicBool}; use std::sync::Arc; use std::thread; @@ -17,6 +18,7 @@ pub const SERVER_ADDRESS: &str = "127.0.0.1:9167"; #[derive(Debug, Clone)] pub struct Client { sender: mpsc::Sender, + is_connected: Arc, _handle: Arc>, } @@ -31,6 +33,9 @@ pub enum Message { at: SystemTime, event: Event, }, + Quit { + at: SystemTime, + }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -47,28 +52,50 @@ impl Client { event, }); } + + pub fn is_connected(&self) -> bool { + self.is_connected.load(atomic::Ordering::Relaxed) + } + + pub fn quit(&self) { + let _ = self.sender.try_send(Message::Quit { + at: SystemTime::now(), + }); + } } #[must_use] pub fn connect(name: String) -> Client { let (sender, receiver) = mpsc::channel(100); + let is_connected = Arc::new(AtomicBool::new(false)); - let handle = std::thread::spawn(move || run(name, receiver)); + let handle = { + let is_connected = is_connected.clone(); + + std::thread::spawn(move || run(name, is_connected.clone(), receiver)) + }; Client { sender, + is_connected, _handle: Arc::new(handle), } } #[tokio::main] -async fn run(name: String, mut receiver: mpsc::Receiver) { +async fn run( + name: String, + is_connected: Arc, + mut receiver: mpsc::Receiver, +) { let version = semver::Version::parse(env!("CARGO_PKG_VERSION")) .expect("Parse package version"); loop { match _connect().await { Ok(mut stream) => { + is_connected.store(true, atomic::Ordering::Relaxed); + let _ = send( &mut stream, Message::Connected { @@ -92,6 +119,7 @@ async fn run(name: String, mut receiver: mpsc::Receiver) { } } Err(_) => { + is_connected.store(false, atomic::Ordering::Relaxed); time::sleep(time::Duration::from_secs(2)).await; } } diff --git a/beacon/src/lib.rs b/beacon/src/lib.rs index 3149d8b5..ff2f36b0 100644 --- a/beacon/src/lib.rs +++ b/beacon/src/lib.rs @@ -35,6 +35,12 @@ pub enum Event { duration: Duration, span: Span, }, + QuitRequested { + at: SystemTime, + }, + AlreadyRunning { + at: SystemTime, + }, } impl Event { @@ -43,19 +49,36 @@ impl Event { Self::Connected { at, .. } | Self::Disconnected { at, .. } | Self::ThemeChanged { at, .. } - | Self::SpanFinished { at, .. } => *at, + | Self::SpanFinished { at, .. } + | Self::QuitRequested { at } + | Self::AlreadyRunning { at } => *at, } } } +pub fn is_running() -> bool { + std::net::TcpListener::bind(client::SERVER_ADDRESS).is_err() +} + pub fn run() -> impl Stream { stream::channel(|mut output| async move { let mut buffer = Vec::new(); loop { - let Ok(mut stream) = connect().await else { - delay().await; - continue; + let mut stream = match connect().await { + Ok(stream) => stream, + Err(error) => { + if error.kind() == io::ErrorKind::AddrInUse { + let _ = output + .send(Event::AlreadyRunning { + at: SystemTime::now(), + }) + .await; + } + + delay().await; + continue; + } }; loop { @@ -124,6 +147,11 @@ pub fn run() -> impl Stream { } } } + client::Message::Quit { at } => { + let _ = output + .send(Event::QuitRequested { at }) + .await; + } }; } Err(Error::IOFailed(_)) => { diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 779cbb2c..125499e4 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -9,7 +9,9 @@ pub fn init(name: &str) { internal::init(name); } -pub fn open_comet() {} +pub fn toggle_comet() { + internal::toggle_comet(); +} pub fn log_message(_message: &impl std::fmt::Debug) {} @@ -65,6 +67,7 @@ mod internal { use beacon::span; use once_cell::sync::Lazy; + use std::process; use std::sync::atomic::{self, AtomicBool}; use std::sync::RwLock; @@ -72,6 +75,18 @@ mod internal { name.clone_into(&mut NAME.write().expect("Write application name")); } + pub fn toggle_comet() { + if BEACON.is_connected() { + BEACON.quit(); + } else { + let _ = process::Command::new("iced_comet") + .stdin(process::Stdio::null()) + .stdout(process::Stdio::null()) + .stderr(process::Stdio::null()) + .spawn(); + } + } + pub fn theme_changed(f: impl FnOnce() -> Option) { let Some(palette) = f() else { return; @@ -166,6 +181,8 @@ mod internal { pub fn init(_name: &str) {} + pub fn toggle_comet() {} + pub fn theme_changed(_f: impl FnOnce() -> Option) {} pub fn boot() -> Span { diff --git a/winit/Cargo.toml b/winit/Cargo.toml index c06afeeb..bb1a8321 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [features] default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] +debug = ["iced_debug/enable"] system = ["sysinfo"] application = [] x11 = ["winit/x11"] @@ -24,6 +25,7 @@ wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] multi-window = ["iced_runtime/multi-window"] [dependencies] +iced_debug.workspace = true iced_futures.workspace = true iced_graphics.workspace = true iced_runtime.workspace = true diff --git a/winit/src/application.rs b/winit/src/application.rs index 49ccb61c..d7949b9e 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -716,6 +716,26 @@ async fn run_instance( break; } + #[cfg(feature = "debug")] + match window_event { + winit::event::WindowEvent::KeyboardInput { + event: + winit::event::KeyEvent { + logical_key: + winit::keyboard::Key::Named( + winit::keyboard::NamedKey::F12, + ), + state: winit::event::ElementState::Pressed, + repeat: false, + .. + }, + .. + } => { + crate::debug::toggle_comet(); + } + _ => {} + } + state.update(&window, &window_event); if let Some(event) = conversion::window_event( diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index da9519ee..e51f5348 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -161,19 +161,6 @@ where WindowEvent::ModifiersChanged(new_modifiers) => { self.modifiers = new_modifiers.state(); } - #[cfg(feature = "debug")] - WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: - winit::keyboard::Key::Named( - winit::keyboard::NamedKey::F12, - ), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } => crate::debug::open_axe(), _ => {} } } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 473913bc..54208412 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -777,6 +777,27 @@ async fn run_instance( event: window_event, window_id, } => { + #[cfg(feature = "debug")] + match window_event { + winit::event::WindowEvent::KeyboardInput { + event: + winit::event::KeyEvent { + logical_key: + winit::keyboard::Key::Named( + winit::keyboard::NamedKey::F12, + ), + state: + winit::event::ElementState::Pressed, + repeat: false, + .. + }, + .. + } => { + crate::debug::toggle_comet(); + } + _ => {} + } + let Some((id, window)) = window_manager.get_mut_alias(window_id) else { diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index bc88c04b..5368b849 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -173,19 +173,6 @@ where WindowEvent::ModifiersChanged(new_modifiers) => { self.modifiers = new_modifiers.state(); } - #[cfg(feature = "debug")] - WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: - winit::keyboard::Key::Named( - winit::keyboard::NamedKey::F12, - ), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } => _debug.toggle(), _ => {} } } From e770804435081a0b3f4d370aa1bd8517c0d7c4bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 12 May 2024 13:43:39 +0200 Subject: [PATCH 19/39] Bind `beacon` server only once --- beacon/src/client.rs | 8 +++++--- beacon/src/lib.rs | 29 +++++++++++------------------ debug/src/lib.rs | 6 ++++++ 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/beacon/src/client.rs b/beacon/src/client.rs index 44f6af52..cd5fec6d 100644 --- a/beacon/src/client.rs +++ b/beacon/src/client.rs @@ -110,9 +110,11 @@ async fn run( match send(&mut stream, output).await { Ok(()) => {} Err(error) => { - log::warn!( - "Error sending message to server: {error}" - ); + if error.kind() != io::ErrorKind::BrokenPipe { + log::warn!( + "Error sending message to server: {error}" + ); + } break; } } diff --git a/beacon/src/lib.rs b/beacon/src/lib.rs index ff2f36b0..d3d8c722 100644 --- a/beacon/src/lib.rs +++ b/beacon/src/lib.rs @@ -64,9 +64,9 @@ pub fn run() -> impl Stream { stream::channel(|mut output| async move { let mut buffer = Vec::new(); - loop { - let mut stream = match connect().await { - Ok(stream) => stream, + let server = loop { + match net::TcpListener::bind(client::SERVER_ADDRESS).await { + Ok(server) => break server, Err(error) => { if error.kind() == io::ErrorKind::AddrInUse { let _ = output @@ -75,11 +75,17 @@ pub fn run() -> impl Stream { }) .await; } - delay().await; - continue; } }; + }; + + loop { + let Ok((mut stream, _)) = server.accept().await else { + continue; + }; + + let _ = stream.set_nodelay(true); loop { match receive(&mut stream, &mut buffer).await { @@ -160,8 +166,6 @@ pub fn run() -> impl Stream { at: SystemTime::now(), }) .await; - - delay().await; break; } Err(Error::DecodingFailed(error)) => { @@ -173,17 +177,6 @@ pub fn run() -> impl Stream { }) } -async fn connect() -> Result { - let listener = net::TcpListener::bind(client::SERVER_ADDRESS).await?; - - let (stream, _) = listener.accept().await?; - - stream.set_nodelay(true)?; - stream.readable().await?; - - Ok(stream) -} - async fn receive( stream: &mut net::TcpStream, buffer: &mut Vec, diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 125499e4..54814b4e 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -84,6 +84,12 @@ mod internal { .stdout(process::Stdio::null()) .stderr(process::Stdio::null()) .spawn(); + + if let Some(palette) = + LAST_PALETTE.read().expect("Read last palette").as_ref() + { + BEACON.log(client::Event::ThemeChanged(*palette)); + } } } From b8adfc9ffe7aaab5fef8dd5df688bfb10205cb22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 13 May 2024 13:21:10 +0200 Subject: [PATCH 20/39] Track commands spawned and subscriptions alive in `beacon` --- beacon/src/client.rs | 3 +++ beacon/src/lib.rs | 41 +++++++++++++++++++++++++++++++++++- beacon/src/span.rs | 32 +++++++++++++++++++++------- debug/src/lib.rs | 41 ++++++++++++++++++++++++++++++------ runtime/src/program/state.rs | 4 +--- src/application.rs | 4 +++- winit/src/application.rs | 22 ++++++++++++------- winit/src/multi_window.rs | 19 +++++++++++------ 8 files changed, 131 insertions(+), 35 deletions(-) diff --git a/beacon/src/client.rs b/beacon/src/client.rs index cd5fec6d..95755d98 100644 --- a/beacon/src/client.rs +++ b/beacon/src/client.rs @@ -43,6 +43,9 @@ pub enum Event { ThemeChanged(theme::Palette), SpanStarted(span::Stage), SpanFinished(span::Stage, Duration), + MessageLogged(String), + CommandsSpawned(usize), + SubscriptionsTracked(usize), } impl Client { diff --git a/beacon/src/lib.rs b/beacon/src/lib.rs index d3d8c722..36d19e30 100644 --- a/beacon/src/lib.rs +++ b/beacon/src/lib.rs @@ -30,6 +30,10 @@ pub enum Event { at: SystemTime, palette: theme::Palette, }, + SubscriptionsTracked { + at: SystemTime, + amount_alive: usize, + }, SpanFinished { at: SystemTime, duration: Duration, @@ -49,6 +53,7 @@ impl Event { Self::Connected { at, .. } | Self::Disconnected { at, .. } | Self::ThemeChanged { at, .. } + | Self::SubscriptionsTracked { at, .. } | Self::SpanFinished { at, .. } | Self::QuitRequested { at } | Self::AlreadyRunning { at } => *at, @@ -87,6 +92,9 @@ pub fn run() -> impl Stream { let _ = stream.set_nodelay(true); + let mut last_message = String::new(); + let mut last_commands_spawned = 0; + loop { match receive(&mut stream, &mut buffer).await { Ok(message) => { @@ -114,6 +122,30 @@ pub fn run() -> impl Stream { }) .await; } + client::Event::SubscriptionsTracked( + amount_alive, + ) => { + let _ = output + .send(Event::SubscriptionsTracked { + at, + amount_alive, + }) + .await; + } + client::Event::MessageLogged(message) => { + last_message = message; + } + client::Event::CommandsSpawned( + commands, + ) => { + last_commands_spawned = commands; + } + client::Event::SpanStarted( + span::Stage::Update, + ) => { + last_message.clear(); + last_commands_spawned = 0; + } client::Event::SpanStarted(_) => {} client::Event::SpanFinished( stage, @@ -121,7 +153,14 @@ pub fn run() -> impl Stream { ) => { let span = match stage { span::Stage::Boot => Span::Boot, - span::Stage::Update => Span::Update, + span::Stage::Update => { + Span::Update { + message: last_message + .clone(), + commands_spawned: + last_commands_spawned, + } + } span::Stage::View(window) => { Span::View { window } } diff --git a/beacon/src/span.rs b/beacon/src/span.rs index 7d673663..38f19acb 100644 --- a/beacon/src/span.rs +++ b/beacon/src/span.rs @@ -5,20 +5,36 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Span { Boot, - Update, - View { window: window::Id }, - Layout { window: window::Id }, - Interact { window: window::Id }, - Draw { window: window::Id }, - Present { window: window::Id }, - Custom { window: window::Id, name: String }, + Update { + message: String, + commands_spawned: usize, + }, + View { + window: window::Id, + }, + Layout { + window: window::Id, + }, + Interact { + window: window::Id, + }, + Draw { + window: window::Id, + }, + Present { + window: window::Id, + }, + Custom { + window: window::Id, + name: String, + }, } impl Span { pub fn stage(&self) -> Stage { match self { Span::Boot => Stage::Boot, - Span::Update => Stage::Update, + Span::Update { .. } => Stage::Update, Span::View { window } => Stage::View(*window), Span::Layout { window } => Stage::Layout(*window), Span::Interact { window } => Stage::Interact(*window), diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 54814b4e..ab7a2853 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -13,18 +13,24 @@ pub fn toggle_comet() { internal::toggle_comet(); } -pub fn log_message(_message: &impl std::fmt::Debug) {} - pub fn theme_changed(f: impl FnOnce() -> Option) { internal::theme_changed(f); } +pub fn commands_spawned(amount: usize) { + internal::commands_spawned(amount) +} + +pub fn subscriptions_tracked(amount: usize) { + internal::subscriptions_tracked(amount) +} + pub fn boot() -> Span { internal::boot() } -pub fn update() -> Span { - internal::update() +pub fn update(message: &impl std::fmt::Debug) -> Span { + internal::update(message) } pub fn view(window: window::Id) -> Span { @@ -107,12 +113,29 @@ mod internal { } } + pub fn commands_spawned(amount: usize) { + BEACON.log(client::Event::CommandsSpawned(amount)); + } + + pub fn subscriptions_tracked(amount: usize) { + BEACON.log(client::Event::SubscriptionsTracked(amount)); + } + pub fn boot() -> Span { span(span::Stage::Boot) } - pub fn update() -> Span { - span(span::Stage::Update) + pub fn update(message: &impl std::fmt::Debug) -> Span { + let span = span(span::Stage::Update); + let message = format!("{message:.30?}"); + + BEACON.log(client::Event::MessageLogged(if message.len() > 29 { + format!("{}...", &message[..29]) + } else { + message + })); + + span } pub fn view(window: window::Id) -> Span { @@ -191,11 +214,15 @@ mod internal { pub fn theme_changed(_f: impl FnOnce() -> Option) {} + pub fn commands_spawned(_amount: usize) {} + + pub fn subscriptions_tracked(_amount: usize) {} + pub fn boot() -> Span { Span } - pub fn update() -> Span { + pub fn update(_message: &impl std::fmt::Debug) -> Span { Span } diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs index 129f2449..033fe018 100644 --- a/runtime/src/program/state.rs +++ b/runtime/src/program/state.rs @@ -143,9 +143,7 @@ where let commands = Command::batch(messages.into_iter().map(|message| { - debug::log_message(&message); - - let update_span = debug::update(); + let update_span = debug::update(&message); let command = self.program.update(message); update_span.finish(); diff --git a/src/application.rs b/src/application.rs index 7bdef972..84aacc07 100644 --- a/src/application.rs +++ b/src/application.rs @@ -119,7 +119,9 @@ where type Flags; /// Returns the unique name of the [`Application`]. - fn name() -> &'static str; + fn name() -> &'static str { + std::any::type_name::() + } /// Initializes the [`Application`] with the flags provided to /// [`run`] as part of the [`Settings`]. diff --git a/winit/src/application.rs b/winit/src/application.rs index d7949b9e..c207fe9b 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -557,7 +557,11 @@ async fn run_instance( &mut proxy, &window, ); - runtime.track(application.subscription().into_recipes()); + + let recipes = application.subscription().into_recipes(); + debug::subscriptions_tracked(recipes.len()); + runtime.track(recipes); + boot_span.finish(); let mut user_interface = ManuallyDrop::new(build_user_interface( @@ -897,11 +901,8 @@ pub fn update( A::Theme: DefaultStyle, { for message in messages.drain(..) { - debug::log_message(&message); - - let update_span = debug::update(); + let update_span = debug::update(&message); let command = runtime.enter(|| application.update(message)); - update_span.finish(); run_command( application, @@ -917,12 +918,14 @@ pub fn update( proxy, window, ); + update_span.finish(); } state.synchronize(application, window); - let subscription = application.subscription(); - runtime.track(subscription.into_recipes()); + let recipes = application.subscription().into_recipes(); + debug::subscriptions_tracked(recipes.len()); + runtime.track(recipes); } /// Runs the actions of a [`Command`]. @@ -949,7 +952,10 @@ pub fn run_command( use crate::runtime::system; use crate::runtime::window; - for action in command.actions() { + let actions = command.actions(); + debug::commands_spawned(actions.len()); + + for action in actions { match action { command::Action::Future(future) => { runtime.spawn(future); diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 54208412..1ef313bc 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -523,7 +523,10 @@ async fn run_instance( &mut ui_caches, ); - runtime.track(application.subscription().into_recipes()); + let recipes = application.subscription().into_recipes(); + debug::subscriptions_tracked(recipes.len()); + runtime.track(recipes); + boot_span.finish(); let mut messages = Vec::new(); @@ -987,9 +990,7 @@ fn update( A::Theme: DefaultStyle, { for message in messages.drain(..) { - debug::log_message(&message); - - let update_span = debug::update(); + let update_span = debug::update(&message); let command = runtime.enter(|| application.update(message)); update_span.finish(); @@ -1006,8 +1007,9 @@ fn update( ); } - let subscription = application.subscription(); - runtime.track(subscription.into_recipes()); + let recipes = application.subscription().into_recipes(); + debug::subscriptions_tracked(recipes.len()); + runtime.track(recipes); } /// Runs the actions of a [`Command`]. @@ -1031,7 +1033,10 @@ fn run_command( use crate::runtime::system; use crate::runtime::window; - for action in command.actions() { + let actions = command.actions(); + debug::commands_spawned(actions.len()); + + for action in actions { match action { command::Action::Future(future) => { runtime.spawn(Box::pin(future)); From 832755fdafa42b844fb3e011b43f578ac010b3b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 13 May 2024 15:22:23 +0200 Subject: [PATCH 21/39] Increase message format limit to `50` characters in `beacon` --- debug/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/debug/src/lib.rs b/debug/src/lib.rs index ab7a2853..f89d97e5 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -127,10 +127,10 @@ mod internal { pub fn update(message: &impl std::fmt::Debug) -> Span { let span = span(span::Stage::Update); - let message = format!("{message:.30?}"); + let message = format!("{message:.50?}"); - BEACON.log(client::Event::MessageLogged(if message.len() > 29 { - format!("{}...", &message[..29]) + BEACON.log(client::Event::MessageLogged(if message.len() > 49 { + format!("{}...", &message[..49]) } else { message })); From 780af771fa8248341b33ee2c6b6eb9dcf3e77c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 14 May 2024 20:13:55 +0200 Subject: [PATCH 22/39] Fix `clippy` lints --- winit/src/application.rs | 32 +++++++++++++++----------------- winit/src/multi_window.rs | 33 +++++++++++++++------------------ 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/winit/src/application.rs b/winit/src/application.rs index c207fe9b..1a4b35bd 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -721,23 +721,21 @@ async fn run_instance( } #[cfg(feature = "debug")] - match window_event { - winit::event::WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: - winit::keyboard::Key::Named( - winit::keyboard::NamedKey::F12, - ), - state: winit::event::ElementState::Pressed, - repeat: false, - .. - }, - .. - } => { - crate::debug::toggle_comet(); - } - _ => {} + if let winit::event::WindowEvent::KeyboardInput { + event: + winit::event::KeyEvent { + logical_key: + winit::keyboard::Key::Named( + winit::keyboard::NamedKey::F12, + ), + state: winit::event::ElementState::Pressed, + repeat: false, + .. + }, + .. + } = &window_event + { + crate::debug::toggle_comet(); } state.update(&window, &window_event); diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 1ef313bc..d803a8d4 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -781,24 +781,21 @@ async fn run_instance( window_id, } => { #[cfg(feature = "debug")] - match window_event { - winit::event::WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: - winit::keyboard::Key::Named( - winit::keyboard::NamedKey::F12, - ), - state: - winit::event::ElementState::Pressed, - repeat: false, - .. - }, - .. - } => { - crate::debug::toggle_comet(); - } - _ => {} + if let winit::event::WindowEvent::KeyboardInput { + event: + winit::event::KeyEvent { + logical_key: + winit::keyboard::Key::Named( + winit::keyboard::NamedKey::F12, + ), + state: winit::event::ElementState::Pressed, + repeat: false, + .. + }, + .. + } = &window_event + { + crate::debug::toggle_comet(); } let Some((id, window)) = From 4890d75012f4a2169d146fd26af395103b18e03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 16 May 2024 13:47:02 +0200 Subject: [PATCH 23/39] Initialize `Application::name` in multi-window runtime --- debug/src/lib.rs | 2 ++ src/multi_window.rs | 9 +++++++++ src/program.rs | 4 +--- winit/src/multi_window.rs | 6 +++++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/debug/src/lib.rs b/debug/src/lib.rs index f89d97e5..f33b23b6 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -78,6 +78,8 @@ mod internal { use std::sync::RwLock; pub fn init(name: &str) { + let name = name.split("::").next().unwrap_or(name); + name.clone_into(&mut NAME.write().expect("Write application name")); } diff --git a/src/multi_window.rs b/src/multi_window.rs index b81297dc..2320911e 100644 --- a/src/multi_window.rs +++ b/src/multi_window.rs @@ -84,6 +84,11 @@ where /// The data needed to initialize your [`Application`]. type Flags; + /// Returns the unique name of the [`Application`]. + fn name() -> &'static str { + std::any::type_name::() + } + /// Initializes the [`Application`] with the flags provided to /// [`run`] as part of the [`Settings`]. /// @@ -232,6 +237,10 @@ where (Instance(app), command) } + fn name() -> &'static str { + A::name() + } + fn title(&self, window: window::Id) -> String { self.0.title(window) } diff --git a/src/program.rs b/src/program.rs index 70d3bd51..7c7b0d37 100644 --- a/src/program.rs +++ b/src/program.rs @@ -107,9 +107,7 @@ where type Executor = executor::Default; fn name() -> &'static str { - let type_name = std::any::type_name::(); - - type_name.split("::").next().unwrap_or(type_name) + std::any::type_name::() } fn load(&self) -> Command { diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index d803a8d4..a05a94d9 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -51,6 +51,9 @@ where /// The data needed to initialize your [`Application`]. type Flags; + /// Returns the unique name of the [`Application`]. + fn name() -> &'static str; + /// Initializes the [`Application`] with the flags provided to /// [`run`] as part of the [`Settings`]. /// @@ -117,6 +120,7 @@ where { use winit::event_loop::EventLoop; + debug::init(A::name()); let boot_span = debug::boot(); let event_loop = EventLoop::with_user_event() @@ -989,7 +993,6 @@ fn update( for message in messages.drain(..) { let update_span = debug::update(&message); let command = runtime.enter(|| application.update(message)); - update_span.finish(); run_command( application, @@ -1002,6 +1005,7 @@ fn update( window_manager, ui_caches, ); + update_span.finish(); } let recipes = application.subscription().into_recipes(); From eca13368166c5ac8bee8f8b71d90cedc0fd42af2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 16 May 2024 13:47:34 +0200 Subject: [PATCH 24/39] Fix truncation of logged message in `debug` --- debug/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debug/src/lib.rs b/debug/src/lib.rs index f33b23b6..4aaae46b 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -129,7 +129,7 @@ mod internal { pub fn update(message: &impl std::fmt::Debug) -> Span { let span = span(span::Stage::Update); - let message = format!("{message:.50?}"); + let message = format!("{message:?}"); BEACON.log(client::Event::MessageLogged(if message.len() > 49 { format!("{}...", &message[..49]) From f618382a0dc85bb25874a6fcd8230dafa526f252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Tue, 4 Mar 2025 19:12:31 +0100 Subject: [PATCH 25/39] Run `cargo fmt` --- beacon/src/client.rs | 2 +- beacon/src/stream.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/beacon/src/client.rs b/beacon/src/client.rs index 95755d98..1ca5bc8c 100644 --- a/beacon/src/client.rs +++ b/beacon/src/client.rs @@ -9,8 +9,8 @@ use tokio::net; use tokio::sync::mpsc; use tokio::time; -use std::sync::atomic::{self, AtomicBool}; use std::sync::Arc; +use std::sync::atomic::{self, AtomicBool}; use std::thread; pub const SERVER_ADDRESS: &str = "127.0.0.1:9167"; diff --git a/beacon/src/stream.rs b/beacon/src/stream.rs index 855576e7..68381040 100644 --- a/beacon/src/stream.rs +++ b/beacon/src/stream.rs @@ -1,6 +1,6 @@ +use futures::Future; use futures::channel::mpsc; use futures::stream::{self, Stream, StreamExt}; -use futures::Future; pub fn channel(f: impl Fn(mpsc::Sender) -> F) -> impl Stream where From a106f7f837d7e52ff4d406ebbe447bcee2f8dd75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 5 Mar 2025 11:52:05 +0100 Subject: [PATCH 26/39] Initialize `debug` with proper `Program` name --- src/program.rs | 4 ++++ winit/src/program.rs | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/program.rs b/src/program.rs index ace4da74..602e8227 100644 --- a/src/program.rs +++ b/src/program.rs @@ -110,6 +110,10 @@ pub trait Program: Sized { type Flags = (P, I); type Executor = P::Executor; + fn name() -> &'static str { + std::any::type_name::() + } + fn new( (program, initialize): Self::Flags, ) -> (Self, Task) { diff --git a/winit/src/program.rs b/winit/src/program.rs index b8e9a067..b0344050 100644 --- a/winit/src/program.rs +++ b/winit/src/program.rs @@ -6,6 +6,7 @@ pub use state::State; use crate::conversion; use crate::core; +use crate::core::keyboard; use crate::core::mouse; use crate::core::renderer; use crate::core::theme; @@ -69,6 +70,9 @@ where /// The data needed to initialize your [`Program`]. type Flags; + /// Returns the unique name of the [`Program`]. + fn name() -> &'static str; + /// Initializes the [`Program`] with the flags provided to /// [`run`] as part of the [`Settings`]. /// @@ -154,6 +158,8 @@ where { use winit::event_loop::EventLoop; + debug::init(P::name()); + let boot_span = debug::boot(); let event_loop = EventLoop::with_user_event() @@ -973,6 +979,20 @@ async fn run_instance( window.state.scale_factor(), window.state.modifiers(), ) { + if matches!( + event, + core::Event::Keyboard( + keyboard::Event::KeyPressed { + modified_key: keyboard::Key::Named( + keyboard::key::Named::F12 + ), + .. + } + ) + ) { + debug::toggle_comet(); + } + events.push((id, event)); } } From fd1101bd5fc74b86ba454ea81b051c9de97e8874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 12 Mar 2025 02:10:42 +0100 Subject: [PATCH 27/39] Unify `Program` definition in `iced_program` subcrate --- Cargo.lock | 12 +- Cargo.toml | 2 + examples/arc/src/main.rs | 2 +- examples/bezier_tool/src/main.rs | 2 +- examples/changelog/src/main.rs | 4 +- examples/checkbox/src/main.rs | 2 +- examples/clock/src/main.rs | 2 +- examples/color_palette/src/main.rs | 2 +- examples/combo_box/src/main.rs | 2 +- examples/counter/src/main.rs | 2 +- examples/custom_quad/src/main.rs | 2 +- examples/custom_shader/src/main.rs | 10 +- examples/custom_widget/src/main.rs | 2 +- examples/download_progress/src/main.rs | 7 +- examples/editor/src/main.rs | 4 +- examples/events/src/main.rs | 2 +- examples/exit/src/main.rs | 2 +- examples/ferris/src/main.rs | 2 +- examples/gallery/src/main.rs | 4 +- examples/game_of_life/src/main.rs | 16 +- examples/geometry/src/main.rs | 2 +- examples/gradient/src/main.rs | 2 +- examples/layout/src/main.rs | 3 +- examples/lazy/src/main.rs | 2 +- examples/loading_spinners/src/main.rs | 2 +- examples/loupe/src/main.rs | 2 +- examples/markdown/src/main.rs | 4 +- examples/modal/src/main.rs | 2 +- examples/multi_window/src/main.rs | 5 +- examples/multitouch/src/main.rs | 2 +- examples/pane_grid/src/main.rs | 2 +- examples/pick_list/src/main.rs | 2 +- examples/pokedex/src/main.rs | 5 +- examples/progress_bar/src/main.rs | 2 +- examples/qr_code/src/main.rs | 2 +- examples/screenshot/src/main.rs | 2 +- examples/scrollable/src/main.rs | 2 +- examples/sierpinski_triangle/src/main.rs | 2 +- examples/slider/src/main.rs | 2 +- examples/solar_system/src/main.rs | 2 +- examples/stopwatch/src/main.rs | 2 +- examples/styling/src/main.rs | 2 +- examples/svg/src/main.rs | 2 +- examples/system_information/src/main.rs | 7 +- examples/the_matrix/src/main.rs | 2 +- examples/toast/src/main.rs | 2 +- examples/todos/src/main.rs | 5 +- examples/tooltip/src/main.rs | 2 +- examples/tour/src/main.rs | 3 +- examples/url_handler/src/main.rs | 2 +- examples/vectorial_text/src/main.rs | 2 +- examples/visible_bounds/src/main.rs | 2 +- examples/websocket/src/main.rs | 4 +- graphics/src/settings.rs | 12 +- program/Cargo.toml | 18 + src/program.rs => program/src/lib.rs | 299 +-- src/application.rs | 80 +- src/daemon.rs | 37 +- src/lib.rs | 21 +- winit/Cargo.toml | 4 +- winit/src/error.rs | 2 +- winit/src/lib.rs | 1495 ++++++++++++++- winit/src/program.rs | 1599 ----------------- winit/src/settings.rs | 26 - .../{program/window_manager.rs => window.rs} | 13 +- winit/src/{program => window}/state.rs | 22 +- 66 files changed, 1862 insertions(+), 1935 deletions(-) create mode 100644 program/Cargo.toml rename src/program.rs => program/src/lib.rs (74%) delete mode 100644 winit/src/program.rs delete mode 100644 winit/src/settings.rs rename winit/src/{program/window_manager.rs => window.rs} (98%) rename winit/src/{program => window}/state.rs (92%) diff --git a/Cargo.lock b/Cargo.lock index e2aa6a34..da8fc3e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2602,6 +2602,14 @@ dependencies = [ "syntect", ] +[[package]] +name = "iced_program" +version = "0.14.0-dev" +dependencies = [ + "iced_graphics", + "iced_runtime", +] + [[package]] name = "iced_renderer" version = "0.14.0-dev" @@ -2694,9 +2702,7 @@ name = "iced_winit" version = "0.14.0-dev" dependencies = [ "iced_debug", - "iced_futures", - "iced_graphics", - "iced_runtime", + "iced_program", "log", "rustc-hash 2.1.1", "sysinfo", diff --git a/Cargo.toml b/Cargo.toml index 44832890..2a1594ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,6 +112,7 @@ members = [ "futures", "graphics", "highlighter", + "program", "renderer", "runtime", "test", @@ -141,6 +142,7 @@ iced_debug = { version = "0.14.0-dev", path = "debug" } iced_futures = { version = "0.14.0-dev", path = "futures" } iced_graphics = { version = "0.14.0-dev", path = "graphics" } iced_highlighter = { version = "0.14.0-dev", path = "highlighter" } +iced_program = { version = "0.14.0-dev", path = "program" } iced_renderer = { version = "0.14.0-dev", path = "renderer" } iced_runtime = { version = "0.14.0-dev", path = "runtime" } iced_test = { version = "0.14.0-dev", path = "test" } diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index f63b82d0..c9cad1b6 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 - Iced", Arc::update, Arc::view) + iced::application(Arc::default, Arc::update, Arc::view) .subscription(Arc::subscription) .theme(|_| Theme::Dark) .antialiasing(true) diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 95ad299d..02d0f9e9 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::{button, container, horizontal_space, hover, right}; use iced::{Element, Theme}; pub fn main() -> iced::Result { - iced::application("Bezier Tool - Iced", Example::update, Example::view) + iced::application(Example::default, Example::update, Example::view) .theme(|_| Theme::CatppuccinMocha) .antialiasing(true) .run() diff --git a/examples/changelog/src/main.rs b/examples/changelog/src/main.rs index a1d0d799..fbb0cfbf 100644 --- a/examples/changelog/src/main.rs +++ b/examples/changelog/src/main.rs @@ -12,9 +12,9 @@ use iced::{Center, Element, Fill, FillPortion, Font, Task, Theme}; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::application("Changelog Generator", Generator::update, Generator::view) + iced::application(Generator::new, Generator::update, Generator::view) .theme(Generator::theme) - .run_with(Generator::new) + .run() } enum Generator { diff --git a/examples/checkbox/src/main.rs b/examples/checkbox/src/main.rs index f06557f8..563b721d 100644 --- a/examples/checkbox/src/main.rs +++ b/examples/checkbox/src/main.rs @@ -4,7 +4,7 @@ use iced::{Element, Font}; const ICON_FONT: Font = Font::with_name("icons"); pub fn main() -> iced::Result { - iced::application("Checkbox - Iced", Example::update, Example::view) + iced::application(Example::default, Example::update, Example::view) .font(include_bytes!("../fonts/icons.ttf").as_slice()) .run() } diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 1c90708f..533cafd8 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 - Iced", Clock::update, Clock::view) + iced::application(Clock::default, Clock::update, Clock::view) .subscription(Clock::subscription) .theme(Clock::theme) .antialiasing(true) diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index b11c8a2b..b16121da 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -12,7 +12,7 @@ use std::ops::RangeInclusive; pub fn main() -> iced::Result { iced::application( - "Color Palette - Iced", + ColorPalette::default, ColorPalette::update, ColorPalette::view, ) diff --git a/examples/combo_box/src/main.rs b/examples/combo_box/src/main.rs index af53b17a..5124c8ef 100644 --- a/examples/combo_box/src/main.rs +++ b/examples/combo_box/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{ use iced::{Center, Element, Fill}; pub fn main() -> iced::Result { - iced::run("Combo Box - Iced", Example::update, Example::view) + iced::run(Example::update, Example::view) } struct Example { diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index 8f5f9754..d097e052 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -2,7 +2,7 @@ use iced::Center; use iced::widget::{Column, button, column, text}; pub fn main() -> iced::Result { - iced::run("A cool counter", Counter::update, Counter::view) + iced::run(Counter::update, Counter::view) } #[derive(Default)] diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index f9c07da9..f0d9c28c 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -87,7 +87,7 @@ use iced::widget::{center, column, slider, text}; use iced::{Center, Color, Element, Shadow, Vector}; pub fn main() -> iced::Result { - iced::run("Custom Quad - Iced", Example::update, Example::view) + iced::run(Example::update, Example::view) } struct Example { diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index 8c187d3c..54a1c709 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -9,13 +9,9 @@ use iced::window; use iced::{Center, Color, Element, Fill, Subscription}; fn main() -> iced::Result { - iced::application( - "Custom Shader - Iced", - IcedCubes::update, - IcedCubes::view, - ) - .subscription(IcedCubes::subscription) - .run() + iced::application(IcedCubes::default, IcedCubes::update, IcedCubes::view) + .subscription(IcedCubes::subscription) + .run() } struct IcedCubes { diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index d561c2e0..db4255be 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -78,7 +78,7 @@ use iced::widget::{center, column, slider, text}; use iced::{Center, Element}; pub fn main() -> iced::Result { - iced::run("Custom Widget - Iced", Example::update, Example::view) + iced::run(Example::update, Example::view) } struct Example { diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index 0ba8a297..9945fcaa 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -7,12 +7,7 @@ use iced::widget::{Column, button, center, column, progress_bar, text}; use iced::{Center, Element, Function, Right, Task}; pub fn main() -> iced::Result { - iced::application( - "Download Progress - Iced", - Example::update, - Example::view, - ) - .run() + iced::application(Example::default, Example::update, Example::view).run() } #[derive(Debug)] diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index c039672e..af75f581 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -12,11 +12,11 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; pub fn main() -> iced::Result { - iced::application("Editor - Iced", Editor::update, Editor::view) + iced::application(Editor::new, Editor::update, Editor::view) .theme(Editor::theme) .font(include_bytes!("../fonts/icons.ttf").as_slice()) .default_font(Font::MONOSPACE) - .run_with(Editor::new) + .run() } struct Editor { diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index a7d98912..c38b22b9 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -4,7 +4,7 @@ use iced::window; use iced::{Center, Element, Fill, Subscription, Task}; pub fn main() -> iced::Result { - iced::application("Events - Iced", Events::update, Events::view) + iced::application(Events::default, Events::update, Events::view) .subscription(Events::subscription) .exit_on_close_request(false) .run() diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index 48b0864c..6d1b5c52 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -3,7 +3,7 @@ use iced::window; use iced::{Center, Element, Task}; pub fn main() -> iced::Result { - iced::application("Exit - Iced", Exit::update, Exit::view).run() + iced::run(Exit::update, Exit::view) } #[derive(Default)] diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs index eaf51354..c28b04c6 100644 --- a/examples/ferris/src/main.rs +++ b/examples/ferris/src/main.rs @@ -9,7 +9,7 @@ use iced::{ }; pub fn main() -> iced::Result { - iced::application("Ferris - Iced", Image::update, Image::view) + iced::application(Image::default, Image::update, Image::view) .subscription(Image::subscription) .theme(|_| Theme::TokyoNight) .run() diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index caa11016..01e6aab4 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -21,10 +21,10 @@ use iced::{ use std::collections::HashMap; fn main() -> iced::Result { - iced::application("Gallery - Iced", Gallery::update, Gallery::view) + iced::application(Gallery::new, Gallery::update, Gallery::view) .subscription(Gallery::subscription) .theme(Gallery::theme) - .run_with(Gallery::new) + .run() } struct Gallery { diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 7793ba77..c18b240e 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -14,16 +14,12 @@ use iced::{Center, Element, Fill, Function, Subscription, Task, Theme}; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::application( - "Game of Life - Iced", - GameOfLife::update, - GameOfLife::view, - ) - .subscription(GameOfLife::subscription) - .theme(|_| Theme::Dark) - .antialiasing(true) - .centered() - .run() + iced::application(GameOfLife::default, GameOfLife::update, GameOfLife::view) + .subscription(GameOfLife::subscription) + .theme(|_| Theme::Dark) + .antialiasing(true) + .centered() + .run() } struct GameOfLife { diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 0255cdfb..f1025675 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -157,7 +157,7 @@ use iced::widget::{center_x, center_y, column, scrollable}; use rainbow::rainbow; pub fn main() -> iced::Result { - iced::run("Custom 2D Geometry - Iced", |_: &mut _, _| {}, view) + iced::run((), view) } fn view(_state: &()) -> Element<'_, ()> { diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs index c103389a..93c40e05 100644 --- a/examples/gradient/src/main.rs +++ b/examples/gradient/src/main.rs @@ -8,7 +8,7 @@ use iced::{Center, Color, Element, Fill, Radians, Theme, color}; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::application("Gradient - Iced", Gradient::update, Gradient::view) + iced::application(Gradient::default, Gradient::update, Gradient::view) .style(Gradient::style) .transparent(true) .run() diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 979a8c30..768180a8 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -12,9 +12,10 @@ use iced::{ }; pub fn main() -> iced::Result { - iced::application(Layout::title, Layout::update, Layout::view) + iced::application(Layout::default, Layout::update, Layout::view) .subscription(Layout::subscription) .theme(Layout::theme) + .title(Layout::title) .run() } diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 8f756210..3d2eb61c 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -8,7 +8,7 @@ use std::collections::HashSet; use std::hash::Hash; pub fn main() -> iced::Result { - iced::run("Lazy - Iced", App::update, App::view) + iced::run(App::update, App::view) } struct App { diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs index 3b178148..59aab315 100644 --- a/examples/loading_spinners/src/main.rs +++ b/examples/loading_spinners/src/main.rs @@ -12,7 +12,7 @@ use linear::Linear; pub fn main() -> iced::Result { iced::application( - "Loading Spinners - Iced", + LoadingSpinners::default, LoadingSpinners::update, LoadingSpinners::view, ) diff --git a/examples/loupe/src/main.rs b/examples/loupe/src/main.rs index 3dde6872..114f481d 100644 --- a/examples/loupe/src/main.rs +++ b/examples/loupe/src/main.rs @@ -4,7 +4,7 @@ use iced::{Center, Element}; use loupe::loupe; pub fn main() -> iced::Result { - iced::run("Loupe - Iced", Loupe::update, Loupe::view) + iced::run(Loupe::update, Loupe::view) } #[derive(Default)] diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 38a56c6b..99eb9ef8 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -18,11 +18,11 @@ use std::io; use std::sync::Arc; pub fn main() -> iced::Result { - iced::application("Markdown - Iced", Markdown::update, Markdown::view) + iced::application(Markdown::new, Markdown::update, Markdown::view) .font(icon::FONT) .subscription(Markdown::subscription) .theme(Markdown::theme) - .run_with(Markdown::new) + .run() } struct Markdown { diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 067ca24d..236f32f8 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -10,7 +10,7 @@ use iced::{Bottom, Color, Element, Fill, Subscription, Task}; use std::fmt; pub fn main() -> iced::Result { - iced::application("Modal - Iced", App::update, App::view) + iced::application(App::default, App::update, App::view) .subscription(App::subscription) .run() } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 8cec9d4c..3f9efc31 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -10,11 +10,12 @@ use iced::{ use std::collections::BTreeMap; fn main() -> iced::Result { - iced::daemon(Example::title, Example::update, Example::view) + iced::daemon(Example::new, Example::update, Example::view) .subscription(Example::subscription) + .title(Example::title) .theme(Example::theme) .scale_factor(Example::scale_factor) - .run_with(Example::new) + .run() } struct Example { diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index bda3b8f7..3a23fd83 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -12,7 +12,7 @@ use std::collections::HashMap; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::application("Multitouch - Iced", Multitouch::update, Multitouch::view) + iced::application(Multitouch::default, Multitouch::update, Multitouch::view) .antialiasing(true) .centered() .run() diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index 17ba5804..aad55122 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -6,7 +6,7 @@ use iced::widget::{ use iced::{Center, Color, Element, Fill, Size, Subscription}; pub fn main() -> iced::Result { - iced::application("Pane Grid - Iced", Example::update, Example::view) + iced::application(Example::default, Example::update, Example::view) .subscription(Example::subscription) .run() } diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs index d8b2b389..33aa6cda 100644 --- a/examples/pick_list/src/main.rs +++ b/examples/pick_list/src/main.rs @@ -2,7 +2,7 @@ use iced::widget::{column, pick_list, scrollable, vertical_space}; use iced::{Center, Element, Fill}; pub fn main() -> iced::Result { - iced::run("Pick List - Iced", Example::update, Example::view) + iced::run(Example::update, Example::view) } #[derive(Default)] diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 2e972f6b..cfbef4af 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -3,8 +3,9 @@ use iced::widget::{self, center, column, image, row, text}; use iced::{Center, Element, Fill, Right, Task}; pub fn main() -> iced::Result { - iced::application(Pokedex::title, Pokedex::update, Pokedex::view) - .run_with(Pokedex::new) + iced::application(Pokedex::new, Pokedex::update, Pokedex::view) + .title(Pokedex::title) + .run() } #[derive(Debug)] diff --git a/examples/progress_bar/src/main.rs b/examples/progress_bar/src/main.rs index 07e07f14..df1c4dc2 100644 --- a/examples/progress_bar/src/main.rs +++ b/examples/progress_bar/src/main.rs @@ -5,7 +5,7 @@ use iced::widget::{ }; pub fn main() -> iced::Result { - iced::run("Progress Bar - Iced", Progress::update, Progress::view) + iced::run(Progress::update, Progress::view) } #[derive(Default)] diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs index 2c892e3f..91625c95 100644 --- a/examples/qr_code/src/main.rs +++ b/examples/qr_code/src/main.rs @@ -7,7 +7,7 @@ use std::ops::RangeInclusive; pub fn main() -> iced::Result { iced::application( - "QR Code Generator - Iced", + QRGenerator::default, QRGenerator::update, QRGenerator::view, ) diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 7766542d..e4d0fc68 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -15,7 +15,7 @@ use ::image::ColorType; fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::application("Screenshot - Iced", Example::update, Example::view) + iced::application(Example::default, Example::update, Example::view) .subscription(Example::subscription) .run() } diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index fec4e1b4..793aae2a 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -11,7 +11,7 @@ static SCROLLABLE_ID: LazyLock = pub fn main() -> iced::Result { iced::application( - "Scrollable - Iced", + ScrollableDemo::default, ScrollableDemo::update, ScrollableDemo::view, ) diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index a4a89455..60fc1d29 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -8,7 +8,7 @@ use std::fmt::Debug; fn main() -> iced::Result { iced::application( - "Sierpinski Triangle - Iced", + SierpinskiEmulator::default, SierpinskiEmulator::update, SierpinskiEmulator::view, ) diff --git a/examples/slider/src/main.rs b/examples/slider/src/main.rs index ffb5475f..ea11aad9 100644 --- a/examples/slider/src/main.rs +++ b/examples/slider/src/main.rs @@ -2,7 +2,7 @@ use iced::widget::{column, container, iced, slider, text, vertical_slider}; use iced::{Center, Element, Fill}; pub fn main() -> iced::Result { - iced::run("Slider - Iced", Slider::update, Slider::view) + iced::run(Slider::update, Slider::view) } #[derive(Debug, Clone)] diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 1e74f2bd..07450309 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -22,7 +22,7 @@ pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); iced::application( - "Solar System - Iced", + SolarSystem::default, SolarSystem::update, SolarSystem::view, ) diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index 5055216f..e4d5d7d0 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{button, center, column, row, text}; use iced::{Center, Element, Subscription, Theme}; pub fn main() -> iced::Result { - iced::application("Stopwatch - Iced", Stopwatch::update, Stopwatch::view) + iced::application(Stopwatch::default, Stopwatch::update, Stopwatch::view) .subscription(Stopwatch::subscription) .theme(Stopwatch::theme) .run() diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index fce2b162..3f2f50db 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -7,7 +7,7 @@ use iced::widget::{ use iced::{Center, Element, Fill, Subscription, Theme}; pub fn main() -> iced::Result { - iced::application("Styling - Iced", Styling::update, Styling::view) + iced::application(Styling::default, Styling::update, Styling::view) .subscription(Styling::subscription) .theme(Styling::theme) .run() diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index 14d8f164..ce580ca1 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -2,7 +2,7 @@ use iced::widget::{center, center_x, checkbox, column, svg}; use iced::{Element, Fill, color}; pub fn main() -> iced::Result { - iced::run("SVG - Iced", Tiger::update, Tiger::view) + iced::run(Tiger::update, Tiger::view) } #[derive(Debug, Default)] diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs index 27980c9b..f0bdb32d 100644 --- a/examples/system_information/src/main.rs +++ b/examples/system_information/src/main.rs @@ -2,12 +2,7 @@ use iced::widget::{button, center, column, text}; use iced::{Element, Task, system}; pub fn main() -> iced::Result { - iced::application( - "System Information - Iced", - Example::update, - Example::view, - ) - .run_with(Example::new) + iced::application(Example::new, Example::update, Example::view).run() } #[derive(Default)] diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs index 53e268c1..7f9383d2 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -10,7 +10,7 @@ use std::cell::RefCell; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::application("The Matrix - Iced", TheMatrix::update, TheMatrix::view) + iced::application(TheMatrix::default, TheMatrix::update, TheMatrix::view) .subscription(TheMatrix::subscription) .antialiasing(true) .run() diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index dc314df8..42ca62fb 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -9,7 +9,7 @@ use iced::{Center, Element, Fill, Subscription, Task}; use toast::{Status, Toast}; pub fn main() -> iced::Result { - iced::application("Toast - Iced", App::update, App::view) + iced::application(App::default, App::update, App::view) .subscription(App::subscription) .run() } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 65a34c64..5425c986 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -15,11 +15,12 @@ pub fn main() -> iced::Result { #[cfg(not(target_arch = "wasm32"))] tracing_subscriber::fmt::init(); - iced::application(Todos::title, Todos::update, Todos::view) + iced::application(Todos::new, Todos::update, Todos::view) .subscription(Todos::subscription) + .title(Todos::title) .font(Todos::ICON_FONT) .window_size((500.0, 800.0)) - .run_with(Todos::new) + .run() } #[derive(Debug)] diff --git a/examples/tooltip/src/main.rs b/examples/tooltip/src/main.rs index 9e4e7cbe..599fcd4e 100644 --- a/examples/tooltip/src/main.rs +++ b/examples/tooltip/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::tooltip::Position; use iced::widget::{button, center, container, tooltip}; pub fn main() -> iced::Result { - iced::run("Tooltip - Iced", Tooltip::update, Tooltip::view) + iced::run(Tooltip::update, Tooltip::view) } #[derive(Default)] diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 060cd6d0..f34c9da0 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -17,7 +17,8 @@ pub fn main() -> iced::Result { #[cfg(not(target_arch = "wasm32"))] tracing_subscriber::fmt::init(); - iced::application(Tour::title, Tour::update, Tour::view) + iced::application(Tour::default, Tour::update, Tour::view) + .title(Tour::title) .centered() .run() } diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs index 50a055f3..7c3e3f35 100644 --- a/examples/url_handler/src/main.rs +++ b/examples/url_handler/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::{center, text}; use iced::{Element, Subscription}; pub fn main() -> iced::Result { - iced::application("URL Handler - Iced", App::update, App::view) + iced::application(App::default, App::update, App::view) .subscription(App::subscription) .run() } diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index c4ce7072..6bdd92bc 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -7,7 +7,7 @@ use iced::{Center, Element, Fill, Point, Rectangle, Renderer, Theme, Vector}; pub fn main() -> iced::Result { iced::application( - "Vectorial Text - Iced", + VectorialText::default, VectorialText::update, VectorialText::view, ) diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index f8f9f420..0b152e44 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -10,7 +10,7 @@ use iced::{ }; pub fn main() -> iced::Result { - iced::application("Visible Bounds - Iced", Example::update, Example::view) + iced::application(Example::default, Example::update, Example::view) .subscription(Example::subscription) .theme(|_| Theme::Dark) .run() diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index b918c479..069ca391 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -7,9 +7,9 @@ use iced::{Center, Element, Fill, Subscription, Task, color}; use std::sync::LazyLock; pub fn main() -> iced::Result { - iced::application("WebSocket - Iced", WebSocket::update, WebSocket::view) + iced::application(WebSocket::new, WebSocket::update, WebSocket::view) .subscription(WebSocket::subscription) - .run_with(WebSocket::new) + .run() } struct WebSocket { diff --git a/graphics/src/settings.rs b/graphics/src/settings.rs index 118ed73b..6773937a 100644 --- a/graphics/src/settings.rs +++ b/graphics/src/settings.rs @@ -1,5 +1,5 @@ use crate::Antialiasing; -use crate::core::{Font, Pixels}; +use crate::core::{self, Font, Pixels}; /// The settings of a renderer. #[derive(Debug, Clone, Copy, PartialEq)] @@ -27,3 +27,13 @@ impl Default for Settings { } } } + +impl From for Settings { + fn from(settings: core::Settings) -> Self { + Self { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: settings.antialiasing.then_some(Antialiasing::MSAAx4), + } + } +} diff --git a/program/Cargo.toml b/program/Cargo.toml new file mode 100644 index 00000000..07880705 --- /dev/null +++ b/program/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "iced_program" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +categories.workspace = true +keywords.workspace = true +rust-version.workspace = true + +[dependencies] +iced_graphics.workspace = true +iced_runtime.workspace = true + +[lints] +workspace = true diff --git a/src/program.rs b/program/src/lib.rs similarity index 74% rename from src/program.rs rename to program/src/lib.rs index 602e8227..7e5757de 100644 --- a/src/program.rs +++ b/program/src/lib.rs @@ -1,14 +1,21 @@ -use crate::core::text; -use crate::graphics::compositor; -use crate::shell; -use crate::theme; -use crate::window; -use crate::{Element, Executor, Result, Settings, Subscription, Task}; +//! The definition of an iced program. +pub use iced_graphics as graphics; +pub use iced_runtime as runtime; +pub use iced_runtime::core; +pub use iced_runtime::futures; -/// The internal definition of a [`Program`]. +use crate::core::Element; +use crate::core::text; +use crate::core::theme; +use crate::core::window; +use crate::futures::{Executor, Subscription}; +use crate::graphics::compositor; +use crate::runtime::Task; + +/// An interactive, native, cross-platform, multi-windowed application. /// -/// You should not need to implement this trait directly. Instead, use the -/// methods available in the [`Program`] struct. +/// A [`Program`] can execute asynchronous actions by returning a +/// [`Task`] in some of its methods. #[allow(missing_docs)] pub trait Program: Sized { /// The state of the program. @@ -26,6 +33,11 @@ pub trait Program: Sized { /// The executor of the program. type Executor: Executor; + /// Returns the unique name of the [`Program`]. + fn name() -> &'static str; + + fn boot(&self) -> (Self::State, Task); + fn update( &self, state: &mut Self::State, @@ -39,7 +51,32 @@ pub trait Program: Sized { ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer>; fn title(&self, _state: &Self::State, _window: window::Id) -> String { - String::from("A cool iced application!") + let mut title = String::new(); + + for (i, part) in Self::name().split("_").enumerate() { + use std::borrow::Cow; + + let part = match part { + "a" | "an" | "of" | "in" | "and" => Cow::Borrowed(part), + _ => { + let mut part = part.to_owned(); + + if let Some(first_letter) = part.get_mut(0..1) { + first_letter.make_ascii_uppercase(); + } + + Cow::Owned(part) + } + }; + + if i > 0 { + title.push(' '); + } + + title.push_str(&part); + } + + format!("{title} - Iced") } fn subscription( @@ -60,142 +97,9 @@ pub trait Program: Sized { fn scale_factor(&self, _state: &Self::State, _window: window::Id) -> f64 { 1.0 } - - /// Runs the [`Program`]. - /// - /// The state of the [`Program`] must implement [`Default`]. - /// If your state does not implement [`Default`], use [`run_with`] - /// instead. - /// - /// [`run_with`]: Self::run_with - fn run( - self, - settings: Settings, - window_settings: Option, - ) -> Result - where - Self: 'static, - Self::State: Default, - { - self.run_with(settings, window_settings, || { - (Self::State::default(), Task::none()) - }) - } - - /// Runs the [`Program`] with the given [`Settings`] and a closure that creates the initial state. - fn run_with( - self, - settings: Settings, - window_settings: Option, - initialize: I, - ) -> Result - where - Self: 'static, - I: FnOnce() -> (Self::State, Task) + 'static, - { - use std::marker::PhantomData; - - struct Instance { - program: P, - state: P::State, - _initialize: PhantomData, - } - - impl (P::State, Task)> - shell::Program for Instance - { - type Message = P::Message; - type Theme = P::Theme; - type Renderer = P::Renderer; - type Flags = (P, I); - type Executor = P::Executor; - - fn name() -> &'static str { - std::any::type_name::() - } - - fn new( - (program, initialize): Self::Flags, - ) -> (Self, Task) { - let (state, task) = initialize(); - - ( - Self { - program, - state, - _initialize: PhantomData, - }, - task, - ) - } - - fn title(&self, window: window::Id) -> String { - self.program.title(&self.state, window) - } - - fn update( - &mut self, - message: Self::Message, - ) -> Task { - self.program.update(&mut self.state, message) - } - - fn view( - &self, - window: window::Id, - ) -> crate::Element<'_, Self::Message, Self::Theme, Self::Renderer> - { - self.program.view(&self.state, window) - } - - fn subscription(&self) -> Subscription { - self.program.subscription(&self.state) - } - - fn theme(&self, window: window::Id) -> Self::Theme { - self.program.theme(&self.state, window) - } - - fn style(&self, theme: &Self::Theme) -> theme::Style { - self.program.style(&self.state, theme) - } - - fn scale_factor(&self, window: window::Id) -> f64 { - self.program.scale_factor(&self.state, window) - } - } - - #[allow(clippy::needless_update)] - let renderer_settings = crate::graphics::Settings { - default_font: settings.default_font, - default_text_size: settings.default_text_size, - antialiasing: if settings.antialiasing { - Some(crate::graphics::Antialiasing::MSAAx4) - } else { - None - }, - ..crate::graphics::Settings::default() - }; - - Ok(shell::program::run::< - Instance, - ::Compositor, - >( - Settings { - id: settings.id, - fonts: settings.fonts, - default_font: settings.default_font, - default_text_size: settings.default_text_size, - antialiasing: settings.antialiasing, - } - .into(), - renderer_settings, - window_settings, - (self, initialize), - )?) - } } +/// Decorates a [`Program`] with the given title function. pub fn with_title( program: P, title: impl Fn(&P::State, window::Id) -> String, @@ -220,6 +124,14 @@ pub fn with_title( (self.title)(state, window) } + fn name() -> &'static str { + P::name() + } + + fn boot(&self) -> (Self::State, Task) { + self.program.boot() + } + fn update( &self, state: &mut Self::State, @@ -267,6 +179,7 @@ pub fn with_title( WithTitle { program, title } } +/// Decorates a [`Program`] with the given subscription function. pub fn with_subscription( program: P, f: impl Fn(&P::State) -> Subscription, @@ -293,6 +206,14 @@ pub fn with_subscription( (self.subscription)(state) } + fn name() -> &'static str { + P::name() + } + + fn boot(&self) -> (Self::State, Task) { + self.program.boot() + } + fn update( &self, state: &mut Self::State, @@ -340,6 +261,7 @@ pub fn with_subscription( } } +/// Decorates a [`Program`] with the given theme function. pub fn with_theme( program: P, f: impl Fn(&P::State, window::Id) -> P::Theme, @@ -367,6 +289,14 @@ pub fn with_theme( (self.theme)(state, window) } + fn name() -> &'static str { + P::name() + } + + fn boot(&self) -> (Self::State, Task) { + self.program.boot() + } + fn title(&self, state: &Self::State, window: window::Id) -> String { self.program.title(state, window) } @@ -410,6 +340,7 @@ pub fn with_theme( WithTheme { program, theme: f } } +/// Decorates a [`Program`] with the given style function. pub fn with_style( program: P, f: impl Fn(&P::State, &P::Theme) -> theme::Style, @@ -437,6 +368,14 @@ pub fn with_style( (self.style)(state, theme) } + fn name() -> &'static str { + P::name() + } + + fn boot(&self) -> (Self::State, Task) { + self.program.boot() + } + fn title(&self, state: &Self::State, window: window::Id) -> String { self.program.title(state, window) } @@ -480,6 +419,7 @@ pub fn with_style( WithStyle { program, style: f } } +/// Decorates a [`Program`] with the given scale factor function. pub fn with_scale_factor( program: P, f: impl Fn(&P::State, window::Id) -> f64, @@ -503,6 +443,14 @@ pub fn with_scale_factor( self.program.title(state, window) } + fn name() -> &'static str { + P::name() + } + + fn boot(&self) -> (Self::State, Task) { + self.program.boot() + } + fn update( &self, state: &mut Self::State, @@ -553,6 +501,7 @@ pub fn with_scale_factor( } } +/// Decorates a [`Program`] with the given executor function. pub fn with_executor( program: P, ) -> impl Program { @@ -577,6 +526,14 @@ pub fn with_executor( self.program.title(state, window) } + fn name() -> &'static str { + P::name() + } + + fn boot(&self) -> (Self::State, Task) { + self.program.boot() + } + fn update( &self, state: &mut Self::State, @@ -631,3 +588,57 @@ pub fn with_executor( pub trait Renderer: text::Renderer + compositor::Default {} impl Renderer for T where T: text::Renderer + compositor::Default {} + +/// A particular instance of a running [`Program`]. +#[allow(missing_debug_implementations)] +pub struct Instance { + program: P, + state: P::State, +} + +impl Instance

{ + /// Creates a new [`Instance`] of the given [`Program`]. + pub fn new(program: P) -> (Self, Task) { + let (state, task) = program.boot(); + + (Self { program, state }, task) + } + + /// Returns the current title of the [`Instance`]. + pub fn title(&self, window: window::Id) -> String { + self.program.title(&self.state, window) + } + + /// Processes the given message and updates the [`Instance`]. + pub fn update(&mut self, message: P::Message) -> Task { + self.program.update(&mut self.state, message) + } + + /// Produces the current widget tree of the [`Instance`]. + pub fn view( + &self, + window: window::Id, + ) -> Element<'_, P::Message, P::Theme, P::Renderer> { + self.program.view(&self.state, window) + } + + /// Returns the current [`Subscription`] of the [`Instance`]. + pub fn subscription(&self) -> Subscription { + self.program.subscription(&self.state) + } + + /// Returns the current theme of the [`Instance`]. + pub fn theme(&self, window: window::Id) -> P::Theme { + self.program.theme(&self.state, window) + } + + /// Returns the current [`theme::Style`] of the [`Instance`]. + pub fn style(&self, theme: &P::Theme) -> theme::Style { + self.program.style(&self.state, theme) + } + + /// Returns the current scale factor of the [`Instance`]. + pub fn scale_factor(&self, window: window::Id) -> f64 { + self.program.scale_factor(&self.state, window) + } +} diff --git a/src/application.rs b/src/application.rs index c79ed62b..da0fbd37 100644 --- a/src/application.rs +++ b/src/application.rs @@ -6,7 +6,7 @@ //! use iced::Theme; //! //! pub fn main() -> iced::Result { -//! iced::application("A counter", update, view) +//! iced::application(u64::default, update, view) //! .theme(|_| Theme::Dark) //! .centered() //! .run() @@ -31,6 +31,7 @@ //! } //! ``` use crate::program::{self, Program}; +use crate::shell; use crate::theme; use crate::window; use crate::{ @@ -39,14 +40,14 @@ use crate::{ use std::borrow::Cow; -/// Creates an iced [`Application`] given its title, update, and view logic. +/// Creates an iced [`Application`] given its update and view logic. /// /// # Example /// ```no_run /// use iced::widget::{button, column, text, Column}; /// /// pub fn main() -> iced::Result { -/// iced::application("A counter", update, view).run() +/// iced::application(u64::default, update, view).run() /// } /// /// #[derive(Debug, Clone)] @@ -68,7 +69,7 @@ use std::borrow::Cow; /// } /// ``` pub fn application( - title: impl Title, + new: impl New, update: impl Update, view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, ) -> Application> @@ -80,7 +81,8 @@ where { use std::marker::PhantomData; - struct Instance { + struct Instance { + new: New, update: Update, view: View, _state: PhantomData, @@ -89,12 +91,13 @@ where _renderer: PhantomData, } - impl Program - for Instance + impl Program + for Instance where Message: Send + std::fmt::Debug + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, + New: self::New, Update: self::Update, View: for<'a> self::View<'a, State, Message, Theme, Renderer>, { @@ -104,6 +107,16 @@ where 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) { + self.new.new() + } + fn update( &self, state: &mut Self::State, @@ -123,6 +136,7 @@ where Application { raw: Instance { + new, update, view, _state: PhantomData, @@ -133,7 +147,6 @@ where settings: Settings::default(), window: window::Settings::default(), } - .title(title) } /// The underlying definition and configuration of an iced application. @@ -161,19 +174,8 @@ impl Application

{ pub fn run(self) -> Result where Self: 'static, - P::State: Default, { - self.raw.run(self.settings, Some(self.window)) - } - - /// Runs the [`Application`] with a closure that creates the initial state. - pub fn run_with(self, initialize: I) -> Result - where - Self: 'static, - I: FnOnce() -> (P::State, Task) + 'static, - { - self.raw - .run_with(self.settings, Some(self.window), initialize) + Ok(shell::run(self.raw, self.settings, Some(self.window))?) } /// Sets the [`Settings`] that will be used to run the [`Application`]. @@ -305,7 +307,7 @@ impl Application

{ } /// Sets the [`Title`] of the [`Application`]. - pub(crate) fn title( + pub fn title( self, title: impl Title, ) -> Application< @@ -395,6 +397,42 @@ impl Application

{ } } +/// The logic to initialize the `State` of some [`Application`]. +pub trait New { + /// Initializes the [`Application`] state. + #[allow(clippy::new_ret_no_self)] + #[allow(clippy::wrong_self_convention)] + fn new(&self) -> (State, Task); +} + +impl New for T +where + T: Fn() -> C, + C: IntoState, +{ + fn new(&self) -> (State, Task) { + self().into_state() + } +} + +/// TODO +pub trait IntoState { + /// TODO + fn into_state(self) -> (State, Task); +} + +impl IntoState for State { + fn into_state(self) -> (State, Task) { + (self, Task::none()) + } +} + +impl IntoState for (State, Task) { + fn into_state(self) -> (State, Task) { + self + } +} + /// The title logic of some [`Application`]. /// /// This trait is implemented both for `&static str` and diff --git a/src/daemon.rs b/src/daemon.rs index fd6d0278..322cf23e 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,6 +1,7 @@ //! Create and run daemons that run in the background. use crate::application; use crate::program::{self, Program}; +use crate::shell; use crate::theme; use crate::window; use crate::{Element, Executor, Font, Result, Settings, Subscription, Task}; @@ -18,7 +19,7 @@ use std::borrow::Cow; /// /// [`exit`]: crate::exit pub fn daemon( - title: impl Title, + boot: impl application::New, update: impl application::Update, view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, ) -> Daemon> @@ -30,7 +31,8 @@ where { use std::marker::PhantomData; - struct Instance { + struct Instance { + boot: Boot, update: Update, view: View, _state: PhantomData, @@ -39,12 +41,13 @@ where _renderer: PhantomData, } - impl Program - for Instance + impl Program + for Instance where Message: Send + std::fmt::Debug + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, + Boot: application::New, Update: application::Update, View: for<'a> self::View<'a, State, Message, Theme, Renderer>, { @@ -54,6 +57,16 @@ where 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_daemon") + } + + fn boot(&self) -> (Self::State, Task) { + self.boot.new() + } + fn update( &self, state: &mut Self::State, @@ -73,6 +86,7 @@ where Daemon { raw: Instance { + boot, update, view, _state: PhantomData, @@ -82,7 +96,6 @@ where }, settings: Settings::default(), } - .title(title) } /// The underlying definition and configuration of an iced daemon. @@ -109,18 +122,8 @@ impl Daemon

{ pub fn run(self) -> Result where Self: 'static, - P::State: Default, { - self.raw.run(self.settings, None) - } - - /// Runs the [`Daemon`] with a closure that creates the initial state. - pub fn run_with(self, initialize: I) -> Result - where - Self: 'static, - I: FnOnce() -> (P::State, Task) + 'static, - { - self.raw.run_with(self.settings, None, initialize) + Ok(shell::run(self.raw, self.settings, None)?) } /// Sets the [`Settings`] that will be used to run the [`Daemon`]. @@ -157,7 +160,7 @@ impl Daemon

{ } /// Sets the [`Title`] of the [`Daemon`]. - pub(crate) fn title( + pub fn title( self, title: impl Title, ) -> Daemon< diff --git a/src/lib.rs b/src/lib.rs index 95820ed7..ed224d0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,7 @@ //! //! ```rust,no_run //! pub fn main() -> iced::Result { -//! iced::run("A cool counter", update, view) +//! iced::run(update, view) //! } //! # fn update(state: &mut (), message: ()) {} //! # fn view(state: &()) -> iced::Element<()> { iced::widget::text("").into() } @@ -198,16 +198,20 @@ //! calling [`run`]: //! //! ```rust,no_run -//! # #[derive(Default)] //! # struct State; //! use iced::Theme; //! //! pub fn main() -> iced::Result { -//! iced::application("A cool application", update, view) +//! iced::application(new, update, view) //! .theme(theme) //! .run() //! } //! +//! fn new() -> State { +//! // ... +//! # State +//! } +//! //! fn theme(state: &State) -> Theme { //! Theme::TokyoNight //! } @@ -335,7 +339,6 @@ //! You will need to define a `subscription` function and use the [`Application`] builder: //! //! ```rust,no_run -//! # #[derive(Default)] //! # struct State; //! use iced::window; //! use iced::{Size, Subscription}; @@ -346,7 +349,7 @@ //! } //! //! pub fn main() -> iced::Result { -//! iced::application("A cool application", update, view) +//! iced::application(new, update, view) //! .subscription(subscription) //! .run() //! } @@ -354,6 +357,7 @@ //! fn subscription(state: &State) -> Subscription { //! window::resize_events().map(|(_id, size)| Message::WindowResized(size)) //! } +//! # fn new() -> State { State } //! # fn update(state: &mut State, message: Message) {} //! # fn view(state: &State) -> iced::Element { iced::widget::text("").into() } //! ``` @@ -475,6 +479,7 @@ use iced_widget::graphics; use iced_widget::renderer; use iced_winit as shell; use iced_winit::core; +use iced_winit::program; use iced_winit::runtime; pub use iced_futures::futures; @@ -487,7 +492,6 @@ pub use iced_highlighter as highlighter; pub use iced_renderer::wgpu::wgpu; mod error; -mod program; pub mod application; pub mod daemon; @@ -658,7 +662,7 @@ pub type Result = std::result::Result<(), Error>; /// use iced::widget::{button, column, text, Column}; /// /// pub fn main() -> iced::Result { -/// iced::run("A counter", update, view) +/// iced::run(update, view) /// } /// /// #[derive(Debug, Clone)] @@ -680,7 +684,6 @@ pub type Result = std::result::Result<(), Error>; /// } /// ``` pub fn run( - title: impl application::Title + 'static, update: impl application::Update + 'static, view: impl for<'a> application::View<'a, State, Message, Theme, Renderer> + 'static, @@ -691,5 +694,5 @@ where Theme: Default + theme::Base + 'static, Renderer: program::Renderer + 'static, { - application(title, update, view).run() + application(State::default, update, view).run() } diff --git a/winit/Cargo.toml b/winit/Cargo.toml index d5eb9563..f2157978 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -26,9 +26,7 @@ unconditional-rendering = [] [dependencies] iced_debug.workspace = true -iced_futures.workspace = true -iced_graphics.workspace = true -iced_runtime.workspace = true +iced_program.workspace = true log.workspace = true rustc-hash.workspace = true diff --git a/winit/src/error.rs b/winit/src/error.rs index 7687fb17..2b7f4344 100644 --- a/winit/src/error.rs +++ b/winit/src/error.rs @@ -18,7 +18,7 @@ pub enum Error { } impl From for Error { - fn from(error: iced_graphics::Error) -> Error { + fn from(error: graphics::Error) -> Error { Error::GraphicsCreationFailed(error) } } diff --git a/winit/src/lib.rs b/winit/src/lib.rs index d1b2ae99..ff5e814a 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -18,30 +18,1501 @@ html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] -pub use iced_graphics as graphics; -pub use iced_runtime as runtime; -pub use iced_runtime::core; -pub use iced_runtime::debug; -pub use iced_runtime::futures; +pub use iced_program as program; +pub use program::core; +pub use program::graphics; +pub use program::runtime; +pub use runtime::debug; +pub use runtime::futures; pub use winit; pub mod clipboard; pub mod conversion; -pub mod settings; - -#[cfg(feature = "program")] -pub mod program; #[cfg(feature = "system")] pub mod system; mod error; mod proxy; +mod window; pub use clipboard::Clipboard; pub use error::Error; pub use proxy::Proxy; -pub use settings::Settings; -#[cfg(feature = "program")] -pub use program::Program; +use crate::core::keyboard; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::theme; +use crate::core::time::Instant; +use crate::core::widget::operation; +use crate::core::{Point, Settings, Size}; +use crate::futures::futures::channel::mpsc; +use crate::futures::futures::channel::oneshot; +use crate::futures::futures::task; +use crate::futures::futures::{Future, StreamExt}; +use crate::futures::subscription; +use crate::futures::{Executor, Runtime}; +use crate::graphics::{Compositor, compositor}; +use crate::runtime::user_interface::{self, UserInterface}; +use crate::runtime::{Action, Task}; + +use program::Program; +use window::WindowManager; + +use rustc_hash::FxHashMap; +use std::borrow::Cow; +use std::mem::ManuallyDrop; +use std::sync::Arc; + +/// Runs a [`Program`] with the provided settings. +pub fn run

( + program: P, + settings: Settings, + window_settings: Option, +) -> Result<(), Error> +where + P: Program + 'static, + P::Theme: theme::Base, +{ + use winit::event_loop::EventLoop; + + debug::init(P::name()); + + let boot_span = debug::boot(); + + let graphics_settings = settings.clone().into(); + let event_loop = EventLoop::with_user_event() + .build() + .expect("Create event loop"); + + let (proxy, worker) = Proxy::new(event_loop.create_proxy()); + + let mut runtime = { + let executor = + P::Executor::new().map_err(Error::ExecutorCreationFailed)?; + executor.spawn(worker); + + Runtime::new(executor, proxy.clone()) + }; + + let (program, task) = runtime.enter(|| program::Instance::new(program)); + let is_daemon = window_settings.is_none(); + + let task = if let Some(window_settings) = window_settings { + let mut task = Some(task); + + let (_id, open) = runtime::window::open(window_settings); + + open.then(move |_| task.take().unwrap_or(Task::none())) + } else { + task + }; + + if let Some(stream) = runtime::task::into_stream(task) { + runtime.run(stream); + } + + runtime.track(subscription::into_recipes( + runtime.enter(|| program.subscription().map(Action::Output)), + )); + + let (event_sender, event_receiver) = mpsc::unbounded(); + let (control_sender, control_receiver) = mpsc::unbounded(); + + let instance = Box::pin(run_instance::

( + program, + runtime, + proxy.clone(), + event_receiver, + control_sender, + is_daemon, + graphics_settings, + settings.fonts, + )); + + let context = task::Context::from_waker(task::noop_waker_ref()); + + struct Runner { + instance: std::pin::Pin>, + context: task::Context<'static>, + id: Option, + sender: mpsc::UnboundedSender>>, + receiver: mpsc::UnboundedReceiver, + error: Option, + + #[cfg(target_arch = "wasm32")] + canvas: Option, + } + + let runner = Runner { + instance, + context, + id: settings.id, + sender: event_sender, + receiver: control_receiver, + error: None, + + #[cfg(target_arch = "wasm32")] + canvas: None, + }; + + boot_span.finish(); + + impl winit::application::ApplicationHandler> + for Runner + where + Message: std::fmt::Debug, + F: Future, + { + fn resumed( + &mut self, + _event_loop: &winit::event_loop::ActiveEventLoop, + ) { + } + + fn new_events( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + cause: winit::event::StartCause, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::NewEvents(cause)), + ); + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + window_id: winit::window::WindowId, + event: winit::event::WindowEvent, + ) { + #[cfg(target_os = "windows")] + let is_move_or_resize = matches!( + event, + winit::event::WindowEvent::Resized(_) + | winit::event::WindowEvent::Moved(_) + ); + + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::WindowEvent { + window_id, + event, + }), + ); + + // TODO: Remove when unnecessary + // On Windows, we emulate an `AboutToWait` event after every `Resized` event + // since the event loop does not resume during resize interaction. + // More details: https://github.com/rust-windowing/winit/issues/3272 + #[cfg(target_os = "windows")] + { + if is_move_or_resize { + self.process_event( + event_loop, + Event::EventLoopAwakened( + winit::event::Event::AboutToWait, + ), + ); + } + } + } + + fn user_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + action: Action, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::UserEvent( + action, + )), + ); + } + + fn received_url( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + url: String, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened( + winit::event::Event::PlatformSpecific( + winit::event::PlatformSpecific::MacOS( + winit::event::MacOS::ReceivedUrl(url), + ), + ), + ), + ); + } + + fn about_to_wait( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::AboutToWait), + ); + } + } + + impl Runner + where + F: Future, + { + fn process_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + event: Event>, + ) { + if event_loop.exiting() { + return; + } + + self.sender.start_send(event).expect("Send event"); + + loop { + let poll = self.instance.as_mut().poll(&mut self.context); + + match poll { + task::Poll::Pending => match self.receiver.try_next() { + Ok(Some(control)) => match control { + Control::ChangeFlow(flow) => { + use winit::event_loop::ControlFlow; + + match (event_loop.control_flow(), flow) { + ( + ControlFlow::WaitUntil(current), + ControlFlow::WaitUntil(new), + ) if current < new => {} + ( + ControlFlow::WaitUntil(target), + ControlFlow::Wait, + ) if target > Instant::now() => {} + _ => { + event_loop.set_control_flow(flow); + } + } + } + Control::CreateWindow { + id, + settings, + title, + monitor, + on_open, + } => { + let exit_on_close_request = + settings.exit_on_close_request; + + let visible = settings.visible; + + #[cfg(target_arch = "wasm32")] + let target = + settings.platform_specific.target.clone(); + + let window_attributes = + conversion::window_attributes( + settings, + &title, + monitor + .or(event_loop.primary_monitor()), + self.id.clone(), + ) + .with_visible(false); + + #[cfg(target_arch = "wasm32")] + let window_attributes = { + use winit::platform::web::WindowAttributesExtWebSys; + window_attributes + .with_canvas(self.canvas.take()) + }; + + log::info!( + "Window attributes for id `{id:#?}`: {window_attributes:#?}" + ); + + // On macOS, the `position` in `WindowAttributes` represents the "inner" + // position of the window; while on other platforms it's the "outer" position. + // We fix the inconsistency on macOS by positioning the window after creation. + #[cfg(target_os = "macos")] + let mut window_attributes = window_attributes; + + #[cfg(target_os = "macos")] + let position = + window_attributes.position.take(); + + let window = event_loop + .create_window(window_attributes) + .expect("Create window"); + + #[cfg(target_os = "macos")] + if let Some(position) = position { + window.set_outer_position(position); + } + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + + let canvas = window + .canvas() + .expect("Get window canvas"); + + let _ = canvas.set_attribute( + "style", + "display: block; width: 100%; height: 100%", + ); + + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let body = document.body().unwrap(); + + let target = target.and_then(|target| { + body.query_selector(&format!( + "#{target}" + )) + .ok() + .unwrap_or(None) + }); + + match target { + Some(node) => { + let _ = node + .replace_with_with_node_1( + &canvas, + ) + .expect(&format!( + "Could not replace #{}", + node.id() + )); + } + None => { + let _ = body + .append_child(&canvas) + .expect( + "Append canvas to HTML body", + ); + } + }; + } + + self.process_event( + event_loop, + Event::WindowCreated { + id, + window: Arc::new(window), + exit_on_close_request, + make_visible: visible, + on_open, + }, + ); + } + Control::Exit => { + event_loop.exit(); + } + Control::Crash(error) => { + self.error = Some(error); + event_loop.exit(); + } + }, + _ => { + break; + } + }, + task::Poll::Ready(_) => { + event_loop.exit(); + break; + } + }; + } + } + } + + #[cfg(not(target_arch = "wasm32"))] + { + let mut runner = runner; + let _ = event_loop.run_app(&mut runner); + + runner.error.map(Err).unwrap_or(Ok(())) + } + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::EventLoopExtWebSys; + let _ = event_loop.spawn_app(runner); + + Ok(()) + } +} + +#[derive(Debug)] +enum Event { + WindowCreated { + id: window::Id, + window: Arc, + exit_on_close_request: bool, + make_visible: bool, + on_open: oneshot::Sender, + }, + EventLoopAwakened(winit::event::Event), +} + +#[derive(Debug)] +enum Control { + ChangeFlow(winit::event_loop::ControlFlow), + Exit, + Crash(Error), + CreateWindow { + id: window::Id, + settings: window::Settings, + title: String, + monitor: Option, + on_open: oneshot::Sender, + }, +} + +async fn run_instance

( + mut program: program::Instance

, + mut runtime: Runtime, Action>, + mut proxy: Proxy, + mut event_receiver: mpsc::UnboundedReceiver>>, + mut control_sender: mpsc::UnboundedSender, + is_daemon: bool, + graphics_settings: graphics::Settings, + default_fonts: Vec>, +) where + P: Program + 'static, + P::Theme: theme::Base, +{ + use winit::event; + use winit::event_loop::ControlFlow; + + let mut window_manager = WindowManager::new(); + let mut is_window_opening = !is_daemon; + + let mut compositor = None; + let mut events = Vec::new(); + let mut messages = Vec::new(); + let mut actions = 0; + + let mut ui_caches = FxHashMap::default(); + let mut user_interfaces = ManuallyDrop::new(FxHashMap::default()); + let mut clipboard = Clipboard::unconnected(); + let mut compositor_receiver: Option> = None; + + loop { + let event = if compositor_receiver.is_some() { + let compositor_receiver = + compositor_receiver.take().expect("Waiting for compositor"); + + match compositor_receiver.await { + Ok(Ok((new_compositor, event))) => { + compositor = Some(new_compositor); + + Some(event) + } + Ok(Err(error)) => { + control_sender + .start_send(Control::Crash( + Error::GraphicsCreationFailed(error), + )) + .expect("Send control action"); + break; + } + Err(error) => { + panic!("Compositor initialization failed: {error}") + } + } + // Empty the queue if possible + } else if let Ok(event) = event_receiver.try_next() { + event + } else { + event_receiver.next().await + }; + + let Some(event) = event else { + break; + }; + + match event { + Event::WindowCreated { + id, + window, + exit_on_close_request, + make_visible, + on_open, + } => { + if compositor.is_none() { + let (compositor_sender, new_compositor_receiver) = + oneshot::channel(); + + compositor_receiver = Some(new_compositor_receiver); + + let create_compositor = { + let default_fonts = default_fonts.clone(); + + async move { + let mut compositor = + ::Compositor::new(graphics_settings, window.clone()).await; + + if let Ok(compositor) = &mut compositor { + for font in default_fonts { + compositor.load_font(font.clone()); + } + } + + compositor_sender + .send(compositor.map(|compositor| { + ( + compositor, + Event::WindowCreated { + id, + window, + exit_on_close_request, + make_visible, + on_open, + }, + ) + })) + .ok() + .expect("Send compositor"); + } + }; + + #[cfg(not(target_arch = "wasm32"))] + crate::futures::futures::executor::block_on( + create_compositor, + ); + + #[cfg(target_arch = "wasm32")] + { + wasm_bindgen_futures::spawn_local(create_compositor); + } + + continue; + } + + let window = window_manager.insert( + id, + window, + &program, + compositor + .as_mut() + .expect("Compositor must be initialized"), + exit_on_close_request, + ); + + let logical_size = window.state.logical_size(); + + let _ = user_interfaces.insert( + id, + build_user_interface( + &program, + user_interface::Cache::default(), + &mut window.renderer, + logical_size, + id, + ), + ); + let _ = ui_caches.insert(id, user_interface::Cache::default()); + + if make_visible { + window.raw.set_visible(true); + } + + events.push(( + id, + core::Event::Window(window::Event::Opened { + position: window.position(), + size: window.size(), + }), + )); + + if clipboard.window_id().is_none() { + clipboard = Clipboard::connect(window.raw.clone()); + } + + let _ = on_open.send(id); + is_window_opening = false; + } + Event::EventLoopAwakened(event) => { + match event { + event::Event::NewEvents(event::StartCause::Init) => { + for (_id, window) in window_manager.iter_mut() { + window.raw.request_redraw(); + } + } + event::Event::NewEvents( + event::StartCause::ResumeTimeReached { .. }, + ) => { + let now = Instant::now(); + + for (_id, window) in window_manager.iter_mut() { + if let Some(redraw_at) = window.redraw_at { + if redraw_at <= now { + window.raw.request_redraw(); + window.redraw_at = None; + } + } + } + + if let Some(redraw_at) = window_manager.redraw_at() { + let _ = + control_sender.start_send(Control::ChangeFlow( + ControlFlow::WaitUntil(redraw_at), + )); + } else { + let _ = control_sender.start_send( + Control::ChangeFlow(ControlFlow::Wait), + ); + } + } + event::Event::PlatformSpecific( + event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + ), + ) => { + runtime.broadcast( + subscription::Event::PlatformSpecific( + subscription::PlatformSpecific::MacOS( + subscription::MacOS::ReceivedUrl(url), + ), + ), + ); + } + event::Event::UserEvent(action) => { + run_action( + action, + &program, + &mut compositor, + &mut events, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + &mut is_window_opening, + ); + actions += 1; + } + event::Event::WindowEvent { + window_id: id, + event: event::WindowEvent::RedrawRequested, + .. + } => { + let Some(compositor) = &mut compositor else { + continue; + }; + + let Some((id, window)) = + window_manager.get_mut_alias(id) + else { + continue; + }; + + let physical_size = window.state.physical_size(); + + if physical_size.width == 0 || physical_size.height == 0 + { + continue; + } + + if window.viewport_version + != window.state.viewport_version() + { + let logical_size = window.state.logical_size(); + + let layout_span = debug::layout(id); + let ui = user_interfaces + .remove(&id) + .expect("Remove user interface"); + + let _ = user_interfaces.insert( + id, + ui.relayout(logical_size, &mut window.renderer), + ); + layout_span.finish(); + + compositor.configure_surface( + &mut window.surface, + physical_size.width, + physical_size.height, + ); + + window.viewport_version = + window.state.viewport_version(); + } + + let redraw_event = core::Event::Window( + window::Event::RedrawRequested(Instant::now()), + ); + + let cursor = window.state.cursor(); + + let ui = user_interfaces + .get_mut(&id) + .expect("Get user interface"); + + let (ui_state, _) = ui.update( + &[redraw_event.clone()], + cursor, + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + let draw_span = debug::draw(id); + let new_mouse_interaction = ui.draw( + &mut window.renderer, + window.state.theme(), + &renderer::Style { + text_color: window.state.text_color(), + }, + cursor, + ); + draw_span.finish(); + + if new_mouse_interaction != window.mouse_interaction { + window.raw.set_cursor( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); + + window.mouse_interaction = new_mouse_interaction; + } + + runtime.broadcast(subscription::Event::Interaction { + window: id, + event: redraw_event, + status: core::event::Status::Ignored, + }); + + if let user_interface::State::Updated { + redraw_request, + input_method, + } = ui_state + { + window.request_redraw(redraw_request); + window.request_input_method(input_method); + } + + window.draw_preedit(); + + let present_span = debug::present(id); + match compositor.present( + &mut window.renderer, + &mut window.surface, + window.state.viewport(), + window.state.background_color(), + ) { + Ok(()) => { + present_span.finish(); + } + Err(error) => match error { + // This is an unrecoverable error. + compositor::SurfaceError::OutOfMemory => { + panic!("{:?}", error); + } + _ => { + present_span.finish(); + + log::error!( + "Error {error:?} when \ + presenting surface." + ); + + // Try rendering all windows again next frame. + for (_id, window) in + window_manager.iter_mut() + { + window.raw.request_redraw(); + } + } + }, + } + } + event::Event::WindowEvent { + event: window_event, + window_id, + } => { + if !is_daemon + && matches!( + window_event, + winit::event::WindowEvent::Destroyed + ) + && !is_window_opening + && window_manager.is_empty() + { + control_sender + .start_send(Control::Exit) + .expect("Send control action"); + + continue; + } + + let Some((id, window)) = + window_manager.get_mut_alias(window_id) + else { + continue; + }; + + if matches!( + window_event, + winit::event::WindowEvent::Resized(_) + ) { + window.raw.request_redraw(); + } + + if matches!( + window_event, + winit::event::WindowEvent::CloseRequested + ) && window.exit_on_close_request + { + run_action( + Action::Window(runtime::window::Action::Close( + id, + )), + &program, + &mut compositor, + &mut events, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + &mut is_window_opening, + ); + } else { + window.state.update(&window.raw, &window_event); + + if let Some(event) = conversion::window_event( + window_event, + window.state.scale_factor(), + window.state.modifiers(), + ) { + if matches!( + event, + core::Event::Keyboard( + keyboard::Event::KeyPressed { + modified_key: keyboard::Key::Named( + keyboard::key::Named::F12 + ), + .. + } + ) + ) { + debug::toggle_comet(); + } + + events.push((id, event)); + } + } + } + event::Event::AboutToWait => { + if actions > 0 { + proxy.free_slots(actions); + actions = 0; + } + + if events.is_empty() + && messages.is_empty() + && window_manager.is_idle() + { + continue; + } + + let mut uis_stale = false; + + for (id, window) in window_manager.iter_mut() { + let interact_span = debug::interact(id); + let mut window_events = vec![]; + + events.retain(|(window_id, event)| { + if *window_id == id { + window_events.push(event.clone()); + false + } else { + true + } + }); + + if window_events.is_empty() && messages.is_empty() { + continue; + } + + let (ui_state, statuses) = user_interfaces + .get_mut(&id) + .expect("Get user interface") + .update( + &window_events, + window.state.cursor(), + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + #[cfg(feature = "unconditional-rendering")] + window.request_redraw( + window::RedrawRequest::NextFrame, + ); + + match ui_state { + user_interface::State::Updated { + redraw_request: _redraw_request, + .. + } => { + #[cfg(not( + feature = "unconditional-rendering" + ))] + window.request_redraw(_redraw_request); + } + user_interface::State::Outdated => { + uis_stale = true; + } + } + + for (event, status) in window_events + .into_iter() + .zip(statuses.into_iter()) + { + runtime.broadcast( + subscription::Event::Interaction { + window: id, + event, + status, + }, + ); + } + + interact_span.finish(); + } + + for (id, event) in events.drain(..) { + runtime.broadcast( + subscription::Event::Interaction { + window: id, + event, + status: core::event::Status::Ignored, + }, + ); + } + + if !messages.is_empty() || uis_stale { + let cached_interfaces: FxHashMap< + window::Id, + user_interface::Cache, + > = ManuallyDrop::into_inner(user_interfaces) + .drain() + .map(|(id, ui)| (id, ui.into_cache())) + .collect(); + + update(&mut program, &mut runtime, &mut messages); + + for (id, window) in window_manager.iter_mut() { + window.state.synchronize( + &program, + id, + &window.raw, + ); + + window.raw.request_redraw(); + } + + user_interfaces = + ManuallyDrop::new(build_user_interfaces( + &program, + &mut window_manager, + cached_interfaces, + )); + } + + if let Some(redraw_at) = window_manager.redraw_at() { + let _ = + control_sender.start_send(Control::ChangeFlow( + ControlFlow::WaitUntil(redraw_at), + )); + } else { + let _ = control_sender.start_send( + Control::ChangeFlow(ControlFlow::Wait), + ); + } + } + _ => {} + } + } + } + } + + let _ = ManuallyDrop::into_inner(user_interfaces); +} + +/// Builds a window's [`UserInterface`] for the [`Program`]. +fn build_user_interface<'a, P: Program>( + program: &'a program::Instance

, + cache: user_interface::Cache, + renderer: &mut P::Renderer, + size: Size, + id: window::Id, +) -> UserInterface<'a, P::Message, P::Theme, P::Renderer> +where + P::Theme: theme::Base, +{ + let view_span = debug::view(id); + let view = program.view(id); + view_span.finish(); + + let layout_span = debug::layout(id); + let user_interface = UserInterface::build(view, size, cache, renderer); + layout_span.finish(); + + user_interface +} + +fn update( + program: &mut program::Instance

, + runtime: &mut Runtime, Action>, + messages: &mut Vec, +) where + P::Theme: theme::Base, +{ + for message in messages.drain(..) { + let update_span = debug::update(&message); + let task = runtime.enter(|| program.update(message)); + update_span.finish(); + + if let Some(stream) = runtime::task::into_stream(task) { + runtime.run(stream); + } + } + + let subscription = runtime.enter(|| program.subscription()); + runtime.track(subscription::into_recipes(subscription.map(Action::Output))); +} + +fn run_action( + action: Action, + program: &program::Instance

, + compositor: &mut Option, + events: &mut Vec<(window::Id, core::Event)>, + messages: &mut Vec, + clipboard: &mut Clipboard, + control_sender: &mut mpsc::UnboundedSender, + interfaces: &mut FxHashMap< + window::Id, + UserInterface<'_, P::Message, P::Theme, P::Renderer>, + >, + window_manager: &mut WindowManager, + ui_caches: &mut FxHashMap, + is_window_opening: &mut bool, +) where + P: Program, + C: Compositor + 'static, + P::Theme: theme::Base, +{ + use crate::runtime::clipboard; + use crate::runtime::system; + use crate::runtime::window; + + match action { + Action::Output(message) => { + messages.push(message); + } + Action::Clipboard(action) => match action { + clipboard::Action::Read { target, channel } => { + let _ = channel.send(clipboard.read(target)); + } + clipboard::Action::Write { target, contents } => { + clipboard.write(target, contents); + } + }, + Action::Window(action) => match action { + window::Action::Open(id, settings, channel) => { + let monitor = window_manager.last_monitor(); + + control_sender + .start_send(Control::CreateWindow { + id, + settings, + title: program.title(id), + monitor, + on_open: channel, + }) + .expect("Send control action"); + + *is_window_opening = true; + } + window::Action::Close(id) => { + let _ = ui_caches.remove(&id); + let _ = interfaces.remove(&id); + + if let Some(window) = window_manager.remove(id) { + if clipboard.window_id() == Some(window.raw.id()) { + *clipboard = window_manager + .first() + .map(|window| window.raw.clone()) + .map(Clipboard::connect) + .unwrap_or_else(Clipboard::unconnected); + } + + events.push(( + id, + core::Event::Window(core::window::Event::Closed), + )); + } + + if window_manager.is_empty() { + *compositor = None; + } + } + window::Action::GetOldest(channel) => { + let id = + window_manager.iter_mut().next().map(|(id, _window)| id); + + let _ = channel.send(id); + } + window::Action::GetLatest(channel) => { + let id = + window_manager.iter_mut().last().map(|(id, _window)| id); + + let _ = channel.send(id); + } + window::Action::Drag(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.drag_window(); + } + } + window::Action::DragResize(id, direction) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.drag_resize_window( + conversion::resize_direction(direction), + ); + } + } + window::Action::Resize(id, size) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.request_inner_size( + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + }, + ); + } + } + window::Action::SetMinSize(id, size) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_min_inner_size(size.map(|size| { + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + } + })); + } + } + window::Action::SetMaxSize(id, size) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_max_inner_size(size.map(|size| { + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + } + })); + } + } + window::Action::SetResizeIncrements(id, increments) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_resize_increments(increments.map(|size| { + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + } + })); + } + } + window::Action::SetResizable(id, resizable) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_resizable(resizable); + } + } + window::Action::GetSize(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let size = window + .raw + .inner_size() + .to_logical(window.raw.scale_factor()); + + let _ = channel.send(Size::new(size.width, size.height)); + } + } + window::Action::GetMaximized(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.is_maximized()); + } + } + window::Action::Maximize(id, maximized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(maximized); + } + } + window::Action::GetMinimized(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.is_minimized()); + } + } + window::Action::Minimize(id, minimized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_minimized(minimized); + } + } + window::Action::GetPosition(id, channel) => { + if let Some(window) = window_manager.get(id) { + let position = window + .raw + .outer_position() + .map(|position| { + let position = position + .to_logical::(window.raw.scale_factor()); + + Point::new(position.x, position.y) + }) + .ok(); + + let _ = channel.send(position); + } + } + window::Action::GetScaleFactor(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let scale_factor = window.raw.scale_factor(); + + let _ = channel.send(scale_factor as f32); + } + } + window::Action::Move(id, position) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_outer_position( + winit::dpi::LogicalPosition { + x: position.x, + y: position.y, + }, + ); + } + } + window::Action::SetMode(id, mode) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_visible(conversion::visible(mode)); + window.raw.set_fullscreen(conversion::fullscreen( + window.raw.current_monitor(), + mode, + )); + } + } + window::Action::SetIcon(id, icon) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_window_icon(conversion::icon(icon)); + } + } + window::Action::GetMode(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let mode = if window.raw.is_visible().unwrap_or(true) { + conversion::mode(window.raw.fullscreen()) + } else { + core::window::Mode::Hidden + }; + + let _ = channel.send(mode); + } + } + window::Action::ToggleMaximize(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(!window.raw.is_maximized()); + } + } + window::Action::ToggleDecorations(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_decorations(!window.raw.is_decorated()); + } + } + window::Action::RequestUserAttention(id, attention_type) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.request_user_attention( + attention_type.map(conversion::user_attention), + ); + } + } + window::Action::GainFocus(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.focus_window(); + } + } + window::Action::SetLevel(id, level) => { + if let Some(window) = window_manager.get_mut(id) { + window + .raw + .set_window_level(conversion::window_level(level)); + } + } + window::Action::ShowSystemMenu(id) => { + if let Some(window) = window_manager.get_mut(id) { + if let mouse::Cursor::Available(point) = + window.state.cursor() + { + window.raw.show_window_menu( + winit::dpi::LogicalPosition { + x: point.x, + y: point.y, + }, + ); + } + } + } + window::Action::GetRawId(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.id().into()); + } + } + window::Action::RunWithHandle(id, f) => { + use window::raw_window_handle::HasWindowHandle; + + if let Some(handle) = window_manager + .get_mut(id) + .and_then(|window| window.raw.window_handle().ok()) + { + f(handle); + } + } + window::Action::Screenshot(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + if let Some(compositor) = compositor { + let bytes = compositor.screenshot( + &mut window.renderer, + window.state.viewport(), + window.state.background_color(), + ); + + let _ = channel.send(core::window::Screenshot::new( + bytes, + window.state.physical_size(), + window.state.viewport().scale_factor(), + )); + } + } + } + window::Action::EnableMousePassthrough(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.set_cursor_hittest(false); + } + } + window::Action::DisableMousePassthrough(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.set_cursor_hittest(true); + } + } + }, + Action::System(action) => match action { + system::Action::QueryInformation(_channel) => { + #[cfg(feature = "system")] + { + if let Some(compositor) = compositor { + let graphics_info = compositor.fetch_information(); + + let _ = std::thread::spawn(move || { + let information = + crate::system::information(graphics_info); + + let _ = _channel.send(information); + }); + } + } + } + }, + Action::Widget(operation) => { + let mut current_operation = Some(operation); + + while let Some(mut operation) = current_operation.take() { + for (id, ui) in interfaces.iter_mut() { + if let Some(window) = window_manager.get_mut(*id) { + ui.operate(&window.renderer, operation.as_mut()); + } + } + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(()) => {} + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + } + } + } + Action::LoadFont { bytes, channel } => { + if let Some(compositor) = compositor { + // TODO: Error handling (?) + compositor.load_font(bytes.clone()); + + let _ = channel.send(Ok(())); + } + } + Action::Exit => { + control_sender + .start_send(Control::Exit) + .expect("Send control action"); + } + } +} + +/// Build the user interface for every window. +pub fn build_user_interfaces<'a, P: Program, C>( + program: &'a program::Instance

, + window_manager: &mut WindowManager, + mut cached_user_interfaces: FxHashMap, +) -> FxHashMap> +where + C: Compositor, + P::Theme: theme::Base, +{ + cached_user_interfaces + .drain() + .filter_map(|(id, cache)| { + let window = window_manager.get_mut(id)?; + + Some(( + id, + build_user_interface( + program, + cache, + &mut window.renderer, + window.state.logical_size(), + id, + ), + )) + }) + .collect() +} + +/// Returns true if the provided event should cause a [`Program`] to +/// exit. +pub fn user_force_quit( + event: &winit::event::WindowEvent, + _modifiers: winit::keyboard::ModifiersState, +) -> bool { + match event { + #[cfg(target_os = "macos")] + winit::event::WindowEvent::KeyboardInput { + event: + winit::event::KeyEvent { + logical_key: winit::keyboard::Key::Character(c), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } if c == "q" && _modifiers.super_key() => true, + _ => false, + } +} diff --git a/winit/src/program.rs b/winit/src/program.rs deleted file mode 100644 index b0344050..00000000 --- a/winit/src/program.rs +++ /dev/null @@ -1,1599 +0,0 @@ -//! Create interactive, native cross-platform applications for WGPU. -mod state; -mod window_manager; - -pub use state::State; - -use crate::conversion; -use crate::core; -use crate::core::keyboard; -use crate::core::mouse; -use crate::core::renderer; -use crate::core::theme; -use crate::core::time::Instant; -use crate::core::widget::operation; -use crate::core::window; -use crate::core::{Element, Point, Size}; -use crate::futures::futures::channel::mpsc; -use crate::futures::futures::channel::oneshot; -use crate::futures::futures::task; -use crate::futures::futures::{Future, StreamExt}; -use crate::futures::subscription::{self, Subscription}; -use crate::futures::{Executor, Runtime}; -use crate::graphics; -use crate::graphics::{Compositor, compositor}; -use crate::runtime::debug; -use crate::runtime::user_interface::{self, UserInterface}; -use crate::runtime::{self, Action, Task}; -use crate::{Clipboard, Error, Proxy, Settings}; - -use window_manager::WindowManager; - -use rustc_hash::FxHashMap; -use std::borrow::Cow; -use std::mem::ManuallyDrop; -use std::sync::Arc; - -/// An interactive, native, cross-platform, multi-windowed application. -/// -/// This trait is the main entrypoint of multi-window Iced. Once implemented, you can run -/// your GUI application by simply calling [`run`]. It will run in -/// its own window. -/// -/// A [`Program`] can execute asynchronous actions by returning a -/// [`Task`] in some of its methods. -/// -/// When using a [`Program`] with the `debug` feature enabled, a debug view -/// can be toggled by pressing `F12`. -pub trait Program -where - Self: Sized, - Self::Theme: theme::Base, -{ - /// The type of __messages__ your [`Program`] will produce. - type Message: std::fmt::Debug + Send; - - /// The theme used to draw the [`Program`]. - type Theme; - - /// The [`Executor`] that will run commands and subscriptions. - /// - /// The [default executor] can be a good starting point! - /// - /// [`Executor`]: Self::Executor - /// [default executor]: crate::futures::backend::default::Executor - type Executor: Executor; - - /// The graphics backend to use to draw the [`Program`]. - type Renderer: core::Renderer + core::text::Renderer; - - /// The data needed to initialize your [`Program`]. - type Flags; - - /// Returns the unique name of the [`Program`]. - fn name() -> &'static str; - - /// Initializes the [`Program`] with the flags provided to - /// [`run`] as part of the [`Settings`]. - /// - /// Here is where you should return the initial state of your app. - /// - /// Additionally, you can return a [`Task`] if you need to perform some - /// async action in the background on startup. This is useful if you want to - /// load state from a file, perform an initial HTTP request, etc. - fn new(flags: Self::Flags) -> (Self, Task); - - /// Returns the current title of the [`Program`]. - /// - /// This title can be dynamic! The runtime will automatically update the - /// title of your application when necessary. - fn title(&self, window: window::Id) -> String; - - /// Handles a __message__ and updates the state of the [`Program`]. - /// - /// This is where you define your __update logic__. All the __messages__, - /// produced by either user interactions or commands, will be handled by - /// this method. - /// - /// Any [`Task`] returned will be executed immediately in the background by the - /// runtime. - fn update(&mut self, message: Self::Message) -> Task; - - /// Returns the widgets to display in the [`Program`] for the `window`. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view( - &self, - window: window::Id, - ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; - - /// Returns the current `Theme` of the [`Program`]. - fn theme(&self, window: window::Id) -> Self::Theme; - - /// Returns the `Style` variation of the `Theme`. - fn style(&self, theme: &Self::Theme) -> theme::Style { - theme::Base::base(theme) - } - - /// Returns the event `Subscription` for the current state of the - /// application. - /// - /// The messages produced by the `Subscription` will be handled by - /// [`update`](#tymethod.update). - /// - /// A `Subscription` will be kept alive as long as you keep returning it! - /// - /// By default, it returns an empty subscription. - fn subscription(&self) -> Subscription { - Subscription::none() - } - - /// Returns the scale factor of the window of the [`Program`]. - /// - /// It can be used to dynamically control the size of the UI at runtime - /// (i.e. zooming). - /// - /// For instance, a scale factor of `2.0` will make widgets twice as big, - /// while a scale factor of `0.5` will shrink them to half their size. - /// - /// By default, it returns `1.0`. - #[allow(unused_variables)] - fn scale_factor(&self, window: window::Id) -> f64 { - 1.0 - } -} - -/// Runs a [`Program`] with an executor, compositor, and the provided -/// settings. -pub fn run( - settings: Settings, - graphics_settings: graphics::Settings, - window_settings: Option, - flags: P::Flags, -) -> Result<(), Error> -where - P: Program + 'static, - C: Compositor + 'static, - P::Theme: theme::Base, -{ - use winit::event_loop::EventLoop; - - debug::init(P::name()); - - let boot_span = debug::boot(); - - let event_loop = EventLoop::with_user_event() - .build() - .expect("Create event loop"); - - let (proxy, worker) = Proxy::new(event_loop.create_proxy()); - - let mut runtime = { - let executor = - P::Executor::new().map_err(Error::ExecutorCreationFailed)?; - executor.spawn(worker); - - Runtime::new(executor, proxy.clone()) - }; - - let (program, task) = runtime.enter(|| P::new(flags)); - let is_daemon = window_settings.is_none(); - - let task = if let Some(window_settings) = window_settings { - let mut task = Some(task); - - let (_id, open) = runtime::window::open(window_settings); - - open.then(move |_| task.take().unwrap_or(Task::none())) - } else { - task - }; - - if let Some(stream) = runtime::task::into_stream(task) { - runtime.run(stream); - } - - runtime.track(subscription::into_recipes( - runtime.enter(|| program.subscription().map(Action::Output)), - )); - - let (event_sender, event_receiver) = mpsc::unbounded(); - let (control_sender, control_receiver) = mpsc::unbounded(); - - let instance = Box::pin(run_instance::( - program, - runtime, - proxy.clone(), - event_receiver, - control_sender, - is_daemon, - graphics_settings, - settings.fonts, - )); - - let context = task::Context::from_waker(task::noop_waker_ref()); - - struct Runner { - instance: std::pin::Pin>, - context: task::Context<'static>, - id: Option, - sender: mpsc::UnboundedSender>>, - receiver: mpsc::UnboundedReceiver, - error: Option, - - #[cfg(target_arch = "wasm32")] - canvas: Option, - } - - let runner = Runner { - instance, - context, - id: settings.id, - sender: event_sender, - receiver: control_receiver, - error: None, - - #[cfg(target_arch = "wasm32")] - canvas: None, - }; - - boot_span.finish(); - - impl winit::application::ApplicationHandler> - for Runner - where - Message: std::fmt::Debug, - F: Future, - { - fn resumed( - &mut self, - _event_loop: &winit::event_loop::ActiveEventLoop, - ) { - } - - fn new_events( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - cause: winit::event::StartCause, - ) { - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::NewEvents(cause)), - ); - } - - fn window_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - window_id: winit::window::WindowId, - event: winit::event::WindowEvent, - ) { - #[cfg(target_os = "windows")] - let is_move_or_resize = matches!( - event, - winit::event::WindowEvent::Resized(_) - | winit::event::WindowEvent::Moved(_) - ); - - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::WindowEvent { - window_id, - event, - }), - ); - - // TODO: Remove when unnecessary - // On Windows, we emulate an `AboutToWait` event after every `Resized` event - // since the event loop does not resume during resize interaction. - // More details: https://github.com/rust-windowing/winit/issues/3272 - #[cfg(target_os = "windows")] - { - if is_move_or_resize { - self.process_event( - event_loop, - Event::EventLoopAwakened( - winit::event::Event::AboutToWait, - ), - ); - } - } - } - - fn user_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - action: Action, - ) { - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::UserEvent( - action, - )), - ); - } - - fn received_url( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - url: String, - ) { - self.process_event( - event_loop, - Event::EventLoopAwakened( - winit::event::Event::PlatformSpecific( - winit::event::PlatformSpecific::MacOS( - winit::event::MacOS::ReceivedUrl(url), - ), - ), - ), - ); - } - - fn about_to_wait( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - ) { - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::AboutToWait), - ); - } - } - - impl Runner - where - F: Future, - { - fn process_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - event: Event>, - ) { - if event_loop.exiting() { - return; - } - - self.sender.start_send(event).expect("Send event"); - - loop { - let poll = self.instance.as_mut().poll(&mut self.context); - - match poll { - task::Poll::Pending => match self.receiver.try_next() { - Ok(Some(control)) => match control { - Control::ChangeFlow(flow) => { - use winit::event_loop::ControlFlow; - - match (event_loop.control_flow(), flow) { - ( - ControlFlow::WaitUntil(current), - ControlFlow::WaitUntil(new), - ) if current < new => {} - ( - ControlFlow::WaitUntil(target), - ControlFlow::Wait, - ) if target > Instant::now() => {} - _ => { - event_loop.set_control_flow(flow); - } - } - } - Control::CreateWindow { - id, - settings, - title, - monitor, - on_open, - } => { - let exit_on_close_request = - settings.exit_on_close_request; - - let visible = settings.visible; - - #[cfg(target_arch = "wasm32")] - let target = - settings.platform_specific.target.clone(); - - let window_attributes = - conversion::window_attributes( - settings, - &title, - monitor - .or(event_loop.primary_monitor()), - self.id.clone(), - ) - .with_visible(false); - - #[cfg(target_arch = "wasm32")] - let window_attributes = { - use winit::platform::web::WindowAttributesExtWebSys; - window_attributes - .with_canvas(self.canvas.take()) - }; - - log::info!( - "Window attributes for id `{id:#?}`: {window_attributes:#?}" - ); - - // On macOS, the `position` in `WindowAttributes` represents the "inner" - // position of the window; while on other platforms it's the "outer" position. - // We fix the inconsistency on macOS by positioning the window after creation. - #[cfg(target_os = "macos")] - let mut window_attributes = window_attributes; - - #[cfg(target_os = "macos")] - let position = - window_attributes.position.take(); - - let window = event_loop - .create_window(window_attributes) - .expect("Create window"); - - #[cfg(target_os = "macos")] - if let Some(position) = position { - window.set_outer_position(position); - } - - #[cfg(target_arch = "wasm32")] - { - use winit::platform::web::WindowExtWebSys; - - let canvas = window - .canvas() - .expect("Get window canvas"); - - let _ = canvas.set_attribute( - "style", - "display: block; width: 100%; height: 100%", - ); - - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let body = document.body().unwrap(); - - let target = target.and_then(|target| { - body.query_selector(&format!( - "#{target}" - )) - .ok() - .unwrap_or(None) - }); - - match target { - Some(node) => { - let _ = node - .replace_with_with_node_1( - &canvas, - ) - .expect(&format!( - "Could not replace #{}", - node.id() - )); - } - None => { - let _ = body - .append_child(&canvas) - .expect( - "Append canvas to HTML body", - ); - } - }; - } - - self.process_event( - event_loop, - Event::WindowCreated { - id, - window: Arc::new(window), - exit_on_close_request, - make_visible: visible, - on_open, - }, - ); - } - Control::Exit => { - event_loop.exit(); - } - Control::Crash(error) => { - self.error = Some(error); - event_loop.exit(); - } - }, - _ => { - break; - } - }, - task::Poll::Ready(_) => { - event_loop.exit(); - break; - } - }; - } - } - } - - #[cfg(not(target_arch = "wasm32"))] - { - let mut runner = runner; - let _ = event_loop.run_app(&mut runner); - - runner.error.map(Err).unwrap_or(Ok(())) - } - - #[cfg(target_arch = "wasm32")] - { - use winit::platform::web::EventLoopExtWebSys; - let _ = event_loop.spawn_app(runner); - - Ok(()) - } -} - -#[derive(Debug)] -enum Event { - WindowCreated { - id: window::Id, - window: Arc, - exit_on_close_request: bool, - make_visible: bool, - on_open: oneshot::Sender, - }, - EventLoopAwakened(winit::event::Event), -} - -#[derive(Debug)] -enum Control { - ChangeFlow(winit::event_loop::ControlFlow), - Exit, - Crash(Error), - CreateWindow { - id: window::Id, - settings: window::Settings, - title: String, - monitor: Option, - on_open: oneshot::Sender, - }, -} - -async fn run_instance( - mut program: P, - mut runtime: Runtime, Action>, - mut proxy: Proxy, - mut event_receiver: mpsc::UnboundedReceiver>>, - mut control_sender: mpsc::UnboundedSender, - is_daemon: bool, - graphics_settings: graphics::Settings, - default_fonts: Vec>, -) where - P: Program + 'static, - C: Compositor + 'static, - P::Theme: theme::Base, -{ - use winit::event; - use winit::event_loop::ControlFlow; - - let mut window_manager = WindowManager::new(); - let mut is_window_opening = !is_daemon; - - let mut compositor = None; - let mut events = Vec::new(); - let mut messages = Vec::new(); - let mut actions = 0; - - let mut ui_caches = FxHashMap::default(); - let mut user_interfaces = ManuallyDrop::new(FxHashMap::default()); - let mut clipboard = Clipboard::unconnected(); - let mut compositor_receiver: Option> = None; - - loop { - let event = if compositor_receiver.is_some() { - let compositor_receiver = - compositor_receiver.take().expect("Waiting for compositor"); - - match compositor_receiver.await { - Ok(Ok((new_compositor, event))) => { - compositor = Some(new_compositor); - - Some(event) - } - Ok(Err(error)) => { - control_sender - .start_send(Control::Crash( - Error::GraphicsCreationFailed(error), - )) - .expect("Send control action"); - break; - } - Err(error) => { - panic!("Compositor initialization failed: {error}") - } - } - // Empty the queue if possible - } else if let Ok(event) = event_receiver.try_next() { - event - } else { - event_receiver.next().await - }; - - let Some(event) = event else { - break; - }; - - match event { - Event::WindowCreated { - id, - window, - exit_on_close_request, - make_visible, - on_open, - } => { - if compositor.is_none() { - let (compositor_sender, new_compositor_receiver) = - oneshot::channel(); - - compositor_receiver = Some(new_compositor_receiver); - - let create_compositor = { - let default_fonts = default_fonts.clone(); - - async move { - let mut compositor = - C::new(graphics_settings, window.clone()).await; - - if let Ok(compositor) = &mut compositor { - for font in default_fonts { - compositor.load_font(font.clone()); - } - } - - compositor_sender - .send(compositor.map(|compositor| { - ( - compositor, - Event::WindowCreated { - id, - window, - exit_on_close_request, - make_visible, - on_open, - }, - ) - })) - .ok() - .expect("Send compositor"); - } - }; - - #[cfg(not(target_arch = "wasm32"))] - crate::futures::futures::executor::block_on( - create_compositor, - ); - - #[cfg(target_arch = "wasm32")] - { - wasm_bindgen_futures::spawn_local(create_compositor); - } - - continue; - } - - let window = window_manager.insert( - id, - window, - &program, - compositor - .as_mut() - .expect("Compositor must be initialized"), - exit_on_close_request, - ); - - let logical_size = window.state.logical_size(); - - let _ = user_interfaces.insert( - id, - build_user_interface( - &program, - user_interface::Cache::default(), - &mut window.renderer, - logical_size, - id, - ), - ); - let _ = ui_caches.insert(id, user_interface::Cache::default()); - - if make_visible { - window.raw.set_visible(true); - } - - events.push(( - id, - core::Event::Window(window::Event::Opened { - position: window.position(), - size: window.size(), - }), - )); - - if clipboard.window_id().is_none() { - clipboard = Clipboard::connect(window.raw.clone()); - } - - let _ = on_open.send(id); - is_window_opening = false; - } - Event::EventLoopAwakened(event) => { - match event { - event::Event::NewEvents(event::StartCause::Init) => { - for (_id, window) in window_manager.iter_mut() { - window.raw.request_redraw(); - } - } - event::Event::NewEvents( - event::StartCause::ResumeTimeReached { .. }, - ) => { - let now = Instant::now(); - - for (_id, window) in window_manager.iter_mut() { - if let Some(redraw_at) = window.redraw_at { - if redraw_at <= now { - window.raw.request_redraw(); - window.redraw_at = None; - } - } - } - - if let Some(redraw_at) = window_manager.redraw_at() { - let _ = - control_sender.start_send(Control::ChangeFlow( - ControlFlow::WaitUntil(redraw_at), - )); - } else { - let _ = control_sender.start_send( - Control::ChangeFlow(ControlFlow::Wait), - ); - } - } - event::Event::PlatformSpecific( - event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), - ), - ) => { - runtime.broadcast( - subscription::Event::PlatformSpecific( - subscription::PlatformSpecific::MacOS( - subscription::MacOS::ReceivedUrl(url), - ), - ), - ); - } - event::Event::UserEvent(action) => { - run_action( - action, - &program, - &mut compositor, - &mut events, - &mut messages, - &mut clipboard, - &mut control_sender, - &mut user_interfaces, - &mut window_manager, - &mut ui_caches, - &mut is_window_opening, - ); - actions += 1; - } - event::Event::WindowEvent { - window_id: id, - event: event::WindowEvent::RedrawRequested, - .. - } => { - let Some(compositor) = &mut compositor else { - continue; - }; - - let Some((id, window)) = - window_manager.get_mut_alias(id) - else { - continue; - }; - - let physical_size = window.state.physical_size(); - - if physical_size.width == 0 || physical_size.height == 0 - { - continue; - } - - if window.viewport_version - != window.state.viewport_version() - { - let logical_size = window.state.logical_size(); - - let layout_span = debug::layout(id); - let ui = user_interfaces - .remove(&id) - .expect("Remove user interface"); - - let _ = user_interfaces.insert( - id, - ui.relayout(logical_size, &mut window.renderer), - ); - layout_span.finish(); - - compositor.configure_surface( - &mut window.surface, - physical_size.width, - physical_size.height, - ); - - window.viewport_version = - window.state.viewport_version(); - } - - let redraw_event = core::Event::Window( - window::Event::RedrawRequested(Instant::now()), - ); - - let cursor = window.state.cursor(); - - let ui = user_interfaces - .get_mut(&id) - .expect("Get user interface"); - - let (ui_state, _) = ui.update( - &[redraw_event.clone()], - cursor, - &mut window.renderer, - &mut clipboard, - &mut messages, - ); - - let draw_span = debug::draw(id); - let new_mouse_interaction = ui.draw( - &mut window.renderer, - window.state.theme(), - &renderer::Style { - text_color: window.state.text_color(), - }, - cursor, - ); - draw_span.finish(); - - if new_mouse_interaction != window.mouse_interaction { - window.raw.set_cursor( - conversion::mouse_interaction( - new_mouse_interaction, - ), - ); - - window.mouse_interaction = new_mouse_interaction; - } - - runtime.broadcast(subscription::Event::Interaction { - window: id, - event: redraw_event, - status: core::event::Status::Ignored, - }); - - if let user_interface::State::Updated { - redraw_request, - input_method, - } = ui_state - { - window.request_redraw(redraw_request); - window.request_input_method(input_method); - } - - window.draw_preedit(); - - let present_span = debug::present(id); - match compositor.present( - &mut window.renderer, - &mut window.surface, - window.state.viewport(), - window.state.background_color(), - ) { - Ok(()) => { - present_span.finish(); - } - Err(error) => match error { - // This is an unrecoverable error. - compositor::SurfaceError::OutOfMemory => { - panic!("{:?}", error); - } - _ => { - present_span.finish(); - - log::error!( - "Error {error:?} when \ - presenting surface." - ); - - // Try rendering all windows again next frame. - for (_id, window) in - window_manager.iter_mut() - { - window.raw.request_redraw(); - } - } - }, - } - } - event::Event::WindowEvent { - event: window_event, - window_id, - } => { - if !is_daemon - && matches!( - window_event, - winit::event::WindowEvent::Destroyed - ) - && !is_window_opening - && window_manager.is_empty() - { - control_sender - .start_send(Control::Exit) - .expect("Send control action"); - - continue; - } - - let Some((id, window)) = - window_manager.get_mut_alias(window_id) - else { - continue; - }; - - if matches!( - window_event, - winit::event::WindowEvent::Resized(_) - ) { - window.raw.request_redraw(); - } - - if matches!( - window_event, - winit::event::WindowEvent::CloseRequested - ) && window.exit_on_close_request - { - run_action( - Action::Window(runtime::window::Action::Close( - id, - )), - &program, - &mut compositor, - &mut events, - &mut messages, - &mut clipboard, - &mut control_sender, - &mut user_interfaces, - &mut window_manager, - &mut ui_caches, - &mut is_window_opening, - ); - } else { - window.state.update(&window.raw, &window_event); - - if let Some(event) = conversion::window_event( - window_event, - window.state.scale_factor(), - window.state.modifiers(), - ) { - if matches!( - event, - core::Event::Keyboard( - keyboard::Event::KeyPressed { - modified_key: keyboard::Key::Named( - keyboard::key::Named::F12 - ), - .. - } - ) - ) { - debug::toggle_comet(); - } - - events.push((id, event)); - } - } - } - event::Event::AboutToWait => { - if actions > 0 { - proxy.free_slots(actions); - actions = 0; - } - - if events.is_empty() - && messages.is_empty() - && window_manager.is_idle() - { - continue; - } - - let mut uis_stale = false; - - for (id, window) in window_manager.iter_mut() { - let interact_span = debug::interact(id); - let mut window_events = vec![]; - - events.retain(|(window_id, event)| { - if *window_id == id { - window_events.push(event.clone()); - false - } else { - true - } - }); - - if window_events.is_empty() && messages.is_empty() { - continue; - } - - let (ui_state, statuses) = user_interfaces - .get_mut(&id) - .expect("Get user interface") - .update( - &window_events, - window.state.cursor(), - &mut window.renderer, - &mut clipboard, - &mut messages, - ); - - #[cfg(feature = "unconditional-rendering")] - window.request_redraw( - window::RedrawRequest::NextFrame, - ); - - match ui_state { - user_interface::State::Updated { - redraw_request: _redraw_request, - .. - } => { - #[cfg(not( - feature = "unconditional-rendering" - ))] - window.request_redraw(_redraw_request); - } - user_interface::State::Outdated => { - uis_stale = true; - } - } - - for (event, status) in window_events - .into_iter() - .zip(statuses.into_iter()) - { - runtime.broadcast( - subscription::Event::Interaction { - window: id, - event, - status, - }, - ); - } - - interact_span.finish(); - } - - for (id, event) in events.drain(..) { - runtime.broadcast( - subscription::Event::Interaction { - window: id, - event, - status: core::event::Status::Ignored, - }, - ); - } - - if !messages.is_empty() || uis_stale { - let cached_interfaces: FxHashMap< - window::Id, - user_interface::Cache, - > = ManuallyDrop::into_inner(user_interfaces) - .drain() - .map(|(id, ui)| (id, ui.into_cache())) - .collect(); - - update(&mut program, &mut runtime, &mut messages); - - for (id, window) in window_manager.iter_mut() { - window.state.synchronize( - &program, - id, - &window.raw, - ); - - window.raw.request_redraw(); - } - - user_interfaces = - ManuallyDrop::new(build_user_interfaces( - &program, - &mut window_manager, - cached_interfaces, - )); - } - - if let Some(redraw_at) = window_manager.redraw_at() { - let _ = - control_sender.start_send(Control::ChangeFlow( - ControlFlow::WaitUntil(redraw_at), - )); - } else { - let _ = control_sender.start_send( - Control::ChangeFlow(ControlFlow::Wait), - ); - } - } - _ => {} - } - } - } - } - - let _ = ManuallyDrop::into_inner(user_interfaces); -} - -/// Builds a window's [`UserInterface`] for the [`Program`]. -fn build_user_interface<'a, P: Program>( - program: &'a P, - cache: user_interface::Cache, - renderer: &mut P::Renderer, - size: Size, - id: window::Id, -) -> UserInterface<'a, P::Message, P::Theme, P::Renderer> -where - P::Theme: theme::Base, -{ - let view_span = debug::view(id); - let view = program.view(id); - view_span.finish(); - - let layout_span = debug::layout(id); - let user_interface = UserInterface::build(view, size, cache, renderer); - layout_span.finish(); - - user_interface -} - -fn update( - program: &mut P, - runtime: &mut Runtime, Action>, - messages: &mut Vec, -) where - P::Theme: theme::Base, -{ - for message in messages.drain(..) { - let update_span = debug::update(&message); - let task = runtime.enter(|| program.update(message)); - update_span.finish(); - - if let Some(stream) = runtime::task::into_stream(task) { - runtime.run(stream); - } - } - - let subscription = runtime.enter(|| program.subscription()); - runtime.track(subscription::into_recipes(subscription.map(Action::Output))); -} - -fn run_action( - action: Action, - program: &P, - compositor: &mut Option, - events: &mut Vec<(window::Id, core::Event)>, - messages: &mut Vec, - clipboard: &mut Clipboard, - control_sender: &mut mpsc::UnboundedSender, - interfaces: &mut FxHashMap< - window::Id, - UserInterface<'_, P::Message, P::Theme, P::Renderer>, - >, - window_manager: &mut WindowManager, - ui_caches: &mut FxHashMap, - is_window_opening: &mut bool, -) where - P: Program, - C: Compositor + 'static, - P::Theme: theme::Base, -{ - use crate::runtime::clipboard; - use crate::runtime::system; - use crate::runtime::window; - - match action { - Action::Output(message) => { - messages.push(message); - } - Action::Clipboard(action) => match action { - clipboard::Action::Read { target, channel } => { - let _ = channel.send(clipboard.read(target)); - } - clipboard::Action::Write { target, contents } => { - clipboard.write(target, contents); - } - }, - Action::Window(action) => match action { - window::Action::Open(id, settings, channel) => { - let monitor = window_manager.last_monitor(); - - control_sender - .start_send(Control::CreateWindow { - id, - settings, - title: program.title(id), - monitor, - on_open: channel, - }) - .expect("Send control action"); - - *is_window_opening = true; - } - window::Action::Close(id) => { - let _ = ui_caches.remove(&id); - let _ = interfaces.remove(&id); - - if let Some(window) = window_manager.remove(id) { - if clipboard.window_id() == Some(window.raw.id()) { - *clipboard = window_manager - .first() - .map(|window| window.raw.clone()) - .map(Clipboard::connect) - .unwrap_or_else(Clipboard::unconnected); - } - - events.push(( - id, - core::Event::Window(core::window::Event::Closed), - )); - } - - if window_manager.is_empty() { - *compositor = None; - } - } - window::Action::GetOldest(channel) => { - let id = - window_manager.iter_mut().next().map(|(id, _window)| id); - - let _ = channel.send(id); - } - window::Action::GetLatest(channel) => { - let id = - window_manager.iter_mut().last().map(|(id, _window)| id); - - let _ = channel.send(id); - } - window::Action::Drag(id) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = window.raw.drag_window(); - } - } - window::Action::DragResize(id, direction) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = window.raw.drag_resize_window( - conversion::resize_direction(direction), - ); - } - } - window::Action::Resize(id, size) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = window.raw.request_inner_size( - winit::dpi::LogicalSize { - width: size.width, - height: size.height, - }, - ); - } - } - window::Action::SetMinSize(id, size) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_min_inner_size(size.map(|size| { - winit::dpi::LogicalSize { - width: size.width, - height: size.height, - } - })); - } - } - window::Action::SetMaxSize(id, size) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_max_inner_size(size.map(|size| { - winit::dpi::LogicalSize { - width: size.width, - height: size.height, - } - })); - } - } - window::Action::SetResizeIncrements(id, increments) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_resize_increments(increments.map(|size| { - winit::dpi::LogicalSize { - width: size.width, - height: size.height, - } - })); - } - } - window::Action::SetResizable(id, resizable) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_resizable(resizable); - } - } - window::Action::GetSize(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { - let size = window - .raw - .inner_size() - .to_logical(window.raw.scale_factor()); - - let _ = channel.send(Size::new(size.width, size.height)); - } - } - window::Action::GetMaximized(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = channel.send(window.raw.is_maximized()); - } - } - window::Action::Maximize(id, maximized) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_maximized(maximized); - } - } - window::Action::GetMinimized(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = channel.send(window.raw.is_minimized()); - } - } - window::Action::Minimize(id, minimized) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_minimized(minimized); - } - } - window::Action::GetPosition(id, channel) => { - if let Some(window) = window_manager.get(id) { - let position = window - .raw - .outer_position() - .map(|position| { - let position = position - .to_logical::(window.raw.scale_factor()); - - Point::new(position.x, position.y) - }) - .ok(); - - let _ = channel.send(position); - } - } - window::Action::GetScaleFactor(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { - let scale_factor = window.raw.scale_factor(); - - let _ = channel.send(scale_factor as f32); - } - } - window::Action::Move(id, position) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_outer_position( - winit::dpi::LogicalPosition { - x: position.x, - y: position.y, - }, - ); - } - } - window::Action::SetMode(id, mode) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_visible(conversion::visible(mode)); - window.raw.set_fullscreen(conversion::fullscreen( - window.raw.current_monitor(), - mode, - )); - } - } - window::Action::SetIcon(id, icon) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_window_icon(conversion::icon(icon)); - } - } - window::Action::GetMode(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { - let mode = if window.raw.is_visible().unwrap_or(true) { - conversion::mode(window.raw.fullscreen()) - } else { - core::window::Mode::Hidden - }; - - let _ = channel.send(mode); - } - } - window::Action::ToggleMaximize(id) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_maximized(!window.raw.is_maximized()); - } - } - window::Action::ToggleDecorations(id) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_decorations(!window.raw.is_decorated()); - } - } - window::Action::RequestUserAttention(id, attention_type) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.request_user_attention( - attention_type.map(conversion::user_attention), - ); - } - } - window::Action::GainFocus(id) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.focus_window(); - } - } - window::Action::SetLevel(id, level) => { - if let Some(window) = window_manager.get_mut(id) { - window - .raw - .set_window_level(conversion::window_level(level)); - } - } - window::Action::ShowSystemMenu(id) => { - if let Some(window) = window_manager.get_mut(id) { - if let mouse::Cursor::Available(point) = - window.state.cursor() - { - window.raw.show_window_menu( - winit::dpi::LogicalPosition { - x: point.x, - y: point.y, - }, - ); - } - } - } - window::Action::GetRawId(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = channel.send(window.raw.id().into()); - } - } - window::Action::RunWithHandle(id, f) => { - use window::raw_window_handle::HasWindowHandle; - - if let Some(handle) = window_manager - .get_mut(id) - .and_then(|window| window.raw.window_handle().ok()) - { - f(handle); - } - } - window::Action::Screenshot(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { - if let Some(compositor) = compositor { - let bytes = compositor.screenshot( - &mut window.renderer, - window.state.viewport(), - window.state.background_color(), - ); - - let _ = channel.send(core::window::Screenshot::new( - bytes, - window.state.physical_size(), - window.state.viewport().scale_factor(), - )); - } - } - } - window::Action::EnableMousePassthrough(id) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = window.raw.set_cursor_hittest(false); - } - } - window::Action::DisableMousePassthrough(id) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = window.raw.set_cursor_hittest(true); - } - } - }, - Action::System(action) => match action { - system::Action::QueryInformation(_channel) => { - #[cfg(feature = "system")] - { - if let Some(compositor) = compositor { - let graphics_info = compositor.fetch_information(); - - let _ = std::thread::spawn(move || { - let information = - crate::system::information(graphics_info); - - let _ = _channel.send(information); - }); - } - } - } - }, - Action::Widget(operation) => { - let mut current_operation = Some(operation); - - while let Some(mut operation) = current_operation.take() { - for (id, ui) in interfaces.iter_mut() { - if let Some(window) = window_manager.get_mut(*id) { - ui.operate(&window.renderer, operation.as_mut()); - } - } - - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(()) => {} - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } - } - } - } - Action::LoadFont { bytes, channel } => { - if let Some(compositor) = compositor { - // TODO: Error handling (?) - compositor.load_font(bytes.clone()); - - let _ = channel.send(Ok(())); - } - } - Action::Exit => { - control_sender - .start_send(Control::Exit) - .expect("Send control action"); - } - } -} - -/// Build the user interface for every window. -pub fn build_user_interfaces<'a, P: Program, C>( - program: &'a P, - window_manager: &mut WindowManager, - mut cached_user_interfaces: FxHashMap, -) -> FxHashMap> -where - C: Compositor, - P::Theme: theme::Base, -{ - cached_user_interfaces - .drain() - .filter_map(|(id, cache)| { - let window = window_manager.get_mut(id)?; - - Some(( - id, - build_user_interface( - program, - cache, - &mut window.renderer, - window.state.logical_size(), - id, - ), - )) - }) - .collect() -} - -/// Returns true if the provided event should cause a [`Program`] to -/// exit. -pub fn user_force_quit( - event: &winit::event::WindowEvent, - _modifiers: winit::keyboard::ModifiersState, -) -> bool { - match event { - #[cfg(target_os = "macos")] - winit::event::WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: winit::keyboard::Key::Character(c), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } if c == "q" && _modifiers.super_key() => true, - _ => false, - } -} diff --git a/winit/src/settings.rs b/winit/src/settings.rs deleted file mode 100644 index e2bf8abf..00000000 --- a/winit/src/settings.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Configure your application. -use crate::core; - -use std::borrow::Cow; - -/// The settings of an application. -#[derive(Debug, Clone, Default)] -pub struct Settings { - /// The identifier of the application. - /// - /// If provided, this identifier may be used to identify the application or - /// communicate with it through the windowing system. - pub id: Option, - - /// The fonts to load on boot. - pub fonts: Vec>, -} - -impl From for Settings { - fn from(settings: core::Settings) -> Self { - Self { - id: settings.id, - fonts: settings.fonts, - } - } -} diff --git a/winit/src/program/window_manager.rs b/winit/src/window.rs similarity index 98% rename from winit/src/program/window_manager.rs rename to winit/src/window.rs index 3ad574ca..801fc086 100644 --- a/winit/src/program/window_manager.rs +++ b/winit/src/window.rs @@ -1,3 +1,9 @@ +mod state; + +use state::State; + +pub use crate::core::window::{Event, Id, RedrawRequest, Settings}; + use crate::conversion; use crate::core::alignment; use crate::core::input_method; @@ -6,12 +12,11 @@ use crate::core::renderer; use crate::core::text; use crate::core::theme; use crate::core::time::Instant; -use crate::core::window::{Id, RedrawRequest}; use crate::core::{ Color, InputMethod, Padding, Point, Rectangle, Size, Text, Vector, }; use crate::graphics::Compositor; -use crate::program::{Program, State}; +use crate::program::{self, Program}; use winit::dpi::{LogicalPosition, LogicalSize}; use winit::monitor::MonitorHandle; @@ -47,11 +52,11 @@ where &mut self, id: Id, window: Arc, - application: &P, + program: &program::Instance

, compositor: &mut C, exit_on_close_request: bool, ) -> &mut Window { - let state = State::new(application, id, &window); + let state = State::new(program, id, &window); let viewport_version = state.viewport_version(); let physical_size = state.physical_size(); let surface = compositor.create_surface( diff --git a/winit/src/program/state.rs b/winit/src/window/state.rs similarity index 92% rename from winit/src/program/state.rs rename to winit/src/window/state.rs index 1b844b82..e17b32a3 100644 --- a/winit/src/program/state.rs +++ b/winit/src/window/state.rs @@ -2,7 +2,7 @@ use crate::conversion; use crate::core::{Color, Size}; use crate::core::{mouse, theme, window}; use crate::graphics::Viewport; -use crate::program::Program; +use crate::program::{self, Program}; use winit::event::{Touch, WindowEvent}; use winit::window::Window; @@ -46,14 +46,14 @@ where { /// Creates a new [`State`] for the provided [`Program`]'s `window`. pub fn new( - application: &P, + program: &program::Instance

, window_id: window::Id, window: &Window, ) -> Self { - let title = application.title(window_id); - let scale_factor = application.scale_factor(window_id); - let theme = application.theme(window_id); - let style = application.style(&theme); + let title = program.title(window_id); + let scale_factor = program.scale_factor(window_id); + let theme = program.theme(window_id); + let style = program.style(&theme); let viewport = { let physical_size = window.inner_size(); @@ -185,12 +185,12 @@ where /// and window after calling [`State::update`]. pub fn synchronize( &mut self, - application: &P, + program: &program::Instance

, window_id: window::Id, window: &Window, ) { // Update window title - let new_title = application.title(window_id); + let new_title = program.title(window_id); if self.title != new_title { window.set_title(&new_title); @@ -198,7 +198,7 @@ where } // Update scale factor and size - let new_scale_factor = application.scale_factor(window_id); + let new_scale_factor = program.scale_factor(window_id); let new_size = window.inner_size(); let current_size = self.viewport.physical_size(); @@ -216,7 +216,7 @@ where } // Update theme and appearance - self.theme = application.theme(window_id); - self.style = application.style(&self.theme); + self.theme = program.theme(window_id); + self.style = program.style(&self.theme); } } From 68c4764494640920c7e7aaed8e470d06e3b66042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 12 Mar 2025 02:14:58 +0100 Subject: [PATCH 28/39] Enable `Settings::antialiasing` by default --- core/src/settings.rs | 4 ++-- examples/arc/src/main.rs | 1 - examples/bezier_tool/src/main.rs | 1 - examples/clock/src/main.rs | 1 - examples/color_palette/src/main.rs | 1 - examples/game_of_life/src/main.rs | 1 - examples/loading_spinners/src/main.rs | 1 - examples/multitouch/src/main.rs | 1 - examples/sierpinski_triangle/src/main.rs | 1 - examples/the_matrix/src/main.rs | 1 - examples/vectorial_text/src/main.rs | 1 - 11 files changed, 2 insertions(+), 12 deletions(-) diff --git a/core/src/settings.rs b/core/src/settings.rs index 3189c8d1..68d88864 100644 --- a/core/src/settings.rs +++ b/core/src/settings.rs @@ -31,7 +31,7 @@ pub struct Settings { /// Enabling it can produce a smoother result in some widgets, like the /// `canvas` widget, at a performance cost. /// - /// By default, it is disabled. + /// By default, it is enabled. pub antialiasing: bool, } @@ -42,7 +42,7 @@ impl Default for Settings { fonts: Vec::new(), default_font: Font::default(), default_text_size: Pixels(16.0), - antialiasing: false, + antialiasing: true, } } } diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index c9cad1b6..b83a36d8 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -11,7 +11,6 @@ pub fn main() -> iced::Result { iced::application(Arc::default, Arc::update, Arc::view) .subscription(Arc::subscription) .theme(|_| Theme::Dark) - .antialiasing(true) .run() } diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 02d0f9e9..113577e6 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -5,7 +5,6 @@ use iced::{Element, Theme}; pub fn main() -> iced::Result { iced::application(Example::default, Example::update, Example::view) .theme(|_| Theme::CatppuccinMocha) - .antialiasing(true) .run() } diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 533cafd8..f5f68eb4 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -14,7 +14,6 @@ pub fn main() -> iced::Result { iced::application(Clock::default, Clock::update, Clock::view) .subscription(Clock::subscription) .theme(Clock::theme) - .antialiasing(true) .run() } diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index b16121da..2568c55c 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -18,7 +18,6 @@ pub fn main() -> iced::Result { ) .theme(ColorPalette::theme) .default_font(Font::MONOSPACE) - .antialiasing(true) .run() } diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index c18b240e..8e0ecc45 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -17,7 +17,6 @@ pub fn main() -> iced::Result { iced::application(GameOfLife::default, GameOfLife::update, GameOfLife::view) .subscription(GameOfLife::subscription) .theme(|_| Theme::Dark) - .antialiasing(true) .centered() .run() } diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs index 59aab315..0cec5095 100644 --- a/examples/loading_spinners/src/main.rs +++ b/examples/loading_spinners/src/main.rs @@ -16,7 +16,6 @@ pub fn main() -> iced::Result { LoadingSpinners::update, LoadingSpinners::view, ) - .antialiasing(true) .run() } diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index 3a23fd83..4f22f552 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -13,7 +13,6 @@ pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); iced::application(Multitouch::default, Multitouch::update, Multitouch::view) - .antialiasing(true) .centered() .run() } diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 60fc1d29..f83ee5fd 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -12,7 +12,6 @@ fn main() -> iced::Result { SierpinskiEmulator::update, SierpinskiEmulator::view, ) - .antialiasing(true) .run() } diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs index 7f9383d2..ee10b0bc 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -12,7 +12,6 @@ pub fn main() -> iced::Result { iced::application(TheMatrix::default, TheMatrix::update, TheMatrix::view) .subscription(TheMatrix::subscription) - .antialiasing(true) .run() } diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index 6bdd92bc..92eacca8 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -12,7 +12,6 @@ pub fn main() -> iced::Result { VectorialText::view, ) .theme(|_| Theme::Dark) - .antialiasing(true) .run() } From 63e66b03205ab4e78f2915b5d5fabcc42afa856b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 12 Mar 2025 02:24:28 +0100 Subject: [PATCH 29/39] Fix documentation of `application` and `daemon` --- src/application.rs | 61 +++++++++++++++++++++++----------------------- src/daemon.rs | 14 +++-------- 2 files changed, 34 insertions(+), 41 deletions(-) diff --git a/src/application.rs b/src/application.rs index da0fbd37..c74ba5c4 100644 --- a/src/application.rs +++ b/src/application.rs @@ -40,7 +40,7 @@ use crate::{ use std::borrow::Cow; -/// Creates an iced [`Application`] given its update and view logic. +/// Creates an iced [`Application`] given its boot, update, and view logic. /// /// # Example /// ```no_run @@ -69,7 +69,7 @@ use std::borrow::Cow; /// } /// ``` pub fn application( - new: impl New, + boot: impl Boot, update: impl Update, view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, ) -> Application> @@ -81,8 +81,8 @@ where { use std::marker::PhantomData; - struct Instance { - new: New, + struct Instance { + boot: Boot, update: Update, view: View, _state: PhantomData, @@ -91,13 +91,13 @@ where _renderer: PhantomData, } - impl Program - for Instance + impl Program + for Instance where Message: Send + std::fmt::Debug + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, - New: self::New, + Boot: self::Boot, Update: self::Update, View: for<'a> self::View<'a, State, Message, Theme, Renderer>, { @@ -114,7 +114,7 @@ where } fn boot(&self) -> (State, Task) { - self.new.new() + self.boot.boot() } fn update( @@ -136,7 +136,7 @@ where Application { raw: Instance { - new, + boot, update, view, _state: PhantomData, @@ -165,12 +165,6 @@ pub struct Application { impl Application

{ /// Runs the [`Application`]. - /// - /// The state of the [`Application`] must implement [`Default`]. - /// If your state does not implement [`Default`], use [`run_with`] - /// instead. - /// - /// [`run_with`]: Self::run_with pub fn run(self) -> Result where Self: 'static, @@ -398,37 +392,42 @@ impl Application

{ } /// The logic to initialize the `State` of some [`Application`]. -pub trait New { +/// +/// This trait is implemented for both `Fn() -> State` and +/// `Fn() -> (State, Task)`. +/// +/// In practice, this means that [`application`] can both take +/// simple functions like `State::default` and more advanced ones +/// that return a [`Task`]. +pub trait Boot { /// Initializes the [`Application`] state. - #[allow(clippy::new_ret_no_self)] - #[allow(clippy::wrong_self_convention)] - fn new(&self) -> (State, Task); + fn boot(&self) -> (State, Task); } -impl New for T +impl Boot for T where T: Fn() -> C, - C: IntoState, + C: IntoBoot, { - fn new(&self) -> (State, Task) { - self().into_state() + fn boot(&self) -> (State, Task) { + self().into_boot() } } -/// TODO -pub trait IntoState { - /// TODO - fn into_state(self) -> (State, Task); +/// The initial state of some [`Application`]. +pub trait IntoBoot { + /// Turns some type into the initial state of some [`Application`]. + fn into_boot(self) -> (State, Task); } -impl IntoState for State { - fn into_state(self) -> (State, Task) { +impl IntoBoot for State { + fn into_boot(self) -> (State, Task) { (self, Task::none()) } } -impl IntoState for (State, Task) { - fn into_state(self) -> (State, Task) { +impl IntoBoot for (State, Task) { + fn into_boot(self) -> (State, Task) { self } } diff --git a/src/daemon.rs b/src/daemon.rs index 322cf23e..384e7582 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -8,7 +8,7 @@ use crate::{Element, Executor, Font, Result, Settings, Subscription, Task}; use std::borrow::Cow; -/// Creates an iced [`Daemon`] given its title, update, and view logic. +/// Creates an iced [`Daemon`] given its boot, update, and view logic. /// /// A [`Daemon`] will not open a window by default, but will run silently /// instead until a [`Task`] from [`window::open`] is returned by its update logic. @@ -19,7 +19,7 @@ use std::borrow::Cow; /// /// [`exit`]: crate::exit pub fn daemon( - boot: impl application::New, + boot: impl application::Boot, update: impl application::Update, view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, ) -> Daemon> @@ -47,7 +47,7 @@ where Message: Send + std::fmt::Debug + 'static, Theme: Default + theme::Base, Renderer: program::Renderer, - Boot: application::New, + Boot: application::Boot, Update: application::Update, View: for<'a> self::View<'a, State, Message, Theme, Renderer>, { @@ -64,7 +64,7 @@ where } fn boot(&self) -> (Self::State, Task) { - self.boot.new() + self.boot.boot() } fn update( @@ -113,12 +113,6 @@ pub struct Daemon { impl Daemon

{ /// Runs the [`Daemon`]. - /// - /// The state of the [`Daemon`] must implement [`Default`]. - /// If your state does not implement [`Default`], use [`run_with`] - /// instead. - /// - /// [`run_with`]: Self::run_with pub fn run(self) -> Result where Self: 'static, From 0079a8a3e9e06d9ab21c34a2972d0b4ce52ff2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 12 Mar 2025 16:29:42 +0100 Subject: [PATCH 30/39] Report subscriptions tracked with `beacon` protocol --- winit/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/winit/src/lib.rs b/winit/src/lib.rs index ff5e814a..5cd5ddcb 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -1094,7 +1094,10 @@ fn update( } let subscription = runtime.enter(|| program.subscription()); - runtime.track(subscription::into_recipes(subscription.map(Action::Output))); + let recipes = subscription::into_recipes(subscription.map(Action::Output)); + + debug::subscriptions_tracked(recipes.len()); + runtime.track(recipes); } fn run_action( From 42f5a618097ff509be007242af0e33360273424b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 12 Mar 2025 16:51:45 +0100 Subject: [PATCH 31/39] Disable `debug` view on Wasm (for now) --- debug/Cargo.toml | 1 + debug/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/debug/Cargo.toml b/debug/Cargo.toml index 0cf8d7af..574cc11a 100644 --- a/debug/Cargo.toml +++ b/debug/Cargo.toml @@ -16,5 +16,6 @@ enable = ["dep:iced_beacon"] [dependencies] iced_core.workspace = true +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] iced_beacon.workspace = true iced_beacon.optional = true diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 3adf251f..a7d9b037 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -61,7 +61,7 @@ pub fn skip_next_timing() { internal::skip_next_timing(); } -#[cfg(feature = "enable")] +#[cfg(all(feature = "enable", not(target_arch = "wasm32")))] mod internal { use crate::core::theme; use crate::core::time::Instant; @@ -204,7 +204,7 @@ mod internal { static SKIP_NEXT_SPAN: AtomicBool = AtomicBool::new(false); } -#[cfg(not(feature = "enable"))] +#[cfg(any(not(feature = "enable"), target_arch = "wasm32"))] mod internal { use crate::core::theme; use crate::core::window; From 5f155223682d16a4cfc433c34478a1f9dca552c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 13 Mar 2025 00:42:28 +0100 Subject: [PATCH 32/39] Report theme changes to `debug` API --- core/src/theme.rs | 17 ++++++++++++++--- winit/src/lib.rs | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/core/src/theme.rs b/core/src/theme.rs index cc5b77df..a0ec538b 100644 --- a/core/src/theme.rs +++ b/core/src/theme.rs @@ -252,7 +252,7 @@ impl fmt::Display for Custom { } } -/// The base style of a [`Theme`]. +/// The base style of a theme. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The background [`Color`] of the application. @@ -262,16 +262,27 @@ pub struct Style { pub text_color: Color, } -/// The default blank style of a [`Theme`]. +/// The default blank style of a theme. pub trait Base { - /// Returns the default base [`Style`] of a [`Theme`]. + /// Returns the default base [`Style`] of a theme. fn base(&self) -> Style; + + /// Returns the color [`Palette`] of the theme. + /// + /// This [`Palette`] may be used by the runtime for + /// debugging purposes; like displaying performance + /// metrics or devtools. + fn palette(&self) -> Option; } impl Base for Theme { fn base(&self) -> Style { default(self) } + + fn palette(&self) -> Option { + Some(self.palette()) + } } /// The default [`Style`] of a built-in [`Theme`]. diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 5cd5ddcb..2c79e7f3 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -597,6 +597,14 @@ async fn run_instance

( continue; } + debug::theme_changed(|| { + if window_manager.is_empty() { + theme::Base::palette(&program.theme(id)) + } else { + None + } + }); + let window = window_manager.insert( id, window, @@ -1026,6 +1034,12 @@ async fn run_instance

( window.raw.request_redraw(); } + debug::theme_changed(|| { + window_manager.first().and_then(|window| { + theme::Base::palette(window.state.theme()) + }) + }); + user_interfaces = ManuallyDrop::new(build_user_interfaces( &program, From 121102e55beaa52c78a43eb5e60b07769c2b5b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Thu, 13 Mar 2025 02:07:06 +0100 Subject: [PATCH 33/39] Track and report `Task::units` to `debug` API --- debug/src/lib.rs | 8 +- runtime/src/task.rs | 190 +++++++++++++++++++++++++++----------------- winit/src/lib.rs | 1 + 3 files changed, 121 insertions(+), 78 deletions(-) diff --git a/debug/src/lib.rs b/debug/src/lib.rs index a7d9b037..a6b73f96 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -17,8 +17,8 @@ pub fn theme_changed(f: impl FnOnce() -> Option) { internal::theme_changed(f); } -pub fn commands_spawned(amount: usize) { - internal::commands_spawned(amount) +pub fn tasks_spawned(amount: usize) { + internal::tasks_spawned(amount) } pub fn subscriptions_tracked(amount: usize) { @@ -114,7 +114,7 @@ mod internal { } } - pub fn commands_spawned(amount: usize) { + pub fn tasks_spawned(amount: usize) { BEACON.log(client::Event::CommandsSpawned(amount)); } @@ -215,7 +215,7 @@ mod internal { pub fn theme_changed(_f: impl FnOnce() -> Option) {} - pub fn commands_spawned(_amount: usize) {} + pub fn tasks_spawned(_amount: usize) {} pub fn subscriptions_tracked(_amount: usize) {} diff --git a/runtime/src/task.rs b/runtime/src/task.rs index fd5970ac..f8960539 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -17,12 +17,18 @@ pub use sipper::{Never, Sender, Sipper, Straw, sipper, stream}; /// A [`Task`] _may_ produce a bunch of values of type `T`. #[allow(missing_debug_implementations)] #[must_use = "`Task` must be returned to the runtime to take effect; normally in your `update` or `new` functions."] -pub struct Task(Option>>); +pub struct Task { + stream: Option>>, + units: usize, +} impl Task { /// Creates a [`Task`] that does nothing. pub fn none() -> Self { - Self(None) + Self { + stream: None, + units: 0, + } } /// Creates a new [`Task`] that instantly produces the given value. @@ -80,9 +86,16 @@ impl Task { where T: 'static, { - Self(Some(boxed_stream(stream::select_all( - tasks.into_iter().filter_map(|task| task.0), - )))) + let select_all = stream::select_all( + tasks.into_iter().filter_map(|task| task.stream), + ); + + let units = select_all.len(); + + Self { + stream: Some(boxed_stream(select_all)), + units, + } } /// Maps the output of a [`Task`] with the given closure. @@ -110,21 +123,26 @@ impl Task { T: MaybeSend + 'static, O: MaybeSend + 'static, { - Task(match self.0 { - None => None, - Some(stream) => { - Some(boxed_stream(stream.flat_map(move |action| { - match action.output() { - Ok(output) => f(output) - .0 - .unwrap_or_else(|| boxed_stream(stream::empty())), - Err(action) => { - boxed_stream(stream::once(async move { action })) + Task { + stream: match self.stream { + None => None, + Some(stream) => { + Some(boxed_stream(stream.flat_map(move |action| { + match action.output() { + Ok(output) => { + f(output).stream.unwrap_or_else(|| { + boxed_stream(stream::empty()) + }) + } + Err(action) => boxed_stream(stream::once( + async move { action }, + )), } - } - }))) - } - }) + }))) + } + }, + units: self.units, + } } /// Chains a new [`Task`] to be performed once the current one finishes completely. @@ -132,11 +150,17 @@ impl Task { where T: 'static, { - match self.0 { + match self.stream { None => task, - Some(first) => match task.0 { - None => Task(Some(first)), - Some(second) => Task(Some(boxed_stream(first.chain(second)))), + Some(first) => match task.stream { + None => Self { + stream: Some(first), + units: self.units, + }, + Some(second) => Self { + stream: Some(boxed_stream(first.chain(second))), + units: self.units + task.units, + }, }, } } @@ -146,35 +170,39 @@ impl Task { where T: MaybeSend + 'static, { - match self.0 { + match self.stream { None => Task::done(Vec::new()), - Some(stream) => Task(Some(boxed_stream( - stream::unfold( - (stream, Some(Vec::new())), - move |(mut stream, outputs)| async move { - let mut outputs = outputs?; + Some(stream) => Task { + stream: Some(boxed_stream( + stream::unfold( + (stream, Some(Vec::new())), + move |(mut stream, outputs)| async move { + let mut outputs = outputs?; - let Some(action) = stream.next().await else { - return Some(( - Some(Action::Output(outputs)), - (stream, None), - )); - }; + let Some(action) = stream.next().await else { + return Some(( + Some(Action::Output(outputs)), + (stream, None), + )); + }; - match action.output() { - Ok(output) => { - outputs.push(output); + match action.output() { + Ok(output) => { + outputs.push(output); - Some((None, (stream, Some(outputs)))) + Some((None, (stream, Some(outputs)))) + } + Err(action) => Some(( + Some(action), + (stream, Some(outputs)), + )), } - Err(action) => { - Some((Some(action), (stream, Some(outputs)))) - } - } - }, - ) - .filter_map(future::ready), - ))), + }, + ) + .filter_map(future::ready), + )), + units: self.units, + }, } } @@ -194,26 +222,25 @@ impl Task { where T: 'static, { - match self.0 { + let (stream, handle) = match self.stream { Some(stream) => { let (stream, handle) = stream::abortable(stream); - ( - Self(Some(boxed_stream(stream))), - Handle { - internal: InternalHandle::Manual(handle), - }, - ) + (Some(boxed_stream(stream)), InternalHandle::Manual(handle)) } None => ( - Self(None), - Handle { - internal: InternalHandle::Manual( - stream::AbortHandle::new_pair().0, - ), - }, + None, + InternalHandle::Manual(stream::AbortHandle::new_pair().0), ), - } + }; + + ( + Self { + stream, + units: self.units, + }, + Handle { internal: handle }, + ) } /// Creates a new [`Task`] that runs the given [`Future`] and produces @@ -231,7 +258,15 @@ impl Task { where T: 'static, { - Self(Some(boxed_stream(stream.map(Action::Output)))) + Self { + stream: Some(boxed_stream(stream.map(Action::Output))), + units: 1, + } + } + + /// Returns the amount of work "units" of the [`Task`]. + pub fn units(&self) -> usize { + self.units } } @@ -365,13 +400,14 @@ where let action = f(sender); - Task(Some(boxed_stream( - stream::once(async move { action }).chain( + Task { + stream: Some(boxed_stream(stream::once(async move { action }).chain( receiver.into_stream().filter_map(|result| async move { Some(Action::Output(result.ok()?)) }), - ), - ))) + ))), + units: 1, + } } /// Creates a new [`Task`] that executes the [`Action`] returned by the closure and @@ -384,22 +420,28 @@ where let action = f(sender); - Task(Some(boxed_stream( - stream::once(async move { action }) - .chain(receiver.map(|result| Action::Output(result))), - ))) + Task { + stream: Some(boxed_stream( + stream::once(async move { action }) + .chain(receiver.map(|result| Action::Output(result))), + )), + units: 1, + } } /// Creates a new [`Task`] that executes the given [`Action`] and produces no output. pub fn effect(action: impl Into>) -> Task { let action = action.into(); - Task(Some(boxed_stream(stream::once(async move { - action.output().expect_err("no output") - })))) + Task { + stream: Some(boxed_stream(stream::once(async move { + action.output().expect_err("no output") + }))), + units: 1, + } } /// Returns the underlying [`Stream`] of the [`Task`]. pub fn into_stream(task: Task) -> Option>> { - task.0 + task.stream } diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 2c79e7f3..33a58c63 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -1100,6 +1100,7 @@ fn update( 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) { From a719b0596c5478623105a8a14509528bcffdccd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 5 Apr 2025 19:27:15 +0200 Subject: [PATCH 34/39] Draft `iced_devtools` subcrate structure --- Cargo.lock | 9 +++ Cargo.toml | 7 +- devtools/Cargo.toml | 18 +++++ devtools/src/lib.rs | 161 ++++++++++++++++++++++++++++++++++++++++++++ src/application.rs | 8 ++- 5 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 devtools/Cargo.toml create mode 100644 devtools/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index fc3734c0..91594dca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2403,6 +2403,7 @@ dependencies = [ "criterion", "iced_core", "iced_debug", + "iced_devtools", "iced_futures", "iced_highlighter", "iced_renderer", @@ -2454,6 +2455,14 @@ dependencies = [ "iced_core", ] +[[package]] +name = "iced_devtools" +version = "0.14.0-dev" +dependencies = [ + "iced_program", + "iced_widget", +] + [[package]] name = "iced_futures" version = "0.14.0-dev" diff --git a/Cargo.toml b/Cargo.toml index eec4e705..7bffcde5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ markdown = ["iced_widget/markdown"] # Enables lazy widgets lazy = ["iced_widget/lazy"] # Enables a debug view in native platforms (press F12) -debug = ["iced_winit/debug"] +debug = ["iced_winit/debug", "iced_devtools"] # 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 @@ -80,6 +80,9 @@ iced_widget.workspace = true iced_winit.features = ["program"] iced_winit.workspace = true +iced_devtools.workspace = true +iced_devtools.optional = true + iced_highlighter.workspace = true iced_highlighter.optional = true @@ -112,6 +115,7 @@ members = [ "beacon", "core", "debug", + "devtools", "futures", "graphics", "highlighter", @@ -142,6 +146,7 @@ iced = { version = "0.14.0-dev", path = "." } iced_beacon = { version = "0.14.0-dev", path = "beacon" } iced_core = { version = "0.14.0-dev", path = "core" } iced_debug = { version = "0.14.0-dev", path = "debug" } +iced_devtools = { version = "0.14.0-dev", path = "devtools" } iced_futures = { version = "0.14.0-dev", path = "futures" } iced_graphics = { version = "0.14.0-dev", path = "graphics" } iced_highlighter = { version = "0.14.0-dev", path = "highlighter" } diff --git a/devtools/Cargo.toml b/devtools/Cargo.toml new file mode 100644 index 00000000..1d43566b --- /dev/null +++ b/devtools/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "iced_devtools" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +categories.workspace = true +keywords.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +iced_program.workspace = true +iced_widget.workspace = true diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs new file mode 100644 index 00000000..7ef39ead --- /dev/null +++ b/devtools/src/lib.rs @@ -0,0 +1,161 @@ +#![allow(missing_docs)] +use crate::runtime::futures; +use iced_program as program; +use iced_widget::core; +use iced_widget::runtime; + +use crate::core::Element; +use crate::core::theme; +use crate::core::window; +use crate::futures::Subscription; +use crate::program::Program; +use crate::runtime::Task; + +use std::fmt; + +pub fn attach(program: impl Program + 'static) -> impl Program { + struct Attach

{ + program: P, + } + + impl

Program for Attach

+ where + P: Program + 'static, + { + type State = DevTools

; + type Message = Message

; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Executor = P::Executor; + + fn name() -> &'static str { + P::name() + } + + fn boot(&self) -> (Self::State, Task) { + let (state, task) = self.program.boot(); + + (DevTools { state }, task.map(Message::Program)) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task { + state.update(&self.program, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + state.view(&self.program, window) + } + + fn title(&self, state: &Self::State, window: window::Id) -> String { + state.title(&self.program, window) + } + + fn subscription( + &self, + state: &Self::State, + ) -> runtime::futures::Subscription { + state.subscription(&self.program) + } + + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + state.theme(&self.program, window) + } + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> theme::Style { + state.style(&self.program, theme) + } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + state.scale_factor(&self.program, window) + } + } + + Attach { program } +} + +struct DevTools

+where + P: Program, +{ + state: P::State, +} + +impl

DevTools

+where + P: Program + 'static, +{ + pub fn title(&self, program: &P, window: window::Id) -> String { + program.title(&self.state, window) + } + + pub fn update( + &mut self, + program: &P, + message: Message

, + ) -> Task> { + match message { + Message::Program(message) => program + .update(&mut self.state, message) + .map(Message::Program), + } + } + + pub fn view( + &self, + program: &P, + window: window::Id, + ) -> Element<'_, Message

, P::Theme, P::Renderer> { + program.view(&self.state, window).map(Message::Program) + } + + pub fn subscription(&self, program: &P) -> Subscription> { + program.subscription(&self.state).map(Message::Program) + } + + pub 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.state, theme) + } + + pub fn scale_factor(&self, program: &P, window: window::Id) -> f64 { + program.scale_factor(&self.state, window) + } +} + +#[derive(Clone)] +enum Message

+where + P: Program, +{ + Program(P::Message), +} + +impl

fmt::Debug for Message

+where + P: Program, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Message::Program(message) => message.fmt(f), + } + } +} diff --git a/src/application.rs b/src/application.rs index e1b70c52..f2ed2d4d 100644 --- a/src/application.rs +++ b/src/application.rs @@ -169,7 +169,13 @@ impl Application

{ where Self: 'static, { - Ok(shell::run(self.raw, self.settings, Some(self.window))?) + #[cfg(feature = "debug")] + let program = iced_devtools::attach(self.raw); + + #[cfg(not(feature = "debug"))] + let program = self.raw; + + Ok(shell::run(program, self.settings, Some(self.window))?) } /// Sets the [`Settings`] that will be used to run the [`Application`]. From 132f60c29c4c66f799b625aa727dde925a4b4f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 5 Apr 2025 20:08:54 +0200 Subject: [PATCH 35/39] Draft toast notification overlay in `devtools` --- Cargo.lock | 2 ++ devtools/Cargo.toml | 4 +++ devtools/src/lib.rs | 75 +++++++++++++++++++++++++++++++++++++++++---- winit/src/lib.rs | 15 --------- 4 files changed, 75 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91594dca..e1cd57e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2459,8 +2459,10 @@ dependencies = [ name = "iced_devtools" version = "0.14.0-dev" dependencies = [ + "iced_debug", "iced_program", "iced_widget", + "smol", ] [[package]] diff --git a/devtools/Cargo.toml b/devtools/Cargo.toml index 1d43566b..927398bf 100644 --- a/devtools/Cargo.toml +++ b/devtools/Cargo.toml @@ -16,3 +16,7 @@ workspace = true [dependencies] iced_program.workspace = true iced_widget.workspace = true +iced_debug.workspace = true + +# TODO: Use program executor? +smol.workspace = true diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 7ef39ead..cec392ff 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -1,15 +1,20 @@ #![allow(missing_docs)] -use crate::runtime::futures; +use iced_debug as debug; use iced_program as program; +use iced_widget as widget; use iced_widget::core; use iced_widget::runtime; +use iced_widget::runtime::futures; use crate::core::Element; -use crate::core::theme; +use crate::core::keyboard; +use crate::core::theme::{self, Base, Theme}; +use crate::core::time::seconds; use crate::core::window; use crate::futures::Subscription; use crate::program::Program; use crate::runtime::Task; +use crate::widget::{bottom_right, container, stack, text, themer}; use std::fmt; @@ -33,9 +38,10 @@ pub fn attach(program: impl Program + 'static) -> impl Program { } fn boot(&self) -> (Self::State, Task) { - let (state, task) = self.program.boot(); + let (state, boot) = self.program.boot(); + let (devtools, task) = DevTools::new(state); - (DevTools { state }, task.map(Message::Program)) + (devtools, Task::batch([boot.map(Message::Program), task])) } fn update( @@ -94,12 +100,25 @@ where P: Program, { state: P::State, + show_notification: bool, } impl

DevTools

where P: Program + 'static, { + pub fn new(state: P::State) -> (Self, Task>) { + ( + Self { + state, + show_notification: true, + }, + Task::perform(smol::Timer::after(seconds(2)), |_| { + Message::HideNotification + }), + ) + } + pub fn title(&self, program: &P, window: window::Id) -> String { program.title(&self.state, window) } @@ -110,6 +129,16 @@ where message: Message

, ) -> Task> { match message { + Message::HideNotification => { + self.show_notification = false; + + Task::none() + } + Message::ToggleComet => { + debug::toggle_comet(); + + Task::none() + } Message::Program(message) => program .update(&mut self.state, message) .map(Message::Program), @@ -121,11 +150,39 @@ where program: &P, window: window::Id, ) -> Element<'_, Message

, P::Theme, P::Renderer> { - program.view(&self.state, window).map(Message::Program) + let view = program.view(&self.state, window).map(Message::Program); + let theme = program.theme(&self.state, window); + + let notification = themer( + theme + .palette() + .map(|palette| Theme::custom("DevTools".to_owned(), palette)) + .unwrap_or_default(), + bottom_right( + container(text("Press F12 to open debug metrics")) + .padding(10) + .style(container::dark), + ), + ); + + stack![view] + .push_maybe(self.show_notification.then_some(notification)) + .into() } pub fn subscription(&self, program: &P) -> Subscription> { - program.subscription(&self.state).map(Message::Program) + let subscription = + program.subscription(&self.state).map(Message::Program); + + let hotkeys = + futures::keyboard::on_key_press(|key, _modifiers| match key { + keyboard::Key::Named(keyboard::key::Named::F12) => { + Some(Message::ToggleComet) + } + _ => None, + }); + + Subscription::batch([subscription, hotkeys]) } pub fn theme(&self, program: &P, window: window::Id) -> P::Theme { @@ -146,6 +203,8 @@ enum Message

where P: Program, { + HideNotification, + ToggleComet, Program(P::Message), } @@ -155,6 +214,10 @@ where { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Message::HideNotification => { + f.write_str("DevTools(HideNotification)") + } + Message::ToggleComet => f.write_str("DevTools(ToggleComet)"), Message::Program(message) => message.fmt(f), } } diff --git a/winit/src/lib.rs b/winit/src/lib.rs index bafab33b..bb9500e8 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -40,7 +40,6 @@ pub use clipboard::Clipboard; pub use error::Error; pub use proxy::Proxy; -use crate::core::keyboard; use crate::core::mouse; use crate::core::renderer; use crate::core::theme; @@ -892,20 +891,6 @@ async fn run_instance

( window.state.scale_factor(), window.state.modifiers(), ) { - if matches!( - event, - core::Event::Keyboard( - keyboard::Event::KeyPressed { - modified_key: keyboard::Key::Named( - keyboard::key::Named::F12 - ), - .. - } - ) - ) { - debug::toggle_comet(); - } - events.push((id, event)); } } From 00ee6ab47a0eefbce3db533eb94c5f954d9e8a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 5 Apr 2025 20:14:51 +0200 Subject: [PATCH 36/39] Spawn a thread to sleep asynchronously in `devtools` ... instead of relying on an external reactor. --- Cargo.lock | 1 - devtools/Cargo.toml | 3 --- devtools/src/lib.rs | 18 +++++++++++++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1cd57e6..0142993c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2462,7 +2462,6 @@ dependencies = [ "iced_debug", "iced_program", "iced_widget", - "smol", ] [[package]] diff --git a/devtools/Cargo.toml b/devtools/Cargo.toml index 927398bf..df7e5012 100644 --- a/devtools/Cargo.toml +++ b/devtools/Cargo.toml @@ -17,6 +17,3 @@ workspace = true iced_program.workspace = true iced_widget.workspace = true iced_debug.workspace = true - -# TODO: Use program executor? -smol.workspace = true diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index cec392ff..19540c87 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -12,11 +12,13 @@ use crate::core::theme::{self, Base, Theme}; use crate::core::time::seconds; use crate::core::window; use crate::futures::Subscription; +use crate::futures::futures::channel::oneshot; use crate::program::Program; use crate::runtime::Task; use crate::widget::{bottom_right, container, stack, text, themer}; use std::fmt; +use std::thread; pub fn attach(program: impl Program + 'static) -> impl Program { struct Attach

{ @@ -113,9 +115,19 @@ where state, show_notification: true, }, - Task::perform(smol::Timer::after(seconds(2)), |_| { - Message::HideNotification - }), + Task::perform( + async move { + let (sender, receiver) = oneshot::channel(); + + let _ = thread::spawn(|| { + thread::sleep(seconds(2)); + let _ = sender.send(()); + }); + + let _ = receiver.await; + }, + |_| Message::HideNotification, + ), ) } From 5c39cd4478553239553765e6890b9fc1dc45b400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sun, 6 Apr 2025 17:21:20 +0200 Subject: [PATCH 37/39] Implement installation wizard for `comet` in `devtools` --- debug/src/lib.rs | 21 ++- devtools/src/executor.rs | 19 +++ devtools/src/lib.rs | 298 +++++++++++++++++++++++++++++++-------- 3 files changed, 274 insertions(+), 64 deletions(-) create mode 100644 devtools/src/executor.rs diff --git a/debug/src/lib.rs b/debug/src/lib.rs index a6b73f96..00d42049 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -5,12 +5,14 @@ use crate::core::window; pub use internal::Span; +use std::io; + pub fn init(name: &str) { internal::init(name); } -pub fn toggle_comet() { - internal::toggle_comet(); +pub fn toggle_comet() -> Result<(), io::Error> { + internal::toggle_comet() } pub fn theme_changed(f: impl FnOnce() -> Option) { @@ -72,6 +74,7 @@ mod internal { use beacon::client::{self, Client}; use beacon::span; + use std::io; use std::process; use std::sync::atomic::{self, AtomicBool}; use std::sync::{LazyLock, RwLock}; @@ -82,21 +85,25 @@ mod internal { name.clone_into(&mut NAME.write().expect("Write application name")); } - pub fn toggle_comet() { + pub fn toggle_comet() -> Result<(), io::Error> { if BEACON.is_connected() { BEACON.quit(); + + Ok(()) } else { let _ = process::Command::new("iced_comet") .stdin(process::Stdio::null()) .stdout(process::Stdio::null()) .stderr(process::Stdio::null()) - .spawn(); + .spawn()?; if let Some(palette) = LAST_PALETTE.read().expect("Read last palette").as_ref() { BEACON.log(client::Event::ThemeChanged(*palette)); } + + Ok(()) } } @@ -209,9 +216,13 @@ mod internal { use crate::core::theme; use crate::core::window; + use std::io; + pub fn init(_name: &str) {} - pub fn toggle_comet() {} + pub fn toggle_comet() -> Result<(), io::Error> { + Ok(()) + } pub fn theme_changed(_f: impl FnOnce() -> Option) {} diff --git a/devtools/src/executor.rs b/devtools/src/executor.rs new file mode 100644 index 00000000..5d7d5397 --- /dev/null +++ b/devtools/src/executor.rs @@ -0,0 +1,19 @@ +use crate::futures::futures::channel::mpsc; +use crate::runtime::Task; + +use std::thread; + +pub fn spawn_blocking( + f: impl FnOnce(mpsc::Sender) + Send + 'static, +) -> Task +where + T: Send + 'static, +{ + let (sender, receiver) = mpsc::channel(1); + + let _ = thread::spawn(move || { + f(sender); + }); + + Task::stream(receiver) +} diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 19540c87..24e04492 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -6,18 +6,23 @@ use iced_widget::core; use iced_widget::runtime; use iced_widget::runtime::futures; -use crate::core::Element; +mod executor; + 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::futures::Subscription; -use crate::futures::futures::channel::oneshot; use crate::program::Program; use crate::runtime::Task; -use crate::widget::{bottom_right, container, stack, text, themer}; +use crate::widget::{ + bottom_right, button, center, column, container, horizontal_space, row, + scrollable, stack, text, themer, +}; use std::fmt; +use std::io; use std::thread; pub fn attach(program: impl Program + 'static) -> impl Program { @@ -30,7 +35,7 @@ pub fn attach(program: impl Program + 'static) -> impl Program { P: Program + 'static, { type State = DevTools

; - type Message = Message

; + type Message = Event

; type Theme = P::Theme; type Renderer = P::Renderer; type Executor = P::Executor; @@ -43,7 +48,13 @@ pub fn attach(program: impl Program + 'static) -> impl Program { let (state, boot) = self.program.boot(); let (devtools, task) = DevTools::new(state); - (devtools, Task::batch([boot.map(Message::Program), task])) + ( + devtools, + Task::batch([ + boot.map(Event::Program), + task.map(Event::Message), + ]), + ) } fn update( @@ -102,32 +113,46 @@ where P: Program, { state: P::State, + mode: Mode, show_notification: bool, } +#[derive(Debug, Clone)] +enum Message { + HideNotification, + ToggleComet, + InstallComet, + InstallationLogged(String), + InstallationFinished, + CancelSetup, +} + +enum Mode { + None, + Setup(Setup), +} + +enum Setup { + Idle, + Running { logs: Vec }, +} + impl

DevTools

where P: Program + 'static, { - pub fn new(state: P::State) -> (Self, Task>) { + pub fn new(state: P::State) -> (Self, Task) { ( Self { state, + mode: Mode::None, show_notification: true, }, - Task::perform( - async move { - let (sender, receiver) = oneshot::channel(); - - let _ = thread::spawn(|| { - thread::sleep(seconds(2)); - let _ = sender.send(()); - }); - - let _ = receiver.await; - }, - |_| Message::HideNotification, - ), + executor::spawn_blocking(|mut sender| { + thread::sleep(seconds(2)); + let _ = sender.try_send(()); + }) + .map(|_| Message::HideNotification), ) } @@ -135,25 +160,96 @@ where program.title(&self.state, window) } - pub fn update( - &mut self, - program: &P, - message: Message

, - ) -> Task> { - match message { - Message::HideNotification => { - self.show_notification = false; + pub fn update(&mut self, program: &P, event: Event

) -> Task> { + match event { + Event::Message(message) => match message { + Message::HideNotification => { + self.show_notification = false; - Task::none() - } - Message::ToggleComet => { - debug::toggle_comet(); + Task::none() + } + Message::ToggleComet => { + if let Mode::Setup(setup) = &self.mode { + 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() + Task::none() + } + Message::InstallComet => { + self.mode = + Mode::Setup(Setup::Running { logs: Vec::new() }); + + executor::spawn_blocking(|mut sender| { + use std::io::{BufRead, BufReader}; + use std::process::{Command, Stdio}; + + let Ok(install) = Command::new("cargo") + .args([ + "install", + "--locked", + "--git", + "https://github.com/iced-rs/comet.git", + ]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn() + else { + return; + }; + + 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(); + } + + 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(); + + Task::none() + } + Message::CancelSetup => { + self.mode = Mode::None; + + Task::none() + } + }, + Event::Program(message) => { + program.update(&mut self.state, message).map(Event::Program) } - Message::Program(message) => program - .update(&mut self.state, message) - .map(Message::Program), } } @@ -161,30 +257,118 @@ where &self, program: &P, window: window::Id, - ) -> Element<'_, Message

, P::Theme, P::Renderer> { - let view = program.view(&self.state, window).map(Message::Program); + ) -> Element<'_, Event

, P::Theme, P::Renderer> { + let view = program.view(&self.state, window).map(Event::Program); let theme = program.theme(&self.state, window); - let notification = themer( + let derive_theme = move || { theme .palette() .map(|palette| Theme::custom("DevTools".to_owned(), palette)) - .unwrap_or_default(), - bottom_right( - container(text("Press F12 to open debug metrics")) - .padding(10) - .style(container::dark), - ), - ); + .unwrap_or_default() + }; + + let mode = match &self.mode { + Mode::None => None, + Mode::Setup(setup) => { + let stage: Element<'_, _, Theme, P::Renderer> = match setup { + Setup::Idle => { + 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), + ]; + + 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) + ) + .width(Fill) + .padding(5) + .style(container::dark), + 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(), + }; + + let setup = center( + container(stage) + .padding(20) + .width(500) + .style(container::bordered_box), + ) + .padding(10) + .style(|_theme| { + container::Style::default() + .background(Color::BLACK.scale_alpha(0.8)) + }); + + Some(setup) + } + } + .map(|mode| { + themer(derive_theme(), Element::from(mode).map(Event::Message)) + }); + + let notification = self.show_notification.then(|| { + themer( + derive_theme(), + bottom_right( + container(text("Press F12 to open debug metrics")) + .padding(10) + .style(container::dark), + ), + ) + }); stack![view] - .push_maybe(self.show_notification.then_some(notification)) + .push_maybe(mode) + .push_maybe(notification) .into() } - pub fn subscription(&self, program: &P) -> Subscription> { + pub fn subscription(&self, program: &P) -> Subscription> { let subscription = - program.subscription(&self.state).map(Message::Program); + program.subscription(&self.state).map(Event::Program); let hotkeys = futures::keyboard::on_key_press(|key, _modifiers| match key { @@ -192,7 +376,8 @@ where Some(Message::ToggleComet) } _ => None, - }); + }) + .map(Event::Message); Subscription::batch([subscription, hotkeys]) } @@ -210,27 +395,22 @@ where } } -#[derive(Clone)] -enum Message

+enum Event

where P: Program, { - HideNotification, - ToggleComet, + Message(Message), Program(P::Message), } -impl

fmt::Debug for Message

+impl

fmt::Debug for Event

where P: Program, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Message::HideNotification => { - f.write_str("DevTools(HideNotification)") - } - Message::ToggleComet => f.write_str("DevTools(ToggleComet)"), - Message::Program(message) => message.fmt(f), + Self::Message(message) => message.fmt(f), + Self::Program(message) => message.fmt(f), } } } From 08f1133a039a7da04d6de5435c194f1a23d31528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 7 Apr 2025 19:51:29 +0200 Subject: [PATCH 38/39] Hardcode specific `comet` revision in `devtools` --- devtools/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 24e04492..51ded4c6 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -195,6 +195,8 @@ where "--locked", "--git", "https://github.com/iced-rs/comet.git", + "--rev", + "5efd34550e42974a0e85af7560c60401bfc13919", ]) .stdin(Stdio::null()) .stdout(Stdio::null()) From dd6c99e1a4d1e05d0a387ca9be8230fc87874c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 7 Apr 2025 20:02:39 +0200 Subject: [PATCH 39/39] Detect and warn slow `Debug` implementations for `Message` --- Cargo.lock | 1 + debug/Cargo.toml | 1 + debug/src/lib.rs | 9 +++++++++ 3 files changed, 11 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 0142993c..d3f139d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2453,6 +2453,7 @@ version = "0.14.0-dev" dependencies = [ "iced_beacon", "iced_core", + "log", ] [[package]] diff --git a/debug/Cargo.toml b/debug/Cargo.toml index 574cc11a..1c5e7324 100644 --- a/debug/Cargo.toml +++ b/debug/Cargo.toml @@ -15,6 +15,7 @@ enable = ["dep:iced_beacon"] [dependencies] iced_core.workspace = true +log.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] iced_beacon.workspace = true diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 00d42049..a15f05c9 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -135,7 +135,16 @@ mod internal { pub fn update(message: &impl std::fmt::Debug) -> Span { let span = span(span::Stage::Update); + + let start = Instant::now(); let message = format!("{message:?}"); + let elapsed = start.elapsed(); + + if elapsed.as_millis() >= 1 { + log::warn!( + "Slow `Debug` implementation of `Message` (took {elapsed:?})!" + ); + } BEACON.log(client::Event::MessageLogged(if message.len() > 49 { format!("{}...", &message[..49])