diff --git a/Cargo.lock b/Cargo.lock index a8ee1a3c..d3f139d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2402,6 +2402,8 @@ version = "0.14.0-dev" dependencies = [ "criterion", "iced_core", + "iced_debug", + "iced_devtools", "iced_futures", "iced_highlighter", "iced_renderer", @@ -2413,6 +2415,20 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "iced_beacon" +version = "0.14.0-dev" +dependencies = [ + "bincode", + "futures", + "iced_core", + "log", + "semver", + "serde", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "iced_core" version = "0.14.0-dev" @@ -2425,11 +2441,30 @@ dependencies = [ "log", "num-traits", "rustc-hash 2.1.1", + "serde", "smol_str", "thiserror 1.0.69", "web-time", ] +[[package]] +name = "iced_debug" +version = "0.14.0-dev" +dependencies = [ + "iced_beacon", + "iced_core", + "log", +] + +[[package]] +name = "iced_devtools" +version = "0.14.0-dev" +dependencies = [ + "iced_debug", + "iced_program", + "iced_widget", +] + [[package]] name = "iced_futures" version = "0.14.0-dev" @@ -2472,6 +2507,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" @@ -2489,6 +2532,7 @@ version = "0.14.0-dev" dependencies = [ "bytes", "iced_core", + "iced_debug", "iced_futures", "raw-window-handle 0.6.2", "sipper", @@ -2562,9 +2606,8 @@ dependencies = [ name = "iced_winit" version = "0.14.0-dev" dependencies = [ - "iced_futures", - "iced_graphics", - "iced_runtime", + "iced_debug", + "iced_program", "log", "rustc-hash 2.1.1", "sysinfo", @@ -4985,6 +5028,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +dependencies = [ + "serde", +] + [[package]] name = "serde" version = "1.0.219" @@ -5768,9 +5820,21 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 8392d735..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 @@ -71,6 +71,7 @@ unconditional-rendering = ["iced_winit/unconditional-rendering"] sipper = ["iced_runtime/sipper"] [dependencies] +iced_debug.workspace = true iced_core.workspace = true iced_futures.workspace = true iced_renderer.workspace = true @@ -79,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 @@ -108,10 +112,14 @@ strip = "debuginfo" [workspace] members = [ + "beacon", "core", + "debug", + "devtools", "futures", "graphics", "highlighter", + "program", "renderer", "runtime", "test", @@ -135,10 +143,14 @@ rust-version = "1.85" [workspace.dependencies] 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" } +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" } @@ -147,6 +159,7 @@ iced_wgpu = { version = "0.14.0-dev", path = "wgpu" } iced_widget = { version = "0.14.0-dev", path = "widget" } iced_winit = { version = "0.14.0-dev", path = "winit" } +bincode = "1.3" bitflags = "2.0" bytemuck = { version = "1.0", features = ["derive"] } bytes = "1.6" @@ -172,6 +185,8 @@ qrcode = { version = "0.13", default-features = false } raw-window-handle = "0.6" resvg = "0.42" rustc-hash = "2.0" +serde = "1.0" +semver = "1.0" sha2 = "0.10" sipper = "0.1" smol = "2" diff --git a/beacon/Cargo.toml b/beacon/Cargo.toml new file mode 100644 index 00000000..f141fabe --- /dev/null +++ b/beacon/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "iced_beacon" +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 +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"] + +serde.workspace = true +serde.features = ["derive"] + +semver.workspace = true +semver.features = ["serde"] diff --git a/beacon/src/client.rs b/beacon/src/client.rs new file mode 100644 index 00000000..1ca5bc8c --- /dev/null +++ b/beacon/src/client.rs @@ -0,0 +1,156 @@ +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::sync::atomic::{self, AtomicBool}; +use std::thread; + +pub const SERVER_ADDRESS: &str = "127.0.0.1:9167"; + +#[derive(Debug, Clone)] +pub struct Client { + sender: mpsc::Sender, + is_connected: Arc, + _handle: Arc>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Message { + Connected { + at: SystemTime, + name: String, + version: Version, + }, + EventLogged { + at: SystemTime, + event: Event, + }, + Quit { + at: SystemTime, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Event { + ThemeChanged(theme::Palette), + SpanStarted(span::Stage), + SpanFinished(span::Stage, Duration), + MessageLogged(String), + CommandsSpawned(usize), + SubscriptionsTracked(usize), +} + +impl Client { + pub fn log(&self, event: Event) { + let _ = self.sender.try_send(Message::EventLogged { + at: SystemTime::now(), + 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 = { + 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, + 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 { + 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) => { + if error.kind() != io::ErrorKind::BrokenPipe { + log::warn!( + "Error sending message to server: {error}" + ); + } + break; + } + } + } + } + Err(_) => { + is_connected.store(false, atomic::Ordering::Relaxed); + 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..36d19e30 --- /dev/null +++ b/beacon/src/lib.rs @@ -0,0 +1,244 @@ +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, + }, + SubscriptionsTracked { + at: SystemTime, + amount_alive: usize, + }, + SpanFinished { + at: SystemTime, + duration: Duration, + span: Span, + }, + QuitRequested { + at: SystemTime, + }, + AlreadyRunning { + at: SystemTime, + }, +} + +impl Event { + pub fn at(&self) -> SystemTime { + match self { + Self::Connected { at, .. } + | Self::Disconnected { at, .. } + | Self::ThemeChanged { at, .. } + | Self::SubscriptionsTracked { 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(); + + 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 + .send(Event::AlreadyRunning { + at: SystemTime::now(), + }) + .await; + } + delay().await; + } + }; + }; + + loop { + let Ok((mut stream, _)) = server.accept().await else { + continue; + }; + + 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) => { + 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::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, + duration, + ) => { + let span = match stage { + span::Stage::Boot => Span::Boot, + span::Stage::Update => { + Span::Update { + message: last_message + .clone(), + commands_spawned: + last_commands_spawned, + } + } + 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; + } + } + } + client::Message::Quit { at } => { + let _ = output + .send(Event::QuitRequested { at }) + .await; + } + }; + } + Err(Error::IOFailed(_)) => { + let _ = output + .send(Event::Disconnected { + at: SystemTime::now(), + }) + .await; + break; + } + Err(Error::DecodingFailed(error)) => { + log::warn!("Error decoding beacon output: {error}") + } + } + } + } + }) +} + +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..38f19acb --- /dev/null +++ b/beacon/src/span.rs @@ -0,0 +1,77 @@ +use crate::core::window; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Span { + Boot, + 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::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..68381040 --- /dev/null +++ b/beacon/src/stream.rs @@ -0,0 +1,15 @@ +use futures::Future; +use futures::channel::mpsc; +use futures::stream::{self, Stream, StreamExt}; + +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/benches/wgpu.rs b/benches/wgpu.rs index b400bfa1..24d8212c 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -133,12 +133,11 @@ fn benchmark<'a>( cache = Some(user_interface.into_cache()); - let submission = renderer.present::<&str>( + let submission = renderer.present( Some(Color::BLACK), format, &texture_view, &viewport, - &[], ); let _ = device.poll(wgpu::Maintain::WaitForSubmissionIndex(submission)); diff --git a/core/Cargo.toml b/core/Cargo.toml index fff5ac09..fb8f778e 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -31,3 +31,7 @@ web-time.workspace = true dark-light.workspace = true dark-light.optional = true + +serde.workspace = true +serde.optional = true +serde.features = ["derive"] diff --git a/core/src/color.rs b/core/src/color.rs index 37a8921d..a280c795 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -1,5 +1,6 @@ /// 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/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/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/core/src/theme/palette.rs b/core/src/theme/palette.rs index 7be5a98e..d9ac6402 100644 --- a/core/src/theme/palette.rs +++ b/core/src/theme/palette.rs @@ -5,6 +5,7 @@ use std::sync::LazyLock; /// 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/core/src/time.rs b/core/src/time.rs index c6e30444..664045fa 100644 --- a/core/src/time.rs +++ b/core/src/time.rs @@ -2,6 +2,7 @@ pub use web_time::Duration; pub use web_time::Instant; +pub use web_time::SystemTime; /// Creates a [`Duration`] representing the given amount of milliseconds. pub fn milliseconds(milliseconds: u64) -> Duration { diff --git a/core/src/window/id.rs b/core/src/window/id.rs index 5d5a817e..ee0a4c59 100644 --- a/core/src/window/id.rs +++ b/core/src/window/id.rs @@ -4,6 +4,7 @@ use std::sync::atomic::{self, AtomicU64}; /// The id of the window. #[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/Cargo.toml b/debug/Cargo.toml new file mode 100644 index 00000000..1c5e7324 --- /dev/null +++ b/debug/Cargo.toml @@ -0,0 +1,22 @@ +[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 = ["dep:iced_beacon"] + +[dependencies] +iced_core.workspace = true +log.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 new file mode 100644 index 00000000..a15f05c9 --- /dev/null +++ b/debug/src/lib.rs @@ -0,0 +1,282 @@ +pub use iced_core as core; + +use crate::core::theme; +use crate::core::window; + +pub use internal::Span; + +use std::io; + +pub fn init(name: &str) { + internal::init(name); +} + +pub fn toggle_comet() -> Result<(), io::Error> { + internal::toggle_comet() +} + +pub fn theme_changed(f: impl FnOnce() -> Option) { + internal::theme_changed(f); +} + +pub fn tasks_spawned(amount: usize) { + internal::tasks_spawned(amount) +} + +pub fn subscriptions_tracked(amount: usize) { + internal::subscriptions_tracked(amount) +} + +pub fn boot() -> Span { + internal::boot() +} + +pub fn update(message: &impl std::fmt::Debug) -> Span { + internal::update(message) +} + +pub fn view(window: window::Id) -> Span { + internal::view(window) +} + +pub fn layout(window: window::Id) -> Span { + internal::layout(window) +} + +pub fn interact(window: window::Id) -> Span { + internal::interact(window) +} + +pub fn draw(window: window::Id) -> Span { + internal::draw(window) +} + +pub fn present(window: window::Id) -> Span { + internal::present(window) +} + +pub fn time(window: window::Id, name: impl AsRef) -> Span { + internal::time(window, name) +} + +pub fn skip_next_timing() { + internal::skip_next_timing(); +} + +#[cfg(all(feature = "enable", not(target_arch = "wasm32")))] +mod internal { + use crate::core::theme; + use crate::core::time::Instant; + use crate::core::window; + + use iced_beacon as beacon; + + 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}; + + pub fn init(name: &str) { + let name = name.split("::").next().unwrap_or(name); + + name.clone_into(&mut NAME.write().expect("Write application name")); + } + + 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()?; + + if let Some(palette) = + LAST_PALETTE.read().expect("Read last palette").as_ref() + { + BEACON.log(client::Event::ThemeChanged(*palette)); + } + + Ok(()) + } + } + + pub fn theme_changed(f: impl FnOnce() -> Option) { + let Some(palette) = f() else { + return; + }; + + 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 tasks_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(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]) + } else { + message + })); + + span + } + + pub fn view(window: window::Id) -> Span { + span(span::Stage::View(window)) + } + + pub fn layout(window: window::Id) -> Span { + span(span::Stage::Layout(window)) + } + + pub fn interact(window: window::Id) -> Span { + span(span::Stage::Interact(window)) + } + + pub fn draw(window: window::Id) -> Span { + span(span::Stage::Draw(window)) + } + + pub fn present(window: window::Id) -> Span { + span(span::Stage::Present(window)) + } + + 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() { + SKIP_NEXT_SPAN.store(true, atomic::Ordering::Relaxed); + } + + fn span(span: span::Stage) -> Span { + BEACON.log(client::Event::SpanStarted(span.clone())); + + Span { + span, + start: Instant::now(), + } + } + + #[derive(Debug)] + pub struct Span { + span: span::Stage, + start: Instant, + } + + impl Span { + pub fn finish(self) { + if SKIP_NEXT_SPAN.fetch_and(false, atomic::Ordering::Relaxed) { + return; + } + + BEACON.log(client::Event::SpanFinished( + self.span, + self.start.elapsed(), + )); + } + } + + static BEACON: LazyLock = LazyLock::new(|| { + client::connect(NAME.read().expect("Read application name").to_owned()) + }); + + static NAME: RwLock = RwLock::new(String::new()); + static LAST_PALETTE: RwLock> = RwLock::new(None); + static SKIP_NEXT_SPAN: AtomicBool = AtomicBool::new(false); +} + +#[cfg(any(not(feature = "enable"), target_arch = "wasm32"))] +mod internal { + use crate::core::theme; + use crate::core::window; + + use std::io; + + pub fn init(_name: &str) {} + + pub fn toggle_comet() -> Result<(), io::Error> { + Ok(()) + } + + pub fn theme_changed(_f: impl FnOnce() -> Option) {} + + pub fn tasks_spawned(_amount: usize) {} + + pub fn subscriptions_tracked(_amount: usize) {} + + pub fn boot() -> Span { + Span + } + + pub fn update(_message: &impl std::fmt::Debug) -> Span { + Span + } + + pub fn view(_window: window::Id) -> Span { + Span + } + + pub fn layout(_window: window::Id) -> Span { + Span + } + + pub fn interact(_window: window::Id) -> Span { + Span + } + + pub fn draw(_window: window::Id) -> Span { + Span + } + + pub fn present(_window: window::Id) -> Span { + Span + } + + pub fn time(_window: window::Id, _name: impl AsRef) -> Span { + Span + } + + pub fn skip_next_timing() {} + + #[derive(Debug)] + pub struct Span; + + impl Span { + pub fn finish(self) {} + } +} diff --git a/devtools/Cargo.toml b/devtools/Cargo.toml new file mode 100644 index 00000000..df7e5012 --- /dev/null +++ b/devtools/Cargo.toml @@ -0,0 +1,19 @@ +[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 +iced_debug.workspace = true 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 new file mode 100644 index 00000000..51ded4c6 --- /dev/null +++ b/devtools/src/lib.rs @@ -0,0 +1,418 @@ +#![allow(missing_docs)] +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; + +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::program::Program; +use crate::runtime::Task; +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 { + struct Attach

