diff options
author | 2023-12-05 01:03:09 +0100 | |
---|---|---|
committer | 2023-12-05 01:03:09 +0100 | |
commit | fc285d3e461626408c56bbc1605fcf0c974b2f69 (patch) | |
tree | 8aca292516d9aa43b78a14f51dd90caf60c691d7 /winit | |
parent | 8727b3fc50ec251d9c117c51ca1289be5ba9b117 (diff) | |
parent | 5c5e7653bed248ba63faa6563e4d673e4441415e (diff) | |
download | iced-fc285d3e461626408c56bbc1605fcf0c974b2f69.tar.gz iced-fc285d3e461626408c56bbc1605fcf0c974b2f69.tar.bz2 iced-fc285d3e461626408c56bbc1605fcf0c974b2f69.zip |
Merge pull request #1964 from bungoboingo/feat/multi-window-support
[Feature] 🪟 Multi Window 🪟 .. redux!
Diffstat (limited to 'winit')
-rw-r--r-- | winit/Cargo.toml | 2 | ||||
-rw-r--r-- | winit/src/application.rs | 140 | ||||
-rw-r--r-- | winit/src/application/profiler.rs | 101 | ||||
-rw-r--r-- | winit/src/conversion.rs | 186 | ||||
-rw-r--r-- | winit/src/lib.rs | 9 | ||||
-rw-r--r-- | winit/src/multi_window.rs | 1212 | ||||
-rw-r--r-- | winit/src/multi_window/state.rs | 240 | ||||
-rw-r--r-- | winit/src/multi_window/window_manager.rs | 156 | ||||
-rw-r--r-- | winit/src/position.rs | 22 | ||||
-rw-r--r-- | winit/src/settings.rs | 232 | ||||
-rw-r--r-- | winit/src/settings/linux.rs | 11 | ||||
-rw-r--r-- | winit/src/settings/macos.rs | 12 | ||||
-rw-r--r-- | winit/src/settings/other.rs | 3 | ||||
-rw-r--r-- | winit/src/settings/wasm.rs | 11 | ||||
-rw-r--r-- | winit/src/settings/windows.rs | 21 |
15 files changed, 1825 insertions, 533 deletions
diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 674a66d3..87e600ae 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -19,6 +19,7 @@ x11 = ["winit/x11"] wayland = ["winit/wayland"] wayland-dlopen = ["winit/wayland-dlopen"] wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] +multi-window = ["iced_runtime/multi-window"] [dependencies] iced_graphics.workspace = true @@ -26,7 +27,6 @@ iced_runtime.workspace = true iced_style.workspace = true log.workspace = true -raw-window-handle.workspace = true thiserror.workspace = true tracing.workspace = true window_clipboard.workspace = true diff --git a/winit/src/application.rs b/winit/src/application.rs index 2c5c864a..d9700075 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1,6 +1,4 @@ //! Create interactive, native cross-platform applications. -#[cfg(feature = "trace")] -mod profiler; mod state; pub use state::State; @@ -27,11 +25,6 @@ use futures::channel::mpsc; use std::mem::ManuallyDrop; -#[cfg(feature = "trace")] -pub use profiler::Profiler; -#[cfg(feature = "trace")] -use tracing::{info_span, instrument::Instrument}; - /// An interactive, native cross-platform application. /// /// This trait is the main entrypoint of Iced. Once implemented, you can run @@ -119,15 +112,9 @@ where use futures::Future; use winit::event_loop::EventLoopBuilder; - #[cfg(feature = "trace")] - let _guard = Profiler::init(); - let mut debug = Debug::new(); debug.startup_started(); - #[cfg(feature = "trace")] - let _ = info_span!("Application", "RUN").entered(); - let event_loop = EventLoopBuilder::with_user_event().build(); let proxy = event_loop.create_proxy(); @@ -148,14 +135,15 @@ where let target = settings.window.platform_specific.target.clone(); let should_be_visible = settings.window.visible; - let builder = settings - .window - .into_builder( - &application.title(), - event_loop.primary_monitor(), - settings.id, - ) - .with_visible(false); + let exit_on_close_request = settings.window.exit_on_close_request; + + let builder = conversion::window_settings( + settings.window, + &application.title(), + event_loop.primary_monitor(), + settings.id, + ) + .with_visible(false); log::debug!("Window builder: {builder:#?}"); @@ -193,8 +181,8 @@ where }; } - let (compositor, mut renderer) = - C::new(compositor_settings, Some(&window))?; + let compositor = C::new(compositor_settings, Some(&window))?; + let mut renderer = compositor.create_renderer(); for font in settings.fonts { use crate::core::text::Renderer; @@ -205,28 +193,20 @@ where let (mut event_sender, event_receiver) = mpsc::unbounded(); let (control_sender, mut control_receiver) = mpsc::unbounded(); - let mut instance = Box::pin({ - let run_instance = run_instance::<A, E, C>( - application, - compositor, - renderer, - runtime, - proxy, - debug, - event_receiver, - control_sender, - init_command, - window, - should_be_visible, - settings.exit_on_close_request, - ); - - #[cfg(feature = "trace")] - let run_instance = - run_instance.instrument(info_span!("Application", "LOOP")); - - run_instance - }); + let mut instance = Box::pin(run_instance::<A, E, C>( + application, + compositor, + renderer, + runtime, + proxy, + debug, + event_receiver, + control_sender, + init_command, + window, + should_be_visible, + exit_on_close_request, + )); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -426,6 +406,7 @@ async fn run_instance<A, E, C>( // Then, we can use the `interface_state` here to decide if a redraw // is needed right away, or simply wait until a specific time. let redraw_event = Event::Window( + window::Id::MAIN, window::Event::RedrawRequested(Instant::now()), ); @@ -488,9 +469,6 @@ async fn run_instance<A, E, C>( messages.push(message); } event::Event::RedrawRequested(_) => { - #[cfg(feature = "trace")] - let _ = info_span!("Application", "FRAME").entered(); - let physical_size = state.physical_size(); if physical_size.width == 0 || physical_size.height == 0 { @@ -578,6 +556,7 @@ async fn run_instance<A, E, C>( state.update(&window, &window_event, &mut debug); if let Some(event) = conversion::window_event( + window::Id::MAIN, &window_event, state.scale_factor(), state.modifiers(), @@ -629,24 +608,12 @@ pub fn build_user_interface<'a, A: Application>( where <A::Renderer as core::Renderer>::Theme: StyleSheet, { - #[cfg(feature = "trace")] - let view_span = info_span!("Application", "VIEW").entered(); - debug.view_started(); let view = application.view(); - - #[cfg(feature = "trace")] - let _ = view_span.exit(); debug.view_finished(); - #[cfg(feature = "trace")] - let layout_span = info_span!("Application", "LAYOUT").entered(); - debug.layout_started(); let user_interface = UserInterface::build(view, size, cache, renderer); - - #[cfg(feature = "trace")] - let _ = layout_span.exit(); debug.layout_finished(); user_interface @@ -673,16 +640,10 @@ pub fn update<A: Application, C, E: Executor>( <A::Renderer as core::Renderer>::Theme: StyleSheet, { for message in messages.drain(..) { - #[cfg(feature = "trace")] - let update_span = info_span!("Application", "UPDATE").entered(); - debug.log_message(&message); debug.update_started(); let command = runtime.enter(|| application.update(message)); - - #[cfg(feature = "trace")] - let _ = update_span.exit(); debug.update_finished(); run_command( @@ -752,20 +713,27 @@ pub fn run_command<A, C, E>( } }, command::Action::Window(action) => match action { - window::Action::Close => { + window::Action::Close(_id) => { *should_exit = true; } - window::Action::Drag => { + window::Action::Drag(_id) => { let _res = window.drag_window(); } - window::Action::Resize(size) => { + window::Action::Spawn { .. } => { + log::warn!( + "Spawning a window is only available with \ + multi-window applications." + ); + } + window::Action::Resize(_id, size) => { window.set_inner_size(winit::dpi::LogicalSize { width: size.width, height: size.height, }); } - window::Action::FetchSize(callback) => { - let size = window.inner_size(); + window::Action::FetchSize(_id, callback) => { + let size = + window.inner_size().to_logical(window.scale_factor()); proxy .send_event(callback(Size::new( @@ -774,29 +742,29 @@ pub fn run_command<A, C, E>( ))) .expect("Send message to event loop"); } - window::Action::Maximize(maximized) => { + window::Action::Maximize(_id, maximized) => { window.set_maximized(maximized); } - window::Action::Minimize(minimized) => { + window::Action::Minimize(_id, minimized) => { window.set_minimized(minimized); } - window::Action::Move { x, y } => { + window::Action::Move(_id, position) => { window.set_outer_position(winit::dpi::LogicalPosition { - x, - y, + x: position.x, + y: position.y, }); } - window::Action::ChangeMode(mode) => { + window::Action::ChangeMode(_id, mode) => { window.set_visible(conversion::visible(mode)); window.set_fullscreen(conversion::fullscreen( window.current_monitor(), mode, )); } - window::Action::ChangeIcon(icon) => { + window::Action::ChangeIcon(_id, icon) => { window.set_window_icon(conversion::icon(icon)); } - window::Action::FetchMode(tag) => { + window::Action::FetchMode(_id, tag) => { let mode = if window.is_visible().unwrap_or(true) { conversion::mode(window.fullscreen()) } else { @@ -807,29 +775,29 @@ pub fn run_command<A, C, E>( .send_event(tag(mode)) .expect("Send message to event loop"); } - window::Action::ToggleMaximize => { + window::Action::ToggleMaximize(_id) => { window.set_maximized(!window.is_maximized()); } - window::Action::ToggleDecorations => { + window::Action::ToggleDecorations(_id) => { window.set_decorations(!window.is_decorated()); } - window::Action::RequestUserAttention(user_attention) => { + window::Action::RequestUserAttention(_id, user_attention) => { window.request_user_attention( user_attention.map(conversion::user_attention), ); } - window::Action::GainFocus => { + window::Action::GainFocus(_id) => { window.focus_window(); } - window::Action::ChangeLevel(level) => { + window::Action::ChangeLevel(_id, level) => { window.set_window_level(conversion::window_level(level)); } - window::Action::FetchId(tag) => { + window::Action::FetchId(_id, tag) => { proxy .send_event(tag(window.id().into())) .expect("Send message to event loop"); } - window::Action::Screenshot(tag) => { + window::Action::Screenshot(_id, tag) => { let bytes = compositor.screenshot( renderer, surface, diff --git a/winit/src/application/profiler.rs b/winit/src/application/profiler.rs deleted file mode 100644 index 7031507a..00000000 --- a/winit/src/application/profiler.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! A simple profiler for Iced. -use std::ffi::OsStr; -use std::path::Path; -use std::time::Duration; -use tracing_subscriber::prelude::*; -use tracing_subscriber::Registry; -#[cfg(feature = "chrome-trace")] -use { - tracing_chrome::FlushGuard, - tracing_subscriber::fmt::{format::DefaultFields, FormattedFields}, -}; - -/// Profiler state. This will likely need to be updated or reworked when adding new tracing backends. -#[allow(missing_debug_implementations)] -pub struct Profiler { - #[cfg(feature = "chrome-trace")] - /// [`FlushGuard`] must not be dropped until the application scope is dropped for accurate tracing. - _guard: FlushGuard, -} - -impl Profiler { - /// Initializes the [`Profiler`]. - pub fn init() -> Self { - // Registry stores the spans & generates unique span IDs - let subscriber = Registry::default(); - - let default_path = Path::new(env!("CARGO_MANIFEST_DIR")); - let curr_exe = std::env::current_exe() - .unwrap_or_else(|_| default_path.to_path_buf()); - let out_dir = curr_exe.parent().unwrap_or(default_path).join("traces"); - - #[cfg(feature = "chrome-trace")] - let (chrome_layer, guard) = { - let mut layer = tracing_chrome::ChromeLayerBuilder::new(); - - // Optional configurable env var: CHROME_TRACE_FILE=/path/to/trace_file/file.json, - // for uploading to chrome://tracing (old) or ui.perfetto.dev (new). - if let Ok(path) = std::env::var("CHROME_TRACE_FILE") { - layer = layer.file(path); - } else if std::fs::create_dir_all(&out_dir).is_ok() { - let time = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or(Duration::from_millis(0)) - .as_millis(); - - let curr_exe_name = curr_exe - .file_name() - .unwrap_or_else(|| OsStr::new("trace")) - .to_str() - .unwrap_or("trace"); - - let path = - out_dir.join(format!("{curr_exe_name}_trace_{time}.json")); - - layer = layer.file(path); - } else { - layer = layer.file(env!("CARGO_MANIFEST_DIR")) - } - - let (chrome_layer, guard) = layer - .name_fn(Box::new(|event_or_span| match event_or_span { - tracing_chrome::EventOrSpan::Event(event) => { - event.metadata().name().into() - } - tracing_chrome::EventOrSpan::Span(span) => { - if let Some(fields) = span - .extensions() - .get::<FormattedFields<DefaultFields>>() - { - format!( - "{}: {}", - span.metadata().name(), - fields.fields.as_str() - ) - } else { - span.metadata().name().into() - } - } - })) - .build(); - - (chrome_layer, guard) - }; - - let fmt_layer = tracing_subscriber::fmt::Layer::default(); - let subscriber = subscriber.with(fmt_layer); - - #[cfg(feature = "chrome-trace")] - let subscriber = subscriber.with(chrome_layer); - - // create dispatcher which will forward span events to the subscriber - // this can only be set once or will panic - tracing::subscriber::set_global_default(subscriber) - .expect("Tracer could not set the global default subscriber."); - - Profiler { - #[cfg(feature = "chrome-trace")] - _guard: guard, - } - } -} diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 3ecd044c..7e51a2d4 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -6,11 +6,128 @@ use crate::core::keyboard; use crate::core::mouse; use crate::core::touch; use crate::core::window; -use crate::core::{Event, Point}; -use crate::Position; +use crate::core::{Event, Point, Size}; + +/// Converts some [`window::Settings`] into a `WindowBuilder` from `winit`. +pub fn window_settings( + settings: window::Settings, + title: &str, + primary_monitor: Option<winit::monitor::MonitorHandle>, + _id: Option<String>, +) -> winit::window::WindowBuilder { + let mut window_builder = winit::window::WindowBuilder::new(); + + window_builder = window_builder + .with_title(title) + .with_inner_size(winit::dpi::LogicalSize { + width: settings.size.width, + height: settings.size.height, + }) + .with_resizable(settings.resizable) + .with_enabled_buttons(if settings.resizable { + winit::window::WindowButtons::all() + } else { + winit::window::WindowButtons::CLOSE + | winit::window::WindowButtons::MINIMIZE + }) + .with_decorations(settings.decorations) + .with_transparent(settings.transparent) + .with_window_icon(settings.icon.and_then(icon)) + .with_window_level(window_level(settings.level)) + .with_visible(settings.visible); + + if let Some(position) = + position(primary_monitor.as_ref(), settings.size, settings.position) + { + window_builder = window_builder.with_position(position); + } + + if let Some(min_size) = settings.min_size { + window_builder = + window_builder.with_min_inner_size(winit::dpi::LogicalSize { + width: min_size.width, + height: min_size.height, + }); + } + + if let Some(max_size) = settings.max_size { + window_builder = + window_builder.with_max_inner_size(winit::dpi::LogicalSize { + width: max_size.width, + height: max_size.height, + }); + } + + #[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + // `with_name` is available on both `WindowBuilderExtWayland` and `WindowBuilderExtX11` and they do + // exactly the same thing. We arbitrarily choose `WindowBuilderExtWayland` here. + use ::winit::platform::wayland::WindowBuilderExtWayland; + + if let Some(id) = _id { + window_builder = window_builder.with_name(id.clone(), id); + } + } + + #[cfg(target_os = "windows")] + { + use winit::platform::windows::WindowBuilderExtWindows; + #[allow(unsafe_code)] + unsafe { + window_builder = window_builder + .with_parent_window(settings.platform_specific.parent); + } + window_builder = window_builder + .with_drag_and_drop(settings.platform_specific.drag_and_drop); + } + + #[cfg(target_os = "macos")] + { + use winit::platform::macos::WindowBuilderExtMacOS; + + window_builder = window_builder + .with_title_hidden(settings.platform_specific.title_hidden) + .with_titlebar_transparent( + settings.platform_specific.titlebar_transparent, + ) + .with_fullsize_content_view( + settings.platform_specific.fullsize_content_view, + ); + } + + #[cfg(target_os = "linux")] + { + #[cfg(feature = "x11")] + { + use winit::platform::x11::WindowBuilderExtX11; + + window_builder = window_builder.with_name( + &settings.platform_specific.application_id, + &settings.platform_specific.application_id, + ); + } + #[cfg(feature = "wayland")] + { + use winit::platform::wayland::WindowBuilderExtWayland; + + window_builder = window_builder.with_name( + &settings.platform_specific.application_id, + &settings.platform_specific.application_id, + ); + } + } + + window_builder +} /// Converts a winit window event into an iced event. pub fn window_event( + id: window::Id, event: &winit::event::WindowEvent<'_>, scale_factor: f64, modifiers: winit::event::ModifiersState, @@ -21,21 +138,27 @@ pub fn window_event( WindowEvent::Resized(new_size) => { let logical_size = new_size.to_logical(scale_factor); - Some(Event::Window(window::Event::Resized { - width: logical_size.width, - height: logical_size.height, - })) + Some(Event::Window( + id, + window::Event::Resized { + width: logical_size.width, + height: logical_size.height, + }, + )) } WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { let logical_size = new_inner_size.to_logical(scale_factor); - Some(Event::Window(window::Event::Resized { - width: logical_size.width, - height: logical_size.height, - })) + Some(Event::Window( + id, + window::Event::Resized { + width: logical_size.width, + height: logical_size.height, + }, + )) } WindowEvent::CloseRequested => { - Some(Event::Window(window::Event::CloseRequested)) + Some(Event::Window(id, window::Event::CloseRequested)) } WindowEvent::CursorMoved { position, .. } => { let position = position.to_logical::<f64>(scale_factor); @@ -113,19 +236,22 @@ pub fn window_event( WindowEvent::ModifiersChanged(new_modifiers) => Some(Event::Keyboard( keyboard::Event::ModifiersChanged(self::modifiers(*new_modifiers)), )), - WindowEvent::Focused(focused) => Some(Event::Window(if *focused { - window::Event::Focused - } else { - window::Event::Unfocused - })), + WindowEvent::Focused(focused) => Some(Event::Window( + id, + if *focused { + window::Event::Focused + } else { + window::Event::Unfocused + }, + )), WindowEvent::HoveredFile(path) => { - Some(Event::Window(window::Event::FileHovered(path.clone()))) + Some(Event::Window(id, window::Event::FileHovered(path.clone()))) } WindowEvent::DroppedFile(path) => { - Some(Event::Window(window::Event::FileDropped(path.clone()))) + Some(Event::Window(id, window::Event::FileDropped(path.clone()))) } WindowEvent::HoveredFileCancelled => { - Some(Event::Window(window::Event::FilesHoveredLeft)) + Some(Event::Window(id, window::Event::FilesHoveredLeft)) } WindowEvent::Touch(touch) => { Some(Event::Touch(touch_event(*touch, scale_factor))) @@ -134,7 +260,7 @@ pub fn window_event( let winit::dpi::LogicalPosition { x, y } = position.to_logical(scale_factor); - Some(Event::Window(window::Event::Moved { x, y })) + Some(Event::Window(id, window::Event::Moved { x, y })) } _ => None, } @@ -153,23 +279,23 @@ pub fn window_level(level: window::Level) -> winit::window::WindowLevel { } } -/// Converts a [`Position`] to a [`winit`] logical position for a given monitor. +/// Converts a [`window::Position`] to a [`winit`] logical position for a given monitor. /// /// [`winit`]: https://github.com/rust-windowing/winit pub fn position( monitor: Option<&winit::monitor::MonitorHandle>, - (width, height): (u32, u32), - position: Position, + size: Size, + position: window::Position, ) -> Option<winit::dpi::Position> { match position { - Position::Default => None, - Position::Specific(x, y) => { + window::Position::Default => None, + window::Position::Specific(position) => { Some(winit::dpi::Position::Logical(winit::dpi::LogicalPosition { - x: f64::from(x), - y: f64::from(y), + x: f64::from(position.x), + y: f64::from(position.y), })) } - Position::Centered => { + window::Position::Centered => { if let Some(monitor) = monitor { let start = monitor.position(); @@ -178,8 +304,8 @@ pub fn position( let centered: winit::dpi::PhysicalPosition<i32> = winit::dpi::LogicalPosition { - x: (resolution.width - f64::from(width)) / 2.0, - y: (resolution.height - f64::from(height)) / 2.0, + x: (resolution.width - f64::from(size.width)) / 2.0, + y: (resolution.height - f64::from(size.height)) / 2.0, } .to_physical(monitor.scale_factor()); diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 95b55bb9..948576a2 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -33,6 +33,9 @@ pub use iced_runtime::futures; pub use iced_style as style; pub use winit; +#[cfg(feature = "multi-window")] +pub mod multi_window; + #[cfg(feature = "application")] pub mod application; pub mod clipboard; @@ -43,17 +46,11 @@ pub mod settings; pub mod system; mod error; -mod position; mod proxy; #[cfg(feature = "application")] pub use application::Application; -#[cfg(feature = "trace")] -pub use application::Profiler; pub use clipboard::Clipboard; pub use error::Error; -pub use position::Position; pub use proxy::Proxy; pub use settings::Settings; - -pub use iced_graphics::Viewport; diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs new file mode 100644 index 00000000..84651d40 --- /dev/null +++ b/winit/src/multi_window.rs @@ -0,0 +1,1212 @@ +//! 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::renderer; +use crate::core::widget::operation; +use crate::core::window; +use crate::core::Size; +use crate::futures::futures::channel::mpsc; +use crate::futures::futures::{task, Future, StreamExt}; +use crate::futures::{Executor, Runtime, Subscription}; +use crate::graphics::{compositor, Compositor}; +use crate::multi_window::window_manager::WindowManager; +use crate::runtime::command::{self, Command}; +use crate::runtime::multi_window::Program; +use crate::runtime::user_interface::{self, UserInterface}; +use crate::runtime::Debug; +use crate::style::application::StyleSheet; +use crate::{Clipboard, Error, Proxy, Settings}; + +use std::collections::HashMap; +use std::mem::ManuallyDrop; +use std::time::Instant; + +/// 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. +/// +/// An [`Application`] can execute asynchronous actions by returning a +/// [`Command`] in some of its methods. +/// +/// When using an [`Application`] with the `debug` feature enabled, a debug view +/// can be toggled by pressing `F12`. +pub trait Application: Program +where + <Self::Renderer as core::Renderer>::Theme: StyleSheet, +{ + /// The data needed to initialize your [`Application`]. + type Flags; + + /// Initializes the [`Application`] 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 [`Command`] 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, Command<Self::Message>); + + /// Returns the current title of the [`Application`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self, window: window::Id) -> String; + + /// Returns the current `Theme` of the [`Application`]. + fn theme( + &self, + window: window::Id, + ) -> <Self::Renderer as core::Renderer>::Theme; + + /// Returns the `Style` variation of the `Theme`. + fn style( + &self, + ) -> <<Self::Renderer as core::Renderer>::Theme as StyleSheet>::Style { + Default::default() + } + + /// 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<Self::Message> { + Subscription::none() + } + + /// Returns the scale factor of the window of the [`Application`]. + /// + /// 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 an [`Application`] with an executor, compositor, and the provided +/// settings. +pub fn run<A, E, C>( + settings: Settings<A::Flags>, + compositor_settings: C::Settings, +) -> Result<(), Error> +where + A: Application + 'static, + E: Executor + 'static, + C: Compositor<Renderer = A::Renderer> + 'static, + <A::Renderer as core::Renderer>::Theme: StyleSheet, +{ + use winit::event_loop::EventLoopBuilder; + + let mut debug = Debug::new(); + debug.startup_started(); + + let event_loop = EventLoopBuilder::with_user_event().build(); + let proxy = event_loop.create_proxy(); + + let runtime = { + let proxy = Proxy::new(event_loop.create_proxy()); + let executor = E::new().map_err(Error::ExecutorCreationFailed)?; + + Runtime::new(executor, proxy) + }; + + let (application, init_command) = { + let flags = settings.flags; + + runtime.enter(|| A::new(flags)) + }; + + let should_main_be_visible = settings.window.visible; + let exit_on_close_request = settings.window.exit_on_close_request; + + let builder = conversion::window_settings( + settings.window, + &application.title(window::Id::MAIN), + event_loop.primary_monitor(), + settings.id, + ) + .with_visible(false); + + log::info!("Window builder: {:#?}", builder); + + let main_window = builder + .build(&event_loop) + .map_err(Error::WindowCreationFailed)?; + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + + let canvas = main_window.canvas(); + + 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"); + } + }; + } + + let mut compositor = C::new(compositor_settings, Some(&main_window))?; + + let mut window_manager = WindowManager::new(); + let _ = window_manager.insert( + window::Id::MAIN, + main_window, + &application, + &mut compositor, + exit_on_close_request, + ); + + let (mut event_sender, event_receiver) = mpsc::unbounded(); + let (control_sender, mut control_receiver) = mpsc::unbounded(); + + let mut instance = Box::pin(run_instance::<A, E, C>( + application, + compositor, + runtime, + proxy, + debug, + event_receiver, + control_sender, + init_command, + window_manager, + should_main_be_visible, + )); + + let mut context = task::Context::from_waker(task::noop_waker_ref()); + + platform::run(event_loop, move |event, window_target, control_flow| { + use winit::event_loop::ControlFlow; + + if let ControlFlow::ExitWithCode(_) = control_flow { + return; + } + + let event = match event { + winit::event::Event::WindowEvent { + event: + winit::event::WindowEvent::ScaleFactorChanged { + new_inner_size, + .. + }, + window_id, + } => Some(winit::event::Event::WindowEvent { + event: winit::event::WindowEvent::Resized(*new_inner_size), + window_id, + }), + _ => event.to_static(), + }; + + if let Some(event) = event { + event_sender + .start_send(Event::EventLoopAwakened(event)) + .expect("Send event"); + + loop { + let poll = instance.as_mut().poll(&mut context); + + match poll { + task::Poll::Pending => match control_receiver.try_next() { + Ok(Some(control)) => match control { + Control::ChangeFlow(flow) => { + *control_flow = flow; + } + Control::CreateWindow { + id, + settings, + title, + monitor, + } => { + let exit_on_close_request = + settings.exit_on_close_request; + + let window = conversion::window_settings( + settings, &title, monitor, None, + ) + .build(window_target) + .expect("Failed to build window"); + + event_sender + .start_send(Event::WindowCreated { + id, + window, + exit_on_close_request, + }) + .expect("Send event"); + } + }, + _ => { + break; + } + }, + task::Poll::Ready(_) => { + *control_flow = ControlFlow::Exit; + break; + } + }; + } + } + }) +} + +enum Event<Message: 'static> { + WindowCreated { + id: window::Id, + window: winit::window::Window, + exit_on_close_request: bool, + }, + EventLoopAwakened(winit::event::Event<'static, Message>), +} + +enum Control { + ChangeFlow(winit::event_loop::ControlFlow), + CreateWindow { + id: window::Id, + settings: window::Settings, + title: String, + monitor: Option<winit::monitor::MonitorHandle>, + }, +} + +async fn run_instance<A, E, C>( + mut application: A, + mut compositor: C, + mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, + mut proxy: winit::event_loop::EventLoopProxy<A::Message>, + mut debug: Debug, + mut event_receiver: mpsc::UnboundedReceiver<Event<A::Message>>, + mut control_sender: mpsc::UnboundedSender<Control>, + init_command: Command<A::Message>, + mut window_manager: WindowManager<A, C>, + should_main_window_be_visible: bool, +) where + A: Application + 'static, + E: Executor + 'static, + C: Compositor<Renderer = A::Renderer> + 'static, + <A::Renderer as core::Renderer>::Theme: StyleSheet, +{ + use winit::event; + use winit::event_loop::ControlFlow; + + let main_window = window_manager + .get_mut(window::Id::MAIN) + .expect("Get main window"); + + if should_main_window_be_visible { + main_window.raw.set_visible(true); + } + + let mut clipboard = Clipboard::connect(&main_window.raw); + let mut events = { + vec![( + Some(window::Id::MAIN), + core::Event::Window( + window::Id::MAIN, + window::Event::Opened { + position: main_window.position(), + size: main_window.size(), + }, + ), + )] + }; + + let mut ui_caches = HashMap::new(); + let mut user_interfaces = ManuallyDrop::new(build_user_interfaces( + &application, + &mut debug, + &mut window_manager, + HashMap::from_iter([( + window::Id::MAIN, + user_interface::Cache::default(), + )]), + )); + + run_command( + &application, + &mut compositor, + init_command, + &mut runtime, + &mut clipboard, + &mut control_sender, + &mut proxy, + &mut debug, + &mut window_manager, + &mut ui_caches, + ); + + runtime.track(application.subscription().into_recipes()); + + let mut messages = Vec::new(); + let mut redraw_pending = false; + + debug.startup_finished(); + + 'main: while let Some(event) = event_receiver.next().await { + match event { + Event::WindowCreated { + id, + window, + exit_on_close_request, + } => { + let window = window_manager.insert( + id, + window, + &application, + &mut compositor, + exit_on_close_request, + ); + + let logical_size = window.state.logical_size(); + + let _ = user_interfaces.insert( + id, + build_user_interface( + &application, + user_interface::Cache::default(), + &mut window.renderer, + logical_size, + &mut debug, + id, + ), + ); + let _ = ui_caches.insert(id, user_interface::Cache::default()); + + events.push(( + Some(id), + core::Event::Window( + id, + window::Event::Opened { + position: window.position(), + size: window.size(), + }, + ), + )); + } + Event::EventLoopAwakened(event) => { + match event { + event::Event::NewEvents(start_cause) => { + redraw_pending = matches!( + start_cause, + event::StartCause::Init + | event::StartCause::Poll + | event::StartCause::ResumeTimeReached { .. } + ); + } + event::Event::MainEventsCleared => { + 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 == Some(id) || window_id.is_none() + { + window_events.push(event.clone()); + false + } else { + true + } + }); + + if !redraw_pending + && 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, + ); + + if !uis_stale { + uis_stale = matches!( + ui_state, + user_interface::State::Outdated + ); + } + + for (event, status) in window_events + .into_iter() + .zip(statuses.into_iter()) + { + runtime.broadcast(event, status); + } + } + + debug.event_processing_finished(); + + // TODO mw application update returns which window IDs to update + if !messages.is_empty() || uis_stale { + let mut cached_interfaces: HashMap< + window::Id, + user_interface::Cache, + > = ManuallyDrop::into_inner(user_interfaces) + .drain() + .map(|(id, ui)| (id, ui.into_cache())) + .collect(); + + // Update application + update( + &mut application, + &mut compositor, + &mut runtime, + &mut clipboard, + &mut control_sender, + &mut proxy, + &mut debug, + &mut messages, + &mut window_manager, + &mut cached_interfaces, + ); + + // we must synchronize all window states with application state after an + // application update since we don't know what changed + for (id, window) in window_manager.iter_mut() { + window.state.synchronize( + &application, + id, + &window.raw, + ); + } + + // rebuild UIs with the synchronized states + user_interfaces = + ManuallyDrop::new(build_user_interfaces( + &application, + &mut debug, + &mut window_manager, + cached_interfaces, + )); + } + + debug.draw_started(); + + for (id, window) in window_manager.iter_mut() { + // TODO: Avoid redrawing all the time by forcing widgets to + // request redraws on state changes + // + // Then, we can use the `interface_state` here to decide if a redraw + // is needed right away, or simply wait until a specific time. + let redraw_event = core::Event::Window( + id, + 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 new_mouse_interaction = { + let state = &window.state; + + ui.draw( + &mut window.renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + cursor, + ) + }; + + if new_mouse_interaction != window.mouse_interaction + { + window.raw.set_cursor_icon( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); + + window.mouse_interaction = + new_mouse_interaction; + } + + // TODO once widgets can request to be redrawn, we can avoid always requesting a + // redraw + window.raw.request_redraw(); + + runtime.broadcast( + redraw_event.clone(), + core::event::Status::Ignored, + ); + + let _ = control_sender.start_send( + Control::ChangeFlow(match ui_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + window::RedrawRequest::NextFrame => { + ControlFlow::Poll + } + window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => ControlFlow::Wait, + }), + ); + } + + redraw_pending = false; + + debug.draw_finished(); + } + event::Event::PlatformSpecific( + event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + ), + ) => { + use crate::core::event; + + events.push(( + None, + event::Event::PlatformSpecific( + event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + ), + ), + )); + } + event::Event::UserEvent(message) => { + messages.push(message); + } + event::Event::RedrawRequested(id) => { + 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; + } + + debug.render_started(); + 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(); + + debug.draw_started(); + let new_mouse_interaction = user_interfaces + .get_mut(&id) + .expect("Get user interface") + .draw( + &mut window.renderer, + window.state.theme(), + &renderer::Style { + text_color: window.state.text_color(), + }, + window.state.cursor(), + ); + + if new_mouse_interaction != window.mouse_interaction + { + window.raw.set_cursor_icon( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); + + window.mouse_interaction = + new_mouse_interaction; + } + debug.draw_finished(); + + compositor.configure_surface( + &mut window.surface, + physical_size.width, + physical_size.height, + ); + + window.viewport_version = + window.state.viewport_version(); + } + + match compositor.present( + &mut window.renderer, + &mut window.surface, + window.state.viewport(), + window.state.background_color(), + &debug.overlay(), + ) { + Ok(()) => { + debug.render_finished(); + + // TODO: Handle animations! + // Maybe we can use `ControlFlow::WaitUntil` for this. + } + Err(error) => match error { + // This is an unrecoverable error. + compositor::SurfaceError::OutOfMemory => { + panic!("{:?}", error); + } + _ => { + debug.render_finished(); + 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, + } => { + let Some((id, window)) = + window_manager.get_mut_alias(window_id) + else { + continue; + }; + + if matches!( + window_event, + winit::event::WindowEvent::CloseRequested + ) && window.exit_on_close_request + { + let _ = window_manager.remove(id); + let _ = user_interfaces.remove(&id); + let _ = ui_caches.remove(&id); + + events.push(( + None, + core::Event::Window(id, window::Event::Closed), + )); + + if window_manager.is_empty() { + break 'main; + } + } else { + window.state.update( + &window.raw, + &window_event, + &mut debug, + ); + + if let Some(event) = conversion::window_event( + id, + &window_event, + window.state.scale_factor(), + window.state.modifiers(), + ) { + events.push((Some(id), event)); + } + } + } + _ => {} + } + } + } + } + + let _ = ManuallyDrop::into_inner(user_interfaces); +} + +/// Builds a window's [`UserInterface`] for the [`Application`]. +fn build_user_interface<'a, A: Application>( + application: &'a A, + cache: user_interface::Cache, + renderer: &mut A::Renderer, + size: Size, + debug: &mut Debug, + id: window::Id, +) -> UserInterface<'a, A::Message, A::Renderer> +where + <A::Renderer as core::Renderer>::Theme: StyleSheet, +{ + debug.view_started(); + let view = application.view(id); + debug.view_finished(); + + debug.layout_started(); + let user_interface = UserInterface::build(view, size, cache, renderer); + debug.layout_finished(); + + user_interface +} + +/// Updates a multi-window [`Application`] by feeding it messages, spawning any +/// resulting [`Command`], and tracking its [`Subscription`]. +fn update<A: Application, C, E: Executor>( + application: &mut A, + compositor: &mut C, + runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, + clipboard: &mut Clipboard, + control_sender: &mut mpsc::UnboundedSender<Control>, + proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, + debug: &mut Debug, + messages: &mut Vec<A::Message>, + window_manager: &mut WindowManager<A, C>, + ui_caches: &mut HashMap<window::Id, user_interface::Cache>, +) where + C: Compositor<Renderer = A::Renderer> + 'static, + <A::Renderer as core::Renderer>::Theme: StyleSheet, +{ + for message in messages.drain(..) { + debug.log_message(&message); + debug.update_started(); + + let command = runtime.enter(|| application.update(message)); + debug.update_finished(); + + run_command( + application, + compositor, + command, + runtime, + clipboard, + control_sender, + proxy, + debug, + window_manager, + ui_caches, + ); + } + + let subscription = application.subscription(); + runtime.track(subscription.into_recipes()); +} + +/// Runs the actions of a [`Command`]. +fn run_command<A, C, E>( + application: &A, + compositor: &mut C, + command: Command<A::Message>, + runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, + clipboard: &mut Clipboard, + control_sender: &mut mpsc::UnboundedSender<Control>, + proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, + debug: &mut Debug, + window_manager: &mut WindowManager<A, C>, + ui_caches: &mut HashMap<window::Id, user_interface::Cache>, +) where + A: Application, + E: Executor, + C: Compositor<Renderer = A::Renderer> + 'static, + <A::Renderer as core::Renderer>::Theme: StyleSheet, +{ + use crate::runtime::clipboard; + use crate::runtime::system; + use crate::runtime::window; + + for action in command.actions() { + match action { + command::Action::Future(future) => { + runtime.spawn(Box::pin(future)); + } + command::Action::Stream(stream) => { + runtime.run(Box::pin(stream)); + } + command::Action::Clipboard(action) => match action { + clipboard::Action::Read(tag) => { + let message = tag(clipboard.read()); + + proxy + .send_event(message) + .expect("Send message to event loop"); + } + clipboard::Action::Write(contents) => { + clipboard.write(contents); + } + }, + command::Action::Window(action) => match action { + window::Action::Spawn(id, settings) => { + let monitor = window_manager.last_monitor(); + + control_sender + .start_send(Control::CreateWindow { + id, + settings, + title: application.title(id), + monitor, + }) + .expect("Send control action"); + } + window::Action::Close(id) => { + use winit::event_loop::ControlFlow; + + let _ = window_manager.remove(id); + let _ = ui_caches.remove(&id); + + if window_manager.is_empty() { + control_sender + .start_send(Control::ChangeFlow( + ControlFlow::ExitWithCode(0), + )) + .expect("Send control action"); + } + } + window::Action::Drag(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.drag_window(); + } + } + window::Action::Resize(id, size) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_inner_size(winit::dpi::LogicalSize { + width: size.width, + height: size.height, + }); + } + } + window::Action::FetchSize(id, callback) => { + if let Some(window) = window_manager.get_mut(id) { + let size = window + .raw + .inner_size() + .to_logical(window.raw.scale_factor()); + + proxy + .send_event(callback(Size::new( + size.width, + size.height, + ))) + .expect("Send message to event loop"); + } + } + window::Action::Maximize(id, maximized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(maximized); + } + } + window::Action::Minimize(id, minimized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_minimized(minimized); + } + } + 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::ChangeMode(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::ChangeIcon(id, icon) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_window_icon(conversion::icon(icon)); + } + } + window::Action::FetchMode(id, tag) => { + 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 + }; + + proxy + .send_event(tag(mode)) + .expect("Event loop doesn't exist."); + } + } + 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::ChangeLevel(id, level) => { + if let Some(window) = window_manager.get_mut(id) { + window + .raw + .set_window_level(conversion::window_level(level)); + } + } + window::Action::FetchId(id, tag) => { + if let Some(window) = window_manager.get_mut(id) { + proxy + .send_event(tag(window.raw.id().into())) + .expect("Event loop doesn't exist."); + } + } + window::Action::Screenshot(id, tag) => { + if let Some(window) = window_manager.get_mut(id) { + let bytes = compositor.screenshot( + &mut window.renderer, + &mut window.surface, + window.state.viewport(), + window.state.background_color(), + &debug.overlay(), + ); + + proxy + .send_event(tag(window::Screenshot::new( + bytes, + window.state.physical_size(), + ))) + .expect("Event loop doesn't exist."); + } + } + }, + command::Action::System(action) => match action { + system::Action::QueryInformation(_tag) => { + #[cfg(feature = "system")] + { + let graphics_info = compositor.fetch_information(); + let proxy = proxy.clone(); + + let _ = std::thread::spawn(move || { + let information = + crate::system::information(graphics_info); + + let message = _tag(information); + + proxy + .send_event(message) + .expect("Event loop doesn't exist."); + }); + } + } + }, + command::Action::Widget(action) => { + let mut current_operation = Some(action); + + let mut uis = build_user_interfaces( + application, + debug, + window_manager, + std::mem::take(ui_caches), + ); + + 'operate: while let Some(mut operation) = + current_operation.take() + { + for (id, ui) in uis.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(message) => { + proxy + .send_event(message) + .expect("Event loop doesn't exist."); + + // operation completed, don't need to try to operate on rest of UIs + break 'operate; + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + } + } + } + } + + *ui_caches = + uis.drain().map(|(id, ui)| (id, ui.into_cache())).collect(); + } + command::Action::LoadFont { bytes, tagger } => { + use crate::core::text::Renderer; + + // TODO change this once we change each renderer to having a single backend reference.. :pain: + // TODO: Error handling (?) + for (_, window) in window_manager.iter_mut() { + window.renderer.load_font(bytes.clone()); + } + + proxy + .send_event(tagger(Ok(()))) + .expect("Send message to event loop"); + } + } + } +} + +/// Build the user interface for every window. +pub fn build_user_interfaces<'a, A: Application, C: Compositor>( + application: &'a A, + debug: &mut Debug, + window_manager: &mut WindowManager<A, C>, + mut cached_user_interfaces: HashMap<window::Id, user_interface::Cache>, +) -> HashMap<window::Id, UserInterface<'a, A::Message, A::Renderer>> +where + <A::Renderer as core::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer>, +{ + cached_user_interfaces + .drain() + .filter_map(|(id, cache)| { + let window = window_manager.get_mut(id)?; + + Some(( + id, + build_user_interface( + application, + cache, + &mut window.renderer, + window.state.logical_size(), + debug, + id, + ), + )) + }) + .collect() +} + +/// Returns true if the provided event should cause an [`Application`] to +/// exit. +pub fn user_force_quit( + event: &winit::event::WindowEvent<'_>, + _modifiers: winit::event::ModifiersState, +) -> bool { + match event { + #[cfg(target_os = "macos")] + winit::event::WindowEvent::KeyboardInput { + input: + winit::event::KeyboardInput { + virtual_keycode: Some(winit::event::VirtualKeyCode::Q), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } if _modifiers.logo() => true, + _ => false, + } +} + +#[cfg(not(target_arch = "wasm32"))] +mod platform { + pub fn run<T, F>( + mut event_loop: winit::event_loop::EventLoop<T>, + event_handler: F, + ) -> Result<(), super::Error> + where + F: 'static + + FnMut( + winit::event::Event<'_, T>, + &winit::event_loop::EventLoopWindowTarget<T>, + &mut winit::event_loop::ControlFlow, + ), + { + use winit::platform::run_return::EventLoopExtRunReturn; + + let _ = event_loop.run_return(event_handler); + + Ok(()) + } +} + +#[cfg(target_arch = "wasm32")] +mod platform { + pub fn run<T, F>( + event_loop: winit::event_loop::EventLoop<T>, + event_handler: F, + ) -> ! + where + F: 'static + + FnMut( + winit::event::Event<'_, T>, + &winit::event_loop::EventLoopWindowTarget<T>, + &mut winit::event_loop::ControlFlow, + ), + { + event_loop.run(event_handler) + } +} diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs new file mode 100644 index 00000000..03da5ad7 --- /dev/null +++ b/winit/src/multi_window/state.rs @@ -0,0 +1,240 @@ +use crate::conversion; +use crate::core; +use crate::core::{mouse, window}; +use crate::core::{Color, Size}; +use crate::graphics::Viewport; +use crate::multi_window::Application; +use crate::style::application; +use std::fmt::{Debug, Formatter}; + +use iced_style::application::StyleSheet; +use winit::event::{Touch, WindowEvent}; +use winit::window::Window; + +/// The state of a multi-windowed [`Application`]. +pub struct State<A: Application> +where + <A::Renderer as core::Renderer>::Theme: application::StyleSheet, +{ + title: String, + scale_factor: f64, + viewport: Viewport, + viewport_version: u64, + cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, + modifiers: winit::event::ModifiersState, + theme: <A::Renderer as core::Renderer>::Theme, + appearance: application::Appearance, +} + +impl<A: Application> Debug for State<A> +where + <A::Renderer as core::Renderer>::Theme: application::StyleSheet, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("multi_window::State") + .field("title", &self.title) + .field("scale_factor", &self.scale_factor) + .field("viewport", &self.viewport) + .field("viewport_version", &self.viewport_version) + .field("cursor_position", &self.cursor_position) + .field("appearance", &self.appearance) + .finish() + } +} + +impl<A: Application> State<A> +where + <A::Renderer as core::Renderer>::Theme: application::StyleSheet, +{ + /// Creates a new [`State`] for the provided [`Application`]'s `window`. + pub fn new( + application: &A, + 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 appearance = theme.appearance(&application.style()); + + let viewport = { + let physical_size = window.inner_size(); + + Viewport::with_physical_size( + Size::new(physical_size.width, physical_size.height), + window.scale_factor() * scale_factor, + ) + }; + + Self { + title, + scale_factor, + viewport, + viewport_version: 0, + cursor_position: None, + modifiers: winit::event::ModifiersState::default(), + theme, + appearance, + } + } + + /// Returns the current [`Viewport`] of the [`State`]. + pub fn viewport(&self) -> &Viewport { + &self.viewport + } + + /// Returns the version of the [`Viewport`] of the [`State`]. + /// + /// The version is incremented every time the [`Viewport`] changes. + pub fn viewport_version(&self) -> u64 { + self.viewport_version + } + + /// Returns the physical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn physical_size(&self) -> Size<u32> { + self.viewport.physical_size() + } + + /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn logical_size(&self) -> Size<f32> { + self.viewport.logical_size() + } + + /// Returns the current scale factor of the [`Viewport`] of the [`State`]. + pub fn scale_factor(&self) -> f64 { + self.viewport.scale_factor() + } + + /// Returns the current cursor position of the [`State`]. + pub fn cursor(&self) -> mouse::Cursor { + self.cursor_position + .map(|cursor_position| { + conversion::cursor_position( + cursor_position, + self.viewport.scale_factor(), + ) + }) + .map(mouse::Cursor::Available) + .unwrap_or(mouse::Cursor::Unavailable) + } + + /// Returns the current keyboard modifiers of the [`State`]. + pub fn modifiers(&self) -> winit::event::ModifiersState { + self.modifiers + } + + /// Returns the current theme of the [`State`]. + pub fn theme(&self) -> &<A::Renderer as core::Renderer>::Theme { + &self.theme + } + + /// Returns the current background [`Color`] of the [`State`]. + pub fn background_color(&self) -> Color { + self.appearance.background_color + } + + /// Returns the current text [`Color`] of the [`State`]. + pub fn text_color(&self) -> Color { + self.appearance.text_color + } + + /// Processes the provided window event and updates the [`State`] accordingly. + pub fn update( + &mut self, + window: &Window, + event: &WindowEvent<'_>, + _debug: &mut crate::runtime::Debug, + ) { + match event { + WindowEvent::Resized(new_size) => { + let size = Size::new(new_size.width, new_size.height); + + self.viewport = Viewport::with_physical_size( + size, + window.scale_factor() * self.scale_factor, + ); + + self.viewport_version = self.viewport_version.wrapping_add(1); + } + WindowEvent::ScaleFactorChanged { + scale_factor: new_scale_factor, + new_inner_size, + } => { + let size = + Size::new(new_inner_size.width, new_inner_size.height); + + self.viewport = Viewport::with_physical_size( + size, + new_scale_factor * self.scale_factor, + ); + + self.viewport_version = self.viewport_version.wrapping_add(1); + } + WindowEvent::CursorMoved { position, .. } + | WindowEvent::Touch(Touch { + location: position, .. + }) => { + self.cursor_position = Some(*position); + } + WindowEvent::CursorLeft { .. } => { + self.cursor_position = None; + } + WindowEvent::ModifiersChanged(new_modifiers) => { + self.modifiers = *new_modifiers; + } + #[cfg(feature = "debug")] + WindowEvent::KeyboardInput { + input: + winit::event::KeyboardInput { + virtual_keycode: Some(winit::event::VirtualKeyCode::F12), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } => _debug.toggle(), + _ => {} + } + } + + /// Synchronizes the [`State`] with its [`Application`] and its respective + /// window. + /// + /// Normally, an [`Application`] should be synchronized with its [`State`] + /// and window after calling [`State::update`]. + pub fn synchronize( + &mut self, + application: &A, + window_id: window::Id, + window: &Window, + ) { + // Update window title + let new_title = application.title(window_id); + + if self.title != new_title { + window.set_title(&new_title); + self.title = new_title; + } + + // Update scale factor and size + let new_scale_factor = application.scale_factor(window_id); + let new_size = window.inner_size(); + let current_size = self.viewport.physical_size(); + + if self.scale_factor != new_scale_factor + || (current_size.width, current_size.height) + != (new_size.width, new_size.height) + { + self.viewport = Viewport::with_physical_size( + Size::new(new_size.width, new_size.height), + window.scale_factor() * new_scale_factor, + ); + self.viewport_version = self.viewport_version.wrapping_add(1); + + self.scale_factor = new_scale_factor; + } + + // Update theme and appearance + self.theme = application.theme(window_id); + self.appearance = self.theme.appearance(&application.style()); + } +} diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs new file mode 100644 index 00000000..d54156e7 --- /dev/null +++ b/winit/src/multi_window/window_manager.rs @@ -0,0 +1,156 @@ +use crate::core::mouse; +use crate::core::window::Id; +use crate::core::{Point, Size}; +use crate::graphics::Compositor; +use crate::multi_window::{Application, State}; +use crate::style::application::StyleSheet; + +use std::collections::BTreeMap; +use winit::monitor::MonitorHandle; + +#[allow(missing_debug_implementations)] +pub struct WindowManager<A: Application, C: Compositor> +where + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer>, +{ + aliases: BTreeMap<winit::window::WindowId, Id>, + entries: BTreeMap<Id, Window<A, C>>, +} + +impl<A, C> WindowManager<A, C> +where + A: Application, + C: Compositor<Renderer = A::Renderer>, + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, +{ + pub fn new() -> Self { + Self { + aliases: BTreeMap::new(), + entries: BTreeMap::new(), + } + } + + pub fn insert( + &mut self, + id: Id, + window: winit::window::Window, + application: &A, + compositor: &mut C, + exit_on_close_request: bool, + ) -> &mut Window<A, C> { + let state = State::new(application, id, &window); + let viewport_version = state.viewport_version(); + let physical_size = state.physical_size(); + let surface = compositor.create_surface( + &window, + physical_size.width, + physical_size.height, + ); + let renderer = compositor.create_renderer(); + + let _ = self.aliases.insert(window.id(), id); + + let _ = self.entries.insert( + id, + Window { + raw: window, + state, + viewport_version, + exit_on_close_request, + surface, + renderer, + mouse_interaction: mouse::Interaction::Idle, + }, + ); + + self.entries + .get_mut(&id) + .expect("Get window that was just inserted") + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + pub fn iter_mut( + &mut self, + ) -> impl Iterator<Item = (Id, &mut Window<A, C>)> { + self.entries.iter_mut().map(|(k, v)| (*k, v)) + } + + pub fn get_mut(&mut self, id: Id) -> Option<&mut Window<A, C>> { + self.entries.get_mut(&id) + } + + pub fn get_mut_alias( + &mut self, + id: winit::window::WindowId, + ) -> Option<(Id, &mut Window<A, C>)> { + let id = self.aliases.get(&id).copied()?; + + Some((id, self.get_mut(id)?)) + } + + pub fn last_monitor(&self) -> Option<MonitorHandle> { + self.entries.values().last()?.raw.current_monitor() + } + + pub fn remove(&mut self, id: Id) -> Option<Window<A, C>> { + let window = self.entries.remove(&id)?; + let _ = self.aliases.remove(&window.raw.id()); + + Some(window) + } +} + +impl<A, C> Default for WindowManager<A, C> +where + A: Application, + C: Compositor<Renderer = A::Renderer>, + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, +{ + fn default() -> Self { + Self::new() + } +} + +#[allow(missing_debug_implementations)] +pub struct Window<A, C> +where + A: Application, + C: Compositor<Renderer = A::Renderer>, + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, +{ + pub raw: winit::window::Window, + pub state: State<A>, + pub viewport_version: u64, + pub exit_on_close_request: bool, + pub mouse_interaction: mouse::Interaction, + pub surface: C::Surface, + pub renderer: A::Renderer, +} + +impl<A, C> Window<A, C> +where + A: Application, + C: Compositor<Renderer = A::Renderer>, + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, +{ + pub fn position(&self) -> Option<Point> { + self.raw + .inner_position() + .ok() + .map(|position| position.to_logical(self.raw.scale_factor())) + .map(|position| Point { + x: position.x, + y: position.y, + }) + } + + pub fn size(&self) -> Size { + let size = self.raw.inner_size().to_logical(self.raw.scale_factor()); + + Size::new(size.width, size.height) + } +} diff --git a/winit/src/position.rs b/winit/src/position.rs deleted file mode 100644 index c260c29e..00000000 --- a/winit/src/position.rs +++ /dev/null @@ -1,22 +0,0 @@ -/// The position of a window in a given screen. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Position { - /// The platform-specific default position for a new window. - Default, - /// The window is completely centered on the screen. - Centered, - /// The window is positioned with specific coordinates: `(X, Y)`. - /// - /// When the decorations of the window are enabled, Windows 10 will add some - /// invisible padding to the window. This padding gets included in the - /// position. So if you have decorations enabled and want the window to be - /// at (0, 0) you would have to set the position to - /// `(PADDING_X, PADDING_Y)`. - Specific(i32, i32), -} - -impl Default for Position { - fn default() -> Self { - Self::Default - } -} diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 599f33f1..2e541128 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -1,40 +1,7 @@ //! Configure your application. -#[cfg(target_os = "windows")] -#[path = "settings/windows.rs"] -mod platform; - -#[cfg(target_os = "macos")] -#[path = "settings/macos.rs"] -mod platform; - -#[cfg(target_os = "linux")] -#[path = "settings/linux.rs"] -mod platform; - -#[cfg(target_arch = "wasm32")] -#[path = "settings/wasm.rs"] -mod platform; - -#[cfg(not(any( - target_os = "windows", - target_os = "macos", - target_os = "linux", - target_arch = "wasm32" -)))] -#[path = "settings/other.rs"] -mod platform; - -pub use platform::PlatformSpecific; - -use crate::conversion; -use crate::core::window::{Icon, Level}; -use crate::Position; - -use winit::monitor::MonitorHandle; -use winit::window::WindowBuilder; +use crate::core::window; use std::borrow::Cow; -use std::fmt; /// The settings of an application. #[derive(Debug, Clone, Default)] @@ -45,8 +12,8 @@ pub struct Settings<Flags> { /// communicate with it through the windowing system. pub id: Option<String>, - /// The [`Window`] settings. - pub window: Window, + /// The [`window::Settings`]. + pub window: window::Settings, /// The data needed to initialize an [`Application`]. /// @@ -55,197 +22,4 @@ pub struct Settings<Flags> { /// The fonts to load on boot. pub fonts: Vec<Cow<'static, [u8]>>, - - /// Whether the [`Application`] should exit when the user requests the - /// window to close (e.g. the user presses the close button). - /// - /// [`Application`]: crate::Application - pub exit_on_close_request: bool, -} - -/// The window settings of an application. -#[derive(Clone)] -pub struct Window { - /// The size of the window. - pub size: (u32, u32), - - /// The position of the window. - pub position: Position, - - /// The minimum size of the window. - pub min_size: Option<(u32, u32)>, - - /// The maximum size of the window. - pub max_size: Option<(u32, u32)>, - - /// Whether the window should be visible or not. - pub visible: bool, - - /// Whether the window should be resizable or not. - pub resizable: bool, - - /// Whether the window should have a border, a title bar, etc. - pub decorations: bool, - - /// Whether the window should be transparent. - pub transparent: bool, - - /// The window [`Level`]. - pub level: Level, - - /// The window icon, which is also usually used in the taskbar - pub icon: Option<Icon>, - - /// Platform specific settings. - pub platform_specific: platform::PlatformSpecific, -} - -impl fmt::Debug for Window { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Window") - .field("size", &self.size) - .field("position", &self.position) - .field("min_size", &self.min_size) - .field("max_size", &self.max_size) - .field("visible", &self.visible) - .field("resizable", &self.resizable) - .field("decorations", &self.decorations) - .field("transparent", &self.transparent) - .field("level", &self.level) - .field("icon", &self.icon.is_some()) - .field("platform_specific", &self.platform_specific) - .finish() - } -} - -impl Window { - /// Converts the window settings into a `WindowBuilder` from `winit`. - pub fn into_builder( - self, - title: &str, - primary_monitor: Option<MonitorHandle>, - _id: Option<String>, - ) -> WindowBuilder { - let mut window_builder = WindowBuilder::new(); - - let (width, height) = self.size; - - window_builder = window_builder - .with_title(title) - .with_inner_size(winit::dpi::LogicalSize { width, height }) - .with_resizable(self.resizable) - .with_enabled_buttons(if self.resizable { - winit::window::WindowButtons::all() - } else { - winit::window::WindowButtons::CLOSE - | winit::window::WindowButtons::MINIMIZE - }) - .with_decorations(self.decorations) - .with_transparent(self.transparent) - .with_window_icon(self.icon.and_then(conversion::icon)) - .with_window_level(conversion::window_level(self.level)) - .with_visible(self.visible); - - if let Some(position) = conversion::position( - primary_monitor.as_ref(), - self.size, - self.position, - ) { - window_builder = window_builder.with_position(position); - } - - if let Some((width, height)) = self.min_size { - window_builder = window_builder - .with_min_inner_size(winit::dpi::LogicalSize { width, height }); - } - - if let Some((width, height)) = self.max_size { - window_builder = window_builder - .with_max_inner_size(winit::dpi::LogicalSize { width, height }); - } - - #[cfg(any( - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - { - // `with_name` is available on both `WindowBuilderExtWayland` and `WindowBuilderExtX11` and they do - // exactly the same thing. We arbitrarily choose `WindowBuilderExtWayland` here. - use ::winit::platform::wayland::WindowBuilderExtWayland; - - if let Some(id) = _id { - window_builder = window_builder.with_name(id.clone(), id); - } - } - - #[cfg(target_os = "windows")] - { - use winit::platform::windows::WindowBuilderExtWindows; - #[allow(unsafe_code)] - unsafe { - window_builder = window_builder - .with_parent_window(self.platform_specific.parent); - } - window_builder = window_builder - .with_drag_and_drop(self.platform_specific.drag_and_drop); - } - - #[cfg(target_os = "macos")] - { - use winit::platform::macos::WindowBuilderExtMacOS; - - window_builder = window_builder - .with_title_hidden(self.platform_specific.title_hidden) - .with_titlebar_transparent( - self.platform_specific.titlebar_transparent, - ) - .with_fullsize_content_view( - self.platform_specific.fullsize_content_view, - ); - } - - #[cfg(target_os = "linux")] - { - #[cfg(feature = "x11")] - { - use winit::platform::x11::WindowBuilderExtX11; - - window_builder = window_builder.with_name( - &self.platform_specific.application_id, - &self.platform_specific.application_id, - ); - } - #[cfg(feature = "wayland")] - { - use winit::platform::wayland::WindowBuilderExtWayland; - - window_builder = window_builder.with_name( - &self.platform_specific.application_id, - &self.platform_specific.application_id, - ); - } - } - - window_builder - } -} - -impl Default for Window { - fn default() -> Window { - Window { - size: (1024, 768), - position: Position::default(), - min_size: None, - max_size: None, - visible: true, - resizable: true, - decorations: true, - transparent: false, - level: Level::default(), - icon: None, - platform_specific: PlatformSpecific::default(), - } - } } diff --git a/winit/src/settings/linux.rs b/winit/src/settings/linux.rs deleted file mode 100644 index 009b9d9e..00000000 --- a/winit/src/settings/linux.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Platform specific settings for Linux. - -/// The platform specific window settings of an application. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct PlatformSpecific { - /// Sets the application id of the window. - /// - /// As a best practice, it is suggested to select an application id that match - /// the basename of the application’s .desktop file. - pub application_id: String, -} diff --git a/winit/src/settings/macos.rs b/winit/src/settings/macos.rs deleted file mode 100644 index f86e63ad..00000000 --- a/winit/src/settings/macos.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Platform specific settings for macOS. - -/// The platform specific window settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct PlatformSpecific { - /// Hides the window title. - pub title_hidden: bool, - /// Makes the titlebar transparent and allows the content to appear behind it. - pub titlebar_transparent: bool, - /// Makes the window content appear behind the titlebar. - pub fullsize_content_view: bool, -} diff --git a/winit/src/settings/other.rs b/winit/src/settings/other.rs deleted file mode 100644 index b1103f62..00000000 --- a/winit/src/settings/other.rs +++ /dev/null @@ -1,3 +0,0 @@ -/// The platform specific window settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct PlatformSpecific; diff --git a/winit/src/settings/wasm.rs b/winit/src/settings/wasm.rs deleted file mode 100644 index 8e0f1bbc..00000000 --- a/winit/src/settings/wasm.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Platform specific settings for WebAssembly. - -/// The platform specific window settings of an application. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct PlatformSpecific { - /// The identifier of a DOM element that will be replaced with the - /// application. - /// - /// If set to `None`, the application will be appended to the HTML body. - pub target: Option<String>, -} diff --git a/winit/src/settings/windows.rs b/winit/src/settings/windows.rs deleted file mode 100644 index 45d753bd..00000000 --- a/winit/src/settings/windows.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Platform specific settings for Windows. -use raw_window_handle::RawWindowHandle; - -/// The platform specific window settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PlatformSpecific { - /// Parent window - pub parent: Option<RawWindowHandle>, - - /// Drag and drop support - pub drag_and_drop: bool, -} - -impl Default for PlatformSpecific { - fn default() -> Self { - Self { - parent: None, - drag_and_drop: true, - } - } -} |