diff options
Diffstat (limited to 'winit')
-rw-r--r-- | winit/Cargo.toml | 1 | ||||
-rw-r--r-- | winit/src/application.rs | 96 | ||||
-rw-r--r-- | winit/src/application/profiler.rs | 101 | ||||
-rw-r--r-- | winit/src/conversion.rs | 165 | ||||
-rw-r--r-- | winit/src/lib.rs | 8 | ||||
-rw-r--r-- | winit/src/multi_window.rs | 1188 | ||||
-rw-r--r-- | winit/src/multi_window/state.rs | 242 | ||||
-rw-r--r-- | winit/src/multi_window/windows.rs | 196 | ||||
-rw-r--r-- | winit/src/position.rs | 22 | ||||
-rw-r--r-- | winit/src/settings.rs | 230 | ||||
-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, 1806 insertions, 501 deletions
diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 674a66d3..bab05b91 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 diff --git a/winit/src/application.rs b/winit/src/application.rs index 2c5c864a..8457fd92 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:#?}"); @@ -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( @@ -751,13 +712,18 @@ pub fn run_command<A, C, E>( clipboard.write(contents); } }, - command::Action::Window(action) => match action { + command::Action::Window(_, action) => match action { window::Action::Close => { *should_exit = true; } window::Action::Drag => { let _res = window.drag_window(); } + window::Action::Spawn { .. } => { + log::info!( + "Spawning a window is only available with multi-window applications." + ) + } window::Action::Resize(size) => { window.set_inner_size(winit::dpi::LogicalSize { width: size.width, 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..22e6b9be 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -7,10 +7,120 @@ use crate::core::mouse; use crate::core::touch; use crate::core::window; use crate::core::{Event, Point}; -use crate::Position; + +/// 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(); + + let (width, height) = settings.size; + + window_builder = window_builder + .with_title(title) + .with_inner_size(winit::dpi::LogicalSize { width, 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((width, height)) = settings.min_size { + window_builder = window_builder + .with_min_inner_size(winit::dpi::LogicalSize { width, height }); + } + + if let Some((width, height)) = settings.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(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 +131,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 +229,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 +253,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, } @@ -159,17 +278,17 @@ pub fn window_level(level: window::Level) -> winit::window::WindowLevel { pub fn position( monitor: Option<&winit::monitor::MonitorHandle>, (width, height): (u32, u32), - position: Position, + position: window::Position, ) -> Option<winit::dpi::Position> { match position { - Position::Default => None, - Position::Specific(x, y) => { + window::Position::Default => None, + window::Position::Specific(x, y) => { Some(winit::dpi::Position::Logical(winit::dpi::LogicalPosition { x: f64::from(x), y: f64::from(y), })) } - Position::Centered => { + window::Position::Centered => { if let Some(monitor) = monitor { let start = monitor.position(); diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 95b55bb9..cc886354 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,14 @@ 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 crate::core::window::*; 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..f2452eb3 --- /dev/null +++ b/winit/src/multi_window.rs @@ -0,0 +1,1188 @@ +//! Create interactive, native cross-platform applications for WGPU. +mod state; +mod windows; + +pub use state::State; + +use crate::conversion; +use crate::core::widget::operation; +use crate::core::{self, mouse, renderer, window, Size}; +use crate::futures::futures::channel::mpsc; +use crate::futures::futures::{task, Future, FutureExt, StreamExt}; +use crate::futures::{Executor, Runtime, Subscription}; +use crate::graphics::{compositor, Compositor}; +use crate::multi_window::windows::Windows; +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::mem::ManuallyDrop; +use std::time::Instant; +use winit::monitor::MonitorHandle; + +/// This is a wrapper around the `Application::Message` associate type +/// to allows the `shell` to create internal messages, while still having +/// the current user-specified custom messages. +#[derive(Debug)] +pub enum Event<Message> { + /// An internal event which contains an [`Application`] generated message. + Application(Message), + /// An internal event which spawns a new window. + NewWindow { + /// The [window::Id] of the newly spawned [`Window`]. + id: window::Id, + /// The [settings::Window] of the newly spawned [`Window`]. + settings: window::Settings, + /// The title of the newly spawned [`Window`]. + title: String, + /// The monitor on which to spawn the window. If `None`, will use monitor of the last window + /// spawned. + monitor: Option<MonitorHandle>, + }, + /// An internal event for closing a window. + CloseWindow(window::Id), + /// An internal event for when the window has finished being created. + WindowCreated { + /// The internal ID of the window. + id: window::Id, + /// The raw window. + window: winit::window::Window, + /// Whether or not the window should close when a user requests it does. + exit_on_close_request: bool, + }, +} + +#[allow(unsafe_code)] +unsafe impl<Message> std::marker::Send for Event<Message> {} + +/// 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, renderer) = + C::new(compositor_settings, Some(&main_window))?; + + let windows = Windows::new( + &application, + &mut compositor, + renderer, + main_window, + 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, + windows, + 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, + }), + winit::event::Event::UserEvent(Event::NewWindow { + 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"); + + Some(winit::event::Event::UserEvent(Event::WindowCreated { + id, + window, + exit_on_close_request, + })) + } + _ => event.to_static(), + }; + + if let Some(event) = event { + event_sender.start_send(event).expect("Send event"); + + let poll = instance.as_mut().poll(&mut context); + + match poll { + task::Poll::Pending => { + if let Ok(Some(flow)) = control_receiver.try_next() { + *control_flow = flow; + } + } + task::Poll::Ready(_) => { + *control_flow = ControlFlow::Exit; + } + }; + } + }) +} + +async fn run_instance<A, E, C>( + mut application: A, + mut compositor: C, + mut runtime: Runtime<E, Proxy<Event<A::Message>>, Event<A::Message>>, + mut proxy: winit::event_loop::EventLoopProxy<Event<A::Message>>, + mut debug: Debug, + mut event_receiver: mpsc::UnboundedReceiver< + winit::event::Event<'_, Event<A::Message>>, + >, + mut control_sender: mpsc::UnboundedSender<winit::event_loop::ControlFlow>, + init_command: Command<A::Message>, + mut windows: Windows<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 mut clipboard = Clipboard::connect(windows.main()); + + let mut ui_caches = vec![user_interface::Cache::default()]; + let mut user_interfaces = ManuallyDrop::new(build_user_interfaces( + &application, + &mut debug, + &mut windows, + vec![user_interface::Cache::default()], + )); + + if should_main_window_be_visible { + windows.main().set_visible(true); + } + + run_command( + &application, + &mut compositor, + init_command, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + &mut windows, + &mut ui_caches, + ); + + runtime.track( + application + .subscription() + .map(Event::Application) + .into_recipes(), + ); + + let mut mouse_interaction = mouse::Interaction::default(); + + let mut events = + if let Some((position, size)) = logical_bounds_of(windows.main()) { + vec![( + Some(window::Id::MAIN), + core::Event::Window( + window::Id::MAIN, + window::Event::Created { position, size }, + ), + )] + } else { + Vec::new() + }; + 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::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 (i, id) in windows.ids.iter().enumerate() { + 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[i].update( + &window_events, + windows.states[i].cursor(), + &mut windows.renderers[i], + &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: Vec<user_interface::Cache> = + ManuallyDrop::into_inner(user_interfaces) + .drain(..) + .map(UserInterface::into_cache) + .collect(); + + // Update application + update( + &mut application, + &mut compositor, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + &mut messages, + &mut windows, + &mut cached_interfaces, + ); + + // we must synchronize all window states with application state after an + // application update since we don't know what changed + for (state, (id, window)) in windows + .states + .iter_mut() + .zip(windows.ids.iter().zip(windows.raw.iter())) + { + state.synchronize(&application, *id, window); + } + + // rebuild UIs with the synchronized states + user_interfaces = ManuallyDrop::new(build_user_interfaces( + &application, + &mut debug, + &mut windows, + cached_interfaces, + )); + } + + debug.draw_started(); + + for (i, id) in windows.ids.iter().enumerate() { + // 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 = windows.states[i].cursor(); + + let (ui_state, _) = user_interfaces[i].update( + &[redraw_event.clone()], + cursor, + &mut windows.renderers[i], + &mut clipboard, + &mut messages, + ); + + let new_mouse_interaction = { + let state = &windows.states[i]; + + user_interfaces[i].draw( + &mut windows.renderers[i], + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + cursor, + ) + }; + + if new_mouse_interaction != mouse_interaction { + windows.raw[i].set_cursor_icon( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); + + mouse_interaction = new_mouse_interaction; + } + + // TODO once widgets can request to be redrawn, we can avoid always requesting a + // redraw + windows.raw[i].request_redraw(); + + runtime.broadcast( + redraw_event.clone(), + core::event::Status::Ignored, + ); + + let _ = control_sender.start_send(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(event) => match event { + Event::Application(message) => { + messages.push(message); + } + Event::WindowCreated { + id, + window, + exit_on_close_request, + } => { + let bounds = logical_bounds_of(&window); + + let (inner_size, i) = windows.add( + &application, + &mut compositor, + id, + window, + exit_on_close_request, + ); + + user_interfaces.push(build_user_interface( + &application, + user_interface::Cache::default(), + &mut windows.renderers[i], + inner_size, + &mut debug, + id, + )); + ui_caches.push(user_interface::Cache::default()); + + if let Some(bounds) = bounds { + events.push(( + Some(id), + core::Event::Window( + id, + window::Event::Created { + position: bounds.0, + size: bounds.1, + }, + ), + )); + } + } + Event::CloseWindow(id) => { + let i = windows.delete(id); + let _ = user_interfaces.remove(i); + let _ = ui_caches.remove(i); + + if windows.is_empty() { + break 'main; + } + } + Event::NewWindow { .. } => unreachable!(), + }, + event::Event::RedrawRequested(id) => { + let i = windows.index_from_raw(id); + let state = &windows.states[i]; + let physical_size = state.physical_size(); + + if physical_size.width == 0 || physical_size.height == 0 { + continue; + } + + debug.render_started(); + let current_viewport_version = state.viewport_version(); + let window_viewport_version = windows.viewport_versions[i]; + + if window_viewport_version != current_viewport_version { + let logical_size = state.logical_size(); + + debug.layout_started(); + + let renderer = &mut windows.renderers[i]; + let ui = user_interfaces.remove(i); + + user_interfaces + .insert(i, ui.relayout(logical_size, renderer)); + + debug.layout_finished(); + + debug.draw_started(); + let new_mouse_interaction = user_interfaces[i].draw( + renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + state.cursor(), + ); + + if new_mouse_interaction != mouse_interaction { + windows.raw[i].set_cursor_icon( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); + + mouse_interaction = new_mouse_interaction; + } + debug.draw_finished(); + + compositor.configure_surface( + &mut windows.surfaces[i], + physical_size.width, + physical_size.height, + ); + + windows.viewport_versions[i] = current_viewport_version; + } + + match compositor.present( + &mut windows.renderers[i], + &mut windows.surfaces[i], + state.viewport(), + 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 window in &windows.raw { + window.request_redraw(); + } + } + }, + } + } + event::Event::WindowEvent { + event: window_event, + window_id, + } => { + let window_index = + windows.raw.iter().position(|w| w.id() == window_id); + + match window_index { + Some(i) => { + let id = windows.ids[i]; + let raw = &windows.raw[i]; + let exit_on_close_request = + windows.exit_on_close_requested[i]; + + if matches!( + window_event, + winit::event::WindowEvent::CloseRequested + ) && exit_on_close_request + { + let i = windows.delete(id); + let _ = user_interfaces.remove(i); + let _ = ui_caches.remove(i); + + if windows.is_empty() { + break 'main; + } + } else { + let state = &mut windows.states[i]; + state.update(raw, &window_event, &mut debug); + + if let Some(event) = conversion::window_event( + id, + &window_event, + state.scale_factor(), + state.modifiers(), + ) { + events.push((Some(id), event)); + } + } + } + None => { + // This is the only special case, since in order to trigger the Destroyed event the + // window reference from winit must be dropped, but we still want to inform the + // user that the window was destroyed so they can clean up any specific window + // code for this window + if matches!( + window_event, + winit::event::WindowEvent::Destroyed + ) { + let id = windows.get_pending_destroy(window_id); + + events.push(( + None, + core::Event::Window( + id, + window::Event::Destroyed, + ), + )); + } + } + } + } + _ => {} + } + } + + let _ = ManuallyDrop::into_inner(user_interfaces); +} + +/// Builds a window's [`UserInterface`] for the [`Application`]. +pub 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`]. +pub fn update<A: Application, C, E: Executor>( + application: &mut A, + compositor: &mut C, + runtime: &mut Runtime<E, Proxy<Event<A::Message>>, Event<A::Message>>, + clipboard: &mut Clipboard, + proxy: &mut winit::event_loop::EventLoopProxy<Event<A::Message>>, + debug: &mut Debug, + messages: &mut Vec<A::Message>, + windows: &mut Windows<A, C>, + ui_caches: &mut Vec<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, + proxy, + debug, + windows, + ui_caches, + ); + } + + let subscription = application.subscription().map(Event::Application); + runtime.track(subscription.into_recipes()); +} + +/// Runs the actions of a [`Command`]. +pub fn run_command<A, C, E>( + application: &A, + compositor: &mut C, + command: Command<A::Message>, + runtime: &mut Runtime<E, Proxy<Event<A::Message>>, Event<A::Message>>, + clipboard: &mut Clipboard, + proxy: &mut winit::event_loop::EventLoopProxy<Event<A::Message>>, + debug: &mut Debug, + windows: &mut Windows<A, C>, + ui_caches: &mut Vec<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.map(Event::Application))); + } + command::Action::Stream(stream) => { + runtime.run(Box::pin(stream.map(Event::Application))); + } + command::Action::Clipboard(action) => match action { + clipboard::Action::Read(tag) => { + let message = tag(clipboard.read()); + + proxy + .send_event(Event::Application(message)) + .expect("Send message to event loop"); + } + clipboard::Action::Write(contents) => { + clipboard.write(contents); + } + }, + command::Action::Window(id, action) => match action { + window::Action::Spawn { settings } => { + let monitor = windows.last_monitor(); + + proxy + .send_event(Event::NewWindow { + id, + settings, + title: application.title(id), + monitor, + }) + .expect("Send message to event loop"); + } + window::Action::Close => { + proxy + .send_event(Event::CloseWindow(id)) + .expect("Send message to event loop"); + } + window::Action::Drag => { + let _ = windows.with_raw(id).drag_window(); + } + window::Action::Resize(size) => { + windows.with_raw(id).set_inner_size( + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + }, + ); + } + window::Action::FetchSize(callback) => { + let window = windows.with_raw(id); + let size = window.inner_size(); + + proxy + .send_event(Event::Application(callback(Size::new( + size.width, + size.height, + )))) + .expect("Send message to event loop") + } + window::Action::Maximize(maximized) => { + windows.with_raw(id).set_maximized(maximized); + } + window::Action::Minimize(minimized) => { + windows.with_raw(id).set_minimized(minimized); + } + window::Action::Move { x, y } => { + windows.with_raw(id).set_outer_position( + winit::dpi::LogicalPosition { x, y }, + ); + } + window::Action::ChangeMode(mode) => { + let window = windows.with_raw(id); + window.set_visible(conversion::visible(mode)); + window.set_fullscreen(conversion::fullscreen( + window.current_monitor(), + mode, + )); + } + window::Action::ChangeIcon(icon) => { + windows.with_raw(id).set_window_icon(conversion::icon(icon)) + } + window::Action::FetchMode(tag) => { + let window = windows.with_raw(id); + let mode = if window.is_visible().unwrap_or(true) { + conversion::mode(window.fullscreen()) + } else { + core::window::Mode::Hidden + }; + + proxy + .send_event(Event::Application(tag(mode))) + .expect("Event loop doesn't exist."); + } + window::Action::ToggleMaximize => { + let window = windows.with_raw(id); + window.set_maximized(!window.is_maximized()); + } + window::Action::ToggleDecorations => { + let window = windows.with_raw(id); + window.set_decorations(!window.is_decorated()); + } + window::Action::RequestUserAttention(attention_type) => { + windows.with_raw(id).request_user_attention( + attention_type.map(conversion::user_attention), + ); + } + window::Action::GainFocus => { + windows.with_raw(id).focus_window(); + } + window::Action::ChangeLevel(level) => { + windows + .with_raw(id) + .set_window_level(conversion::window_level(level)); + } + window::Action::FetchId(tag) => proxy + .send_event(Event::Application(tag(windows + .with_raw(id) + .id() + .into()))) + .expect("Event loop doesn't exist."), + window::Action::Screenshot(tag) => { + let i = windows.index_from_id(id); + let state = &windows.states[i]; + let surface = &mut windows.surfaces[i]; + let renderer = &mut windows.renderers[i]; + + let bytes = compositor.screenshot( + renderer, + surface, + state.viewport(), + state.background_color(), + &debug.overlay(), + ); + + proxy + .send_event(Event::Application(tag( + window::Screenshot::new( + bytes, + 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(Event::Application(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, + windows, + std::mem::take(ui_caches), + ); + + 'operate: while let Some(mut operation) = + current_operation.take() + { + for (i, ui) in uis.iter_mut().enumerate() { + ui.operate(&windows.renderers[i], operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => { + proxy + .send_event(Event::Application(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(UserInterface::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 renderer in &mut windows.renderers { + renderer.load_font(bytes.clone()); + } + + proxy + .send_event(Event::Application(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, + windows: &mut Windows<A, C>, + mut cached_user_interfaces: Vec<user_interface::Cache>, +) -> Vec<UserInterface<'a, A::Message, A::Renderer>> +where + <A::Renderer as core::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer>, +{ + cached_user_interfaces + .drain(..) + .zip( + windows + .ids + .iter() + .zip(windows.states.iter().zip(windows.renderers.iter_mut())), + ) + .fold(vec![], |mut uis, (cache, (id, (state, renderer)))| { + uis.push(build_user_interface( + application, + cache, + renderer, + state.logical_size(), + debug, + *id, + )); + + uis + }) +} + +/// 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, + } +} + +fn logical_bounds_of( + window: &winit::window::Window, +) -> Option<((i32, i32), Size<u32>)> { + let scale = window.scale_factor(); + let pos = window + .inner_position() + .map(|pos| { + ((pos.x as f64 / scale) as i32, (pos.y as f64 / scale) as i32) + }) + .ok()?; + let size = { + let size = window.inner_size(); + Size::new( + (size.width as f64 / scale) as u32, + (size.height as f64 / scale) as u32, + ) + }; + + Some((pos, size)) +} + +#[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..f2741c3c --- /dev/null +++ b/winit/src/multi_window/state.rs @@ -0,0 +1,242 @@ +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: usize, + 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) -> usize { + 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 [`Application::update`]. + /// + /// [`Application::update`]: crate::Program::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/windows.rs b/winit/src/multi_window/windows.rs new file mode 100644 index 00000000..1f606b31 --- /dev/null +++ b/winit/src/multi_window/windows.rs @@ -0,0 +1,196 @@ +use crate::core::{window, Size}; +use crate::multi_window::{Application, State}; +use iced_graphics::Compositor; +use iced_style::application::StyleSheet; +use std::fmt::{Debug, Formatter}; +use winit::monitor::MonitorHandle; + +pub struct Windows<A: Application, C: Compositor> +where + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer>, +{ + pub ids: Vec<window::Id>, + pub raw: Vec<winit::window::Window>, + pub states: Vec<State<A>>, + pub viewport_versions: Vec<usize>, + pub exit_on_close_requested: Vec<bool>, + pub surfaces: Vec<C::Surface>, + pub renderers: Vec<A::Renderer>, + pub pending_destroy: Vec<(window::Id, winit::window::WindowId)>, +} + +impl<A: Application, C: Compositor> Debug for Windows<A, C> +where + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer>, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Windows") + .field("ids", &self.ids) + .field( + "raw", + &self + .raw + .iter() + .map(|raw| raw.id()) + .collect::<Vec<winit::window::WindowId>>(), + ) + .field("states", &self.states) + .field("viewport_versions", &self.viewport_versions) + .finish() + } +} + +impl<A: Application, C: Compositor> Windows<A, C> +where + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer>, +{ + /// Creates a new [`Windows`] with a single `window::Id::MAIN` window. + pub fn new( + application: &A, + compositor: &mut C, + renderer: A::Renderer, + main: winit::window::Window, + exit_on_close_requested: bool, + ) -> Self { + let state = State::new(application, window::Id::MAIN, &main); + let viewport_version = state.viewport_version(); + let physical_size = state.physical_size(); + let surface = compositor.create_surface( + &main, + physical_size.width, + physical_size.height, + ); + + Self { + ids: vec![window::Id::MAIN], + raw: vec![main], + states: vec![state], + viewport_versions: vec![viewport_version], + exit_on_close_requested: vec![exit_on_close_requested], + surfaces: vec![surface], + renderers: vec![renderer], + pending_destroy: vec![], + } + } + + /// Adds a new window to [`Windows`]. Returns the size of the newly created window in logical + /// pixels & the index of the window within [`Windows`]. + pub fn add( + &mut self, + application: &A, + compositor: &mut C, + id: window::Id, + window: winit::window::Window, + exit_on_close_requested: bool, + ) -> (Size, usize) { + let state = State::new(application, id, &window); + let window_size = state.logical_size(); + 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.renderer(); + + self.ids.push(id); + self.raw.push(window); + self.states.push(state); + self.exit_on_close_requested.push(exit_on_close_requested); + self.viewport_versions.push(viewport_version); + self.surfaces.push(surface); + self.renderers.push(renderer); + + (window_size, self.ids.len() - 1) + } + + pub fn is_empty(&self) -> bool { + self.ids.is_empty() + } + + pub fn main(&self) -> &winit::window::Window { + &self.raw[0] + } + + pub fn index_from_raw(&self, id: winit::window::WindowId) -> usize { + self.raw + .iter() + .position(|window| window.id() == id) + .expect("No raw window in multi_window::Windows") + } + + pub fn index_from_id(&self, id: window::Id) -> usize { + self.ids + .iter() + .position(|window_id| *window_id == id) + .expect("No window in multi_window::Windows") + } + + pub fn last_monitor(&self) -> Option<MonitorHandle> { + self.raw.last().and_then(|w| w.current_monitor()) + } + + pub fn last(&self) -> usize { + self.ids.len() - 1 + } + + pub fn with_raw(&self, id: window::Id) -> &winit::window::Window { + let i = self.index_from_id(id); + &self.raw[i] + } + + /// Deletes the window with `id` from [`Windows`]. Returns the index that the window had. + pub fn delete(&mut self, id: window::Id) -> usize { + let i = self.index_from_id(id); + + let id = self.ids.remove(i); + let window = self.raw.remove(i); + let _ = self.states.remove(i); + let _ = self.exit_on_close_requested.remove(i); + let _ = self.viewport_versions.remove(i); + let _ = self.surfaces.remove(i); + + self.pending_destroy.push((id, window.id())); + + i + } + + /// Gets the winit `window` that is pending to be destroyed if it exists. + pub fn get_pending_destroy( + &mut self, + window: winit::window::WindowId, + ) -> window::Id { + let i = self + .pending_destroy + .iter() + .position(|(_, window_id)| window == *window_id) + .unwrap(); + + let (id, _) = self.pending_destroy.remove(i); + id + } + + /// Returns the windows that need to be requested to closed, and also the windows that can be + /// closed immediately. + pub fn partition_close_requests(&self) -> (Vec<window::Id>, Vec<window::Id>) { + self.exit_on_close_requested.iter().enumerate().fold( + (vec![], vec![]), + |(mut close_immediately, mut needs_request_closed), + (i, close)| { + let id = self.ids[i]; + + if *close { + close_immediately.push(id); + } else { + needs_request_closed.push(id); + } + + (close_immediately, needs_request_closed) + }, + ) + } +} 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..dc0f65a5 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)] @@ -46,7 +13,7 @@ pub struct Settings<Flags> { pub id: Option<String>, /// The [`Window`] settings. - pub window: Window, + 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, - } - } -} |