diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index d4c94fcd20..09b3169791 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -25,7 +25,6 @@ body: Before filing an issue... - If you are using `wgpu`, you need an environment that supports Vulkan, Metal, or DirectX 12. Please, make sure you can run [the `wgpu` examples]. - - If you are using `glow`, you need support for OpenGL 2.1+. Please, make sure you can run [the `glow` examples]. If you have any issues running any of the examples, make sure your graphics drivers are up-to-date. If the issues persist, please report them to the authors of the libraries directly! diff --git a/Cargo.toml b/Cargo.toml index ad4cd1bbd3..0afbcd513d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,8 @@ web-colors = ["iced_renderer/web-colors"] webgl = ["iced_renderer/webgl"] # Enables the syntax `highlighter` module highlighter = ["iced_highlighter"] +# Enables experimental multi-window support. +multi-window = ["iced_winit/multi-window"] # Enables the advanced module advanced = [] diff --git a/ECOSYSTEM.md b/ECOSYSTEM.md index 86581e4a18..da3066d8be 100644 --- a/ECOSYSTEM.md +++ b/ECOSYSTEM.md @@ -45,7 +45,7 @@ The widgets of a _graphical_ user interface produce some primitives that eventua Currently, there are two different official renderers: - [`iced_wgpu`] is powered by [`wgpu`] and supports Vulkan, DirectX 12, and Metal. -- [`iced_glow`] is powered by [`glow`] and supports OpenGL 2.1+ and OpenGL ES 2.0+. +- [`tiny-skia`] is used as a fallback software renderer when `wgpu` is not supported. Additionally, the [`iced_graphics`] subcrate contains a bunch of backend-agnostic types that can be leveraged to build renderers. Both of the renderers rely on the graphical foundations provided by this crate. @@ -54,10 +54,7 @@ The widgets of a graphical user _interface_ are interactive. __Shells__ gather a Normally, a shell will be responsible of creating a window and managing the lifecycle of a user interface, implementing a runtime of [The Elm Architecture]. -As of now, there are two official shells: - -- [`iced_winit`] implements a shell runtime on top of [`winit`]. -- [`iced_glutin`] is similar to [`iced_winit`], but it also deals with [OpenGL context creation]. +As of now, there is one official shell: [`iced_winit`] implements a shell runtime on top of [`winit`]. ## The web target The Web platform provides all the abstractions necessary to draw widgets and gather users interactions. @@ -91,5 +88,4 @@ Finally, [`iced`] unifies everything into a simple abstraction to create cross-p [`winit`]: https://github.com/rust-windowing/winit [`glutin`]: https://github.com/rust-windowing/glutin [`dodrio`]: https://github.com/fitzgen/dodrio -[OpenGL context creation]: https://www.khronos.org/opengl/wiki/Creating_an_OpenGL_Context [The Elm Architecture]: https://guide.elm-lang.org/architecture/ diff --git a/core/Cargo.toml b/core/Cargo.toml index 8294684726..4672c754ac 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -23,5 +23,8 @@ palette.optional = true [target.'cfg(target_arch = "wasm32")'.dependencies] instant.workspace = true +[target.'cfg(windows)'.dependencies] +raw-window-handle.workspace = true + [dev-dependencies] approx = "0.5" diff --git a/core/src/event.rs b/core/src/event.rs index 953cd73f94..870b3074e4 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -19,7 +19,7 @@ pub enum Event { Mouse(mouse::Event), /// A window event - Window(window::Event), + Window(window::Id, window::Event), /// A touch event Touch(touch::Event), diff --git a/core/src/point.rs b/core/src/point.rs index 9bf7726b8e..ef42852f1b 100644 --- a/core/src/point.rs +++ b/core/src/point.rs @@ -1,26 +1,34 @@ use crate::Vector; +use num_traits::{Float, Num}; +use std::fmt; + /// A 2D point. -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct Point { +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Point { /// The X coordinate. - pub x: f32, + pub x: T, /// The Y coordinate. - pub y: f32, + pub y: T, } impl Point { /// The origin (i.e. a [`Point`] at (0, 0)). - pub const ORIGIN: Point = Point::new(0.0, 0.0); + pub const ORIGIN: Self = Self::new(0.0, 0.0); +} +impl Point { /// Creates a new [`Point`] with the given coordinates. - pub const fn new(x: f32, y: f32) -> Self { + pub const fn new(x: T, y: T) -> Self { Self { x, y } } /// Computes the distance to another [`Point`]. - pub fn distance(&self, to: Point) -> f32 { + pub fn distance(&self, to: Self) -> T + where + T: Float, + { let a = self.x - to.x; let b = self.y - to.y; @@ -34,9 +42,9 @@ impl From<[f32; 2]> for Point { } } -impl From<[u16; 2]> for Point { +impl From<[u16; 2]> for Point { fn from([x, y]: [u16; 2]) -> Self { - Point::new(x.into(), y.into()) + Point::new(x, y) } } @@ -46,10 +54,13 @@ impl From for [f32; 2] { } } -impl std::ops::Add for Point { +impl std::ops::Add> for Point +where + T: std::ops::Add, +{ type Output = Self; - fn add(self, vector: Vector) -> Self { + fn add(self, vector: Vector) -> Self { Self { x: self.x + vector.x, y: self.y + vector.y, @@ -57,10 +68,13 @@ impl std::ops::Add for Point { } } -impl std::ops::Sub for Point { +impl std::ops::Sub> for Point +where + T: std::ops::Sub, +{ type Output = Self; - fn sub(self, vector: Vector) -> Self { + fn sub(self, vector: Vector) -> Self { Self { x: self.x - vector.x, y: self.y - vector.y, @@ -68,10 +82,22 @@ impl std::ops::Sub for Point { } } -impl std::ops::Sub for Point { - type Output = Vector; +impl std::ops::Sub> for Point +where + T: std::ops::Sub, +{ + type Output = Vector; - fn sub(self, point: Point) -> Vector { + fn sub(self, point: Self) -> Vector { Vector::new(self.x - point.x, self.y - point.y) } } + +impl fmt::Display for Point +where + T: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y) + } +} diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index d4b8828ae5..ff52b1ce42 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -67,7 +67,7 @@ impl Tree { } } - /// Reconciliates the children of the tree with the provided list of widgets. + /// Reconciles the children of the tree with the provided list of widgets. pub fn diff_children<'a, Message, Renderer>( &mut self, new_children: &[impl Borrow + 'a>], diff --git a/core/src/window.rs b/core/src/window.rs index a6dbdfb446..448ffc4503 100644 --- a/core/src/window.rs +++ b/core/src/window.rs @@ -1,15 +1,21 @@ //! Build window-based GUI applications. pub mod icon; +pub mod settings; mod event; +mod id; mod level; mod mode; +mod position; mod redraw_request; mod user_attention; pub use event::Event; pub use icon::Icon; +pub use id::Id; pub use level::Level; pub use mode::Mode; +pub use position::Position; pub use redraw_request::RedrawRequest; +pub use settings::Settings; pub use user_attention::UserAttention; diff --git a/core/src/window/event.rs b/core/src/window/event.rs index e2fb5e665b..b9ee7aca78 100644 --- a/core/src/window/event.rs +++ b/core/src/window/event.rs @@ -1,10 +1,27 @@ use crate::time::Instant; +use crate::{Point, Size}; use std::path::PathBuf; /// A window-related event. -#[derive(PartialEq, Eq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug)] pub enum Event { + /// A window was opened. + Opened { + /// The position of the opened window. This is relative to the top-left corner of the desktop + /// the window is on, including virtual desktops. Refers to window's "inner" position, + /// or the client area, in logical pixels. + /// + /// **Note**: Not available in Wayland. + position: Option, + /// The size of the created window. This is its "inner" size, or the size of the + /// client area, in logical pixels. + size: Size, + }, + + /// A window was closed. + Closed, + /// A window was moved. Moved { /// The new logical x location of the window @@ -27,9 +44,6 @@ pub enum Event { RedrawRequested(Instant), /// The user has requested for the window to close. - /// - /// Usually, you will want to terminate the execution whenever this event - /// occurs. CloseRequested, /// A window was focused. diff --git a/core/src/window/id.rs b/core/src/window/id.rs new file mode 100644 index 0000000000..20474c8f31 --- /dev/null +++ b/core/src/window/id.rs @@ -0,0 +1,21 @@ +use std::hash::Hash; + +use std::sync::atomic::{self, AtomicU64}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +/// The id of the window. +/// +/// Internally Iced reserves `window::Id::MAIN` for the first window spawned. +pub struct Id(u64); + +static COUNT: AtomicU64 = AtomicU64::new(1); + +impl Id { + /// The reserved window [`Id`] for the first window in an Iced application. + pub const MAIN: Self = Id(0); + + /// Creates a new unique window [`Id`]. + pub fn unique() -> Id { + Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed)) + } +} diff --git a/winit/src/position.rs b/core/src/window/position.rs similarity index 89% rename from winit/src/position.rs rename to core/src/window/position.rs index c260c29eb1..73391e75eb 100644 --- a/winit/src/position.rs +++ b/core/src/window/position.rs @@ -1,5 +1,7 @@ +use crate::Point; + /// The position of a window in a given screen. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Position { /// The platform-specific default position for a new window. Default, @@ -12,7 +14,7 @@ pub enum Position { /// 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), + Specific(Point), } impl Default for Position { diff --git a/src/window/settings.rs b/core/src/window/settings.rs similarity index 50% rename from src/window/settings.rs rename to core/src/window/settings.rs index 0ee573e563..fbbf86abd8 100644 --- a/src/window/settings.rs +++ b/core/src/window/settings.rs @@ -1,21 +1,47 @@ -use crate::window::{Icon, Level, Position}; +//! Configure your windows. +#[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; -pub use iced_winit::settings::PlatformSpecific; +#[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_arch = "wasm32" +)))] +#[path = "settings/other.rs"] +mod platform; +use crate::window::{Icon, Level, Position}; +use crate::Size; + +pub use platform::PlatformSpecific; /// The window settings of an application. #[derive(Debug, Clone)] pub struct Settings { - /// The initial size of the window. - pub size: (u32, u32), + /// The initial logical dimensions of the window. + pub size: Size, /// The initial position of the window. pub position: Position, /// The minimum size of the window. - pub min_size: Option<(u32, u32)>, + pub min_size: Option, /// The maximum size of the window. - pub max_size: Option<(u32, u32)>, + pub max_size: Option, /// Whether the window should be visible or not. pub visible: bool, @@ -37,12 +63,22 @@ pub struct Settings { /// Platform specific settings. pub platform_specific: PlatformSpecific, + + /// Whether the window will close when the user requests it, e.g. when a user presses the + /// close button. + /// + /// This can be useful if you want to have some behavior that executes before the window is + /// actually destroyed. If you disable this, you must manually close the window with the + /// `window::close` command. + /// + /// By default this is enabled. + pub exit_on_close_request: bool, } impl Default for Settings { - fn default() -> Settings { - Settings { - size: (1024, 768), + fn default() -> Self { + Self { + size: Size::new(1024.0, 768.0), position: Position::default(), min_size: None, max_size: None, @@ -52,25 +88,8 @@ impl Default for Settings { transparent: false, level: Level::default(), icon: None, + exit_on_close_request: true, platform_specific: PlatformSpecific::default(), } } } - -impl From for iced_winit::settings::Window { - fn from(settings: Settings) -> Self { - Self { - size: settings.size, - position: iced_winit::Position::from(settings.position), - min_size: settings.min_size, - max_size: settings.max_size, - visible: settings.visible, - resizable: settings.resizable, - decorations: settings.decorations, - transparent: settings.transparent, - level: settings.level, - icon: settings.icon.map(Icon::into), - platform_specific: settings.platform_specific, - } - } -} diff --git a/winit/src/settings/linux.rs b/core/src/window/settings/linux.rs similarity index 100% rename from winit/src/settings/linux.rs rename to core/src/window/settings/linux.rs diff --git a/winit/src/settings/macos.rs b/core/src/window/settings/macos.rs similarity index 100% rename from winit/src/settings/macos.rs rename to core/src/window/settings/macos.rs diff --git a/winit/src/settings/other.rs b/core/src/window/settings/other.rs similarity index 100% rename from winit/src/settings/other.rs rename to core/src/window/settings/other.rs diff --git a/winit/src/settings/wasm.rs b/core/src/window/settings/wasm.rs similarity index 100% rename from winit/src/settings/wasm.rs rename to core/src/window/settings/wasm.rs diff --git a/winit/src/settings/windows.rs b/core/src/window/settings/windows.rs similarity index 100% rename from winit/src/settings/windows.rs rename to core/src/window/settings/windows.rs diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 32d0da2c8c..334b012d80 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -10,7 +10,10 @@ use iced::{ pub fn main() -> iced::Result { Events::run(Settings { - exit_on_close_request: false, + window: window::Settings { + exit_on_close_request: false, + ..window::Settings::default() + }, ..Settings::default() }) } @@ -54,8 +57,9 @@ impl Application for Events { Command::none() } Message::EventOccurred(event) => { - if let Event::Window(window::Event::CloseRequested) = event { - window::close() + if let Event::Window(id, window::Event::CloseRequested) = event + { + window::close(id) } else { Command::none() } @@ -65,7 +69,7 @@ impl Application for Events { Command::none() } - Message::Exit => window::close(), + Message::Exit => window::close(window::Id::MAIN), } } diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index 6152f62706..ec618dc140 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -34,7 +34,7 @@ impl Application for Exit { fn update(&mut self, message: Message) -> Command { match message { - Message::Confirm => window::close(), + Message::Confirm => window::close(window::Id::MAIN), Message::Exit => { self.show_confirm = true; diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 0f32fca005..276794c820 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -6,13 +6,17 @@ use scene::Scene; use iced_wgpu::graphics::Viewport; use iced_wgpu::{wgpu, Backend, Renderer, Settings}; +use iced_winit::conversion; use iced_winit::core::mouse; use iced_winit::core::renderer; +use iced_winit::core::window; use iced_winit::core::{Color, Font, Pixels, Size}; +use iced_winit::futures; use iced_winit::runtime::program; use iced_winit::runtime::Debug; use iced_winit::style::Theme; -use iced_winit::{conversion, futures, winit, Clipboard}; +use iced_winit::winit; +use iced_winit::Clipboard; use winit::{ event::{Event, ModifiersState, WindowEvent}, @@ -180,6 +184,7 @@ pub fn main() -> Result<(), Box> { // Map window event to iced event if let Some(event) = iced_winit::conversion::window_event( + window::Id::MAIN, &event, window.scale_factor(), modifiers, diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index bf01c3b44f..dca8046af0 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -279,7 +279,7 @@ where let state = tree.state.downcast_mut::(); - if let Event::Window(window::Event::RedrawRequested(now)) = event { + if let Event::Window(_, window::Event::RedrawRequested(now)) = event { state.animation = state.animation.timed_transition( self.cycle_duration, self.rotation_duration, diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index c5bb4791ce..db10bfbaf5 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -200,7 +200,7 @@ where let state = tree.state.downcast_mut::(); - if let Event::Window(window::Event::RedrawRequested(now)) = event { + if let Event::Window(_, window::Event::RedrawRequested(now)) = event { *state = state.timed_transition(self.cycle_duration, now); shell.request_redraw(RedrawRequest::At( diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml new file mode 100644 index 0000000000..2e222dfbb1 --- /dev/null +++ b/examples/multi_window/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "multi_window" +version = "0.1.0" +authors = ["Bingus "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["debug", "multi-window"] } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs new file mode 100644 index 0000000000..5a5e70c16c --- /dev/null +++ b/examples/multi_window/src/main.rs @@ -0,0 +1,215 @@ +use iced::event; +use iced::executor; +use iced::multi_window::{self, Application}; +use iced::widget::{button, column, container, scrollable, text, text_input}; +use iced::window; +use iced::{ + Alignment, Command, Element, Length, Point, Settings, Subscription, Theme, + Vector, +}; + +use std::collections::HashMap; + +fn main() -> iced::Result { + Example::run(Settings::default()) +} + +#[derive(Default)] +struct Example { + windows: HashMap, + next_window_pos: window::Position, +} + +#[derive(Debug)] +struct Window { + title: String, + scale_input: String, + current_scale: f64, + theme: Theme, + input_id: iced::widget::text_input::Id, +} + +#[derive(Debug, Clone)] +enum Message { + ScaleInputChanged(window::Id, String), + ScaleChanged(window::Id, String), + TitleChanged(window::Id, String), + CloseWindow(window::Id), + WindowOpened(window::Id, Option), + WindowClosed(window::Id), + NewWindow, +} + +impl multi_window::Application for Example { + type Executor = executor::Default; + type Message = Message; + type Theme = Theme; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command) { + ( + Example { + windows: HashMap::from([(window::Id::MAIN, Window::new(1))]), + next_window_pos: window::Position::Default, + }, + Command::none(), + ) + } + + fn title(&self, window: window::Id) -> String { + self.windows + .get(&window) + .map(|window| window.title.clone()) + .unwrap_or("Example".to_string()) + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::ScaleInputChanged(id, scale) => { + let window = + self.windows.get_mut(&id).expect("Window not found!"); + window.scale_input = scale; + + Command::none() + } + Message::ScaleChanged(id, scale) => { + let window = + self.windows.get_mut(&id).expect("Window not found!"); + + window.current_scale = scale + .parse::() + .unwrap_or(window.current_scale) + .clamp(0.5, 5.0); + + Command::none() + } + Message::TitleChanged(id, title) => { + let window = + self.windows.get_mut(&id).expect("Window not found."); + + window.title = title; + + Command::none() + } + Message::CloseWindow(id) => window::close(id), + Message::WindowClosed(id) => { + self.windows.remove(&id); + Command::none() + } + Message::WindowOpened(id, position) => { + if let Some(position) = position { + self.next_window_pos = window::Position::Specific( + position + Vector::new(20.0, 20.0), + ); + } + + if let Some(window) = self.windows.get(&id) { + text_input::focus(window.input_id.clone()) + } else { + Command::none() + } + } + Message::NewWindow => { + let count = self.windows.len() + 1; + + let (id, spawn_window) = window::spawn(window::Settings { + position: self.next_window_pos, + exit_on_close_request: count % 2 == 0, + ..Default::default() + }); + + self.windows.insert(id, Window::new(count)); + + spawn_window + } + } + } + + fn view(&self, window: window::Id) -> Element { + let content = self.windows.get(&window).unwrap().view(window); + + container(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } + + fn theme(&self, window: window::Id) -> Self::Theme { + self.windows.get(&window).unwrap().theme.clone() + } + + fn scale_factor(&self, window: window::Id) -> f64 { + self.windows + .get(&window) + .map(|window| window.current_scale) + .unwrap_or(1.0) + } + + fn subscription(&self) -> Subscription { + event::listen_with(|event, _| { + if let iced::Event::Window(id, window_event) = event { + match window_event { + window::Event::CloseRequested => { + Some(Message::CloseWindow(id)) + } + window::Event::Opened { position, .. } => { + Some(Message::WindowOpened(id, position)) + } + window::Event::Closed => Some(Message::WindowClosed(id)), + _ => None, + } + } else { + None + } + }) + } +} + +impl Window { + fn new(count: usize) -> Self { + Self { + title: format!("Window_{}", count), + scale_input: "1.0".to_string(), + current_scale: 1.0, + theme: if count % 2 == 0 { + Theme::Light + } else { + Theme::Dark + }, + input_id: text_input::Id::unique(), + } + } + + fn view(&self, id: window::Id) -> Element { + let scale_input = column![ + text("Window scale factor:"), + text_input("Window Scale", &self.scale_input) + .on_input(move |msg| { Message::ScaleInputChanged(id, msg) }) + .on_submit(Message::ScaleChanged( + id, + self.scale_input.to_string() + )) + ]; + + let title_input = column![ + text("Window title:"), + text_input("Window Title", &self.title) + .on_input(move |msg| { Message::TitleChanged(id, msg) }) + .id(self.input_id.clone()) + ]; + + let new_window_button = + button(text("New Window")).on_press(Message::NewWindow); + + let content = scrollable( + column![scale_input, title_input, new_window_button] + .spacing(50) + .width(Length::Fill) + .align_items(Alignment::Center), + ); + + container(content).width(200).center_x().into() + } +} diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index f781a401c9..20d34be697 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -1,8 +1,8 @@ -use iced::alignment; use iced::keyboard::KeyCode; use iced::theme::{Button, Container}; use iced::widget::{button, column, container, image, row, text, text_input}; use iced::window::screenshot::{self, Screenshot}; +use iced::{alignment, window}; use iced::{ event, executor, keyboard, Alignment, Application, Command, ContentFit, Element, Event, Length, Rectangle, Renderer, Subscription, Theme, @@ -70,7 +70,10 @@ impl Application for Example { fn update(&mut self, message: Self::Message) -> Command { match message { Message::Screenshot => { - return iced::window::screenshot(Message::ScreenshotData); + return iced::window::screenshot( + window::Id::MAIN, + Message::ScreenshotData, + ); } Message::ScreenshotData(screenshot) => { self.screenshot = Some(screenshot); diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 8295ddeded..82421a8684 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -114,14 +114,14 @@ impl State { pub fn new() -> State { let now = Instant::now(); - let (width, height) = window::Settings::default().size; + let size = window::Settings::default().size; State { space_cache: canvas::Cache::default(), system_cache: canvas::Cache::default(), start: now, now, - stars: Self::generate_stars(width, height), + stars: Self::generate_stars(size.width, size.height), } } @@ -130,7 +130,7 @@ impl State { self.system_cache.clear(); } - fn generate_stars(width: u32, height: u32) -> Vec<(Point, f32)> { + fn generate_stars(width: f32, height: f32) -> Vec<(Point, f32)> { use rand::Rng; let mut rng = rand::thread_rng(); @@ -139,12 +139,8 @@ impl State { .map(|_| { ( Point::new( - rng.gen_range( - (-(width as f32) / 2.0)..(width as f32 / 2.0), - ), - rng.gen_range( - (-(height as f32) / 2.0)..(height as f32 / 2.0), - ), + rng.gen_range((-width / 2.0)..(width / 2.0)), + rng.gen_range((-height / 2.0)..(height / 2.0)), ), rng.gen_range(0.5..1.0), ) diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 934049d5a5..31b6f19166 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -539,7 +539,9 @@ mod toast { clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - if let Event::Window(window::Event::RedrawRequested(now)) = &event { + if let Event::Window(_, window::Event::RedrawRequested(now)) = + &event + { let mut next_redraw: Option = None; self.instants.iter_mut().enumerate().for_each( diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 1ad3aba7f0..4dac032c83 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -8,7 +8,7 @@ use iced::widget::{ }; use iced::window; use iced::{Application, Element}; -use iced::{Color, Command, Length, Settings, Subscription}; +use iced::{Color, Command, Length, Settings, Size, Subscription}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -22,7 +22,7 @@ pub fn main() -> iced::Result { Todos::run(Settings { window: window::Settings { - size: (500, 800), + size: Size::new(500.0, 800.0), ..window::Settings::default() }, ..Settings::default() @@ -54,7 +54,7 @@ enum Message { FilterChanged(Filter), TaskMessage(usize, TaskMessage), TabPressed { shift: bool }, - ChangeWindowMode(window::Mode), + ToggleFullscreen(window::Mode), } impl Application for Todos { @@ -165,8 +165,8 @@ impl Application for Todos { widget::focus_next() } } - Message::ChangeWindowMode(mode) => { - window::change_mode(mode) + Message::ToggleFullscreen(mode) => { + window::change_mode(window::Id::MAIN, mode) } _ => Command::none(), }; @@ -272,10 +272,10 @@ impl Application for Todos { shift: modifiers.shift(), }), (keyboard::KeyCode::Up, keyboard::Modifiers::SHIFT) => { - Some(Message::ChangeWindowMode(window::Mode::Fullscreen)) + Some(Message::ToggleFullscreen(window::Mode::Fullscreen)) } (keyboard::KeyCode::Down, keyboard::Modifiers::SHIFT) => { - Some(Message::ChangeWindowMode(window::Mode::Windowed)) + Some(Message::ToggleFullscreen(window::Mode::Windowed)) } _ => None, } diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index 697badb4ff..fdf1e0f94c 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -167,7 +167,7 @@ impl Application for Example { Event::Mouse(mouse::Event::CursorMoved { position }) => { Some(Message::MouseMoved(position)) } - Event::Window(window::Event::Resized { .. }) => { + Event::Window(_, window::Event::Resized { .. }) => { Some(Message::WindowResized) } _ => None, diff --git a/futures/src/event.rs b/futures/src/event.rs index 214d2d406d..9722450621 100644 --- a/futures/src/event.rs +++ b/futures/src/event.rs @@ -35,7 +35,7 @@ where subscription::filter_map( (EventsWith, f), move |event, status| match event { - Event::Window(window::Event::RedrawRequested(_)) => None, + Event::Window(_, window::Event::RedrawRequested(_)) => None, _ => f(event, status), }, ) diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 0222a80f01..b8b575b41c 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -22,7 +22,10 @@ pub trait Compositor: Sized { fn new( settings: Self::Settings, compatible_window: Option<&W>, - ) -> Result<(Self, Self::Renderer), Error>; + ) -> Result; + + /// Creates a [`Self::Renderer`] for the [`Compositor`]. + fn create_renderer(&self) -> Self::Renderer; /// Crates a new [`Surface`] for the given window. /// diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index d15000895d..9d0ff9ab3c 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -26,7 +26,7 @@ impl crate::graphics::Compositor for Compositor { fn new( settings: Self::Settings, compatible_window: Option<&W>, - ) -> Result<(Self, Self::Renderer), Error> { + ) -> Result { let candidates = Candidate::list_from_env().unwrap_or(Candidate::default_list()); @@ -34,9 +34,7 @@ impl crate::graphics::Compositor for Compositor { for candidate in candidates { match candidate.build(settings, compatible_window) { - Ok((compositor, renderer)) => { - return Ok((compositor, renderer)) - } + Ok(compositor) => return Ok(compositor), Err(new_error) => { error = new_error; } @@ -46,6 +44,18 @@ impl crate::graphics::Compositor for Compositor { Err(error) } + fn create_renderer(&self) -> Self::Renderer { + match self { + Compositor::TinySkia(compositor) => { + Renderer::TinySkia(compositor.create_renderer()) + } + #[cfg(feature = "wgpu")] + Compositor::Wgpu(compositor) => { + Renderer::Wgpu(compositor.create_renderer()) + } + } + } + fn create_surface( &mut self, window: &W, @@ -220,24 +230,21 @@ impl Candidate { self, settings: Settings, _compatible_window: Option<&W>, - ) -> Result<(Compositor, Renderer), Error> { + ) -> Result, Error> { match self { Self::TinySkia => { - let (compositor, backend) = - iced_tiny_skia::window::compositor::new(); + let compositor = iced_tiny_skia::window::compositor::new( + iced_tiny_skia::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + }, + ); - Ok(( - Compositor::TinySkia(compositor), - Renderer::TinySkia(iced_tiny_skia::Renderer::new( - backend, - settings.default_font, - settings.default_text_size, - )), - )) + Ok(Compositor::TinySkia(compositor)) } #[cfg(feature = "wgpu")] Self::Wgpu => { - let (compositor, backend) = iced_wgpu::window::compositor::new( + let compositor = iced_wgpu::window::compositor::new( iced_wgpu::Settings { default_font: settings.default_font, default_text_size: settings.default_text_size, @@ -247,14 +254,7 @@ impl Candidate { _compatible_window, )?; - Ok(( - Compositor::Wgpu(compositor), - Renderer::Wgpu(iced_wgpu::Renderer::new( - backend, - settings.default_font, - settings.default_text_size, - )), - )) + Ok(Compositor::Wgpu(compositor)) } #[cfg(not(feature = "wgpu"))] Self::Wgpu => { diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index d19aedd33d..8089d545a1 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -12,6 +12,7 @@ keywords.workspace = true [features] debug = [] +multi-window = [] [dependencies] iced_core.workspace = true diff --git a/runtime/src/command/action.rs b/runtime/src/command/action.rs index 6551e23302..cb0936df74 100644 --- a/runtime/src/command/action.rs +++ b/runtime/src/command/action.rs @@ -84,7 +84,9 @@ impl fmt::Debug for Action { Self::Clipboard(action) => { write!(f, "Action::Clipboard({action:?})") } - Self::Window(action) => write!(f, "Action::Window({action:?})"), + Self::Window(action) => { + write!(f, "Action::Window({action:?})") + } Self::System(action) => write!(f, "Action::System({action:?})"), Self::Widget(_action) => write!(f, "Action::Widget"), Self::LoadFont { .. } => write!(f, "Action::LoadFont"), diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 29e94d65fd..03906f459a 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -26,6 +26,9 @@ pub mod system; pub mod user_interface; pub mod window; +#[cfg(feature = "multi-window")] +pub mod multi_window; + // We disable debug capabilities on release builds unless the `debug` feature // is explicitly enabled. #[cfg(feature = "debug")] diff --git a/runtime/src/multi_window.rs b/runtime/src/multi_window.rs new file mode 100644 index 0000000000..cf778a2075 --- /dev/null +++ b/runtime/src/multi_window.rs @@ -0,0 +1,6 @@ +//! A multi-window application. +pub mod program; +pub mod state; + +pub use program::Program; +pub use state::State; diff --git a/runtime/src/multi_window/program.rs b/runtime/src/multi_window/program.rs new file mode 100644 index 0000000000..591b3e9a7b --- /dev/null +++ b/runtime/src/multi_window/program.rs @@ -0,0 +1,32 @@ +//! Build interactive programs using The Elm Architecture. +use crate::core::text; +use crate::core::window; +use crate::core::{Element, Renderer}; +use crate::Command; + +/// The core of a user interface for a multi-window application following The Elm Architecture. +pub trait Program: Sized { + /// The graphics backend to use to draw the [`Program`]. + type Renderer: Renderer + text::Renderer; + + /// The type of __messages__ your [`Program`] will produce. + type Message: std::fmt::Debug + Send; + + /// Handles a __message__ and updates the state of the [`Program`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Command`] returned will be executed immediately in the + /// background by shells. + fn update(&mut self, message: Self::Message) -> Command; + + /// Returns the widgets to display in the [`Program`] for the `window`. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, Self::Renderer>; +} diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs new file mode 100644 index 0000000000..49f72c3971 --- /dev/null +++ b/runtime/src/multi_window/state.rs @@ -0,0 +1,280 @@ +//! The internal state of a multi-window [`Program`]. +use crate::core::event::{self, Event}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::operation::{self, Operation}; +use crate::core::{Clipboard, Size}; +use crate::user_interface::{self, UserInterface}; +use crate::{Command, Debug, Program}; + +/// The execution state of a multi-window [`Program`]. It leverages caching, event +/// processing, and rendering primitive storage. +#[allow(missing_debug_implementations)] +pub struct State

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