{ + program: P, + } + + impl

Program for Attach

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

; + type Message = Event

; + 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, boot) = self.program.boot(); + let (devtools, task) = DevTools::new(state); + + ( + devtools, + Task::batch([ + boot.map(Event::Program), + task.map(Event::Message), + ]), + ) + } + + 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, + 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) { + ( + Self { + state, + mode: Mode::None, + show_notification: true, + }, + executor::spawn_blocking(|mut sender| { + thread::sleep(seconds(2)); + let _ = sender.try_send(()); + }) + .map(|_| Message::HideNotification), + ) + } + + pub fn title(&self, program: &P, window: window::Id) -> String { + program.title(&self.state, window) + } + + 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 => { + 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() + } + 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", + "--rev", + "5efd34550e42974a0e85af7560c60401bfc13919", + ]) + .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) + } + } + } + + pub fn view( + &self, + program: &P, + window: window::Id, + ) -> Element<'_, Event

, P::Theme, P::Renderer> { + let view = program.view(&self.state, window).map(Event::Program); + let theme = program.theme(&self.state, window); + + let derive_theme = move || { + theme + .palette() + .map(|palette| Theme::custom("DevTools".to_owned(), palette)) + .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(mode) + .push_maybe(notification) + .into() + } + + pub fn subscription(&self, program: &P) -> Subscription> { + let subscription = + program.subscription(&self.state).map(Event::Program); + + let hotkeys = + futures::keyboard::on_key_press(|key, _modifiers| match key { + keyboard::Key::Named(keyboard::key::Named::F12) => { + Some(Message::ToggleComet) + } + _ => None, + }) + .map(Event::Message); + + Subscription::batch([subscription, hotkeys]) + } + + 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) + } +} + +enum Event

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

fmt::Debug for Event

+where + P: Program, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Message(message) => message.fmt(f), + Self::Program(message) => message.fmt(f), + } + } +} 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/examples/arc/src/main.rs b/examples/arc/src/main.rs index f63b82d0..b83a36d8 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -8,10 +8,9 @@ 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) .run() } diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 95ad299d..113577e6 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -3,9 +3,8 @@ 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..f5f68eb4 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -11,10 +11,9 @@ 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) .run() } diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index 2662d063..e705c40d 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -12,13 +12,12 @@ use std::ops::RangeInclusive; pub fn main() -> iced::Result { iced::application( - "Color Palette - Iced", + ColorPalette::default, ColorPalette::update, ColorPalette::view, ) .theme(ColorPalette::theme) .default_font(Font::MONOSPACE) - .antialiasing(true) .run() } 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 30559401..c27c6204 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..8e0ecc45 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -14,16 +14,11 @@ 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) + .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/integration/src/controls.rs b/examples/integration/src/controls.rs index b92e4987..72db4c24 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -1,7 +1,6 @@ use iced_wgpu::Renderer; use iced_widget::{bottom, column, row, slider, text, text_input}; use iced_winit::core::{Color, Element, Theme}; -use iced_winit::runtime::{Program, Task}; pub struct Controls { background_color: Color, @@ -27,12 +26,8 @@ impl Controls { } } -impl Program for Controls { - type Theme = Theme; - type Message = Message; - type Renderer = Renderer; - - fn update(&mut self, message: Message) -> Task { +impl Controls { + pub fn update(&mut self, message: Message) { match message { Message::BackgroundColorChanged(color) => { self.background_color = color; @@ -41,11 +36,9 @@ impl Program for Controls { self.input = input; } } - - Task::none() } - fn view(&self) -> Element { + pub fn view(&self) -> Element { let background_color = self.background_color; let sliders = row![ diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 025971a4..639858fd 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -10,10 +10,11 @@ use iced_winit::Clipboard; use iced_winit::conversion; use iced_winit::core::mouse; use iced_winit::core::renderer; -use iced_winit::core::{Color, Font, Pixels, Size, Theme}; +use iced_winit::core::time::Instant; +use iced_winit::core::window; +use iced_winit::core::{Event, Font, Pixels, Size, Theme}; use iced_winit::futures; -use iced_winit::runtime::Debug; -use iced_winit::runtime::program; +use iced_winit::runtime::user_interface::{self, UserInterface}; use iced_winit::winit; use winit::{ @@ -41,13 +42,14 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { format: wgpu::TextureFormat, renderer: Renderer, scene: Scene, - state: program::State, - cursor_position: Option>, + controls: Controls, + events: Vec, + cursor: mouse::Cursor, + cache: user_interface::Cache, clipboard: Clipboard, viewport: Viewport, modifiers: ModifiersState, resized: bool, - debug: Debug, }, } @@ -143,9 +145,8 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { let controls = Controls::new(); // Initialize iced - let mut debug = Debug::new(); - let mut renderer = { + let renderer = { let engine = Engine::new( &adapter, device.clone(), @@ -157,13 +158,6 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { Renderer::new(engine, Font::default(), Pixels::from(16)) }; - let state = program::State::new( - controls, - viewport.logical_size(), - &mut renderer, - &mut debug, - ); - // You should change this if you want to render continuously event_loop.set_control_flow(ControlFlow::Wait); @@ -175,13 +169,14 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { surface, format, scene, - state, - cursor_position: None, + controls, + events: Vec::new(), + cursor: mouse::Cursor::Unavailable, modifiers: ModifiersState::default(), + cache: user_interface::Cache::new(), clipboard, viewport, resized: false, - debug, }; } } @@ -200,13 +195,14 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { format, renderer, scene, - state, + controls, + events, viewport, - cursor_position, + cursor, modifiers, clipboard, + cache, resized, - debug, } = self else { return; @@ -241,8 +237,6 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { match surface.get_current_texture() { Ok(frame) => { - let program = state.program(); - let view = frame.texture.create_view( &wgpu::TextureViewDescriptor::default(), ); @@ -256,7 +250,7 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { let mut render_pass = Scene::clear( &view, &mut encoder, - program.background_color(), + controls.background_color(), ); // Draw the scene @@ -267,29 +261,53 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { queue.submit([encoder.finish()]); // Draw iced on top + let mut interface = UserInterface::build( + controls.view(), + viewport.logical_size(), + std::mem::take(cache), + renderer, + ); + + let _ = interface.update( + &[Event::Window( + window::Event::RedrawRequested( + Instant::now(), + ), + )], + *cursor, + renderer, + clipboard, + &mut Vec::new(), + ); + + let mouse_interaction = interface.draw( + renderer, + &Theme::Dark, + &renderer::Style::default(), + *cursor, + ); + *cache = interface.into_cache(); + renderer.present( None, frame.texture.format(), &view, viewport, - &debug.overlay(), ); // Present the frame frame.present(); // Update the mouse cursor - window.set_cursor( - iced_winit::conversion::mouse_interaction( - state.mouse_interaction(), - ), - ); + window.set_cursor(conversion::mouse_interaction( + mouse_interaction, + )); } Err(error) => match error { wgpu::SurfaceError::OutOfMemory => { panic!( "Swapchain error: {error}. \ - Rendering cannot continue." + Rendering cannot continue." ) } _ => { @@ -300,7 +318,11 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { } } WindowEvent::CursorMoved { position, .. } => { - *cursor_position = Some(position); + *cursor = + mouse::Cursor::Available(conversion::cursor_position( + position, + viewport.scale_factor(), + )); } WindowEvent::ModifiersChanged(new_modifiers) => { *modifiers = new_modifiers.state(); @@ -315,37 +337,42 @@ pub fn main() -> Result<(), winit::error::EventLoopError> { } // Map window event to iced event - if let Some(event) = iced_winit::conversion::window_event( + if let Some(event) = conversion::window_event( event, window.scale_factor(), *modifiers, ) { - state.queue_event(event); + events.push(event); } // If there are events pending - if !state.is_queue_empty() { - // We update iced - let _ = state.update( + if !events.is_empty() { + // We process them + let mut interface = UserInterface::build( + controls.view(), viewport.logical_size(), - cursor_position - .map(|p| { - conversion::cursor_position( - p, - viewport.scale_factor(), - ) - }) - .map(mouse::Cursor::Available) - .unwrap_or(mouse::Cursor::Unavailable), + std::mem::take(cache), renderer, - &Theme::Dark, - &renderer::Style { - text_color: Color::WHITE, - }, - clipboard, - debug, ); + let mut messages = Vec::new(); + + let _ = interface.update( + events, + *cursor, + renderer, + clipboard, + &mut messages, + ); + + events.clear(); + *cache = interface.into_cache(); + + // update our UI with any messages + for message in messages { + controls.update(message); + } + // and request a redraw window.request_redraw(); } 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..0cec5095 100644 --- a/examples/loading_spinners/src/main.rs +++ b/examples/loading_spinners/src/main.rs @@ -12,11 +12,10 @@ use linear::Linear; pub fn main() -> iced::Result { iced::application( - "Loading Spinners - Iced", + LoadingSpinners::default, LoadingSpinners::update, LoadingSpinners::view, ) - .antialiasing(true) .run() } 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 bc2a8908..4470c834 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..4f22f552 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -12,8 +12,7 @@ use std::collections::HashMap; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::application("Multitouch - Iced", Multitouch::update, Multitouch::view) - .antialiasing(true) + iced::application(Multitouch::default, Multitouch::update, Multitouch::view) .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 d8ccd27c..8fe23717 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..f83ee5fd 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -8,11 +8,10 @@ use std::fmt::Debug; fn main() -> iced::Result { iced::application( - "Sierpinski Triangle - Iced", + SierpinskiEmulator::default, SierpinskiEmulator::update, SierpinskiEmulator::view, ) - .antialiasing(true) .run() } 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 b0adc8e8..b3386d35 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..ee10b0bc 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -10,9 +10,8 @@ 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 b5d6d523..87d4f107 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 03e62d3c..8dcbd000 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..92eacca8 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -7,12 +7,11 @@ 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, ) .theme(|_| Theme::Dark) - .antialiasing(true) .run() } 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/compositor.rs b/graphics/src/compositor.rs index e9063678..338d648f 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -73,13 +73,12 @@ 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], on_pre_present: impl FnOnce(), ) -> Result<(), SurfaceError>; @@ -186,13 +185,12 @@ impl Compositor for () { } } - fn present>( + fn present( &mut self, _renderer: &mut Self::Renderer, _surface: &mut Self::Surface, _viewport: &Viewport, _background_color: Color, - _overlay: &[T], _on_pre_present: impl FnOnce(), ) -> Result<(), SurfaceError> { Ok(()) 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 ace4da74..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,138 +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 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, @@ -216,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, @@ -263,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, @@ -289,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, @@ -336,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, @@ -363,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) } @@ -406,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, @@ -433,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) } @@ -476,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, @@ -499,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, @@ -549,6 +501,7 @@ pub fn with_scale_factor( } } +/// Decorates a [`Program`] with the given executor function. pub fn with_executor( program: P, ) -> impl Program { @@ -573,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, @@ -627,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/renderer/src/fallback.rs b/renderer/src/fallback.rs index 958870b3..4cea1a15 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -316,13 +316,12 @@ where delegate!(self, compositor, compositor.fetch_information()) } - fn present>( + fn present( &mut self, renderer: &mut Self::Renderer, surface: &mut Self::Surface, viewport: &graphics::Viewport, background_color: Color, - overlay: &[T], on_pre_present: impl FnOnce(), ) -> Result<(), compositor::SurfaceError> { match (self, renderer, surface) { @@ -335,7 +334,6 @@ where surface, viewport, background_color, - overlay, on_pre_present, ), ( @@ -347,7 +345,6 @@ where surface, viewport, background_color, - overlay, on_pre_present, ), _ => unreachable!(), diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 5fc67b97..2dc60474 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -13,13 +13,11 @@ keywords.workspace = true [lints] workspace = true -[features] -debug = [] -multi-window = [] - [dependencies] bytes.workspace = true iced_core.workspace = true +iced_debug.workspace = true + iced_futures.workspace = true raw-window-handle.workspace = true 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 ae6d1dce..47bd92d9 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -13,29 +13,15 @@ pub mod clipboard; pub mod font; pub mod keyboard; pub mod overlay; -pub mod program; pub mod system; pub mod task; pub mod user_interface; 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 debug::Debug; -pub use program::Program; pub use task::Task; pub use user_interface::UserInterface; diff --git a/runtime/src/multi_window.rs b/runtime/src/multi_window.rs deleted file mode 100644 index cf778a20..00000000 --- a/runtime/src/multi_window.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! A multi-window application. -pub mod program; -pub mod state; - -pub use program::Program; -pub use state::State; diff --git a/runtime/src/multi_window/program.rs b/runtime/src/multi_window/program.rs deleted file mode 100644 index 4ea44791..00000000 --- a/runtime/src/multi_window/program.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Build interactive programs using The Elm Architecture. -use crate::Task; -use crate::core::text; -use crate::core::window; -use crate::core::{Element, Renderer}; - -/// 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 [`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>; -} diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs deleted file mode 100644 index 0bec555f..00000000 --- a/runtime/src/multi_window/state.rs +++ /dev/null @@ -1,278 +0,0 @@ -//! The internal state of a multi-window [`Program`]. -use crate::core::event::{self, Event}; -use crate::core::mouse; -use crate::core::renderer; -use crate::core::widget::operation::{self, Operation}; -use crate::core::{Clipboard, Size}; -use crate::user_interface::{self, UserInterface}; -use crate::{Debug, Program, Task}; - -/// The execution state of a multi-window [`Program`]. It leverages caching, event -/// processing, and rendering primitive storage. -#[allow(missing_debug_implementations)] -pub struct State

-where - P: Program + 'static, -{ - program: P, - caches: Option>, - queued_events: Vec, - queued_messages: Vec, - mouse_interaction: mouse::Interaction, -} - -impl

State

-where - P: Program + 'static, -{ - /// 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 { - let user_interface = build_user_interface( - &program, - user_interface::Cache::default(), - renderer, - bounds, - debug, - ); - - let caches = Some(vec![user_interface.into_cache()]); - - State { - program, - caches, - queued_events: Vec::new(), - queued_messages: Vec::new(), - mouse_interaction: mouse::Interaction::None, - } - } - - /// Returns a reference to the [`Program`] of the [`State`]. - pub fn program(&self) -> &P { - &self.program - } - - /// Queues an event in the [`State`] for processing during an [`update`]. - /// - /// [`update`]: Self::update - pub fn queue_event(&mut self, event: Event) { - self.queued_events.push(event); - } - - /// Queues a message in the [`State`] for processing during an [`update`]. - /// - /// [`update`]: Self::update - pub fn queue_message(&mut self, message: P::Message) { - self.queued_messages.push(message); - } - - /// Returns whether the event queue of the [`State`] is empty or not. - pub fn is_queue_empty(&self) -> bool { - self.queued_events.is_empty() && self.queued_messages.is_empty() - } - - /// Returns the current [`mouse::Interaction`] of the [`State`]. - pub fn mouse_interaction(&self) -> mouse::Interaction { - self.mouse_interaction - } - - /// Processes all the queued events and messages, rebuilding and redrawing - /// the widgets of the linked [`Program`] if necessary. - /// - /// Returns a list containing the instances of [`Event`] that were not - /// captured by any widget, and the [`Task`] obtained from [`Program`] - /// after updating it, only if an update was necessary. - pub fn update( - &mut self, - bounds: Size, - cursor: mouse::Cursor, - renderer: &mut P::Renderer, - 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 mut messages = Vec::new(); - - let uncaptured_events = user_interfaces.iter_mut().fold( - vec![], - |mut uncaptured_events, ui| { - let (_, event_statuses) = ui.update( - &self.queued_events, - cursor, - renderer, - clipboard, - &mut messages, - ); - - uncaptured_events.extend( - self.queued_events - .iter() - .zip(event_statuses) - .filter_map(|(event, status)| { - matches!(status, event::Status::Ignored) - .then_some(event) - }) - .cloned(), - ); - uncaptured_events - }, - ); - - self.queued_events.clear(); - messages.append(&mut self.queued_messages); - debug.event_processing_finished(); - - let commands = if messages.is_empty() { - debug.draw_started(); - - for ui in &mut user_interfaces { - self.mouse_interaction = - ui.draw(renderer, theme, style, cursor); - } - - debug.draw_finished(); - - self.caches = Some( - user_interfaces - .drain(..) - .map(UserInterface::into_cache) - .collect(), - ); - - None - } else { - let temp_caches = user_interfaces - .drain(..) - .map(UserInterface::into_cache) - .collect(); - - drop(user_interfaces); - - let commands = Task::batch(messages.into_iter().map(|msg| { - debug.log_message(&msg); - - debug.update_started(); - let task = self.program.update(msg); - debug.update_finished(); - - task - })); - - let mut user_interfaces = build_user_interfaces( - &self.program, - temp_caches, - renderer, - bounds, - debug, - ); - - debug.draw_started(); - for ui in &mut user_interfaces { - self.mouse_interaction = - ui.draw(renderer, theme, style, cursor); - } - debug.draw_finished(); - - self.caches = Some( - user_interfaces - .drain(..) - .map(UserInterface::into_cache) - .collect(), - ); - - Some(commands) - }; - - (uncaptured_events, commands) - } - - /// Applies widget [`Operation`]s to the [`State`]. - pub fn operate( - &mut self, - 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 { - let mut current_operation = Some(operation); - - while let Some(mut operation) = current_operation.take() { - for ui in &mut user_interfaces { - ui.operate(renderer, operation.as_mut()); - } - - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(()) => {} - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } - }; - } - } - - self.caches = Some( - user_interfaces - .drain(..) - .map(UserInterface::into_cache) - .collect(), - ); - } -} - -fn build_user_interfaces<'a, P: Program>( - program: &'a P, - 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) - }) - .collect() -} - -fn build_user_interface<'a, P: Program>( - program: &'a P, - 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 = program.view(); - debug.view_finished(); - - debug.layout_started(); - let user_interface = UserInterface::build(view, size, cache, renderer); - debug.layout_finished(); - - user_interface -} diff --git a/runtime/src/program.rs b/runtime/src/program.rs deleted file mode 100644 index 964157db..00000000 --- a/runtime/src/program.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Build interactive programs using The Elm Architecture. -use crate::Task; - -use iced_core::Element; -use iced_core::text; - -mod state; - -pub use state::State; - -/// The core of a user interface application following The Elm Architecture. -pub trait Program: Sized { - /// The graphics backend to use to draw the [`Program`]. - type Renderer: text::Renderer; - - /// The theme used to draw the [`Program`]. - type Theme; - - /// The type of __messages__ your [`Program`] will produce. - type Message: std::fmt::Debug + Send; - - /// 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 shells. - fn update(&mut self, message: Self::Message) -> Task; - - /// Returns the widgets to display in the [`Program`]. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view(&self) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; -} diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs deleted file mode 100644 index c377814a..00000000 --- a/runtime/src/program/state.rs +++ /dev/null @@ -1,229 +0,0 @@ -use crate::core::event::{self, Event}; -use crate::core::mouse; -use crate::core::renderer; -use crate::core::widget::operation::{self, Operation}; -use crate::core::{Clipboard, Size}; -use crate::user_interface::{self, UserInterface}; -use crate::{Debug, Program, Task}; - -/// The execution state of a [`Program`]. It leverages caching, event -/// processing, and rendering primitive storage. -#[allow(missing_debug_implementations)] -pub struct State

-where - P: Program + 'static, -{ - program: P, - cache: Option, - queued_events: Vec, - queued_messages: Vec, - mouse_interaction: mouse::Interaction, -} - -impl

State

-where - P: Program + 'static, -{ - /// Creates a new [`State`] with the provided [`Program`], initializing its - /// primitive with the given logical bounds and renderer. - pub fn new( - 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()); - - State { - program, - cache, - queued_events: Vec::new(), - queued_messages: Vec::new(), - mouse_interaction: mouse::Interaction::None, - } - } - - /// Returns a reference to the [`Program`] of the [`State`]. - pub fn program(&self) -> &P { - &self.program - } - - /// Queues an event in the [`State`] for processing during an [`update`]. - /// - /// [`update`]: Self::update - pub fn queue_event(&mut self, event: Event) { - self.queued_events.push(event); - } - - /// Queues a message in the [`State`] for processing during an [`update`]. - /// - /// [`update`]: Self::update - pub fn queue_message(&mut self, message: P::Message) { - self.queued_messages.push(message); - } - - /// Returns whether the event queue of the [`State`] is empty or not. - pub fn is_queue_empty(&self) -> bool { - self.queued_events.is_empty() && self.queued_messages.is_empty() - } - - /// Returns the current [`mouse::Interaction`] of the [`State`]. - pub fn mouse_interaction(&self) -> mouse::Interaction { - self.mouse_interaction - } - - /// Processes all the queued events and messages, rebuilding and redrawing - /// the widgets of the linked [`Program`] if necessary. - /// - /// Returns a list containing the instances of [`Event`] that were not - /// captured by any widget, and the [`Task`] obtained from [`Program`] - /// after updating it, only if an update was necessary. - pub fn update( - &mut self, - bounds: Size, - cursor: mouse::Cursor, - renderer: &mut P::Renderer, - 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 mut messages = Vec::new(); - - let (_, event_statuses) = user_interface.update( - &self.queued_events, - cursor, - renderer, - clipboard, - &mut messages, - ); - - let uncaptured_events = self - .queued_events - .iter() - .zip(event_statuses) - .filter_map(|(event, status)| { - matches!(status, event::Status::Ignored).then_some(event) - }) - .cloned() - .collect(); - - self.queued_events.clear(); - messages.append(&mut self.queued_messages); - debug.event_processing_finished(); - - let task = if messages.is_empty() { - debug.draw_started(); - self.mouse_interaction = - user_interface.draw(renderer, theme, style, cursor); - debug.draw_finished(); - - self.cache = Some(user_interface.into_cache()); - - None - } else { - // When there are messages, we are forced to rebuild twice - // for now :^) - let temp_cache = user_interface.into_cache(); - - let tasks = Task::batch(messages.into_iter().map(|message| { - debug.log_message(&message); - - debug.update_started(); - let task = self.program.update(message); - debug.update_finished(); - - task - })); - - let mut user_interface = build_user_interface( - &mut self.program, - temp_cache, - renderer, - bounds, - debug, - ); - - debug.draw_started(); - self.mouse_interaction = - user_interface.draw(renderer, theme, style, cursor); - debug.draw_finished(); - - self.cache = Some(user_interface.into_cache()); - - Some(tasks) - }; - - (uncaptured_events, task) - } - - /// Applies [`Operation`]s to the [`State`] - pub fn operate( - &mut self, - 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 { - let mut current_operation = Some(operation); - - while let Some(mut operation) = current_operation.take() { - user_interface.operate(renderer, operation.as_mut()); - - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(()) => {} - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } - }; - } - } - - self.cache = Some(user_interface.into_cache()); - } -} - -fn build_user_interface<'a, P: Program>( - program: &'a mut P, - 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 = program.view(); - debug.view_finished(); - - debug.layout_started(); - let user_interface = UserInterface::build(view, size, cache, renderer); - debug.layout_finished(); - - user_interface -} diff --git a/runtime/src/task.rs b/runtime/src/task.rs index 624fc3a7..81973e23 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -19,12 +19,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. @@ -83,9 +89,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. @@ -113,21 +126,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. @@ -135,11 +153,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, + }, }, } } @@ -149,35 +173,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, + }, } } @@ -197,26 +225,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 @@ -234,7 +261,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 } } @@ -368,13 +403,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 @@ -387,22 +423,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/src/advanced.rs b/src/advanced.rs index ac88776e..5a2ac990 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -23,4 +23,7 @@ pub use crate::core::renderer::{self, Renderer}; pub use crate::core::svg; pub use crate::core::text::{self, Text}; pub use crate::renderer::graphics; + +pub use iced_debug as debug; + pub use widget::Widget; diff --git a/src/application.rs b/src/application.rs index c220131a..f2ed2d4d 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 boot, update, and view logic. /// /// # Example /// ```no_run,standalone_crate /// 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, + boot: impl Boot, 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 { + boot: Boot, 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, + Boot: self::Boot, 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.boot.boot() + } + fn update( &self, state: &mut Self::State, @@ -123,6 +136,7 @@ where Application { raw: Instance { + boot, 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. @@ -152,28 +165,17 @@ 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, - P::State: Default, { - self.raw.run(self.settings, Some(self.window)) - } + #[cfg(feature = "debug")] + let program = iced_devtools::attach(self.raw); - /// 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) + #[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`]. @@ -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,47 @@ impl Application

{ } } +/// The logic to initialize the `State` of some [`Application`]. +/// +/// 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. + fn boot(&self) -> (State, Task); +} + +impl Boot for T +where + T: Fn() -> C, + C: IntoBoot, +{ + fn boot(&self) -> (State, Task) { + self().into_boot() + } +} + +/// 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 IntoBoot for State { + fn into_boot(self) -> (State, Task) { + (self, Task::none()) + } +} + +impl IntoBoot for (State, Task) { + fn into_boot(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..384e7582 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,13 +1,14 @@ //! 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}; 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. @@ -18,7 +19,7 @@ use std::borrow::Cow; /// /// [`exit`]: crate::exit pub fn daemon( - title: impl Title, + boot: impl application::Boot, 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::Boot, 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.boot() + } + 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. @@ -100,27 +113,11 @@ 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, - 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 +154,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 3ad685fa..980e6564 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,7 @@ //! //! ```no_run,standalone_crate //! 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`]: //! //! ```no_run,standalone_crate -//! # #[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: //! //! ```no_run,standalone_crate -//! # #[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; @@ -499,7 +504,6 @@ pub use iced_highlighter as highlighter; pub use iced_renderer::wgpu::wgpu; mod error; -mod program; pub mod application; pub mod daemon; @@ -660,7 +664,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)] @@ -682,7 +686,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, @@ -693,5 +696,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/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 65980e6a..5ea3cbe9 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -63,47 +63,16 @@ impl Renderer { self.layers.as_slice() } - pub fn draw>( + pub fn draw( &mut self, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: &mut tiny_skia::Mask, 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(engine::into_color( - Color { - a: 0.1, - ..background_color - }, - )), - anti_alias: false, - ..Default::default() - }, - tiny_skia::FillRule::default(), - tiny_skia::Transform::identity(), - None, - ); - } - self.layers.flush(); for ®ion in damage { @@ -201,25 +170,6 @@ impl Renderer { } } } - - if !overlay.is_empty() { - pixels.stroke_path( - &path, - &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor( - engine::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.engine.trim(); diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index a36be106..321a003f 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -107,13 +107,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], on_pre_present: impl FnOnce(), ) -> Result<(), compositor::SurfaceError> { present( @@ -121,7 +120,6 @@ impl crate::graphics::Compositor for Compositor { surface, viewport, background_color, - overlay, on_pre_present, ) } @@ -147,12 +145,11 @@ pub fn new( Compositor { context, settings } } -pub fn present>( +pub fn present( renderer: &mut Renderer, surface: &mut Surface, viewport: &Viewport, background_color: Color, - overlay: &[T], on_pre_present: impl FnOnce(), ) -> Result<(), compositor::SurfaceError> { let physical_size = viewport.physical_size(); @@ -211,7 +208,6 @@ pub fn present>( viewport, &damage, background_color, - overlay, ); on_pre_present(); @@ -231,7 +227,7 @@ pub fn screenshot( let mut clip_mask = tiny_skia::Mask::new(size.width, size.height) .expect("Create clip mask"); - renderer.draw::<&str>( + renderer.draw( &mut tiny_skia::PixmapMut::from_bytes( bytemuck::cast_slice_mut(&mut offscreen_buffer), size.width, @@ -245,7 +241,6 @@ pub fn screenshot( size.height as f32, ))], background_color, - &[], ); offscreen_buffer.iter().fold( diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index e5885547..cfb6a64f 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -63,7 +63,6 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::{ Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, - Vector, }; use crate::graphics::Viewport; use crate::graphics::text::{Editor, Paragraph}; @@ -164,16 +163,13 @@ impl Renderer { encoder } - pub fn present>( + pub fn present( &mut self, clear_color: Option, _format: wgpu::TextureFormat, frame: &wgpu::TextureView, viewport: &Viewport, - overlay: &[T], ) -> wgpu::SubmissionIndex { - self.draw_overlay(overlay, viewport); - let encoder = self.draw(clear_color, frame, viewport); self.staging_belt.finish(); @@ -577,50 +573,6 @@ impl Renderer { let _ = ManuallyDrop::into_inner(render_pass); } - - fn draw_overlay( - &mut self, - overlay: &[impl AsRef], - viewport: &Viewport, - ) { - use crate::core::Renderer as _; - use crate::core::alignment; - use crate::core::text::Renderer as _; - - self.with_layer( - Rectangle::with_size(viewport.logical_size()), - |renderer| { - for (i, line) in overlay.iter().enumerate() { - let text = crate::core::Text { - content: line.as_ref().to_owned(), - bounds: viewport.logical_size(), - size: Pixels(20.0), - line_height: core::text::LineHeight::default(), - font: Font::MONOSPACE, - align_x: core::text::Alignment::Default, - align_y: alignment::Vertical::Top, - shaping: core::text::Shaping::Basic, - wrapping: core::text::Wrapping::Word, - }; - - renderer.fill_text( - text.clone(), - Point::new(11.0, 11.0 + 25.0 * i as f32), - Color::from_rgba(0.9, 0.9, 0.9, 1.0), - Rectangle::with_size(Size::INFINITY), - ); - - renderer.fill_text( - text, - Point::new(11.0, 11.0 + 25.0 * i as f32) - + Vector::new(-1.0, -1.0), - Color::BLACK, - Rectangle::with_size(Size::INFINITY), - ); - } - }, - ); - } } impl core::Renderer for Renderer { @@ -716,7 +668,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) } @@ -728,7 +680,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) } @@ -760,7 +712,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/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 053d3a19..a29c1ce1 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -210,12 +210,11 @@ pub async fn new( } /// Presents the given primitives with the given [`Compositor`]. -pub fn present>( +pub fn present( renderer: &mut Renderer, surface: &mut wgpu::Surface<'static>, viewport: &Viewport, background_color: Color, - overlay: &[T], on_pre_present: impl FnOnce(), ) -> Result<(), compositor::SurfaceError> { match surface.get_current_texture() { @@ -229,7 +228,6 @@ pub fn present>( frame.texture.format(), view, viewport, - overlay, ); // Present the frame @@ -342,13 +340,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], on_pre_present: impl FnOnce(), ) -> Result<(), compositor::SurfaceError> { present( @@ -356,7 +353,6 @@ impl graphics::Compositor for Compositor { surface, viewport, background_color, - overlay, on_pre_present, ) } diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 118a2414..ff178e4f 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -2028,7 +2028,7 @@ pub fn focus_next() -> Task { task::effect(Action::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> diff --git a/winit/Cargo.toml b/winit/Cargo.toml index f8f8e26e..f2157978 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -15,7 +15,7 @@ workspace = true [features] default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] -debug = ["iced_runtime/debug"] +debug = ["iced_debug/enable"] system = ["sysinfo"] program = [] x11 = ["winit/x11"] @@ -25,9 +25,8 @@ wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] unconditional-rendering = [] [dependencies] -iced_futures.workspace = true -iced_graphics.workspace = true -iced_runtime.workspace = true +iced_debug.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 ed2910d6..bb9500e8 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -18,29 +18,1490 @@ 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::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::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(); + + loop { + // Empty the queue if possible + let event = 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, compositor_receiver) = + oneshot::channel(); + + let create_compositor = { + let window = window.clone(); + let mut proxy = proxy.clone(); + let default_fonts = default_fonts.clone(); + + async move { + let mut compositor = + ::Compositor::new(graphics_settings, window).await; + + if let Ok(compositor) = &mut compositor { + for font in default_fonts { + compositor.load_font(font.clone()); + } + } + + compositor_sender + .send(compositor) + .ok() + .expect("Send compositor"); + + // HACK! Send a proxy event on completion to trigger + // a runtime re-poll + // TODO: Send compositor through proxy (?) + { + let (sender, _receiver) = oneshot::channel(); + + proxy.send_action(Action::Window( + runtime::window::Action::GetLatest(sender), + )); + } + } + }; + + #[cfg(target_arch = "wasm32")] + wasm_bindgen_futures::spawn_local(create_compositor); + + #[cfg(not(target_arch = "wasm32"))] + runtime.block_on(create_compositor); + + match compositor_receiver + .await + .expect("Wait for compositor") + { + Ok(new_compositor) => { + compositor = Some(new_compositor); + } + Err(error) => { + let _ = control_sender + .start_send(Control::Crash(error.into())); + continue; + } + } + } + + debug::theme_changed(|| { + if window_manager.is_empty() { + theme::Base::palette(&program.theme(id)) + } else { + None + } + }); + + 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(), + || window.raw.pre_present_notify(), + ) { + 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(), + ) { + 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(); + } + + debug::theme_changed(|| { + window_manager.first().and_then(|window| { + theme::Base::palette(window.state.theme()) + }) + }); + + 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)); + debug::tasks_spawned(task.units()); + update_span.finish(); + + if let Some(stream) = runtime::task::into_stream(task) { + runtime.run(stream); + } + } + + let subscription = runtime.enter(|| program.subscription()); + let recipes = subscription::into_recipes(subscription.map(Action::Output)); + + debug::subscriptions_tracked(recipes.len()); + runtime.track(recipes); +} + +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 f47dfdf5..00000000 --- a/winit/src/program.rs +++ /dev/null @@ -1,1582 +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::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; - - /// 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; - - let mut debug = Debug::new(); - debug.startup_started(); - - 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(), - debug, - 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, - }; - - 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 debug: Debug, - 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(); - - debug.startup_finished(); - - loop { - let event = 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, compositor_receiver) = - oneshot::channel(); - - let create_compositor = { - let window = window.clone(); - let mut proxy = proxy.clone(); - let default_fonts = default_fonts.clone(); - - async move { - let mut compositor = - C::new(graphics_settings, window).await; - - if let Ok(compositor) = &mut compositor { - for font in default_fonts { - compositor.load_font(font.clone()); - } - } - - compositor_sender - .send(compositor) - .ok() - .expect("Send compositor"); - - // HACK! Send a proxy event on completion to trigger - // a runtime re-poll - // TODO: Send compositor through proxy (?) - { - let (sender, _receiver) = oneshot::channel(); - - proxy.send_action(Action::Window( - runtime::window::Action::GetLatest(sender), - )); - } - } - }; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_futures::spawn_local(create_compositor); - - #[cfg(not(target_arch = "wasm32"))] - runtime.block_on(create_compositor); - - match compositor_receiver - .await - .expect("Wait for compositor") - { - Ok(new_compositor) => { - compositor = Some(new_compositor); - } - Err(error) => { - let _ = control_sender - .start_send(Control::Crash(error.into())); - 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, - &mut debug, - 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(); - - debug.layout_started(); - let ui = user_interfaces - .remove(&id) - .expect("Remove user interface"); - - let _ = user_interfaces.insert( - id, - ui.relayout(logical_size, &mut window.renderer), - ); - debug.layout_finished(); - - 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, - ); - - debug.draw_started(); - let new_mouse_interaction = ui.draw( - &mut window.renderer, - window.state.theme(), - &renderer::Style { - text_color: window.state.text_color(), - }, - cursor, - ); - debug.draw_finished(); - - 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(); - - debug.render_started(); - match compositor.present( - &mut window.renderer, - &mut window.surface, - window.state.viewport(), - window.state.background_color(), - &debug.overlay(), - || window.raw.pre_present_notify(), - ) { - Ok(()) => { - debug.render_finished(); - } - Err(error) => match error { - // This is an unrecoverable error. - compositor::SurfaceError::OutOfMemory => { - panic!("{error}"); - } - _ => { - debug.render_finished(); - - log::warn!( - "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, - &mut debug, - ); - - if let Some(event) = conversion::window_event( - window_event, - window.state.scale_factor(), - window.state.modifiers(), - ) { - 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; - } - - debug.event_processing_started(); - let mut uis_stale = false; - - for (id, window) in window_manager.iter_mut() { - 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, - }, - ); - } - } - - for (id, event) in events.drain(..) { - runtime.broadcast( - subscription::Event::Interaction { - window: id, - event, - status: core::event::Status::Ignored, - }, - ); - } - - debug.event_processing_finished(); - - 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 debug, - &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 debug, - &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, - debug: &mut Debug, - id: window::Id, -) -> UserInterface<'a, P::Message, P::Theme, P::Renderer> -where - P::Theme: theme::Base, -{ - debug.view_started(); - let view = program.view(id); - debug.view_finished(); - - debug.layout_started(); - let user_interface = UserInterface::build(view, size, cache, renderer); - debug.layout_finished(); - - user_interface -} - -fn update( - program: &mut P, - runtime: &mut Runtime, Action>, - debug: &mut Debug, - messages: &mut Vec, -) where - P::Theme: theme::Base, -{ - for message in messages.drain(..) { - debug.log_message(&message); - debug.update_started(); - - let task = runtime.enter(|| program.update(message)); - debug.update_finished(); - - 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, - debug: &mut Debug, - 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(), - debug, - 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 83% rename from winit/src/program/state.rs rename to winit/src/window/state.rs index 911e84fe..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(); @@ -137,12 +137,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); @@ -179,22 +174,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(); - window.request_redraw(); - } _ => {} } } @@ -206,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); @@ -219,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(); @@ -237,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); } }