State

+where + P: Program + 'static, +{ + /// Creates a new [`State`] with the provided [`Program`], initializing its + /// primitive with the given logical bounds and renderer. + pub fn new( + program: P, + bounds: Size, + renderer: &mut P::Renderer, + debug: &mut Debug, + ) -> Self { + let user_interface = build_user_interface( + &program, + user_interface::Cache::default(), + renderer, + bounds, + debug, + ); + + let caches = Some(vec![user_interface.into_cache()]); + + State { + program, + caches, + queued_events: Vec::new(), + queued_messages: Vec::new(), + mouse_interaction: mouse::Interaction::Idle, + } + } + + /// Returns a reference to the [`Program`] of the [`State`]. + pub fn program(&self) -> &P { + &self.program + } + + /// Queues an event in the [`State`] for processing during an [`update`]. + /// + /// [`update`]: Self::update + pub fn queue_event(&mut self, event: Event) { + self.queued_events.push(event); + } + + /// Queues a message in the [`State`] for processing during an [`update`]. + /// + /// [`update`]: Self::update + pub fn queue_message(&mut self, message: P::Message) { + self.queued_messages.push(message); + } + + /// Returns whether the event queue of the [`State`] is empty or not. + pub fn is_queue_empty(&self) -> bool { + self.queued_events.is_empty() && self.queued_messages.is_empty() + } + + /// Returns the current [`mouse::Interaction`] of the [`State`]. + pub fn mouse_interaction(&self) -> mouse::Interaction { + self.mouse_interaction + } + + /// Processes all the queued events and messages, rebuilding and redrawing + /// the widgets of the linked [`Program`] if necessary. + /// + /// Returns a list containing the instances of [`Event`] that were not + /// captured by any widget, and the [`Command`] obtained from [`Program`] + /// after updating it, only if an update was necessary. + pub fn update( + &mut self, + bounds: Size, + cursor: mouse::Cursor, + renderer: &mut P::Renderer, + theme: &::Theme, + style: &renderer::Style, + clipboard: &mut dyn Clipboard, + debug: &mut Debug, + ) -> (Vec, Option>) { + let mut user_interfaces = build_user_interfaces( + &self.program, + self.caches.take().unwrap(), + renderer, + bounds, + debug, + ); + + debug.event_processing_started(); + let mut messages = Vec::new(); + + let uncaptured_events = user_interfaces.iter_mut().fold( + vec![], + |mut uncaptured_events, ui| { + let (_, event_statuses) = ui.update( + &self.queued_events, + cursor, + renderer, + clipboard, + &mut messages, + ); + + uncaptured_events.extend( + self.queued_events + .iter() + .zip(event_statuses) + .filter_map(|(event, status)| { + matches!(status, event::Status::Ignored) + .then_some(event) + }) + .cloned(), + ); + uncaptured_events + }, + ); + + self.queued_events.clear(); + messages.append(&mut self.queued_messages); + debug.event_processing_finished(); + + let commands = if messages.is_empty() { + debug.draw_started(); + + for ui in &mut user_interfaces { + self.mouse_interaction = + ui.draw(renderer, theme, style, cursor); + } + + debug.draw_finished(); + + self.caches = Some( + user_interfaces + .drain(..) + .map(UserInterface::into_cache) + .collect(), + ); + + None + } else { + let temp_caches = user_interfaces + .drain(..) + .map(UserInterface::into_cache) + .collect(); + + drop(user_interfaces); + + let commands = Command::batch(messages.into_iter().map(|msg| { + debug.log_message(&msg); + + debug.update_started(); + let command = self.program.update(msg); + debug.update_finished(); + + command + })); + + let mut user_interfaces = build_user_interfaces( + &self.program, + temp_caches, + renderer, + bounds, + debug, + ); + + debug.draw_started(); + for ui in &mut user_interfaces { + self.mouse_interaction = + ui.draw(renderer, theme, style, cursor); + } + debug.draw_finished(); + + self.caches = Some( + user_interfaces + .drain(..) + .map(UserInterface::into_cache) + .collect(), + ); + + Some(commands) + }; + + (uncaptured_events, commands) + } + + /// Applies widget [`Operation`]s to the [`State`]. + pub fn operate( + &mut self, + renderer: &mut P::Renderer, + operations: impl Iterator>>, + bounds: Size, + debug: &mut Debug, + ) { + let mut user_interfaces = build_user_interfaces( + &self.program, + self.caches.take().unwrap(), + renderer, + bounds, + debug, + ); + + for operation in operations { + let mut current_operation = Some(operation); + + while let Some(mut operation) = current_operation.take() { + for ui in &mut user_interfaces { + ui.operate(renderer, operation.as_mut()); + } + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => { + self.queued_messages.push(message); + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + }; + } + } + + self.caches = Some( + user_interfaces + .drain(..) + .map(UserInterface::into_cache) + .collect(), + ); + } +} + +fn build_user_interfaces<'a, P: Program>( + program: &'a P, + mut caches: Vec, + renderer: &mut P::Renderer, + size: Size, + debug: &mut Debug, +) -> Vec> { + caches + .drain(..) + .map(|cache| { + build_user_interface(program, cache, renderer, size, debug) + }) + .collect() +} + +fn build_user_interface<'a, P: Program>( + program: &'a P, + cache: user_interface::Cache, + renderer: &mut P::Renderer, + size: Size, + debug: &mut Debug, +) -> UserInterface<'a, P::Message, P::Renderer> { + debug.view_started(); + let view = program.view(); + debug.view_finished(); + + debug.layout_started(); + let user_interface = UserInterface::build(view, size, cache, renderer); + debug.layout_finished(); + + user_interface +} diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 41816967d7..f9d943f6aa 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -8,95 +8,113 @@ pub use screenshot::Screenshot; use crate::command::{self, Command}; use crate::core::time::Instant; -use crate::core::window::{Event, Icon, Level, Mode, UserAttention}; -use crate::core::Size; +use crate::core::window::{ + Event, Icon, Id, Level, Mode, Settings, UserAttention, +}; +use crate::core::{Point, Size}; use crate::futures::event; use crate::futures::Subscription; /// Subscribes to the frames of the window of the running application. /// /// The resulting [`Subscription`] will produce items at a rate equal to the -/// refresh rate of the window. Note that this rate may be variable, as it is +/// refresh rate of the first application window. Note that this rate may be variable, as it is /// normally managed by the graphics driver and/or the OS. /// /// In any case, this [`Subscription`] is useful to smoothly draw application-driven /// animations without missing any frames. pub fn frames() -> Subscription { event::listen_raw(|event, _status| match event { - iced_core::Event::Window(Event::RedrawRequested(at)) => Some(at), + crate::core::Event::Window(_, Event::RedrawRequested(at)) => Some(at), _ => None, }) } -/// Closes the current window and exits the application. -pub fn close() -> Command { - Command::single(command::Action::Window(Action::Close)) +/// Spawns a new window with the given `settings`. +/// +/// Returns the new window [`Id`] alongside the [`Command`]. +pub fn spawn(settings: Settings) -> (Id, Command) { + let id = Id::unique(); + + ( + id, + Command::single(command::Action::Window(Action::Spawn(id, settings))), + ) +} + +/// Closes the window with `id`. +pub fn close(id: Id) -> Command { + Command::single(command::Action::Window(Action::Close(id))) } /// Begins dragging the window while the left mouse button is held. -pub fn drag() -> Command { - Command::single(command::Action::Window(Action::Drag)) +pub fn drag(id: Id) -> Command { + Command::single(command::Action::Window(Action::Drag(id))) } /// Resizes the window to the given logical dimensions. -pub fn resize(new_size: Size) -> Command { - Command::single(command::Action::Window(Action::Resize(new_size))) +pub fn resize(id: Id, new_size: Size) -> Command { + Command::single(command::Action::Window(Action::Resize(id, new_size))) } -/// Fetches the current window size in logical dimensions. +/// Fetches the window's size in logical dimensions. pub fn fetch_size( - f: impl FnOnce(Size) -> Message + 'static, + id: Id, + f: impl FnOnce(Size) -> Message + 'static, ) -> Command { - Command::single(command::Action::Window(Action::FetchSize(Box::new(f)))) + Command::single(command::Action::Window(Action::FetchSize(id, Box::new(f)))) } /// Maximizes the window. -pub fn maximize(maximized: bool) -> Command { - Command::single(command::Action::Window(Action::Maximize(maximized))) +pub fn maximize(id: Id, maximized: bool) -> Command { + Command::single(command::Action::Window(Action::Maximize(id, maximized))) } -/// Minimes the window. -pub fn minimize(minimized: bool) -> Command { - Command::single(command::Action::Window(Action::Minimize(minimized))) +/// Minimizes the window. +pub fn minimize(id: Id, minimized: bool) -> Command { + Command::single(command::Action::Window(Action::Minimize(id, minimized))) } -/// Moves a window to the given logical coordinates. -pub fn move_to(x: i32, y: i32) -> Command { - Command::single(command::Action::Window(Action::Move { x, y })) +/// Moves the window to the given logical coordinates. +pub fn move_to(id: Id, position: Point) -> Command { + Command::single(command::Action::Window(Action::Move(id, position))) } /// Changes the [`Mode`] of the window. -pub fn change_mode(mode: Mode) -> Command { - Command::single(command::Action::Window(Action::ChangeMode(mode))) +pub fn change_mode(id: Id, mode: Mode) -> Command { + Command::single(command::Action::Window(Action::ChangeMode(id, mode))) } /// Fetches the current [`Mode`] of the window. pub fn fetch_mode( + id: Id, f: impl FnOnce(Mode) -> Message + 'static, ) -> Command { - Command::single(command::Action::Window(Action::FetchMode(Box::new(f)))) + Command::single(command::Action::Window(Action::FetchMode(id, Box::new(f)))) } /// Toggles the window to maximized or back. -pub fn toggle_maximize() -> Command { - Command::single(command::Action::Window(Action::ToggleMaximize)) +pub fn toggle_maximize(id: Id) -> Command { + Command::single(command::Action::Window(Action::ToggleMaximize(id))) } /// Toggles the window decorations. -pub fn toggle_decorations() -> Command { - Command::single(command::Action::Window(Action::ToggleDecorations)) +pub fn toggle_decorations(id: Id) -> Command { + Command::single(command::Action::Window(Action::ToggleDecorations(id))) } -/// Request user attention to the window, this has no effect if the application +/// Request user attention to the window. This has no effect if the application /// is already focused. How requesting for user attention manifests is platform dependent, /// see [`UserAttention`] for details. /// /// Providing `None` will unset the request for user attention. Unsetting the request for /// user attention might not be done automatically by the WM when the window receives input. pub fn request_user_attention( + id: Id, user_attention: Option, ) -> Command { Command::single(command::Action::Window(Action::RequestUserAttention( + id, user_attention, ))) } @@ -107,30 +125,36 @@ pub fn request_user_attention( /// This [`Command`] steals input focus from other applications. Do not use this method unless /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive /// user experience. -pub fn gain_focus() -> Command { - Command::single(command::Action::Window(Action::GainFocus)) +pub fn gain_focus(id: Id) -> Command { + Command::single(command::Action::Window(Action::GainFocus(id))) } /// Changes the window [`Level`]. -pub fn change_level(level: Level) -> Command { - Command::single(command::Action::Window(Action::ChangeLevel(level))) +pub fn change_level(id: Id, level: Level) -> Command { + Command::single(command::Action::Window(Action::ChangeLevel(id, level))) } -/// Fetches an identifier unique to the window. +/// Fetches an identifier unique to the window, provided by the underlying windowing system. This is +/// not to be confused with [`Id`]. pub fn fetch_id( + id: Id, f: impl FnOnce(u64) -> Message + 'static, ) -> Command { - Command::single(command::Action::Window(Action::FetchId(Box::new(f)))) + Command::single(command::Action::Window(Action::FetchId(id, Box::new(f)))) } /// Changes the [`Icon`] of the window. -pub fn change_icon(icon: Icon) -> Command { - Command::single(command::Action::Window(Action::ChangeIcon(icon))) +pub fn change_icon(id: Id, icon: Icon) -> Command { + Command::single(command::Action::Window(Action::ChangeIcon(id, icon))) } /// Captures a [`Screenshot`] from the window. pub fn screenshot( + id: Id, f: impl FnOnce(Screenshot) -> Message + Send + 'static, ) -> Command { - Command::single(command::Action::Window(Action::Screenshot(Box::new(f)))) + Command::single(command::Action::Window(Action::Screenshot( + id, + Box::new(f), + ))) } diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index b6964e36e3..2d98b60766 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -1,5 +1,5 @@ -use crate::core::window::{Icon, Level, Mode, UserAttention}; -use crate::core::Size; +use crate::core::window::{Icon, Id, Level, Mode, Settings, UserAttention}; +use crate::core::{Point, Size}; use crate::futures::MaybeSend; use crate::window::Screenshot; @@ -7,43 +7,40 @@ use std::fmt; /// An operation to be performed on some window. pub enum Action { - /// Close the current window and exits the application. - Close, + /// Spawns a new window with some [`Settings`]. + Spawn(Id, Settings), + /// Close the window and exits the application. + Close(Id), /// Move the window with the left mouse button until the button is /// released. /// /// There’s no guarantee that this will work unless the left mouse /// button was pressed immediately before this function is called. - Drag, - /// Resize the window. - Resize(Size), - /// Fetch the current size of the window. - FetchSize(Box) -> T + 'static>), + Drag(Id), + /// Resize the window to the given logical dimensions. + Resize(Id, Size), + /// Fetch the current logical dimensions of the window. + FetchSize(Id, Box T + 'static>), /// Set the window to maximized or back - Maximize(bool), + Maximize(Id, bool), /// Set the window to minimized or back - Minimize(bool), - /// Move the window. + Minimize(Id, bool), + /// Move the window to the given logical coordinates. /// /// Unsupported on Wayland. - Move { - /// The new logical x location of the window - x: i32, - /// The new logical y location of the window - y: i32, - }, + Move(Id, Point), /// Change the [`Mode`] of the window. - ChangeMode(Mode), + ChangeMode(Id, Mode), /// Fetch the current [`Mode`] of the window. - FetchMode(Box T + 'static>), + FetchMode(Id, Box T + 'static>), /// Toggle the window to maximized or back - ToggleMaximize, + ToggleMaximize(Id), /// Toggle whether window has decorations. /// /// ## Platform-specific /// - **X11:** Not implemented. /// - **Web:** Unsupported. - ToggleDecorations, + ToggleDecorations(Id), /// Request user attention to the window, this has no effect if the application /// is already focused. How requesting for user attention manifests is platform dependent, /// see [`UserAttention`] for details. @@ -57,7 +54,7 @@ pub enum Action { /// - **macOS:** `None` has no effect. /// - **X11:** Requests for user attention must be manually cleared. /// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect. - RequestUserAttention(Option), + RequestUserAttention(Id, Option), /// Bring the window to the front and sets input focus. Has no effect if the window is /// already in focus, minimized, or not visible. /// @@ -68,11 +65,11 @@ pub enum Action { /// ## Platform-specific /// /// - **Web / Wayland:** Unsupported. - GainFocus, + GainFocus(Id), /// Change the window [`Level`]. - ChangeLevel(Level), - /// Fetch an identifier unique to the window. - FetchId(Box T + 'static>), + ChangeLevel(Id, Level), + /// Fetch the raw identifier unique to the window. + FetchId(Id, Box T + 'static>), /// Change the window [`Icon`]. /// /// On Windows and X11, this is typically the small icon in the top-left @@ -87,9 +84,9 @@ pub enum Action { /// /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That /// said, it's usually in the same ballpark as on Windows. - ChangeIcon(Icon), + ChangeIcon(Id, Icon), /// Screenshot the viewport of the window. - Screenshot(Box T + 'static>), + Screenshot(Id, Box T + 'static>), } impl Action { @@ -102,29 +99,35 @@ impl Action { T: 'static, { match self { - Self::Close => Action::Close, - Self::Drag => Action::Drag, - Self::Resize(size) => Action::Resize(size), - Self::FetchSize(o) => Action::FetchSize(Box::new(move |s| f(o(s)))), - Self::Maximize(maximized) => Action::Maximize(maximized), - Self::Minimize(minimized) => Action::Minimize(minimized), - Self::Move { x, y } => Action::Move { x, y }, - Self::ChangeMode(mode) => Action::ChangeMode(mode), - Self::FetchMode(o) => Action::FetchMode(Box::new(move |s| f(o(s)))), - Self::ToggleMaximize => Action::ToggleMaximize, - Self::ToggleDecorations => Action::ToggleDecorations, - Self::RequestUserAttention(attention_type) => { - Action::RequestUserAttention(attention_type) + Self::Spawn(id, settings) => Action::Spawn(id, settings), + Self::Close(id) => Action::Close(id), + Self::Drag(id) => Action::Drag(id), + Self::Resize(id, size) => Action::Resize(id, size), + Self::FetchSize(id, o) => { + Action::FetchSize(id, Box::new(move |s| f(o(s)))) } - Self::GainFocus => Action::GainFocus, - Self::ChangeLevel(level) => Action::ChangeLevel(level), - Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))), - Self::ChangeIcon(icon) => Action::ChangeIcon(icon), - Self::Screenshot(tag) => { - Action::Screenshot(Box::new(move |screenshot| { - f(tag(screenshot)) - })) + Self::Maximize(id, maximized) => Action::Maximize(id, maximized), + Self::Minimize(id, minimized) => Action::Minimize(id, minimized), + Self::Move(id, position) => Action::Move(id, position), + Self::ChangeMode(id, mode) => Action::ChangeMode(id, mode), + Self::FetchMode(id, o) => { + Action::FetchMode(id, Box::new(move |s| f(o(s)))) } + Self::ToggleMaximize(id) => Action::ToggleMaximize(id), + Self::ToggleDecorations(id) => Action::ToggleDecorations(id), + Self::RequestUserAttention(id, attention_type) => { + Action::RequestUserAttention(id, attention_type) + } + Self::GainFocus(id) => Action::GainFocus(id), + Self::ChangeLevel(id, level) => Action::ChangeLevel(id, level), + Self::FetchId(id, o) => { + Action::FetchId(id, Box::new(move |s| f(o(s)))) + } + Self::ChangeIcon(id, icon) => Action::ChangeIcon(id, icon), + Self::Screenshot(id, tag) => Action::Screenshot( + id, + Box::new(move |screenshot| f(tag(screenshot))), + ), } } } @@ -132,35 +135,46 @@ impl Action { impl fmt::Debug for Action { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Close => write!(f, "Action::Close"), - Self::Drag => write!(f, "Action::Drag"), - Self::Resize(size) => write!(f, "Action::Resize({size:?})"), - Self::FetchSize(_) => write!(f, "Action::FetchSize"), - Self::Maximize(maximized) => { - write!(f, "Action::Maximize({maximized})") + Self::Spawn(id, settings) => { + write!(f, "Action::Spawn({id:?}, {settings:?})") + } + Self::Close(id) => write!(f, "Action::Close({id:?})"), + Self::Drag(id) => write!(f, "Action::Drag({id:?})"), + Self::Resize(id, size) => { + write!(f, "Action::Resize({id:?}, {size:?})") + } + Self::FetchSize(id, _) => write!(f, "Action::FetchSize({id:?})"), + Self::Maximize(id, maximized) => { + write!(f, "Action::Maximize({id:?}, {maximized})") + } + Self::Minimize(id, minimized) => { + write!(f, "Action::Minimize({id:?}, {minimized}") + } + Self::Move(id, position) => { + write!(f, "Action::Move({id:?}, {position})") + } + Self::ChangeMode(id, mode) => { + write!(f, "Action::SetMode({id:?}, {mode:?})") } - Self::Minimize(minimized) => { - write!(f, "Action::Minimize({minimized}") + Self::FetchMode(id, _) => write!(f, "Action::FetchMode({id:?})"), + Self::ToggleMaximize(id) => { + write!(f, "Action::ToggleMaximize({id:?})") } - Self::Move { x, y } => { - write!(f, "Action::Move {{ x: {x}, y: {y} }}") + Self::ToggleDecorations(id) => { + write!(f, "Action::ToggleDecorations({id:?})") } - Self::ChangeMode(mode) => write!(f, "Action::SetMode({mode:?})"), - Self::FetchMode(_) => write!(f, "Action::FetchMode"), - Self::ToggleMaximize => write!(f, "Action::ToggleMaximize"), - Self::ToggleDecorations => write!(f, "Action::ToggleDecorations"), - Self::RequestUserAttention(_) => { - write!(f, "Action::RequestUserAttention") + Self::RequestUserAttention(id, _) => { + write!(f, "Action::RequestUserAttention({id:?})") } - Self::GainFocus => write!(f, "Action::GainFocus"), - Self::ChangeLevel(level) => { - write!(f, "Action::ChangeLevel({level:?})") + Self::GainFocus(id) => write!(f, "Action::GainFocus({id:?})"), + Self::ChangeLevel(id, level) => { + write!(f, "Action::ChangeLevel({id:?}, {level:?})") } - Self::FetchId(_) => write!(f, "Action::FetchId"), - Self::ChangeIcon(_icon) => { - write!(f, "Action::ChangeIcon(icon)") + Self::FetchId(id, _) => write!(f, "Action::FetchId({id:?})"), + Self::ChangeIcon(id, _icon) => { + write!(f, "Action::ChangeIcon({id:?})") } - Self::Screenshot(_) => write!(f, "Action::Screenshot"), + Self::Screenshot(id, _) => write!(f, "Action::Screenshot({id:?})"), } } } diff --git a/src/lib.rs b/src/lib.rs index 47766e6fd1..002d2a79c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,6 +182,9 @@ pub mod window; #[cfg(feature = "advanced")] pub mod advanced; +#[cfg(feature = "multi-window")] +pub mod multi_window; + pub use style::theme; pub use crate::core::alignment; diff --git a/src/multi_window.rs b/src/multi_window.rs new file mode 100644 index 0000000000..5b7a00b4db --- /dev/null +++ b/src/multi_window.rs @@ -0,0 +1,4 @@ +//! Leverage multi-window support in your application. +mod application; + +pub use application::Application; diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs new file mode 100644 index 0000000000..4a91bdf4f1 --- /dev/null +++ b/src/multi_window/application.rs @@ -0,0 +1,245 @@ +use crate::style::application::StyleSheet; +use crate::window; +use crate::{Command, Element, Executor, Settings, Subscription}; + +/// An interactive cross-platform multi-window application. +/// +/// This trait is the main entrypoint of Iced. Once implemented, you can run +/// your GUI application by simply calling [`run`](#method.run). +/// +/// - On native platforms, it will run in its own windows. +/// - On the web, it will take control of the `` and the `<body>` of the +/// document and display only the contents of the `window::Id::MAIN` window. +/// +/// An [`Application`] can execute asynchronous actions by returning a +/// [`Command`] in some of its methods. If you do not intend to perform any +/// background work in your program, the [`Sandbox`] trait offers a simplified +/// interface. +/// +/// When using an [`Application`] with the `debug` feature enabled, a debug view +/// can be toggled by pressing `F12`. +/// +/// # Examples +/// See the `examples/multi-window` example to see this multi-window `Application` trait in action. +/// +/// ## A simple "Hello, world!" +/// +/// If you just want to get started, here is a simple [`Application`] that +/// says "Hello, world!": +/// +/// ```no_run +/// use iced::{executor, window}; +/// use iced::{Command, Element, Settings, Theme}; +/// use iced::multi_window::{self, Application}; +/// +/// pub fn main() -> iced::Result { +/// Hello::run(Settings::default()) +/// } +/// +/// struct Hello; +/// +/// impl multi_window::Application for Hello { +/// type Executor = executor::Default; +/// type Flags = (); +/// type Message = (); +/// type Theme = Theme; +/// +/// fn new(_flags: ()) -> (Hello, Command<Self::Message>) { +/// (Hello, Command::none()) +/// } +/// +/// fn title(&self, _window: window::Id) -> String { +/// String::from("A cool application") +/// } +/// +/// fn update(&mut self, _message: Self::Message) -> Command<Self::Message> { +/// Command::none() +/// } +/// +/// fn view(&self, _window: window::Id) -> Element<Self::Message> { +/// "Hello, world!".into() +/// } +/// } +/// ``` +/// +/// [`Sandbox`]: crate::Sandbox +pub trait Application: Sized { + /// The [`Executor`] that will run commands and subscriptions. + /// + /// The [default executor] can be a good starting point! + /// + /// [`Executor`]: Self::Executor + /// [default executor]: crate::executor::Default + type Executor: Executor; + + /// The type of __messages__ your [`Application`] will produce. + type Message: std::fmt::Debug + Send; + + /// The theme of your [`Application`]. + type Theme: Default + 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. + /// + /// [`run`]: Self::run + fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); + + /// Returns the current title of the `window` of the [`Application`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your window when necessary. + fn title(&self, window: window::Id) -> String; + + /// Handles a __message__ and updates the state of the [`Application`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Command`] returned will be executed immediately in the background. + fn update(&mut self, message: Self::Message) -> Command<Self::Message>; + + /// Returns the widgets to display in the `window` of the [`Application`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, crate::Renderer<Self::Theme>>; + + /// Returns the current [`Theme`] of the `window` of the [`Application`]. + /// + /// [`Theme`]: Self::Theme + #[allow(unused_variables)] + fn theme(&self, window: window::Id) -> Self::Theme { + Self::Theme::default() + } + + /// Returns the current `Style` of the [`Theme`]. + /// + /// [`Theme`]: Self::Theme + fn style(&self) -> <Self::Theme as StyleSheet>::Style { + <Self::Theme as StyleSheet>::Style::default() + } + + /// Returns the event [`Subscription`] for the current state of the + /// application. + /// + /// A [`Subscription`] will be kept alive as long as you keep returning it, + /// and the __messages__ produced will be handled by + /// [`update`](#tymethod.update). + /// + /// By default, this method 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 the multi-window [`Application`]. + /// + /// On native platforms, this method will take control of the current thread + /// until the [`Application`] exits. + /// + /// On the web platform, this method __will NOT return__ unless there is an + /// [`Error`] during startup. + /// + /// [`Error`]: crate::Error + fn run(settings: Settings<Self::Flags>) -> crate::Result + where + Self: 'static, + { + #[allow(clippy::needless_update)] + let renderer_settings = crate::renderer::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: if settings.antialiasing { + Some(crate::graphics::Antialiasing::MSAAx4) + } else { + None + }, + ..crate::renderer::Settings::default() + }; + + Ok(crate::shell::multi_window::run::< + Instance<Self>, + Self::Executor, + crate::renderer::Compositor<Self::Theme>, + >(settings.into(), renderer_settings)?) + } +} + +struct Instance<A: Application>(A); + +impl<A> crate::runtime::multi_window::Program for Instance<A> +where + A: Application, +{ + type Renderer = crate::Renderer<A::Theme>; + type Message = A::Message; + + fn update(&mut self, message: Self::Message) -> Command<Self::Message> { + self.0.update(message) + } + + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, Self::Renderer> { + self.0.view(window) + } +} + +impl<A> crate::shell::multi_window::Application for Instance<A> +where + A: Application, +{ + type Flags = A::Flags; + + fn new(flags: Self::Flags) -> (Self, Command<A::Message>) { + let (app, command) = A::new(flags); + + (Instance(app), command) + } + + fn title(&self, window: window::Id) -> String { + self.0.title(window) + } + + fn theme(&self, window: window::Id) -> A::Theme { + self.0.theme(window) + } + + fn style(&self) -> <A::Theme as StyleSheet>::Style { + self.0.style() + } + + fn subscription(&self) -> Subscription<Self::Message> { + self.0.subscription() + } + + fn scale_factor(&self, window: window::Id) -> f64 { + self.0.scale_factor(window) + } +} diff --git a/src/settings.rs b/src/settings.rs index 42df31e468..d9476b614e 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -46,14 +46,6 @@ pub struct Settings<Flags> { /// /// [`Canvas`]: crate::widget::Canvas pub antialiasing: bool, - - /// Whether the [`Application`] should exit when the user requests the - /// window to close (e.g. the user presses the close button). - /// - /// By default, it is enabled. - /// - /// [`Application`]: crate::Application - pub exit_on_close_request: bool, } impl<Flags> Settings<Flags> { @@ -71,7 +63,6 @@ impl<Flags> Settings<Flags> { default_font: default_settings.default_font, default_text_size: default_settings.default_text_size, antialiasing: default_settings.antialiasing, - exit_on_close_request: default_settings.exit_on_close_request, } } } @@ -89,7 +80,6 @@ where default_font: Font::default(), default_text_size: Pixels(16.0), antialiasing: false, - exit_on_close_request: true, } } } @@ -98,10 +88,9 @@ impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> { fn from(settings: Settings<Flags>) -> iced_winit::Settings<Flags> { iced_winit::Settings { id: settings.id, - window: settings.window.into(), + window: settings.window, flags: settings.flags, fonts: settings.fonts, - exit_on_close_request: settings.exit_on_close_request, } } } diff --git a/src/window.rs b/src/window.rs index e46015750c..9f96da5245 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,12 +1,8 @@ //! Configure the window of your application in native platforms. -mod position; -mod settings; pub mod icon; pub use icon::Icon; -pub use position::Position; -pub use settings::{PlatformSpecific, Settings}; pub use crate::core::window::*; pub use crate::runtime::window::*; diff --git a/src/window/position.rs b/src/window/position.rs deleted file mode 100644 index 6b9fac417b..0000000000 --- a/src/window/position.rs +++ /dev/null @@ -1,32 +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 - } -} - -impl From<Position> for iced_winit::Position { - fn from(position: Position) -> Self { - match position { - Position::Default => Self::Default, - Position::Centered => Self::Centered, - Position::Specific(x, y) => Self::Specific(x, y), - } - } -} diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 828e522f23..87ded74640 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -8,6 +8,7 @@ use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use std::marker::PhantomData; pub struct Compositor<Theme> { + settings: Settings, _theme: PhantomData<Theme>, } @@ -27,17 +28,16 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( settings: Self::Settings, _compatible_window: Option<&W>, - ) -> Result<(Self, Self::Renderer), Error> { - let (compositor, backend) = new(); + ) -> Result<Self, Error> { + Ok(new(settings)) + } - Ok(( - compositor, - Renderer::new( - backend, - settings.default_font, - settings.default_text_size, - ), - )) + fn create_renderer(&self) -> Self::Renderer { + Renderer::new( + Backend::new(), + self.settings.default_font, + self.settings.default_text_size, + ) } fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( @@ -121,13 +121,11 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { } } -pub fn new<Theme>() -> (Compositor<Theme>, Backend) { - ( - Compositor { - _theme: PhantomData, - }, - Backend::new(), - ) +pub fn new<Theme>(settings: Settings) -> Compositor<Theme> { + Compositor { + settings, + _theme: PhantomData, + } } pub fn present<T: AsRef<str>>( diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 90d64e172e..090e0e9f40 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -139,16 +139,14 @@ impl<Theme> Compositor<Theme> { pub fn new<Theme, W: HasRawWindowHandle + HasRawDisplayHandle>( settings: Settings, compatible_window: Option<&W>, -) -> Result<(Compositor<Theme>, Backend), Error> { +) -> Result<Compositor<Theme>, Error> { let compositor = futures::executor::block_on(Compositor::request( settings, compatible_window, )) .ok_or(Error::GraphicsAdapterNotFound)?; - let backend = compositor.create_backend(); - - Ok((compositor, backend)) + Ok(compositor) } /// Presents the given primitives with the given [`Compositor`] and [`Backend`]. @@ -214,17 +212,16 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( settings: Self::Settings, compatible_window: Option<&W>, - ) -> Result<(Self, Self::Renderer), Error> { - let (compositor, backend) = new(settings, compatible_window)?; + ) -> Result<Self, Error> { + new(settings, compatible_window) + } - Ok(( - compositor, - Renderer::new( - backend, - settings.default_font, - settings.default_text_size, - ), - )) + fn create_renderer(&self) -> Self::Renderer { + Renderer::new( + self.create_backend(), + self.settings.default_font, + self.settings.default_text_size, + ) } fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( diff --git a/widget/src/shader.rs b/widget/src/shader.rs index fe6214db52..8e3346938b 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -109,7 +109,7 @@ where Some(Event::Keyboard(keyboard_event)) } core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), - core::Event::Window(window::Event::RedrawRequested(instant)) => { + core::Event::Window(_, window::Event::RedrawRequested(instant)) => { Some(Event::RedrawRequested(instant)) } _ => None, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 27efe755c9..f168874603 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -1000,14 +1000,14 @@ where state.keyboard_modifiers = modifiers; } - Event::Window(window::Event::Unfocused) => { + Event::Window(_, window::Event::Unfocused) => { let state = state(); if let Some(focus) = &mut state.is_focused { focus.is_window_focused = false; } } - Event::Window(window::Event::Focused) => { + Event::Window(_, window::Event::Focused) => { let state = state(); if let Some(focus) = &mut state.is_focused { @@ -1017,7 +1017,7 @@ where shell.request_redraw(window::RedrawRequest::NextFrame); } } - Event::Window(window::Event::RedrawRequested(now)) => { + Event::Window(_, window::Event::RedrawRequested(now)) => { let state = state(); if let Some(focus) = &mut state.is_focused { diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 674a66d3e1..87e600aeba 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 2c5c864ac0..d970007573 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 7031507a21..0000000000 --- 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 3ecd044c8a..7e51a2d400 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 95b55bb990..948576a28a 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 0000000000..84651d40de --- /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 0000000000..03da5ad751 --- /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 0000000000..d54156e767 --- /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/settings.rs b/winit/src/settings.rs index 599f33f19a..2e541128b5 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(), - } - } }