From 341c9a3c12aa9d327ef1d8f168ea0adb9b5ad10b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 19 Jun 2024 01:53:40 +0200 Subject: Introduce `daemon` API and unify shell runtimes --- Cargo.toml | 2 +- examples/arc/src/main.rs | 2 +- examples/bezier_tool/src/main.rs | 2 +- examples/checkbox/src/main.rs | 2 +- examples/clock/src/main.rs | 2 +- examples/color_palette/src/main.rs | 2 +- examples/custom_shader/src/main.rs | 10 +- examples/download_progress/src/main.rs | 10 +- examples/editor/src/main.rs | 2 +- examples/events/src/main.rs | 2 +- examples/exit/src/main.rs | 2 +- examples/ferris/src/main.rs | 2 +- examples/game_of_life/src/main.rs | 16 +- examples/gradient/src/main.rs | 10 +- examples/layout/src/main.rs | 2 +- examples/loading_spinners/src/main.rs | 2 +- examples/modal/src/main.rs | 2 +- examples/multi_window/src/main.rs | 39 +- examples/multitouch/src/main.rs | 2 +- examples/pane_grid/src/main.rs | 2 +- examples/pokedex/src/main.rs | 2 +- examples/qr_code/src/main.rs | 2 +- examples/screenshot/src/main.rs | 2 +- examples/scrollable/src/main.rs | 2 +- examples/sierpinski_triangle/src/main.rs | 2 +- examples/solar_system/src/main.rs | 2 +- examples/stopwatch/src/main.rs | 2 +- examples/styling/src/main.rs | 2 +- examples/system_information/src/main.rs | 8 +- examples/the_matrix/src/main.rs | 2 +- examples/toast/src/main.rs | 2 +- examples/todos/src/main.rs | 2 +- examples/tour/src/main.rs | 2 +- examples/url_handler/src/main.rs | 2 +- examples/vectorial_text/src/main.rs | 2 +- examples/visible_bounds/src/main.rs | 2 +- examples/websocket/src/main.rs | 2 +- runtime/src/lib.rs | 16 + src/advanced.rs | 1 - src/application.rs | 590 ++++++++----- src/daemon.rs | 298 +++++++ src/lib.rs | 33 +- src/multi_window.rs | 254 ------ src/program.rs | 754 ++++++----------- src/settings.rs | 49 +- winit/Cargo.toml | 2 +- winit/src/application.rs | 1082 ------------------------ winit/src/application/state.rs | 221 ----- winit/src/lib.rs | 15 +- winit/src/multi_window.rs | 1322 ----------------------------- winit/src/multi_window/state.rs | 239 ------ winit/src/multi_window/window_manager.rs | 157 ---- winit/src/program.rs | 1356 ++++++++++++++++++++++++++++++ winit/src/program/state.rs | 239 ++++++ winit/src/program/window_manager.rs | 157 ++++ winit/src/proxy.rs | 13 +- winit/src/settings.rs | 12 +- 57 files changed, 2820 insertions(+), 4145 deletions(-) create mode 100644 src/daemon.rs delete mode 100644 src/multi_window.rs delete mode 100644 winit/src/application.rs delete mode 100644 winit/src/application/state.rs delete mode 100644 winit/src/multi_window.rs delete mode 100644 winit/src/multi_window/state.rs delete mode 100644 winit/src/multi_window/window_manager.rs create mode 100644 winit/src/program.rs create mode 100644 winit/src/program/state.rs create mode 100644 winit/src/program/window_manager.rs diff --git a/Cargo.toml b/Cargo.toml index 44b3d307..b85900cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,7 @@ iced_core.workspace = true iced_futures.workspace = true iced_renderer.workspace = true iced_widget.workspace = true -iced_winit.features = ["application"] +iced_winit.features = ["program"] iced_winit.workspace = true iced_highlighter.workspace = true diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index 4576404f..b1e8402a 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -7,7 +7,7 @@ use iced::widget::canvas::{ use iced::{Element, Length, Point, Rectangle, Renderer, Subscription, Theme}; pub fn main() -> iced::Result { - iced::program("Arc - Iced", Arc::update, Arc::view) + iced::application("Arc - Iced", Arc::update, Arc::view) .subscription(Arc::subscription) .theme(|_| Theme::Dark) .antialiasing(true) diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 29df3eeb..eaf84b97 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{button, container, horizontal_space, hover}; use iced::{Element, Length, Theme}; pub fn main() -> iced::Result { - iced::program("Bezier Tool - Iced", Example::update, Example::view) + iced::application("Bezier Tool - Iced", Example::update, Example::view) .theme(|_| Theme::CatppuccinMocha) .antialiasing(true) .run() diff --git a/examples/checkbox/src/main.rs b/examples/checkbox/src/main.rs index bec4a954..f06557f8 100644 --- a/examples/checkbox/src/main.rs +++ b/examples/checkbox/src/main.rs @@ -4,7 +4,7 @@ use iced::{Element, Font}; const ICON_FONT: Font = Font::with_name("icons"); pub fn main() -> iced::Result { - iced::program("Checkbox - Iced", Example::update, Example::view) + iced::application("Checkbox - Iced", Example::update, Example::view) .font(include_bytes!("../fonts/icons.ttf").as_slice()) .run() } diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 7c4685c4..4584a0c7 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -11,7 +11,7 @@ use iced::{ pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::program("Clock - Iced", Clock::update, Clock::view) + iced::application("Clock - Iced", Clock::update, Clock::view) .subscription(Clock::subscription) .theme(Clock::theme) .antialiasing(true) diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index d9325edb..e4b19731 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -11,7 +11,7 @@ use std::marker::PhantomData; use std::ops::RangeInclusive; pub fn main() -> iced::Result { - iced::program( + iced::application( "Color Palette - Iced", ColorPalette::update, ColorPalette::view, diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index 463b2df9..b04a8183 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -9,9 +9,13 @@ use iced::window; use iced::{Alignment, Color, Element, Length, Subscription}; fn main() -> iced::Result { - iced::program("Custom Shader - Iced", IcedCubes::update, IcedCubes::view) - .subscription(IcedCubes::subscription) - .run() + iced::application( + "Custom Shader - Iced", + IcedCubes::update, + IcedCubes::view, + ) + .subscription(IcedCubes::subscription) + .run() } struct IcedCubes { diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index 7974d5a0..d91e5eab 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -4,9 +4,13 @@ use iced::widget::{button, center, column, progress_bar, text, Column}; use iced::{Alignment, Element, Subscription}; pub fn main() -> iced::Result { - iced::program("Download Progress - Iced", Example::update, Example::view) - .subscription(Example::subscription) - .run() + iced::application( + "Download Progress - Iced", + Example::update, + Example::view, + ) + .subscription(Example::subscription) + .run() } #[derive(Debug)] diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index ec65e2fa..bed9d94a 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -12,7 +12,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; pub fn main() -> iced::Result { - iced::program("Editor - Iced", Editor::update, Editor::view) + iced::application("Editor - Iced", Editor::update, Editor::view) .load(Editor::load) .subscription(Editor::subscription) .theme(Editor::theme) diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 504ed5d8..4f0f07b0 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -5,7 +5,7 @@ use iced::window; use iced::{Alignment, Element, Length, Subscription, Task}; pub fn main() -> iced::Result { - iced::program("Events - Iced", Events::update, Events::view) + iced::application("Events - Iced", Events::update, Events::view) .subscription(Events::subscription) .exit_on_close_request(false) .run() diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index 8ba180a5..b998016e 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -3,7 +3,7 @@ use iced::window; use iced::{Alignment, Element, Task}; pub fn main() -> iced::Result { - iced::program("Exit - Iced", Exit::update, Exit::view).run() + iced::application("Exit - Iced", Exit::update, Exit::view).run() } #[derive(Default)] diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs index 0400c376..88006898 100644 --- a/examples/ferris/src/main.rs +++ b/examples/ferris/src/main.rs @@ -9,7 +9,7 @@ use iced::{ }; pub fn main() -> iced::Result { - iced::program("Ferris - Iced", Image::update, Image::view) + iced::application("Ferris - Iced", Image::update, Image::view) .subscription(Image::subscription) .theme(|_| Theme::TokyoNight) .run() diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 7e6d461d..421f862a 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -15,12 +15,16 @@ use std::time::Duration; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::program("Game of Life - Iced", GameOfLife::update, GameOfLife::view) - .subscription(GameOfLife::subscription) - .theme(|_| Theme::Dark) - .antialiasing(true) - .centered() - .run() + iced::application( + "Game of Life - Iced", + GameOfLife::update, + GameOfLife::view, + ) + .subscription(GameOfLife::subscription) + .theme(|_| Theme::Dark) + .antialiasing(true) + .centered() + .run() } struct GameOfLife { diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs index 2b906c32..e5b19443 100644 --- a/examples/gradient/src/main.rs +++ b/examples/gradient/src/main.rs @@ -1,5 +1,5 @@ +use iced::application; use iced::gradient; -use iced::program; use iced::widget::{ checkbox, column, container, horizontal_space, row, slider, text, }; @@ -8,7 +8,7 @@ use iced::{Alignment, Color, Element, Length, Radians, Theme}; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::program("Gradient - Iced", Gradient::update, Gradient::view) + iced::application("Gradient - Iced", Gradient::update, Gradient::view) .style(Gradient::style) .transparent(true) .run() @@ -95,11 +95,11 @@ impl Gradient { .into() } - fn style(&self, theme: &Theme) -> program::Appearance { - use program::DefaultStyle; + fn style(&self, theme: &Theme) -> application::Appearance { + use application::DefaultStyle; if self.transparent { - program::Appearance { + application::Appearance { background_color: Color::TRANSPARENT, text_color: theme.palette().text, } diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index c40ac820..2e774415 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -10,7 +10,7 @@ use iced::{ }; pub fn main() -> iced::Result { - iced::program(Layout::title, Layout::update, Layout::view) + iced::application(Layout::title, Layout::update, Layout::view) .subscription(Layout::subscription) .theme(Layout::theme) .run() diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs index a63c51d4..503f2d7a 100644 --- a/examples/loading_spinners/src/main.rs +++ b/examples/loading_spinners/src/main.rs @@ -11,7 +11,7 @@ use circular::Circular; use linear::Linear; pub fn main() -> iced::Result { - iced::program( + iced::application( "Loading Spinners - Iced", LoadingSpinners::update, LoadingSpinners::view, diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index d185cf3b..413485e7 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -10,7 +10,7 @@ use iced::{Alignment, Color, Element, Length, Subscription, Task}; use std::fmt; pub fn main() -> iced::Result { - iced::program("Modal - Iced", App::update, App::view) + iced::application("Modal - Iced", App::update, App::view) .subscription(App::subscription) .run() } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index b82ad1f3..dfb816cf 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -1,18 +1,21 @@ -use iced::executor; -use iced::multi_window::{self, Application}; use iced::widget::{ button, center, column, container, horizontal_space, scrollable, text, text_input, }; use iced::window; -use iced::{ - Alignment, Element, Length, Settings, Subscription, Task, Theme, Vector, -}; +use iced::{Alignment, Element, Length, Subscription, Task, Theme, Vector}; use std::collections::BTreeMap; fn main() -> iced::Result { - Example::run(Settings::default()) + iced::daemon(Example::title, Example::update, Example::view) + .load(|| { + window::open(window::Settings::default()).map(Message::WindowOpened) + }) + .subscription(Example::subscription) + .theme(Example::theme) + .scale_factor(Example::scale_factor) + .run() } #[derive(Default)] @@ -39,21 +42,7 @@ enum Message { TitleChanged(window::Id, String), } -impl multi_window::Application for Example { - type Executor = executor::Default; - type Message = Message; - type Theme = Theme; - type Flags = (); - - fn new(_flags: ()) -> (Self, Task) { - ( - Example { - windows: BTreeMap::from([(window::Id::MAIN, Window::new(1))]), - }, - Task::none(), - ) - } - +impl Example { fn title(&self, window: window::Id) -> String { self.windows .get(&window) @@ -97,7 +86,11 @@ impl multi_window::Application for Example { Message::WindowClosed(id) => { self.windows.remove(&id); - Task::none() + if self.windows.is_empty() { + iced::exit() + } else { + Task::none() + } } Message::ScaleInputChanged(id, scale) => { if let Some(window) = self.windows.get_mut(&id) { @@ -149,7 +142,7 @@ impl multi_window::Application for Example { .unwrap_or(1.0) } - fn subscription(&self) -> Subscription { + fn subscription(&self) -> Subscription { window::close_events().map(Message::WindowClosed) } } diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index 2453c7f5..69717310 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -13,7 +13,7 @@ use std::collections::HashMap; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::program("Multitouch - Iced", Multitouch::update, Multitouch::view) + iced::application("Multitouch - Iced", Multitouch::update, Multitouch::view) .antialiasing(true) .centered() .run() diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index 6b5bd332..db9f7a05 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -7,7 +7,7 @@ use iced::widget::{ use iced::{Color, Element, Length, Size, Subscription}; pub fn main() -> iced::Result { - iced::program("Pane Grid - Iced", Example::update, Example::view) + iced::application("Pane Grid - Iced", Example::update, Example::view) .subscription(Example::subscription) .run() } diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index e62ed70b..b22ffe7f 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::{self, center, column, image, row, text}; use iced::{Alignment, Element, Length, Task}; pub fn main() -> iced::Result { - iced::program(Pokedex::title, Pokedex::update, Pokedex::view) + iced::application(Pokedex::title, Pokedex::update, Pokedex::view) .load(Pokedex::search) .run() } diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs index c6a90458..b30ecf15 100644 --- a/examples/qr_code/src/main.rs +++ b/examples/qr_code/src/main.rs @@ -2,7 +2,7 @@ use iced::widget::{center, column, pick_list, qr_code, row, text, text_input}; use iced::{Alignment, Element, Theme}; pub fn main() -> iced::Result { - iced::program( + iced::application( "QR Code Generator - Iced", QRGenerator::update, QRGenerator::view, diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 78d3e9ff..1ea53e8f 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -13,7 +13,7 @@ use ::image::ColorType; fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::program("Screenshot - Iced", Example::update, Example::view) + iced::application("Screenshot - Iced", Example::update, Example::view) .subscription(Example::subscription) .run() } diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index a0dcf82c..f2a853e1 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -10,7 +10,7 @@ use once_cell::sync::Lazy; static SCROLLABLE_ID: Lazy = Lazy::new(scrollable::Id::unique); pub fn main() -> iced::Result { - iced::program( + iced::application( "Scrollable - Iced", ScrollableDemo::update, ScrollableDemo::view, diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 7dd7be5e..4c751937 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -8,7 +8,7 @@ use rand::Rng; use std::fmt::Debug; fn main() -> iced::Result { - iced::program( + iced::application( "Sierpinski Triangle - Iced", SierpinskiEmulator::update, SierpinskiEmulator::view, diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index deb211d8..2a67e23e 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -22,7 +22,7 @@ use std::time::Instant; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::program( + iced::application( "Solar System - Iced", SolarSystem::update, SolarSystem::view, diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index a8149753..bd56785a 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -7,7 +7,7 @@ use iced::{Alignment, Element, Subscription, Theme}; use std::time::{Duration, Instant}; pub fn main() -> iced::Result { - iced::program("Stopwatch - Iced", Stopwatch::update, Stopwatch::view) + iced::application("Stopwatch - Iced", Stopwatch::update, Stopwatch::view) .subscription(Stopwatch::subscription) .theme(Stopwatch::theme) .run() diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 57e8f47e..3124493b 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -6,7 +6,7 @@ use iced::widget::{ use iced::{Alignment, Element, Length, Theme}; pub fn main() -> iced::Result { - iced::program("Styling - Iced", Styling::update, Styling::view) + iced::application("Styling - Iced", Styling::update, Styling::view) .theme(Styling::theme) .run() } diff --git a/examples/system_information/src/main.rs b/examples/system_information/src/main.rs index e2808edd..363df590 100644 --- a/examples/system_information/src/main.rs +++ b/examples/system_information/src/main.rs @@ -2,8 +2,12 @@ use iced::widget::{button, center, column, text}; use iced::{system, Element, Task}; pub fn main() -> iced::Result { - iced::program("System Information - Iced", Example::update, Example::view) - .run() + iced::application( + "System Information - Iced", + Example::update, + Example::view, + ) + .run() } #[derive(Default)] diff --git a/examples/the_matrix/src/main.rs b/examples/the_matrix/src/main.rs index f3a67ac8..2ae1cc3a 100644 --- a/examples/the_matrix/src/main.rs +++ b/examples/the_matrix/src/main.rs @@ -11,7 +11,7 @@ use std::cell::RefCell; pub fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::program("The Matrix - Iced", TheMatrix::update, TheMatrix::view) + iced::application("The Matrix - Iced", TheMatrix::update, TheMatrix::view) .subscription(TheMatrix::subscription) .antialiasing(true) .run() diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index aee2479e..232133b1 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -9,7 +9,7 @@ use iced::{Alignment, Element, Length, Subscription, Task}; use toast::{Status, Toast}; pub fn main() -> iced::Result { - iced::program("Toast - Iced", App::update, App::view) + iced::application("Toast - Iced", App::update, App::view) .subscription(App::subscription) .run() } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index c21e1a96..a834c946 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -17,7 +17,7 @@ pub fn main() -> iced::Result { #[cfg(not(target_arch = "wasm32"))] tracing_subscriber::fmt::init(); - iced::program(Todos::title, Todos::update, Todos::view) + iced::application(Todos::title, Todos::update, Todos::view) .load(Todos::load) .subscription(Todos::subscription) .font(include_bytes!("../fonts/icons.ttf").as_slice()) diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 78086ce9..94ba78ee 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -16,7 +16,7 @@ pub fn main() -> iced::Result { #[cfg(not(target_arch = "wasm32"))] tracing_subscriber::fmt::init(); - iced::program(Tour::title, Tour::update, Tour::view) + iced::application(Tour::title, Tour::update, Tour::view) .centered() .run() } diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs index 3ab19252..50a055f3 100644 --- a/examples/url_handler/src/main.rs +++ b/examples/url_handler/src/main.rs @@ -3,7 +3,7 @@ use iced::widget::{center, text}; use iced::{Element, Subscription}; pub fn main() -> iced::Result { - iced::program("URL Handler - Iced", App::update, App::view) + iced::application("URL Handler - Iced", App::update, App::view) .subscription(App::subscription) .run() } diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index 1ed7a2b1..6dd3273a 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -6,7 +6,7 @@ use iced::widget::{ use iced::{Element, Length, Point, Rectangle, Renderer, Theme, Vector}; pub fn main() -> iced::Result { - iced::program( + iced::application( "Vectorial Text - Iced", VectorialText::update, VectorialText::view, diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index b43c0cca..e46d1ff0 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -10,7 +10,7 @@ use iced::{ }; pub fn main() -> iced::Result { - iced::program("Visible Bounds - Iced", Example::update, Example::view) + iced::application("Visible Bounds - Iced", Example::update, Example::view) .subscription(Example::subscription) .theme(|_| Theme::Dark) .run() diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 8c0fa1d0..8422ce16 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -8,7 +8,7 @@ use iced::{color, Element, Length, Subscription, Task}; use once_cell::sync::Lazy; pub fn main() -> iced::Result { - iced::program("WebSocket - Iced", WebSocket::update, WebSocket::view) + iced::application("WebSocket - Iced", WebSocket::update, WebSocket::view) .load(WebSocket::load) .subscription(WebSocket::subscription) .run() diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5fde3039..b4a5e819 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -70,6 +70,12 @@ pub enum Action { /// Run a system action. System(system::Action), + + /// Exits the runtime. + /// + /// This will normally close any application windows and + /// terminate the runtime loop. + Exit, } impl Action { @@ -88,6 +94,7 @@ impl Action { Action::Clipboard(action) => Err(Action::Clipboard(action)), Action::Window(action) => Err(Action::Window(action)), Action::System(action) => Err(Action::System(action)), + Action::Exit => Err(Action::Exit), } } } @@ -110,6 +117,15 @@ where } Action::Window(_) => write!(f, "Action::Window"), Action::System(action) => write!(f, "Action::System({action:?})"), + Action::Exit => write!(f, "Action::Exit"), } } } + +/// Creates a [`Task`] that exits the iced runtime. +/// +/// This will normally close any application windows and +/// terminate the runtime loop. +pub fn exit() -> Task { + Task::effect(Action::Exit) +} diff --git a/src/advanced.rs b/src/advanced.rs index 5826ba0f..8d06e805 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -1,5 +1,4 @@ //! Leverage advanced concepts like custom widgets. -pub use crate::application::Application; pub use crate::core::clipboard::{self, Clipboard}; pub use crate::core::image; pub use crate::core::layout::{self, Layout}; diff --git a/src/application.rs b/src/application.rs index 4cd4a87d..edca6e79 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,278 +1,426 @@ -//! Build interactive cross-platform applications. -use crate::core::text; -use crate::graphics::compositor; -use crate::shell::application; -use crate::{Element, Executor, Settings, Subscription, Task}; +//! Create and run iced applications step by step. +//! +//! # Example +//! ```no_run +//! use iced::widget::{button, column, text, Column}; +//! use iced::Theme; +//! +//! pub fn main() -> iced::Result { +//! iced::application("A counter", update, view) +//! .theme(|_| Theme::Dark) +//! .centered() +//! .run() +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! Increment, +//! } +//! +//! fn update(value: &mut u64, message: Message) { +//! match message { +//! Message::Increment => *value += 1, +//! } +//! } +//! +//! fn view(value: &u64) -> Column { +//! column![ +//! text(value), +//! button("+").on_press(Message::Increment), +//! ] +//! } +//! ``` +use crate::program::{self, Program}; +use crate::window; +use crate::{Element, Font, Result, Settings, Size, Subscription, Task}; -pub use application::{Appearance, DefaultStyle}; +use std::borrow::Cow; -/// An interactive cross-platform 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 window. -/// - On the web, it will take control of the `` and the `<body>` of the -/// document. -/// -/// An [`Application`] can execute asynchronous actions by returning a -/// [`Task`] in some of its methods. -/// -/// When using an [`Application`] with the `debug` feature enabled, a debug view -/// can be toggled by pressing `F12`. -/// -/// # Examples -/// [The repository has a bunch of examples] that use the [`Application`] trait: -/// -/// - [`clock`], an application that uses the [`Canvas`] widget to draw a clock -/// and its hands to display the current time. -/// - [`download_progress`], a basic application that asynchronously downloads -/// a dummy file of 100 MB and tracks the download progress. -/// - [`events`], a log of native events displayed using a conditional -/// [`Subscription`]. -/// - [`game_of_life`], an interactive version of the [Game of Life], invented -/// by [John Horton Conway]. -/// - [`pokedex`], an application that displays a random Pokédex entry (sprite -/// included!) by using the [PokéAPI]. -/// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget -/// and showcasing how to compose different transforms. -/// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how -/// to listen to time. -/// - [`todos`], a todos tracker inspired by [TodoMVC]. -/// -/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.12/examples -/// [`clock`]: https://github.com/iced-rs/iced/tree/0.12/examples/clock -/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.12/examples/download_progress -/// [`events`]: https://github.com/iced-rs/iced/tree/0.12/examples/events -/// [`game_of_life`]: https://github.com/iced-rs/iced/tree/0.12/examples/game_of_life -/// [`pokedex`]: https://github.com/iced-rs/iced/tree/0.12/examples/pokedex -/// [`solar_system`]: https://github.com/iced-rs/iced/tree/0.12/examples/solar_system -/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.12/examples/stopwatch -/// [`todos`]: https://github.com/iced-rs/iced/tree/0.12/examples/todos -/// [`Sandbox`]: crate::Sandbox -/// [`Canvas`]: crate::widget::Canvas -/// [PokéAPI]: https://pokeapi.co/ -/// [TodoMVC]: http://todomvc.com/ -/// -/// ## A simple "Hello, world!" -/// -/// If you just want to get started, here is a simple [`Application`] that -/// says "Hello, world!": +pub use crate::shell::program::{Appearance, DefaultStyle}; + +/// Creates an iced [`Application`] given its title, update, and view logic. /// +/// # Example /// ```no_run -/// use iced::advanced::Application; -/// use iced::executor; -/// use iced::{Task, Element, Settings, Theme, Renderer}; +/// use iced::widget::{button, column, text, Column}; /// /// pub fn main() -> iced::Result { -/// Hello::run(Settings::default()) +/// iced::application("A counter", update, view).run() /// } /// -/// struct Hello; -/// -/// impl Application for Hello { -/// type Executor = executor::Default; -/// type Flags = (); -/// type Message = (); -/// type Theme = Theme; -/// type Renderer = Renderer; -/// -/// fn new(_flags: ()) -> (Hello, Task<Self::Message>) { -/// (Hello, Task::none()) -/// } -/// -/// fn title(&self) -> String { -/// String::from("A cool application") -/// } +/// #[derive(Debug, Clone)] +/// enum Message { +/// Increment, +/// } /// -/// fn update(&mut self, _message: Self::Message) -> Task<Self::Message> { -/// Task::none() +/// fn update(value: &mut u64, message: Message) { +/// match message { +/// Message::Increment => *value += 1, /// } +/// } /// -/// fn view(&self) -> Element<Self::Message> { -/// "Hello, world!".into() -/// } +/// fn view(value: &u64) -> Column<Message> { +/// column![ +/// text(value), +/// button("+").on_press(Message::Increment), +/// ] /// } /// ``` -pub trait Application: Sized +pub fn application<State, Message, Theme, Renderer>( + title: impl Title<State>, + update: impl Update<State, Message>, + view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, +) -> Application<impl Program<State = State, Message = Message, Theme = Theme>> where - Self::Theme: DefaultStyle, + State: 'static, + Message: Send + std::fmt::Debug + 'static, + Theme: Default + DefaultStyle, + Renderer: program::Renderer, { - /// 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; + use std::marker::PhantomData; - /// The type of __messages__ your [`Application`] will produce. - type Message: std::fmt::Debug + Send; + struct Instance<State, Message, Theme, Renderer, Update, View> { + update: Update, + view: View, + _state: PhantomData<State>, + _message: PhantomData<Message>, + _theme: PhantomData<Theme>, + _renderer: PhantomData<Renderer>, + } - /// The theme of your [`Application`]. - type Theme: Default; + impl<State, Message, Theme, Renderer, Update, View> Program + for Instance<State, Message, Theme, Renderer, Update, View> + where + Message: Send + std::fmt::Debug + 'static, + Theme: Default + DefaultStyle, + Renderer: program::Renderer, + Update: self::Update<State, Message>, + View: for<'a> self::View<'a, State, Message, Theme, Renderer>, + { + type State = State; + type Message = Message; + type Theme = Theme; + type Renderer = Renderer; + type Executor = iced_futures::backend::default::Executor; - /// The renderer of your [`Application`]. - type Renderer: text::Renderer + compositor::Default; + fn load(&self) -> Task<Self::Message> { + Task::none() + } - /// The data needed to initialize your [`Application`]. - type Flags; + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task<Self::Message> { + self.update.update(state, message).into() + } - /// 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 [`Task`] if you need to perform some - /// async action in the background on startup. This is useful if you want to - /// load state from a file, perform an initial HTTP request, etc. - /// - /// [`run`]: Self::run - fn new(flags: Self::Flags) -> (Self, Task<Self::Message>); + fn view<'a>( + &self, + state: &'a Self::State, + _window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.view.view(state).into() + } + } - /// 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) -> String; + Application { + raw: Instance { + update, + view, + _state: PhantomData, + _message: PhantomData, + _theme: PhantomData, + _renderer: PhantomData, + }, + settings: Settings::default(), + window: window::Settings::default(), + } + .title(title) +} - /// Handles a __message__ and updates the state of the [`Application`]. +/// The underlying definition and configuration of an iced application. +/// +/// You can use this API to create and run iced applications +/// step by step—without coupling your logic to a trait +/// or a specific type. +/// +/// You can create an [`Application`] with the [`application`] helper. +#[derive(Debug)] +pub struct Application<P: Program> { + raw: P, + settings: Settings, + window: window::Settings, +} + +impl<P: Program> Application<P> { + /// Runs 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. + /// The state of the [`Application`] must implement [`Default`]. + /// If your state does not implement [`Default`], use [`run_with`] + /// instead. /// - /// Any [`Task`] returned will be executed immediately in the background. - fn update(&mut self, message: Self::Message) -> Task<Self::Message>; + /// [`run_with`]: Self::run_with + pub fn run(self) -> Result + where + Self: 'static, + P::State: Default, + { + self.run_with(P::State::default) + } - /// Returns the widgets to display in the [`Application`]. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view(&self) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; + /// Runs the [`Application`] with a closure that creates the initial state. + pub fn run_with<I>(self, initialize: I) -> Result + where + Self: 'static, + I: Fn() -> P::State + Clone + 'static, + { + self.raw + .run_with(self.settings, Some(self.window), initialize) + } - /// Returns the current [`Theme`] of the [`Application`]. - /// - /// [`Theme`]: Self::Theme - fn theme(&self) -> Self::Theme { - Self::Theme::default() + /// Sets the [`Settings`] that will be used to run the [`Application`]. + pub fn settings(self, settings: Settings) -> Self { + Self { settings, ..self } } - /// Returns the current [`Appearance`] of the [`Application`]. - fn style(&self, theme: &Self::Theme) -> Appearance { - theme.default_style() + /// Sets the [`Settings::antialiasing`] of the [`Application`]. + pub fn antialiasing(self, antialiasing: bool) -> Self { + Self { + settings: Settings { + antialiasing, + ..self.settings + }, + ..self + } } - /// 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() + /// Sets the default [`Font`] of the [`Application`]. + pub fn default_font(self, default_font: Font) -> Self { + Self { + settings: Settings { + default_font, + ..self.settings + }, + ..self + } } - /// Returns the scale factor 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`. - fn scale_factor(&self) -> f64 { - 1.0 + /// Adds a font to the list of fonts that will be loaded at the start of the [`Application`]. + pub fn font(mut self, font: impl Into<Cow<'static, [u8]>>) -> Self { + self.settings.fonts.push(font.into()); + self } - /// Runs the [`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::graphics::Settings { - default_font: settings.default_font, - default_text_size: settings.default_text_size, - antialiasing: if settings.antialiasing { - Some(crate::graphics::Antialiasing::MSAAx4) - } else { - None + /// Sets the [`window::Settings::position`] to [`window::Position::Centered`] in the [`Application`]. + pub fn centered(self) -> Self { + Self { + window: window::Settings { + position: window::Position::Centered, + ..self.window }, - ..crate::graphics::Settings::default() - }; - - Ok(crate::shell::application::run::< - Instance<Self>, - Self::Executor, - <Self::Renderer as compositor::Default>::Compositor, - >(settings.into(), renderer_settings)?) + ..self + } } -} -struct Instance<A>(A) -where - A: Application, - A::Theme: DefaultStyle; + /// Sets the [`window::Settings::exit_on_close_request`] of the [`Application`]. + pub fn exit_on_close_request(self, exit_on_close_request: bool) -> Self { + Self { + window: window::Settings { + exit_on_close_request, + ..self.window + }, + ..self + } + } -impl<A> crate::runtime::Program for Instance<A> -where - A: Application, - A::Theme: DefaultStyle, -{ - type Message = A::Message; - type Theme = A::Theme; - type Renderer = A::Renderer; + /// Sets the [`window::Settings::size`] of the [`Application`]. + pub fn window_size(self, size: impl Into<Size>) -> Self { + Self { + window: window::Settings { + size: size.into(), + ..self.window + }, + ..self + } + } - fn update(&mut self, message: Self::Message) -> Task<Self::Message> { - self.0.update(message) + /// Sets the [`window::Settings::transparent`] of the [`Application`]. + pub fn transparent(self, transparent: bool) -> Self { + Self { + window: window::Settings { + transparent, + ..self.window + }, + ..self + } } - fn view(&self) -> Element<'_, Self::Message, Self::Theme, Self::Renderer> { - self.0.view() + /// Sets the [`Title`] of the [`Application`]. + pub(crate) fn title( + self, + title: impl Title<P::State>, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_title(self.raw, move |state, _window| { + title.title(state) + }), + settings: self.settings, + window: self.window, + } } -} -impl<A> application::Application for Instance<A> -where - A: Application, - A::Theme: DefaultStyle, -{ - type Flags = A::Flags; + /// Runs the [`Task`] produced by the closure at startup. + pub fn load( + self, + f: impl Fn() -> Task<P::Message>, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_load(self.raw, f), + settings: self.settings, + window: self.window, + } + } - fn new(flags: Self::Flags) -> (Self, Task<A::Message>) { - let (app, command) = A::new(flags); + /// Sets the subscription logic of the [`Application`]. + pub fn subscription( + self, + f: impl Fn(&P::State) -> Subscription<P::Message>, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_subscription(self.raw, f), + settings: self.settings, + window: self.window, + } + } - (Instance(app), command) + /// Sets the theme logic of the [`Application`]. + pub fn theme( + self, + f: impl Fn(&P::State) -> P::Theme, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_theme(self.raw, move |state, _window| f(state)), + settings: self.settings, + window: self.window, + } } - fn title(&self) -> String { - self.0.title() + /// Sets the style logic of the [`Application`]. + pub fn style( + self, + f: impl Fn(&P::State, &P::Theme) -> Appearance, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_style(self.raw, f), + settings: self.settings, + window: self.window, + } } - fn theme(&self) -> A::Theme { - self.0.theme() + /// Sets the scale factor of the [`Application`]. + pub fn scale_factor( + self, + f: impl Fn(&P::State) -> f64, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_scale_factor(self.raw, move |state, _window| { + f(state) + }), + settings: self.settings, + window: self.window, + } } +} + +/// The title logic of some [`Application`]. +/// +/// This trait is implemented both for `&static str` and +/// any closure `Fn(&State) -> String`. +/// +/// This trait allows the [`application`] builder to take any of them. +pub trait Title<State> { + /// Produces the title of the [`Application`]. + fn title(&self, state: &State) -> String; +} - fn style(&self, theme: &A::Theme) -> Appearance { - self.0.style(theme) +impl<State> Title<State> for &'static str { + fn title(&self, _state: &State) -> String { + self.to_string() } +} - fn subscription(&self) -> Subscription<Self::Message> { - self.0.subscription() +impl<T, State> Title<State> for T +where + T: Fn(&State) -> String, +{ + fn title(&self, state: &State) -> String { + self(state) } +} - fn scale_factor(&self) -> f64 { - self.0.scale_factor() +/// The update logic of some [`Application`]. +/// +/// This trait allows the [`application`] builder to take any closure that +/// returns any `Into<Task<Message>>`. +pub trait Update<State, Message> { + /// Processes the message and updates the state of the [`Application`]. + fn update( + &self, + state: &mut State, + message: Message, + ) -> impl Into<Task<Message>>; +} + +impl<T, State, Message, C> Update<State, Message> for T +where + T: Fn(&mut State, Message) -> C, + C: Into<Task<Message>>, +{ + fn update( + &self, + state: &mut State, + message: Message, + ) -> impl Into<Task<Message>> { + self(state, message) + } +} + +/// The view logic of some [`Application`]. +/// +/// This trait allows the [`application`] builder to take any closure that +/// returns any `Into<Element<'_, Message>>`. +pub trait View<'a, State, Message, Theme, Renderer> { + /// Produces the widget of the [`Application`]. + fn view( + &self, + state: &'a State, + ) -> impl Into<Element<'a, Message, Theme, Renderer>>; +} + +impl<'a, T, State, Message, Theme, Renderer, Widget> + View<'a, State, Message, Theme, Renderer> for T +where + T: Fn(&'a State) -> Widget, + State: 'static, + Widget: Into<Element<'a, Message, Theme, Renderer>>, +{ + fn view( + &self, + state: &'a State, + ) -> impl Into<Element<'a, Message, Theme, Renderer>> { + self(state) } } diff --git a/src/daemon.rs b/src/daemon.rs new file mode 100644 index 00000000..58293949 --- /dev/null +++ b/src/daemon.rs @@ -0,0 +1,298 @@ +//! Create and run daemons that run in the background. +use crate::application; +use crate::program::{self, Program}; +use crate::window; +use crate::{Element, Font, Result, Settings, Subscription, Task}; + +use std::borrow::Cow; + +pub use crate::shell::program::{Appearance, DefaultStyle}; + +/// Creates an iced [`Daemon`] given its title, update, and view logic. +/// +/// A [`Daemon`] will not open a window by default, but will run silently +/// instead until a [`Task`] from [`window::open`] is returned by its update logic. +/// +/// Furthermore, a [`Daemon`] will not stop running when all its windows are closed. +/// In order to completely terminate a [`Daemon`], its process must be interrupted or +/// its update logic must produce a [`Task`] from [`exit`]. +/// +/// [`exit`]: crate::exit +pub fn daemon<State, Message, Theme, Renderer>( + title: impl Title<State>, + update: impl application::Update<State, Message>, + view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, +) -> Daemon<impl Program<State = State, Message = Message, Theme = Theme>> +where + State: 'static, + Message: Send + std::fmt::Debug + 'static, + Theme: Default + DefaultStyle, + Renderer: program::Renderer, +{ + use std::marker::PhantomData; + + struct Instance<State, Message, Theme, Renderer, Update, View> { + update: Update, + view: View, + _state: PhantomData<State>, + _message: PhantomData<Message>, + _theme: PhantomData<Theme>, + _renderer: PhantomData<Renderer>, + } + + impl<State, Message, Theme, Renderer, Update, View> Program + for Instance<State, Message, Theme, Renderer, Update, View> + where + Message: Send + std::fmt::Debug + 'static, + Theme: Default + DefaultStyle, + Renderer: program::Renderer, + Update: application::Update<State, Message>, + View: for<'a> self::View<'a, State, Message, Theme, Renderer>, + { + type State = State; + type Message = Message; + type Theme = Theme; + type Renderer = Renderer; + type Executor = iced_futures::backend::default::Executor; + + fn load(&self) -> Task<Self::Message> { + Task::none() + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task<Self::Message> { + self.update.update(state, message).into() + } + + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.view.view(state, window).into() + } + } + + Daemon { + raw: Instance { + update, + view, + _state: PhantomData, + _message: PhantomData, + _theme: PhantomData, + _renderer: PhantomData, + }, + settings: Settings::default(), + } + .title(title) +} + +/// The underlying definition and configuration of an iced daemon. +/// +/// You can use this API to create and run iced applications +/// step by step—without coupling your logic to a trait +/// or a specific type. +/// +/// You can create a [`Daemon`] with the [`daemon`] helper. +#[derive(Debug)] +pub struct Daemon<P: Program> { + raw: P, + settings: Settings, +} + +impl<P: Program> Daemon<P> { + /// Runs the [`Daemon`]. + /// + /// The state of the [`Daemon`] must implement [`Default`]. + /// If your state does not implement [`Default`], use [`run_with`] + /// instead. + /// + /// [`run_with`]: Self::run_with + pub fn run(self) -> Result + where + Self: 'static, + P::State: Default, + { + self.run_with(P::State::default) + } + + /// Runs the [`Daemon`] with a closure that creates the initial state. + pub fn run_with<I>(self, initialize: I) -> Result + where + Self: 'static, + I: Fn() -> P::State + Clone + 'static, + { + self.raw.run_with(self.settings, None, initialize) + } + + /// Sets the [`Settings`] that will be used to run the [`Daemon`]. + pub fn settings(self, settings: Settings) -> Self { + Self { settings, ..self } + } + + /// Sets the [`Settings::antialiasing`] of the [`Daemon`]. + pub fn antialiasing(self, antialiasing: bool) -> Self { + Self { + settings: Settings { + antialiasing, + ..self.settings + }, + ..self + } + } + + /// Sets the default [`Font`] of the [`Daemon`]. + pub fn default_font(self, default_font: Font) -> Self { + Self { + settings: Settings { + default_font, + ..self.settings + }, + ..self + } + } + + /// Adds a font to the list of fonts that will be loaded at the start of the [`Daemon`]. + pub fn font(mut self, font: impl Into<Cow<'static, [u8]>>) -> Self { + self.settings.fonts.push(font.into()); + self + } + + /// Sets the [`Title`] of the [`Daemon`]. + pub(crate) fn title( + self, + title: impl Title<P::State>, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_title(self.raw, move |state, window| { + title.title(state, window) + }), + settings: self.settings, + } + } + + /// Runs the [`Task`] produced by the closure at startup. + pub fn load( + self, + f: impl Fn() -> Task<P::Message>, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_load(self.raw, f), + settings: self.settings, + } + } + + /// Sets the subscription logic of the [`Daemon`]. + pub fn subscription( + self, + f: impl Fn(&P::State) -> Subscription<P::Message>, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_subscription(self.raw, f), + settings: self.settings, + } + } + + /// Sets the theme logic of the [`Daemon`]. + pub fn theme( + self, + f: impl Fn(&P::State, window::Id) -> P::Theme, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_theme(self.raw, f), + settings: self.settings, + } + } + + /// Sets the style logic of the [`Daemon`]. + pub fn style( + self, + f: impl Fn(&P::State, &P::Theme) -> Appearance, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_style(self.raw, f), + settings: self.settings, + } + } + + /// Sets the scale factor of the [`Daemon`]. + pub fn scale_factor( + self, + f: impl Fn(&P::State, window::Id) -> f64, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_scale_factor(self.raw, f), + settings: self.settings, + } + } +} + +/// The title logic of some [`Daemon`]. +/// +/// This trait is implemented both for `&static str` and +/// any closure `Fn(&State, window::Id) -> String`. +/// +/// This trait allows the [`daemon`] builder to take any of them. +pub trait Title<State> { + /// Produces the title of the [`Daemon`]. + fn title(&self, state: &State, window: window::Id) -> String; +} + +impl<State> Title<State> for &'static str { + fn title(&self, _state: &State, _window: window::Id) -> String { + self.to_string() + } +} + +impl<T, State> Title<State> for T +where + T: Fn(&State, window::Id) -> String, +{ + fn title(&self, state: &State, window: window::Id) -> String { + self(state, window) + } +} + +/// The view logic of some [`Daemon`]. +/// +/// This trait allows the [`daemon`] builder to take any closure that +/// returns any `Into<Element<'_, Message>>`. +pub trait View<'a, State, Message, Theme, Renderer> { + /// Produces the widget of the [`Daemon`]. + fn view( + &self, + state: &'a State, + window: window::Id, + ) -> impl Into<Element<'a, Message, Theme, Renderer>>; +} + +impl<'a, T, State, Message, Theme, Renderer, Widget> + View<'a, State, Message, Theme, Renderer> for T +where + T: Fn(&'a State, window::Id) -> Widget, + State: 'static, + Widget: Into<Element<'a, Message, Theme, Renderer>>, +{ + fn view( + &self, + state: &'a State, + window: window::Id, + ) -> impl Into<Element<'a, Message, Theme, Renderer>> { + self(state, window) + } +} diff --git a/src/lib.rs b/src/lib.rs index cf0bc7d7..957c7ecc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -158,11 +158,11 @@ //! 1. Draw the resulting user interface. //! //! # Usage -//! Use [`run`] or the [`program`] builder. +//! Use [`run`] or the [`application`] builder. //! //! [Elm]: https://elm-lang.org/ //! [The Elm Architecture]: https://guide.elm-lang.org/architecture/ -//! [`program`]: program() +//! [`application`]: application() #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] @@ -179,10 +179,11 @@ pub use iced_futures::futures; #[cfg(feature = "highlighter")] pub use iced_highlighter as highlighter; -mod application; mod error; +mod program; -pub mod program; +pub mod application; +pub mod daemon; pub mod settings; pub mod time; pub mod window; @@ -190,9 +191,6 @@ pub mod window; #[cfg(feature = "advanced")] pub mod advanced; -#[cfg(feature = "multi-window")] -pub mod multi_window; - pub use crate::core::alignment; pub use crate::core::border; pub use crate::core::color; @@ -203,7 +201,7 @@ pub use crate::core::{ Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size, Theme, Transformation, Vector, }; -pub use crate::runtime::Task; +pub use crate::runtime::{exit, Task}; pub mod clipboard { //! Access the clipboard. @@ -308,11 +306,12 @@ pub mod widget { mod runtime {} } +pub use application::{application, Application}; +pub use daemon::{daemon, Daemon}; pub use error::Error; pub use event::Event; pub use executor::Executor; pub use font::Font; -pub use program::Program; pub use renderer::Renderer; pub use settings::Settings; pub use subscription::Subscription; @@ -327,13 +326,13 @@ pub type Element< Renderer = crate::Renderer, > = crate::core::Element<'a, Message, Theme, Renderer>; -/// The result of running a [`Program`]. +/// The result of running an iced program. pub type Result = std::result::Result<(), Error>; /// Runs a basic iced application with default [`Settings`] given its title, /// update, and view logic. /// -/// This is equivalent to chaining [`program`] with [`Program::run`]. +/// This is equivalent to chaining [`application()`] with [`Application::run`]. /// /// [`program`]: program() /// @@ -364,9 +363,10 @@ pub type Result = std::result::Result<(), Error>; /// } /// ``` pub fn run<State, Message, Theme, Renderer>( - title: impl program::Title<State> + 'static, - update: impl program::Update<State, Message> + 'static, - view: impl for<'a> program::View<'a, State, Message, Theme, Renderer> + 'static, + title: impl application::Title<State> + 'static, + update: impl application::Update<State, Message> + 'static, + view: impl for<'a> application::View<'a, State, Message, Theme, Renderer> + + 'static, ) -> Result where State: Default + 'static, @@ -374,8 +374,5 @@ where Theme: Default + program::DefaultStyle + 'static, Renderer: program::Renderer + 'static, { - program(title, update, view).run() + application(title, update, view).run() } - -#[doc(inline)] -pub use program::program; diff --git a/src/multi_window.rs b/src/multi_window.rs deleted file mode 100644 index 4900bb85..00000000 --- a/src/multi_window.rs +++ /dev/null @@ -1,254 +0,0 @@ -//! Leverage multi-window support in your application. -use crate::window; -use crate::{Element, Executor, Settings, Subscription, Task}; - -pub use crate::application::{Appearance, DefaultStyle}; - -/// 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 `<title>` 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 -/// [`Task`] in some of its methods. -/// -/// 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::{Task, 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, Task<Self::Message>) { -/// (Hello, Task::none()) -/// } -/// -/// fn title(&self, _window: window::Id) -> String { -/// String::from("A cool application") -/// } -/// -/// fn update(&mut self, _message: Self::Message) -> Task<Self::Message> { -/// Task::none() -/// } -/// -/// fn view(&self, _window: window::Id) -> Element<Self::Message> { -/// "Hello, world!".into() -/// } -/// } -/// ``` -/// -/// [`Sandbox`]: crate::Sandbox -pub trait Application: Sized -where - Self::Theme: DefaultStyle, -{ - /// 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; - - /// 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 [`Task`] if you need to perform some - /// async action in the background on startup. This is useful if you want to - /// load state from a file, perform an initial HTTP request, etc. - /// - /// [`run`]: Self::run - fn new(flags: Self::Flags) -> (Self, Task<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 [`Task`] returned will be executed immediately in the background. - fn update(&mut self, message: Self::Message) -> Task<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, Self::Theme, crate::Renderer>; - - /// 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, theme: &Self::Theme) -> Appearance { - Self::Theme::default_style(theme) - } - - /// 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::graphics::Settings { - default_font: settings.default_font, - default_text_size: settings.default_text_size, - antialiasing: if settings.antialiasing { - Some(crate::graphics::Antialiasing::MSAAx4) - } else { - None - }, - ..crate::graphics::Settings::default() - }; - - Ok(crate::shell::multi_window::run::< - Instance<Self>, - Self::Executor, - crate::renderer::Compositor, - >(settings.into(), renderer_settings)?) - } -} - -struct Instance<A>(A) -where - A: Application, - A::Theme: DefaultStyle; - -impl<A> crate::runtime::multi_window::Program for Instance<A> -where - A: Application, - A::Theme: DefaultStyle, -{ - type Message = A::Message; - type Theme = A::Theme; - type Renderer = crate::Renderer; - - fn update(&mut self, message: Self::Message) -> Task<Self::Message> { - self.0.update(message) - } - - fn view( - &self, - window: window::Id, - ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer> { - self.0.view(window) - } -} - -impl<A> crate::shell::multi_window::Application for Instance<A> -where - A: Application, - A::Theme: DefaultStyle, -{ - type Flags = A::Flags; - - fn new(flags: Self::Flags) -> (Self, Task<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, theme: &Self::Theme) -> Appearance { - self.0.style(theme) - } - - 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/program.rs b/src/program.rs index ea6b0e8e..3f9d2d0c 100644 --- a/src/program.rs +++ b/src/program.rs @@ -1,194 +1,108 @@ -//! Create and run iced applications step by step. -//! -//! # Example -//! ```no_run -//! use iced::widget::{button, column, text, Column}; -//! use iced::Theme; -//! -//! pub fn main() -> iced::Result { -//! iced::program("A counter", update, view) -//! .theme(|_| Theme::Dark) -//! .centered() -//! .run() -//! } -//! -//! #[derive(Debug, Clone)] -//! enum Message { -//! Increment, -//! } -//! -//! fn update(value: &mut u64, message: Message) { -//! match message { -//! Message::Increment => *value += 1, -//! } -//! } -//! -//! fn view(value: &u64) -> Column<Message> { -//! column![ -//! text(value), -//! button("+").on_press(Message::Increment), -//! ] -//! } -//! ``` -use crate::application::Application; use crate::core::text; -use crate::executor::{self, Executor}; use crate::graphics::compositor; +use crate::shell; use crate::window; -use crate::{Element, Font, Result, Settings, Size, Subscription, Task}; +use crate::{Element, Executor, Result, Settings, Subscription, Task}; -pub use crate::application::{Appearance, DefaultStyle}; +pub use crate::shell::program::{Appearance, DefaultStyle}; -use std::borrow::Cow; - -/// Creates an iced [`Program`] given its title, update, and view logic. -/// -/// # Example -/// ```no_run -/// use iced::widget::{button, column, text, Column}; -/// -/// pub fn main() -> iced::Result { -/// iced::program("A counter", update, view).run() -/// } -/// -/// #[derive(Debug, Clone)] -/// enum Message { -/// Increment, -/// } -/// -/// fn update(value: &mut u64, message: Message) { -/// match message { -/// Message::Increment => *value += 1, -/// } -/// } +/// The internal definition of a [`Program`]. /// -/// fn view(value: &u64) -> Column<Message> { -/// column![ -/// text(value), -/// button("+").on_press(Message::Increment), -/// ] -/// } -/// ``` -pub fn program<State, Message, Theme, Renderer>( - title: impl Title<State>, - update: impl Update<State, Message>, - view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, -) -> Program<impl Definition<State = State, Message = Message, Theme = Theme>> -where - State: 'static, - Message: Send + std::fmt::Debug + 'static, - Theme: Default + DefaultStyle, - Renderer: self::Renderer, -{ - use std::marker::PhantomData; - - struct Application<State, Message, Theme, Renderer, Update, View> { - update: Update, - view: View, - _state: PhantomData<State>, - _message: PhantomData<Message>, - _theme: PhantomData<Theme>, - _renderer: PhantomData<Renderer>, - } +/// You should not need to implement this trait directly. Instead, use the +/// methods available in the [`Program`] struct. +#[allow(missing_docs)] +pub trait Program: Sized { + /// The state of the program. + type State; - impl<State, Message, Theme, Renderer, Update, View> Definition - for Application<State, Message, Theme, Renderer, Update, View> - where - Message: Send + std::fmt::Debug + 'static, - Theme: Default + DefaultStyle, - Renderer: self::Renderer, - Update: self::Update<State, Message>, - View: for<'a> self::View<'a, State, Message, Theme, Renderer>, - { - type State = State; - type Message = Message; - type Theme = Theme; - type Renderer = Renderer; - type Executor = executor::Default; + /// The message of the program. + type Message: Send + std::fmt::Debug + 'static; - fn load(&self) -> Task<Self::Message> { - Task::none() - } + /// The theme of the program. + type Theme: Default + DefaultStyle; - fn update( - &self, - state: &mut Self::State, - message: Self::Message, - ) -> Task<Self::Message> { - self.update.update(state, message).into() - } + /// The renderer of the program. + type Renderer: Renderer; - fn view<'a>( - &self, - state: &'a Self::State, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.view.view(state).into() - } + /// The executor of the program. + type Executor: Executor; + + fn load(&self) -> Task<Self::Message>; + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task<Self::Message>; + + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer>; + + fn title(&self, _state: &Self::State, _window: window::Id) -> String { + String::from("A cool iced application!") } - Program { - raw: Application { - update, - view, - _state: PhantomData, - _message: PhantomData, - _theme: PhantomData, - _renderer: PhantomData, - }, - settings: Settings::default(), + fn subscription( + &self, + _state: &Self::State, + ) -> Subscription<Self::Message> { + Subscription::none() } - .title(title) -} -/// The underlying definition and configuration of an iced application. -/// -/// You can use this API to create and run iced applications -/// step by step—without coupling your logic to a trait -/// or a specific type. -/// -/// You can create a [`Program`] with the [`program`] helper. -/// -/// [`run`]: Program::run -#[derive(Debug)] -pub struct Program<P: Definition> { - raw: P, - settings: Settings, -} + fn theme(&self, _state: &Self::State, _window: window::Id) -> Self::Theme { + Self::Theme::default() + } + + fn style(&self, _state: &Self::State, theme: &Self::Theme) -> Appearance { + DefaultStyle::default_style(theme) + } -impl<P: Definition> Program<P> { - /// Runs the underlying [`Application`] of the [`Program`]. + fn scale_factor(&self, _state: &Self::State, _window: window::Id) -> f64 { + 1.0 + } + + /// Runs the [`Program`]. /// /// The state of the [`Program`] must implement [`Default`]. /// If your state does not implement [`Default`], use [`run_with`] /// instead. /// /// [`run_with`]: Self::run_with - pub fn run(self) -> Result + fn run( + self, + settings: Settings, + window_settings: Option<window::Settings>, + ) -> Result where Self: 'static, - P::State: Default, + Self::State: Default, { - self.run_with(P::State::default) + self.run_with(settings, window_settings, Self::State::default) } - /// Runs the underlying [`Application`] of the [`Program`] with a - /// closure that creates the initial state. - pub fn run_with( + /// Runs the [`Program`] with the given [`Settings`] and a closure that creates the initial state. + fn run_with<I>( self, - initialize: impl Fn() -> P::State + Clone + 'static, + settings: Settings, + window_settings: Option<window::Settings>, + initialize: I, ) -> Result where Self: 'static, + I: Fn() -> Self::State + Clone + 'static, { use std::marker::PhantomData; - struct Instance<P: Definition, I> { + struct Instance<P: Program, I> { program: P, state: P::State, _initialize: PhantomData<I>, } - impl<P: Definition, I: Fn() -> P::State> Application for Instance<P, I> { + impl<P: Program, I: Fn() -> P::State> shell::Program for Instance<P, I> { type Message = P::Message; type Theme = P::Theme; type Renderer = P::Renderer; @@ -211,8 +125,8 @@ impl<P: Definition> Program<P> { ) } - fn title(&self) -> String { - self.program.title(&self.state) + fn title(&self, window: window::Id) -> String { + self.program.title(&self.state, window) } fn update( @@ -224,259 +138,73 @@ impl<P: Definition> Program<P> { fn view( &self, + window: window::Id, ) -> crate::Element<'_, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(&self.state) + self.program.view(&self.state, window) } fn subscription(&self) -> Subscription<Self::Message> { self.program.subscription(&self.state) } - fn theme(&self) -> Self::Theme { - self.program.theme(&self.state) + fn theme(&self, window: window::Id) -> Self::Theme { + self.program.theme(&self.state, window) } fn style(&self, theme: &Self::Theme) -> Appearance { self.program.style(&self.state, theme) } - } - let Self { raw, settings } = self; + fn scale_factor(&self, window: window::Id) -> f64 { + self.program.scale_factor(&self.state, window) + } + } - Instance::run(Settings { - flags: (raw, initialize), - id: settings.id, - window: settings.window, - fonts: settings.fonts, + #[allow(clippy::needless_update)] + let renderer_settings = crate::graphics::Settings { default_font: settings.default_font, default_text_size: settings.default_text_size, - antialiasing: settings.antialiasing, - }) - } - - /// Sets the [`Settings`] that will be used to run the [`Program`]. - pub fn settings(self, settings: Settings) -> Self { - Self { settings, ..self } - } - - /// Sets the [`Settings::antialiasing`] of the [`Program`]. - pub fn antialiasing(self, antialiasing: bool) -> Self { - Self { - settings: Settings { - antialiasing, - ..self.settings - }, - ..self - } - } - - /// Sets the default [`Font`] of the [`Program`]. - pub fn default_font(self, default_font: Font) -> Self { - Self { - settings: Settings { - default_font, - ..self.settings - }, - ..self - } - } - - /// Adds a font to the list of fonts that will be loaded at the start of the [`Program`]. - pub fn font(mut self, font: impl Into<Cow<'static, [u8]>>) -> Self { - self.settings.fonts.push(font.into()); - self - } - - /// Sets the [`window::Settings::position`] to [`window::Position::Centered`] in the [`Program`]. - pub fn centered(self) -> Self { - Self { - settings: Settings { - window: window::Settings { - position: window::Position::Centered, - ..self.settings.window - }, - ..self.settings - }, - ..self - } - } - - /// Sets the [`window::Settings::exit_on_close_request`] of the [`Program`]. - pub fn exit_on_close_request(self, exit_on_close_request: bool) -> Self { - Self { - settings: Settings { - window: window::Settings { - exit_on_close_request, - ..self.settings.window - }, - ..self.settings - }, - ..self - } - } - - /// Sets the [`window::Settings::size`] of the [`Program`]. - pub fn window_size(self, size: impl Into<Size>) -> Self { - Self { - settings: Settings { - window: window::Settings { - size: size.into(), - ..self.settings.window - }, - ..self.settings - }, - ..self - } - } - - /// Sets the [`window::Settings::transparent`] of the [`Program`]. - pub fn transparent(self, transparent: bool) -> Self { - Self { - settings: Settings { - window: window::Settings { - transparent, - ..self.settings.window - }, - ..self.settings + antialiasing: if settings.antialiasing { + Some(crate::graphics::Antialiasing::MSAAx4) + } else { + None }, - ..self - } - } - - /// Sets the [`Title`] of the [`Program`]. - pub(crate) fn title( - self, - title: impl Title<P::State>, - ) -> Program< - impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Program { - raw: with_title(self.raw, title), - settings: self.settings, - } - } - - /// Runs the [`Task`] produced by the closure at startup. - pub fn load( - self, - f: impl Fn() -> Task<P::Message>, - ) -> Program< - impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Program { - raw: with_load(self.raw, f), - settings: self.settings, - } - } - - /// Sets the subscription logic of the [`Program`]. - pub fn subscription( - self, - f: impl Fn(&P::State) -> Subscription<P::Message>, - ) -> Program< - impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Program { - raw: with_subscription(self.raw, f), - settings: self.settings, - } - } - - /// Sets the theme logic of the [`Program`]. - pub fn theme( - self, - f: impl Fn(&P::State) -> P::Theme, - ) -> Program< - impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Program { - raw: with_theme(self.raw, f), - settings: self.settings, - } - } - - /// Sets the style logic of the [`Program`]. - pub fn style( - self, - f: impl Fn(&P::State, &P::Theme) -> Appearance, - ) -> Program< - impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Program { - raw: with_style(self.raw, f), - settings: self.settings, - } - } -} - -/// The internal definition of a [`Program`]. -/// -/// You should not need to implement this trait directly. Instead, use the -/// methods available in the [`Program`] struct. -#[allow(missing_docs)] -pub trait Definition: Sized { - /// The state of the program. - type State; - - /// The message of the program. - type Message: Send + std::fmt::Debug + 'static; - - /// The theme of the program. - type Theme: Default + DefaultStyle; - - /// The renderer of the program. - type Renderer: Renderer; - - /// The executor of the program. - type Executor: Executor; - - fn load(&self) -> Task<Self::Message>; - - fn update( - &self, - state: &mut Self::State, - message: Self::Message, - ) -> Task<Self::Message>; - - fn view<'a>( - &self, - state: &'a Self::State, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer>; - - fn title(&self, _state: &Self::State) -> String { - String::from("A cool iced application!") - } - - fn subscription( - &self, - _state: &Self::State, - ) -> Subscription<Self::Message> { - Subscription::none() - } - - fn theme(&self, _state: &Self::State) -> Self::Theme { - Self::Theme::default() - } - - fn style(&self, _state: &Self::State, theme: &Self::Theme) -> Appearance { - DefaultStyle::default_style(theme) + ..crate::graphics::Settings::default() + }; + + Ok(shell::program::run::< + Instance<Self, I>, + <Self::Renderer as compositor::Default>::Compositor, + >( + Settings { + id: settings.id, + fonts: settings.fonts, + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: settings.antialiasing, + } + .into(), + renderer_settings, + window_settings, + (self, initialize), + )?) } } -fn with_title<P: Definition>( +pub fn with_title<P: Program>( program: P, - title: impl Title<P::State>, -) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { + title: impl Fn(&P::State, window::Id) -> String, +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { struct WithTitle<P, Title> { program: P, title: Title, } - impl<P, Title> Definition for WithTitle<P, Title> + impl<P, Title> Program for WithTitle<P, Title> where - P: Definition, - Title: self::Title<P::State>, + P: Program, + Title: Fn(&P::State, window::Id) -> String, { type State = P::State; type Message = P::Message; @@ -488,8 +216,8 @@ fn with_title<P: Definition>( self.program.load() } - fn title(&self, state: &Self::State) -> String { - self.title.title(state) + fn title(&self, state: &Self::State, window: window::Id) -> String { + (self.title)(state, window) } fn update( @@ -503,12 +231,17 @@ fn with_title<P: Definition>( fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state) + self.program.view(state, window) } - fn theme(&self, state: &Self::State) -> Self::Theme { - self.program.theme(state) + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) } fn subscription( @@ -525,21 +258,25 @@ fn with_title<P: Definition>( ) -> Appearance { self.program.style(state, theme) } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) + } } WithTitle { program, title } } -fn with_load<P: Definition>( +pub fn with_load<P: Program>( program: P, f: impl Fn() -> Task<P::Message>, -) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { struct WithLoad<P, F> { program: P, load: F, } - impl<P: Definition, F> Definition for WithLoad<P, F> + impl<P: Program, F> Program for WithLoad<P, F> where F: Fn() -> Task<P::Message>, { @@ -547,7 +284,7 @@ fn with_load<P: Definition>( type Message = P::Message; type Theme = P::Theme; type Renderer = P::Renderer; - type Executor = executor::Default; + type Executor = P::Executor; fn load(&self) -> Task<Self::Message> { Task::batch([self.program.load(), (self.load)()]) @@ -564,12 +301,13 @@ fn with_load<P: Definition>( fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state) + self.program.view(state, window) } - fn title(&self, state: &Self::State) -> String { - self.program.title(state) + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) } fn subscription( @@ -579,8 +317,12 @@ fn with_load<P: Definition>( self.program.subscription(state) } - fn theme(&self, state: &Self::State) -> Self::Theme { - self.program.theme(state) + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) } fn style( @@ -590,21 +332,25 @@ fn with_load<P: Definition>( ) -> Appearance { self.program.style(state, theme) } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) + } } WithLoad { program, load: f } } -fn with_subscription<P: Definition>( +pub fn with_subscription<P: Program>( program: P, f: impl Fn(&P::State) -> Subscription<P::Message>, -) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { struct WithSubscription<P, F> { program: P, subscription: F, } - impl<P: Definition, F> Definition for WithSubscription<P, F> + impl<P: Program, F> Program for WithSubscription<P, F> where F: Fn(&P::State) -> Subscription<P::Message>, { @@ -612,7 +358,7 @@ fn with_subscription<P: Definition>( type Message = P::Message; type Theme = P::Theme; type Renderer = P::Renderer; - type Executor = executor::Default; + type Executor = P::Executor; fn subscription( &self, @@ -636,16 +382,21 @@ fn with_subscription<P: Definition>( fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state) + self.program.view(state, window) } - fn title(&self, state: &Self::State) -> String { - self.program.title(state) + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) } - fn theme(&self, state: &Self::State) -> Self::Theme { - self.program.theme(state) + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) } fn style( @@ -655,6 +406,10 @@ fn with_subscription<P: Definition>( ) -> Appearance { self.program.style(state, theme) } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) + } } WithSubscription { @@ -663,18 +418,18 @@ fn with_subscription<P: Definition>( } } -fn with_theme<P: Definition>( +pub fn with_theme<P: Program>( program: P, - f: impl Fn(&P::State) -> P::Theme, -) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { + f: impl Fn(&P::State, window::Id) -> P::Theme, +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { struct WithTheme<P, F> { program: P, theme: F, } - impl<P: Definition, F> Definition for WithTheme<P, F> + impl<P: Program, F> Program for WithTheme<P, F> where - F: Fn(&P::State) -> P::Theme, + F: Fn(&P::State, window::Id) -> P::Theme, { type State = P::State; type Message = P::Message; @@ -682,16 +437,20 @@ fn with_theme<P: Definition>( type Renderer = P::Renderer; type Executor = P::Executor; - fn theme(&self, state: &Self::State) -> Self::Theme { - (self.theme)(state) + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + (self.theme)(state, window) } fn load(&self) -> Task<Self::Message> { self.program.load() } - fn title(&self, state: &Self::State) -> String { - self.program.title(state) + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) } fn update( @@ -705,8 +464,9 @@ fn with_theme<P: Definition>( fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state) + self.program.view(state, window) } fn subscription( @@ -723,21 +483,25 @@ fn with_theme<P: Definition>( ) -> Appearance { self.program.style(state, theme) } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) + } } WithTheme { program, theme: f } } -fn with_style<P: Definition>( +pub fn with_style<P: Program>( program: P, f: impl Fn(&P::State, &P::Theme) -> Appearance, -) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { struct WithStyle<P, F> { program: P, style: F, } - impl<P: Definition, F> Definition for WithStyle<P, F> + impl<P: Program, F> Program for WithStyle<P, F> where F: Fn(&P::State, &P::Theme) -> Appearance, { @@ -759,8 +523,8 @@ fn with_style<P: Definition>( self.program.load() } - fn title(&self, state: &Self::State) -> String { - self.program.title(state) + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) } fn update( @@ -774,8 +538,9 @@ fn with_style<P: Definition>( fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state) + self.program.view(state, window) } fn subscription( @@ -785,91 +550,96 @@ fn with_style<P: Definition>( self.program.subscription(state) } - fn theme(&self, state: &Self::State) -> Self::Theme { - self.program.theme(state) + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) + } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) } } WithStyle { program, style: f } } -/// The title logic of some [`Program`]. -/// -/// This trait is implemented both for `&static str` and -/// any closure `Fn(&State) -> String`. -/// -/// This trait allows the [`program`] builder to take any of them. -pub trait Title<State> { - /// Produces the title of the [`Program`]. - fn title(&self, state: &State) -> String; -} - -impl<State> Title<State> for &'static str { - fn title(&self, _state: &State) -> String { - self.to_string() +pub fn with_scale_factor<P: Program>( + program: P, + f: impl Fn(&P::State, window::Id) -> f64, +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { + struct WithScaleFactor<P, F> { + program: P, + scale_factor: F, } -} -impl<T, State> Title<State> for T -where - T: Fn(&State) -> String, -{ - fn title(&self, state: &State) -> String { - self(state) - } -} + impl<P: Program, F> Program for WithScaleFactor<P, F> + where + F: Fn(&P::State, window::Id) -> f64, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Executor = P::Executor; -/// The update logic of some [`Program`]. -/// -/// This trait allows the [`program`] builder to take any closure that -/// returns any `Into<Task<Message>>`. -pub trait Update<State, Message> { - /// Processes the message and updates the state of the [`Program`]. - fn update( - &self, - state: &mut State, - message: Message, - ) -> impl Into<Task<Message>>; -} + fn load(&self) -> Task<Self::Message> { + self.program.load() + } -impl<T, State, Message, C> Update<State, Message> for T -where - T: Fn(&mut State, Message) -> C, - C: Into<Task<Message>>, -{ - fn update( - &self, - state: &mut State, - message: Message, - ) -> impl Into<Task<Message>> { - self(state, message) - } -} + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) + } -/// The view logic of some [`Program`]. -/// -/// This trait allows the [`program`] builder to take any closure that -/// returns any `Into<Element<'_, Message>>`. -pub trait View<'a, State, Message, Theme, Renderer> { - /// Produces the widget of the [`Program`]. - fn view( - &self, - state: &'a State, - ) -> impl Into<Element<'a, Message, Theme, Renderer>>; -} + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task<Self::Message> { + self.program.update(state, message) + } -impl<'a, T, State, Message, Theme, Renderer, Widget> - View<'a, State, Message, Theme, Renderer> for T -where - T: Fn(&'a State) -> Widget, - State: 'static, - Widget: Into<Element<'a, Message, Theme, Renderer>>, -{ - fn view( - &self, - state: &'a State, - ) -> impl Into<Element<'a, Message, Theme, Renderer>> { - self(state) + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.program.view(state, window) + } + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription<Self::Message> { + self.program.subscription(state) + } + + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) + } + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> Appearance { + self.program.style(state, theme) + } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + (self.scale_factor)(state, window) + } + } + + WithScaleFactor { + program, + scale_factor: f, } } diff --git a/src/settings.rs b/src/settings.rs index f7947841..ebac7a86 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,30 +1,17 @@ //! Configure your application. -use crate::window; use crate::{Font, Pixels}; use std::borrow::Cow; -/// The settings of an iced [`Program`]. -/// -/// [`Program`]: crate::Program +/// The settings of an iced program. #[derive(Debug, Clone)] -pub struct Settings<Flags = ()> { +pub struct Settings { /// The identifier of the application. /// /// If provided, this identifier may be used to identify the application or /// communicate with it through the windowing system. pub id: Option<String>, - /// The window settings. - /// - /// They will be ignored on the Web. - pub window: window::Settings, - - /// The data needed to initialize the [`Program`]. - /// - /// [`Program`]: crate::Program - pub flags: Flags, - /// The fonts to load on boot. pub fonts: Vec<Cow<'static, [u8]>>, @@ -50,34 +37,10 @@ pub struct Settings<Flags = ()> { pub antialiasing: bool, } -impl<Flags> Settings<Flags> { - /// Initialize [`Program`] settings using the given data. - /// - /// [`Program`]: crate::Program - pub fn with_flags(flags: Flags) -> Self { - let default_settings = Settings::<()>::default(); - - Self { - flags, - id: default_settings.id, - window: default_settings.window, - fonts: default_settings.fonts, - default_font: default_settings.default_font, - default_text_size: default_settings.default_text_size, - antialiasing: default_settings.antialiasing, - } - } -} - -impl<Flags> Default for Settings<Flags> -where - Flags: Default, -{ +impl Default for Settings { fn default() -> Self { Self { id: None, - window: window::Settings::default(), - flags: Default::default(), fonts: Vec::new(), default_font: Font::default(), default_text_size: Pixels(16.0), @@ -86,12 +49,10 @@ where } } -impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> { - fn from(settings: Settings<Flags>) -> iced_winit::Settings<Flags> { +impl From<Settings> for iced_winit::Settings { + fn from(settings: Settings) -> iced_winit::Settings { iced_winit::Settings { id: settings.id, - window: settings.window, - flags: settings.flags, fonts: settings.fonts, } } diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 6d3dddde..68368aa1 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -17,7 +17,7 @@ workspace = true default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] debug = ["iced_runtime/debug"] system = ["sysinfo"] -application = [] +program = [] x11 = ["winit/x11"] wayland = ["winit/wayland"] wayland-dlopen = ["winit/wayland-dlopen"] diff --git a/winit/src/application.rs b/winit/src/application.rs deleted file mode 100644 index a93878ea..00000000 --- a/winit/src/application.rs +++ /dev/null @@ -1,1082 +0,0 @@ -//! Create interactive, native cross-platform applications. -mod state; - -pub use state::State; - -use crate::conversion; -use crate::core; -use crate::core::mouse; -use crate::core::renderer; -use crate::core::time::Instant; -use crate::core::widget::operation; -use crate::core::window; -use crate::core::{Color, Event, Point, Size, Theme}; -use crate::futures::futures; -use crate::futures::subscription::{self, Subscription}; -use crate::futures::{Executor, Runtime}; -use crate::graphics; -use crate::graphics::compositor::{self, Compositor}; -use crate::runtime::clipboard; -use crate::runtime::program::Program; -use crate::runtime::user_interface::{self, UserInterface}; -use crate::runtime::{Action, Debug, Task}; -use crate::{Clipboard, Error, Proxy, Settings}; - -use futures::channel::mpsc; -use futures::channel::oneshot; - -use std::borrow::Cow; -use std::mem::ManuallyDrop; -use std::sync::Arc; - -/// An interactive, native cross-platform application. -/// -/// This trait is the main entrypoint of 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 -/// [`Task`] 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::Theme: DefaultStyle, -{ - /// 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 [`Task`] if you need to perform some - /// async action in the background on startup. This is useful if you want to - /// load state from a file, perform an initial HTTP request, etc. - fn new(flags: Self::Flags) -> (Self, Task<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) -> String; - - /// Returns the current `Theme` of the [`Application`]. - fn theme(&self) -> Self::Theme; - - /// Returns the `Style` variation of the `Theme`. - fn style(&self, theme: &Self::Theme) -> Appearance { - theme.default_style() - } - - /// 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 [`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`. - fn scale_factor(&self) -> f64 { - 1.0 - } -} - -/// The appearance of an application. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Appearance { - /// The background [`Color`] of the application. - pub background_color: Color, - - /// The default text [`Color`] of the application. - pub text_color: Color, -} - -/// The default style of an [`Application`]. -pub trait DefaultStyle { - /// Returns the default style of an [`Application`]. - fn default_style(&self) -> Appearance; -} - -impl DefaultStyle for Theme { - fn default_style(&self) -> Appearance { - default(self) - } -} - -/// The default [`Appearance`] of an [`Application`] with the built-in [`Theme`]. -pub fn default(theme: &Theme) -> Appearance { - let palette = theme.extended_palette(); - - Appearance { - background_color: palette.background.base.color, - text_color: palette.background.base.text, - } -} - -/// Runs an [`Application`] with an executor, compositor, and the provided -/// settings. -pub fn run<A, E, C>( - settings: Settings<A::Flags>, - graphics_settings: graphics::Settings, -) -> Result<(), Error> -where - A: Application + 'static, - E: Executor + 'static, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - use futures::task; - use futures::Future; - use winit::event_loop::EventLoop; - - let mut debug = Debug::new(); - debug.startup_started(); - - let event_loop = EventLoop::with_user_event() - .build() - .expect("Create event loop"); - - let (proxy, worker) = Proxy::new(event_loop.create_proxy()); - - let mut runtime = { - let executor = E::new().map_err(Error::ExecutorCreationFailed)?; - executor.spawn(worker); - - Runtime::new(executor, proxy.clone()) - }; - - let (application, task) = { - let flags = settings.flags; - - runtime.enter(|| A::new(flags)) - }; - - if let Some(stream) = task.into_stream() { - runtime.run(stream); - } - - let id = settings.id; - let title = application.title(); - - let (boot_sender, boot_receiver) = oneshot::channel(); - let (event_sender, event_receiver) = mpsc::unbounded(); - let (control_sender, control_receiver) = mpsc::unbounded(); - - let instance = Box::pin(run_instance::<A, E, C>( - application, - runtime, - proxy, - debug, - boot_receiver, - event_receiver, - control_sender, - settings.fonts, - )); - - let context = task::Context::from_waker(task::noop_waker_ref()); - - struct Runner<Message: 'static, F, C> { - instance: std::pin::Pin<Box<F>>, - context: task::Context<'static>, - boot: Option<BootConfig<C>>, - sender: mpsc::UnboundedSender<winit::event::Event<Action<Message>>>, - receiver: mpsc::UnboundedReceiver<winit::event_loop::ControlFlow>, - error: Option<Error>, - #[cfg(target_arch = "wasm32")] - is_booted: std::rc::Rc<std::cell::RefCell<bool>>, - #[cfg(target_arch = "wasm32")] - queued_events: Vec<winit::event::Event<Action<Message>>>, - } - - struct BootConfig<C> { - sender: oneshot::Sender<Boot<C>>, - id: Option<String>, - title: String, - window_settings: window::Settings, - graphics_settings: graphics::Settings, - } - - let runner = Runner { - instance, - context, - boot: Some(BootConfig { - sender: boot_sender, - id, - title, - window_settings: settings.window, - graphics_settings, - }), - sender: event_sender, - receiver: control_receiver, - error: None, - #[cfg(target_arch = "wasm32")] - is_booted: std::rc::Rc::new(std::cell::RefCell::new(false)), - #[cfg(target_arch = "wasm32")] - queued_events: Vec::new(), - }; - - impl<Message, F, C> winit::application::ApplicationHandler<Action<Message>> - for Runner<Message, F, C> - where - F: Future<Output = ()>, - C: Compositor + 'static, - { - fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { - let Some(BootConfig { - sender, - id, - title, - window_settings, - graphics_settings, - }) = self.boot.take() - else { - return; - }; - - let should_be_visible = window_settings.visible; - let exit_on_close_request = window_settings.exit_on_close_request; - - #[cfg(target_arch = "wasm32")] - let target = window_settings.platform_specific.target.clone(); - - let window_attributes = conversion::window_attributes( - window_settings, - &title, - event_loop.primary_monitor(), - id, - ) - .with_visible(false); - - log::debug!("Window attributes: {window_attributes:#?}"); - - let window = match event_loop.create_window(window_attributes) { - Ok(window) => Arc::new(window), - Err(error) => { - self.error = Some(Error::WindowCreationFailed(error)); - event_loop.exit(); - return; - } - }; - - let finish_boot = { - let window = window.clone(); - - async move { - let compositor = - C::new(graphics_settings, window.clone()).await?; - - sender - .send(Boot { - window, - compositor, - should_be_visible, - exit_on_close_request, - }) - .ok() - .expect("Send boot event"); - - Ok::<_, graphics::Error>(()) - } - }; - - #[cfg(not(target_arch = "wasm32"))] - if let Err(error) = futures::executor::block_on(finish_boot) { - self.error = Some(Error::GraphicsCreationFailed(error)); - event_loop.exit(); - } - - #[cfg(target_arch = "wasm32")] - { - use winit::platform::web::WindowExtWebSys; - - let canvas = window.canvas().expect("Get window canvas"); - let _ = canvas.set_attribute( - "style", - "display: block; width: 100%; height: 100%", - ); - - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let body = document.body().unwrap(); - - let target = target.and_then(|target| { - body.query_selector(&format!("#{target}")) - .ok() - .unwrap_or(None) - }); - - match target { - Some(node) => { - let _ = node.replace_with_with_node_1(&canvas).expect( - &format!("Could not replace #{}", node.id()), - ); - } - None => { - let _ = body - .append_child(&canvas) - .expect("Append canvas to HTML body"); - } - }; - - let is_booted = self.is_booted.clone(); - - wasm_bindgen_futures::spawn_local(async move { - finish_boot.await.expect("Finish boot!"); - - *is_booted.borrow_mut() = true; - }); - } - } - - fn new_events( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - cause: winit::event::StartCause, - ) { - if self.boot.is_some() { - return; - } - - self.process_event( - event_loop, - winit::event::Event::NewEvents(cause), - ); - } - - fn window_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - window_id: winit::window::WindowId, - event: winit::event::WindowEvent, - ) { - #[cfg(target_os = "windows")] - let is_move_or_resize = matches!( - event, - winit::event::WindowEvent::Resized(_) - | winit::event::WindowEvent::Moved(_) - ); - - self.process_event( - event_loop, - winit::event::Event::WindowEvent { window_id, event }, - ); - - // TODO: Remove when unnecessary - // On Windows, we emulate an `AboutToWait` event after every `Resized` event - // since the event loop does not resume during resize interaction. - // More details: https://github.com/rust-windowing/winit/issues/3272 - #[cfg(target_os = "windows")] - { - if is_move_or_resize { - self.process_event( - event_loop, - winit::event::Event::AboutToWait, - ); - } - } - } - - fn user_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - action: Action<Message>, - ) { - self.process_event( - event_loop, - winit::event::Event::UserEvent(action), - ); - } - - fn about_to_wait( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - ) { - self.process_event(event_loop, winit::event::Event::AboutToWait); - } - } - - impl<Message, F, C> Runner<Message, F, C> - where - F: Future<Output = ()>, - { - fn process_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - event: winit::event::Event<Action<Message>>, - ) { - // On Wasm, events may start being processed before the compositor - // boots up. We simply queue them and process them once ready. - #[cfg(target_arch = "wasm32")] - if !*self.is_booted.borrow() { - self.queued_events.push(event); - return; - } else if !self.queued_events.is_empty() { - let queued_events = std::mem::take(&mut self.queued_events); - - // This won't infinitely recurse, since we `mem::take` - for event in queued_events { - self.process_event(event_loop, event); - } - } - - if event_loop.exiting() { - return; - } - - self.sender.start_send(event).expect("Send event"); - - let poll = self.instance.as_mut().poll(&mut self.context); - - match poll { - task::Poll::Pending => { - if let Ok(Some(flow)) = self.receiver.try_next() { - event_loop.set_control_flow(flow); - } - } - task::Poll::Ready(_) => { - event_loop.exit(); - } - } - } - } - - #[cfg(not(target_arch = "wasm32"))] - { - let mut runner = runner; - let _ = event_loop.run_app(&mut runner); - - runner.error.map(Err).unwrap_or(Ok(())) - } - - #[cfg(target_arch = "wasm32")] - { - use winit::platform::web::EventLoopExtWebSys; - let _ = event_loop.spawn_app(runner); - - Ok(()) - } -} - -struct Boot<C> { - window: Arc<winit::window::Window>, - compositor: C, - should_be_visible: bool, - exit_on_close_request: bool, -} - -async fn run_instance<A, E, C>( - mut application: A, - mut runtime: Runtime<E, Proxy<A::Message>, Action<A::Message>>, - mut proxy: Proxy<A::Message>, - mut debug: Debug, - mut boot: oneshot::Receiver<Boot<C>>, - mut event_receiver: mpsc::UnboundedReceiver< - winit::event::Event<Action<A::Message>>, - >, - mut control_sender: mpsc::UnboundedSender<winit::event_loop::ControlFlow>, - fonts: Vec<Cow<'static, [u8]>>, -) where - A: Application + 'static, - E: Executor + 'static, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - use futures::stream::StreamExt; - use winit::event; - use winit::event_loop::ControlFlow; - - let Boot { - window, - mut compositor, - should_be_visible, - exit_on_close_request, - } = boot.try_recv().ok().flatten().expect("Receive boot"); - - let mut renderer = compositor.create_renderer(); - - for font in fonts { - compositor.load_font(font); - } - - let mut state = State::new(&application, &window); - let mut viewport_version = state.viewport_version(); - let physical_size = state.physical_size(); - - let mut clipboard = Clipboard::connect(&window); - let cache = user_interface::Cache::default(); - let mut surface = compositor.create_surface( - window.clone(), - physical_size.width, - physical_size.height, - ); - let mut should_exit = false; - - if should_be_visible { - window.set_visible(true); - } - - runtime.track( - application - .subscription() - .map(Action::Output) - .into_recipes(), - ); - - let mut user_interface = ManuallyDrop::new(build_user_interface( - &application, - cache, - &mut renderer, - state.logical_size(), - &mut debug, - )); - - let mut mouse_interaction = mouse::Interaction::default(); - let mut events = Vec::new(); - let mut messages = Vec::new(); - let mut user_events = 0; - let mut redraw_pending = false; - - debug.startup_finished(); - - while let Some(event) = event_receiver.next().await { - match event { - event::Event::NewEvents( - event::StartCause::Init - | event::StartCause::ResumeTimeReached { .. }, - ) if !redraw_pending => { - window.request_redraw(); - redraw_pending = true; - } - event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), - )) => { - runtime.broadcast(subscription::Event::PlatformSpecific( - subscription::PlatformSpecific::MacOS( - subscription::MacOS::ReceivedUrl(url), - ), - )); - } - event::Event::UserEvent(action) => { - run_action( - action, - &mut user_interface, - &mut compositor, - &mut surface, - &state, - &mut renderer, - &mut messages, - &mut clipboard, - &mut should_exit, - &mut debug, - &window, - ); - - user_events += 1; - } - event::Event::WindowEvent { - event: event::WindowEvent::RedrawRequested { .. }, - .. - } => { - let physical_size = state.physical_size(); - - if physical_size.width == 0 || physical_size.height == 0 { - continue; - } - - let current_viewport_version = state.viewport_version(); - - if viewport_version != current_viewport_version { - let logical_size = state.logical_size(); - - debug.layout_started(); - user_interface = ManuallyDrop::new( - ManuallyDrop::into_inner(user_interface) - .relayout(logical_size, &mut renderer), - ); - debug.layout_finished(); - - compositor.configure_surface( - &mut surface, - physical_size.width, - physical_size.height, - ); - - viewport_version = current_viewport_version; - } - - // 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 = Event::Window( - window::Event::RedrawRequested(Instant::now()), - ); - - let (interface_state, _) = user_interface.update( - &[redraw_event.clone()], - state.cursor(), - &mut renderer, - &mut clipboard, - &mut messages, - ); - - let _ = control_sender.start_send(match interface_state { - user_interface::State::Updated { - redraw_request: Some(redraw_request), - } => match redraw_request { - window::RedrawRequest::NextFrame => { - window.request_redraw(); - - ControlFlow::Wait - } - window::RedrawRequest::At(at) => { - ControlFlow::WaitUntil(at) - } - }, - _ => ControlFlow::Wait, - }); - - runtime.broadcast(subscription::Event::Interaction { - window: window::Id::MAIN, - event: redraw_event, - status: core::event::Status::Ignored, - }); - - debug.draw_started(); - let new_mouse_interaction = user_interface.draw( - &mut renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - state.cursor(), - ); - redraw_pending = false; - debug.draw_finished(); - - if new_mouse_interaction != mouse_interaction { - window.set_cursor(conversion::mouse_interaction( - new_mouse_interaction, - )); - - mouse_interaction = new_mouse_interaction; - } - - debug.render_started(); - match compositor.present( - &mut renderer, - &mut surface, - state.viewport(), - state.background_color(), - &debug.overlay(), - ) { - Ok(()) => { - debug.render_finished(); - - // TODO: Handle animations! - // Maybe we can use `ControlFlow::WaitUntil` for this. - } - Err(error) => match error { - // This is an unrecoverable error. - compositor::SurfaceError::OutOfMemory => { - panic!("{error:?}"); - } - _ => { - debug.render_finished(); - - // Try rendering again next frame. - window.request_redraw(); - } - }, - } - } - event::Event::WindowEvent { - event: window_event, - .. - } => { - if requests_exit(&window_event, state.modifiers()) - && exit_on_close_request - { - break; - } - - state.update(&window, &window_event, &mut debug); - - if let Some(event) = conversion::window_event( - window_event, - state.scale_factor(), - state.modifiers(), - ) { - events.push(event); - } - } - event::Event::AboutToWait => { - if events.is_empty() && messages.is_empty() { - continue; - } - - debug.event_processing_started(); - - let (interface_state, statuses) = user_interface.update( - &events, - state.cursor(), - &mut renderer, - &mut clipboard, - &mut messages, - ); - - debug.event_processing_finished(); - - for (event, status) in - events.drain(..).zip(statuses.into_iter()) - { - runtime.broadcast(subscription::Event::Interaction { - window: window::Id::MAIN, - event, - status, - }); - } - - if !messages.is_empty() - || matches!( - interface_state, - user_interface::State::Outdated - ) - { - let cache = - ManuallyDrop::into_inner(user_interface).into_cache(); - - // Update application - update( - &mut application, - &mut state, - &mut runtime, - &mut debug, - &mut messages, - &window, - ); - - user_interface = ManuallyDrop::new(build_user_interface( - &application, - cache, - &mut renderer, - state.logical_size(), - &mut debug, - )); - - if should_exit { - break; - } - - if user_events > 0 { - proxy.free_slots(user_events); - user_events = 0; - } - } - - if !redraw_pending { - window.request_redraw(); - redraw_pending = true; - } - } - _ => {} - } - } - - // Manually drop the user interface - drop(ManuallyDrop::into_inner(user_interface)); -} - -/// Returns true if the provided event should cause an [`Application`] to -/// exit. -pub fn requests_exit( - event: &winit::event::WindowEvent, - _modifiers: winit::keyboard::ModifiersState, -) -> bool { - use winit::event::WindowEvent; - - match event { - WindowEvent::CloseRequested => true, - #[cfg(target_os = "macos")] - WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: winit::keyboard::Key::Character(c), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } if c == "q" && _modifiers.super_key() => true, - _ => false, - } -} - -/// Builds a [`UserInterface`] for the provided [`Application`], logging -/// [`struct@Debug`] information accordingly. -pub fn build_user_interface<'a, A: Application>( - application: &'a A, - cache: user_interface::Cache, - renderer: &mut A::Renderer, - size: Size, - debug: &mut Debug, -) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> -where - A::Theme: DefaultStyle, -{ - debug.view_started(); - let view = application.view(); - debug.view_finished(); - - debug.layout_started(); - let user_interface = UserInterface::build(view, size, cache, renderer); - debug.layout_finished(); - - user_interface -} - -/// Updates an [`Application`] by feeding it the provided messages, spawning any -/// resulting [`Task`], and tracking its [`Subscription`]. -pub fn update<A: Application, E: Executor>( - application: &mut A, - state: &mut State<A>, - runtime: &mut Runtime<E, Proxy<A::Message>, Action<A::Message>>, - debug: &mut Debug, - messages: &mut Vec<A::Message>, - window: &winit::window::Window, -) where - A::Theme: DefaultStyle, -{ - for message in messages.drain(..) { - debug.log_message(&message); - - debug.update_started(); - let task = runtime.enter(|| application.update(message)); - debug.update_finished(); - - if let Some(stream) = task.into_stream() { - runtime.run(stream); - } - } - - state.synchronize(application, window); - - let subscription = application.subscription(); - runtime.track(subscription.map(Action::Output).into_recipes()); -} - -/// Runs the actions of a [`Task`]. -pub fn run_action<A, C>( - action: Action<A::Message>, - user_interface: &mut UserInterface<'_, A::Message, A::Theme, C::Renderer>, - compositor: &mut C, - surface: &mut C::Surface, - state: &State<A>, - renderer: &mut A::Renderer, - messages: &mut Vec<A::Message>, - clipboard: &mut Clipboard, - should_exit: &mut bool, - debug: &mut Debug, - window: &winit::window::Window, -) where - A: Application, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - use crate::runtime::system; - use crate::runtime::window; - - match action { - Action::Clipboard(action) => match action { - clipboard::Action::Read { target, channel } => { - let _ = channel.send(clipboard.read(target)); - } - clipboard::Action::Write { target, contents } => { - clipboard.write(target, contents); - } - }, - Action::Window(action) => match action { - window::Action::Close(_id) => { - *should_exit = true; - } - window::Action::Drag(_id) => { - let _res = window.drag_window(); - } - window::Action::Open { .. } => { - log::warn!( - "Spawning a window is only available with \ - multi-window applications." - ); - } - window::Action::Resize(_id, size) => { - let _ = window.request_inner_size(winit::dpi::LogicalSize { - width: size.width, - height: size.height, - }); - } - window::Action::FetchSize(_id, channel) => { - let size = - window.inner_size().to_logical(window.scale_factor()); - - let _ = channel.send(Size::new(size.width, size.height)); - } - window::Action::FetchMaximized(_id, channel) => { - let _ = channel.send(window.is_maximized()); - } - window::Action::Maximize(_id, maximized) => { - window.set_maximized(maximized); - } - window::Action::FetchMinimized(_id, channel) => { - let _ = channel.send(window.is_minimized()); - } - window::Action::Minimize(_id, minimized) => { - window.set_minimized(minimized); - } - window::Action::FetchPosition(_id, channel) => { - let position = window - .inner_position() - .map(|position| { - let position = - position.to_logical::<f32>(window.scale_factor()); - - Point::new(position.x, position.y) - }) - .ok(); - - let _ = channel.send(position); - } - window::Action::Move(_id, position) => { - window.set_outer_position(winit::dpi::LogicalPosition { - x: position.x, - y: position.y, - }); - } - window::Action::ChangeMode(_id, mode) => { - window.set_visible(conversion::visible(mode)); - window.set_fullscreen(conversion::fullscreen( - window.current_monitor(), - mode, - )); - } - window::Action::ChangeIcon(_id, icon) => { - window.set_window_icon(conversion::icon(icon)); - } - window::Action::FetchMode(_id, channel) => { - let mode = if window.is_visible().unwrap_or(true) { - conversion::mode(window.fullscreen()) - } else { - core::window::Mode::Hidden - }; - - let _ = channel.send(mode); - } - window::Action::ToggleMaximize(_id) => { - window.set_maximized(!window.is_maximized()); - } - window::Action::ToggleDecorations(_id) => { - window.set_decorations(!window.is_decorated()); - } - window::Action::RequestUserAttention(_id, user_attention) => { - window.request_user_attention( - user_attention.map(conversion::user_attention), - ); - } - window::Action::GainFocus(_id) => { - window.focus_window(); - } - window::Action::ChangeLevel(_id, level) => { - window.set_window_level(conversion::window_level(level)); - } - window::Action::ShowSystemMenu(_id) => { - if let mouse::Cursor::Available(point) = state.cursor() { - window.show_window_menu(winit::dpi::LogicalPosition { - x: point.x, - y: point.y, - }); - } - } - window::Action::FetchRawId(_id, channel) => { - let _ = channel.send(window.id().into()); - } - window::Action::RunWithHandle(_id, f) => { - use window::raw_window_handle::HasWindowHandle; - - if let Ok(handle) = window.window_handle() { - f(handle); - } - } - - window::Action::Screenshot(_id, channel) => { - let bytes = compositor.screenshot( - renderer, - surface, - state.viewport(), - state.background_color(), - &debug.overlay(), - ); - - let _ = channel.send(window::Screenshot::new( - bytes, - state.physical_size(), - state.viewport().scale_factor(), - )); - } - }, - Action::System(action) => match action { - system::Action::QueryInformation(_channel) => { - #[cfg(feature = "system")] - { - let graphics_info = compositor.fetch_information(); - - let _ = std::thread::spawn(move || { - let information = - crate::system::information(graphics_info); - - let _ = _channel.send(information); - }); - } - } - }, - Action::Widget(operation) => { - let mut current_operation = Some(operation); - - while let Some(mut operation) = current_operation.take() { - user_interface.operate(renderer, operation.as_mut()); - - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(()) => {} - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } - } - } - } - Action::LoadFont { bytes, channel } => { - // TODO: Error handling (?) - compositor.load_font(bytes); - - let _ = channel.send(Ok(())); - } - Action::Output(message) => { - messages.push(message); - } - } -} diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs deleted file mode 100644 index a0a06933..00000000 --- a/winit/src/application/state.rs +++ /dev/null @@ -1,221 +0,0 @@ -use crate::application; -use crate::conversion; -use crate::core::mouse; -use crate::core::{Color, Size}; -use crate::graphics::Viewport; -use crate::runtime::Debug; -use crate::Application; - -use std::marker::PhantomData; -use winit::event::{Touch, WindowEvent}; -use winit::window::Window; - -/// The state of a windowed [`Application`]. -#[allow(missing_debug_implementations)] -pub struct State<A: Application> -where - A::Theme: application::DefaultStyle, -{ - title: String, - scale_factor: f64, - viewport: Viewport, - viewport_version: usize, - cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, - modifiers: winit::keyboard::ModifiersState, - theme: A::Theme, - appearance: application::Appearance, - application: PhantomData<A>, -} - -impl<A: Application> State<A> -where - A::Theme: application::DefaultStyle, -{ - /// Creates a new [`State`] for the provided [`Application`] and window. - pub fn new(application: &A, window: &Window) -> Self { - let title = application.title(); - let scale_factor = application.scale_factor(); - let theme = application.theme(); - let appearance = application.style(&theme); - - 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::keyboard::ModifiersState::default(), - theme, - appearance, - application: PhantomData, - } - } - - /// Returns the current [`Viewport`] of the [`State`]. - pub fn viewport(&self) -> &Viewport { - &self.viewport - } - - /// Returns the version of the [`Viewport`] of the [`State`]. - /// - /// The version is incremented every time the [`Viewport`] changes. - pub fn viewport_version(&self) -> usize { - self.viewport_version - } - - /// Returns the physical [`Size`] of the [`Viewport`] of the [`State`]. - pub fn physical_size(&self) -> Size<u32> { - self.viewport.physical_size() - } - - /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`]. - pub fn logical_size(&self) -> Size<f32> { - self.viewport.logical_size() - } - - /// Returns the current scale factor of the [`Viewport`] of the [`State`]. - pub fn scale_factor(&self) -> f64 { - self.viewport.scale_factor() - } - - /// Returns the current cursor position of the [`State`]. - pub fn cursor(&self) -> mouse::Cursor { - self.cursor_position - .map(|cursor_position| { - conversion::cursor_position( - cursor_position, - self.viewport.scale_factor(), - ) - }) - .map(mouse::Cursor::Available) - .unwrap_or(mouse::Cursor::Unavailable) - } - - /// Returns the current keyboard modifiers of the [`State`]. - pub fn modifiers(&self) -> winit::keyboard::ModifiersState { - self.modifiers - } - - /// Returns the current theme of the [`State`]. - pub fn theme(&self) -> &A::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 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, - .. - } => { - let size = self.viewport.physical_size(); - - 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.state(); - } - #[cfg(feature = "debug")] - WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: - winit::keyboard::Key::Named( - winit::keyboard::NamedKey::F12, - ), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } => _debug.toggle(), - _ => {} - } - } - - /// Synchronizes the [`State`] with its [`Application`] and its respective - /// window. - /// - /// Normally an [`Application`] should be synchronized with its [`State`] - /// and window after calling [`crate::application::update`]. - pub fn synchronize(&mut self, application: &A, window: &Window) { - // Update window title - let new_title = application.title(); - - 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(); - 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(); - self.appearance = application.style(&self.theme); - } -} diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 3619cde8..3c11b72a 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -5,7 +5,7 @@ //! `iced_winit` offers some convenient abstractions on top of [`iced_runtime`] //! to quickstart development when using [`winit`]. //! -//! It exposes a renderer-agnostic [`Application`] trait that can be implemented +//! It exposes a renderer-agnostic [`Program`] trait that can be implemented //! and then run with a simple call. The use of this trait is optional. //! //! Additionally, a [`conversion`] module is available for users that decide to @@ -24,24 +24,23 @@ pub use iced_runtime::core; pub use iced_runtime::futures; pub use winit; -#[cfg(feature = "multi-window")] -pub mod multi_window; - -#[cfg(feature = "application")] -pub mod application; pub mod clipboard; pub mod conversion; pub mod settings; +#[cfg(feature = "program")] +pub mod program; + #[cfg(feature = "system")] pub mod system; mod error; mod proxy; -#[cfg(feature = "application")] -pub use application::Application; pub use clipboard::Clipboard; pub use error::Error; pub use proxy::Proxy; pub use settings::Settings; + +#[cfg(feature = "program")] +pub use program::Program; diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs deleted file mode 100644 index 8bd8a64d..00000000 --- a/winit/src/multi_window.rs +++ /dev/null @@ -1,1322 +0,0 @@ -//! Create interactive, native cross-platform applications for WGPU. -mod state; -mod window_manager; - -pub use state::State; - -use crate::conversion; -use crate::core; -use crate::core::mouse; -use crate::core::renderer; -use crate::core::widget::operation; -use crate::core::window; -use crate::core::{Point, Size}; -use crate::futures::futures::channel::mpsc; -use crate::futures::futures::channel::oneshot; -use crate::futures::futures::executor; -use crate::futures::futures::task; -use crate::futures::futures::{Future, StreamExt}; -use crate::futures::subscription::{self, Subscription}; -use crate::futures::{Executor, Runtime}; -use crate::graphics; -use crate::graphics::{compositor, Compositor}; -use crate::multi_window::window_manager::WindowManager; -use crate::runtime::multi_window::Program; -use crate::runtime::user_interface::{self, UserInterface}; -use crate::runtime::Debug; -use crate::runtime::{Action, Task}; -use crate::{Clipboard, Error, Proxy, Settings}; - -pub use crate::application::{default, Appearance, DefaultStyle}; - -use rustc_hash::FxHashMap; -use std::mem::ManuallyDrop; -use std::sync::Arc; -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 -/// [`Task`] 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::Theme: DefaultStyle, -{ - /// 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 [`Task`] if you need to perform some - /// async action in the background on startup. This is useful if you want to - /// load state from a file, perform an initial HTTP request, etc. - fn new(flags: Self::Flags) -> (Self, Task<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::Theme; - - /// Returns the `Style` variation of the `Theme`. - fn style(&self, theme: &Self::Theme) -> Appearance { - theme.default_style() - } - - /// 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>, - graphics_settings: graphics::Settings, -) -> Result<(), Error> -where - A: Application + 'static, - E: Executor + 'static, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - use winit::event_loop::EventLoop; - - let mut debug = Debug::new(); - debug.startup_started(); - - let event_loop = EventLoop::with_user_event() - .build() - .expect("Create event loop"); - - let (proxy, worker) = Proxy::new(event_loop.create_proxy()); - - let mut runtime = { - let executor = E::new().map_err(Error::ExecutorCreationFailed)?; - executor.spawn(worker); - - Runtime::new(executor, proxy.clone()) - }; - - let (application, task) = { - let flags = settings.flags; - - runtime.enter(|| A::new(flags)) - }; - - if let Some(stream) = task.into_stream() { - runtime.run(stream); - } - - let id = settings.id; - let title = application.title(window::Id::MAIN); - - let (boot_sender, boot_receiver) = oneshot::channel(); - let (event_sender, event_receiver) = mpsc::unbounded(); - let (control_sender, control_receiver) = mpsc::unbounded(); - - let instance = Box::pin(run_instance::<A, E, C>( - application, - runtime, - proxy, - debug, - boot_receiver, - event_receiver, - control_sender, - )); - - let context = task::Context::from_waker(task::noop_waker_ref()); - - struct Runner<Message: 'static, F, C> { - instance: std::pin::Pin<Box<F>>, - context: task::Context<'static>, - boot: Option<BootConfig<C>>, - sender: mpsc::UnboundedSender<Event<Message>>, - receiver: mpsc::UnboundedReceiver<Control>, - error: Option<Error>, - } - - struct BootConfig<C> { - sender: oneshot::Sender<Boot<C>>, - id: Option<String>, - title: String, - window_settings: window::Settings, - graphics_settings: graphics::Settings, - } - - let mut runner = Runner { - instance, - context, - boot: Some(BootConfig { - sender: boot_sender, - id, - title, - window_settings: settings.window, - graphics_settings, - }), - sender: event_sender, - receiver: control_receiver, - error: None, - }; - - impl<Message, F, C> winit::application::ApplicationHandler<Message> - for Runner<Message, F, C> - where - F: Future<Output = ()>, - C: Compositor, - { - fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { - let Some(BootConfig { - sender, - id, - title, - window_settings, - graphics_settings, - }) = self.boot.take() - else { - return; - }; - - let should_be_visible = window_settings.visible; - let exit_on_close_request = window_settings.exit_on_close_request; - - let window_attributes = conversion::window_attributes( - window_settings, - &title, - event_loop.primary_monitor(), - id, - ) - .with_visible(false); - - log::debug!("Window attributes: {window_attributes:#?}"); - - let window = match event_loop.create_window(window_attributes) { - Ok(window) => Arc::new(window), - Err(error) => { - self.error = Some(Error::WindowCreationFailed(error)); - event_loop.exit(); - return; - } - }; - - let finish_boot = async move { - let compositor = - C::new(graphics_settings, window.clone()).await?; - - sender - .send(Boot { - window, - compositor, - should_be_visible, - exit_on_close_request, - }) - .ok() - .expect("Send boot event"); - - Ok::<_, graphics::Error>(()) - }; - - if let Err(error) = executor::block_on(finish_boot) { - self.error = Some(Error::GraphicsCreationFailed(error)); - event_loop.exit(); - } - } - - fn new_events( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - cause: winit::event::StartCause, - ) { - if self.boot.is_some() { - return; - } - - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::NewEvents(cause)), - ); - } - - fn window_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - window_id: winit::window::WindowId, - event: winit::event::WindowEvent, - ) { - #[cfg(target_os = "windows")] - let is_move_or_resize = matches!( - event, - winit::event::WindowEvent::Resized(_) - | winit::event::WindowEvent::Moved(_) - ); - - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::WindowEvent { - window_id, - event, - }), - ); - - // TODO: Remove when unnecessary - // On Windows, we emulate an `AboutToWait` event after every `Resized` event - // since the event loop does not resume during resize interaction. - // More details: https://github.com/rust-windowing/winit/issues/3272 - #[cfg(target_os = "windows")] - { - if is_move_or_resize { - self.process_event( - event_loop, - Event::EventLoopAwakened( - winit::event::Event::AboutToWait, - ), - ); - } - } - } - - fn user_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - message: Message, - ) { - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::UserEvent( - message, - )), - ); - } - - fn about_to_wait( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - ) { - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::AboutToWait), - ); - } - } - - impl<Message, F, C> Runner<Message, F, C> - where - F: Future<Output = ()>, - C: Compositor, - { - fn process_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - event: Event<Message>, - ) { - if event_loop.exiting() { - return; - } - - self.sender.start_send(event).expect("Send event"); - - loop { - let poll = self.instance.as_mut().poll(&mut self.context); - - match poll { - task::Poll::Pending => match self.receiver.try_next() { - Ok(Some(control)) => match control { - Control::ChangeFlow(flow) => { - use winit::event_loop::ControlFlow; - - match (event_loop.control_flow(), flow) { - ( - ControlFlow::WaitUntil(current), - ControlFlow::WaitUntil(new), - ) if new < current => {} - ( - ControlFlow::WaitUntil(target), - ControlFlow::Wait, - ) if target > Instant::now() => {} - _ => { - event_loop.set_control_flow(flow); - } - } - } - Control::CreateWindow { - id, - settings, - title, - monitor, - } => { - let exit_on_close_request = - settings.exit_on_close_request; - - let window = event_loop - .create_window( - conversion::window_attributes( - settings, &title, monitor, None, - ), - ) - .expect("Create window"); - - self.process_event( - event_loop, - Event::WindowCreated { - id, - window, - exit_on_close_request, - }, - ); - } - Control::Exit => { - event_loop.exit(); - } - }, - _ => { - break; - } - }, - task::Poll::Ready(_) => { - event_loop.exit(); - break; - } - }; - } - } - } - - let _ = event_loop.run_app(&mut runner); - - Ok(()) -} - -struct Boot<C> { - window: Arc<winit::window::Window>, - compositor: C, - should_be_visible: bool, - exit_on_close_request: bool, -} - -enum Event<Message: 'static> { - WindowCreated { - id: window::Id, - window: winit::window::Window, - exit_on_close_request: bool, - }, - EventLoopAwakened(winit::event::Event<Message>), -} - -enum Control { - ChangeFlow(winit::event_loop::ControlFlow), - Exit, - 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 runtime: Runtime<E, Proxy<A::Message>, Action<A::Message>>, - mut proxy: Proxy<A::Message>, - mut debug: Debug, - mut boot: oneshot::Receiver<Boot<C>>, - mut event_receiver: mpsc::UnboundedReceiver<Event<Action<A::Message>>>, - mut control_sender: mpsc::UnboundedSender<Control>, -) where - A: Application + 'static, - E: Executor + 'static, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - use winit::event; - use winit::event_loop::ControlFlow; - - let Boot { - window: main_window, - mut compositor, - should_be_visible, - exit_on_close_request, - } = boot.try_recv().ok().flatten().expect("Receive boot"); - - let mut window_manager = WindowManager::new(); - - let _ = window_manager.insert( - window::Id::MAIN, - main_window, - &application, - &mut compositor, - exit_on_close_request, - ); - - let main_window = window_manager - .get_mut(window::Id::MAIN) - .expect("Get main window"); - - if should_be_visible { - main_window.raw.set_visible(true); - } - - let mut clipboard = Clipboard::connect(&main_window.raw); - let mut events = { - vec![( - window::Id::MAIN, - core::Event::Window(window::Event::Opened { - position: main_window.position(), - size: main_window.size(), - }), - )] - }; - - let mut ui_caches = FxHashMap::default(); - let mut user_interfaces = ManuallyDrop::new(build_user_interfaces( - &application, - &mut debug, - &mut window_manager, - FxHashMap::from_iter([( - window::Id::MAIN, - user_interface::Cache::default(), - )]), - )); - - runtime.track( - application - .subscription() - .map(Action::Output) - .into_recipes(), - ); - - let mut messages = Vec::new(); - let mut user_events = 0; - - 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, - Arc::new(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(( - id, - core::Event::Window(window::Event::Opened { - position: window.position(), - size: window.size(), - }), - )); - } - Event::EventLoopAwakened(event) => { - match event { - event::Event::NewEvents( - event::StartCause::Init - | event::StartCause::ResumeTimeReached { .. }, - ) => { - for (_id, window) in window_manager.iter_mut() { - // TODO once widgets can request to be redrawn, we can avoid always requesting a - // redraw - window.raw.request_redraw(); - } - } - event::Event::PlatformSpecific( - event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), - ), - ) => { - runtime.broadcast( - subscription::Event::PlatformSpecific( - subscription::PlatformSpecific::MacOS( - subscription::MacOS::ReceivedUrl(url), - ), - ), - ); - } - event::Event::UserEvent(action) => { - run_action( - action, - &application, - &mut compositor, - &mut messages, - &mut clipboard, - &mut control_sender, - &mut debug, - &mut user_interfaces, - &mut window_manager, - &mut ui_caches, - ); - user_events += 1; - } - event::Event::WindowEvent { - window_id: id, - event: event::WindowEvent::RedrawRequested, - .. - } => { - let Some((id, window)) = - window_manager.get_mut_alias(id) - else { - continue; - }; - - // 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( - window::Event::RedrawRequested(Instant::now()), - ); - - let cursor = window.state.cursor(); - - let ui = user_interfaces - .get_mut(&id) - .expect("Get user interface"); - - let (ui_state, _) = ui.update( - &[redraw_event.clone()], - cursor, - &mut window.renderer, - &mut clipboard, - &mut messages, - ); - - debug.draw_started(); - let new_mouse_interaction = ui.draw( - &mut window.renderer, - window.state.theme(), - &renderer::Style { - text_color: window.state.text_color(), - }, - cursor, - ); - debug.draw_finished(); - - if new_mouse_interaction != window.mouse_interaction { - window.raw.set_cursor( - conversion::mouse_interaction( - new_mouse_interaction, - ), - ); - - window.mouse_interaction = new_mouse_interaction; - } - - runtime.broadcast(subscription::Event::Interaction { - window: id, - event: redraw_event, - status: core::event::Status::Ignored, - }); - - 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 => { - window.raw.request_redraw(); - - ControlFlow::Wait - } - window::RedrawRequest::At(at) => { - ControlFlow::WaitUntil(at) - } - }, - _ => ControlFlow::Wait, - }, - )); - - let physical_size = window.state.physical_size(); - - if physical_size.width == 0 || physical_size.height == 0 - { - continue; - } - - if window.viewport_version - != window.state.viewport_version() - { - let logical_size = window.state.logical_size(); - - debug.layout_started(); - let ui = user_interfaces - .remove(&id) - .expect("Remove user interface"); - - let _ = user_interfaces.insert( - id, - ui.relayout(logical_size, &mut window.renderer), - ); - debug.layout_finished(); - - 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(), - ); - debug.draw_finished(); - - if new_mouse_interaction != window.mouse_interaction - { - window.raw.set_cursor( - conversion::mouse_interaction( - new_mouse_interaction, - ), - ); - - window.mouse_interaction = - new_mouse_interaction; - } - - compositor.configure_surface( - &mut window.surface, - physical_size.width, - physical_size.height, - ); - - window.viewport_version = - window.state.viewport_version(); - } - - debug.render_started(); - 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(( - id, - core::Event::Window(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( - window_event, - window.state.scale_factor(), - window.state.modifiers(), - ) { - events.push((id, event)); - } - } - } - event::Event::AboutToWait => { - if events.is_empty() && messages.is_empty() { - continue; - } - - debug.event_processing_started(); - let mut uis_stale = false; - - for (id, window) in window_manager.iter_mut() { - let mut window_events = vec![]; - - events.retain(|(window_id, event)| { - if *window_id == id { - window_events.push(event.clone()); - false - } else { - true - } - }); - - if window_events.is_empty() && messages.is_empty() { - continue; - } - - let (ui_state, statuses) = user_interfaces - .get_mut(&id) - .expect("Get user interface") - .update( - &window_events, - window.state.cursor(), - &mut window.renderer, - &mut clipboard, - &mut messages, - ); - - window.raw.request_redraw(); - - 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( - subscription::Event::Interaction { - window: id, - event, - status, - }, - ); - } - } - - for (id, event) in events.drain(..) { - runtime.broadcast( - subscription::Event::Interaction { - window: id, - event, - status: core::event::Status::Ignored, - }, - ); - } - - debug.event_processing_finished(); - - // TODO mw application update returns which window IDs to update - if !messages.is_empty() || uis_stale { - let cached_interfaces: FxHashMap< - window::Id, - user_interface::Cache, - > = ManuallyDrop::into_inner(user_interfaces) - .drain() - .map(|(id, ui)| (id, ui.into_cache())) - .collect(); - - // Update application - update( - &mut application, - &mut runtime, - &mut debug, - &mut messages, - ); - - // 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, - ); - - // TODO once widgets can request to be redrawn, we can avoid always requesting a - // redraw - window.raw.request_redraw(); - } - - // rebuild UIs with the synchronized states - user_interfaces = - ManuallyDrop::new(build_user_interfaces( - &application, - &mut debug, - &mut window_manager, - cached_interfaces, - )); - - if user_events > 0 { - proxy.free_slots(user_events); - user_events = 0; - } - } - } - _ => {} - } - } - } - } - - 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::Theme, A::Renderer> -where - A::Theme: DefaultStyle, -{ - 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 -} - -fn update<A: Application, E: Executor>( - application: &mut A, - runtime: &mut Runtime<E, Proxy<A::Message>, Action<A::Message>>, - debug: &mut Debug, - messages: &mut Vec<A::Message>, -) where - A::Theme: DefaultStyle, -{ - for message in messages.drain(..) { - debug.log_message(&message); - debug.update_started(); - - let task = runtime.enter(|| application.update(message)); - debug.update_finished(); - - if let Some(stream) = task.into_stream() { - runtime.run(stream); - } - } - - let subscription = application.subscription(); - runtime.track(subscription.map(Action::Output).into_recipes()); -} - -fn run_action<A, C>( - action: Action<A::Message>, - application: &A, - compositor: &mut C, - messages: &mut Vec<A::Message>, - clipboard: &mut Clipboard, - control_sender: &mut mpsc::UnboundedSender<Control>, - debug: &mut Debug, - interfaces: &mut FxHashMap< - window::Id, - UserInterface<'_, A::Message, A::Theme, A::Renderer>, - >, - window_manager: &mut WindowManager<A, C>, - ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>, -) where - A: Application, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - use crate::runtime::clipboard; - use crate::runtime::system; - use crate::runtime::window; - - match action { - Action::Output(message) => { - messages.push(message); - } - Action::Clipboard(action) => match action { - clipboard::Action::Read { target, channel } => { - let _ = channel.send(clipboard.read(target)); - } - clipboard::Action::Write { target, contents } => { - clipboard.write(target, contents); - } - }, - Action::Window(action) => match action { - window::Action::Open(id, settings, channel) => { - let monitor = window_manager.last_monitor(); - - control_sender - .start_send(Control::CreateWindow { - id, - settings, - title: application.title(id), - monitor, - }) - .expect("Send control action"); - - let _ = channel.send(id); - } - window::Action::Close(id) => { - let _ = window_manager.remove(id); - let _ = ui_caches.remove(&id); - - if window_manager.is_empty() { - control_sender - .start_send(Control::Exit) - .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) { - let _ = window.raw.request_inner_size( - winit::dpi::LogicalSize { - width: size.width, - height: size.height, - }, - ); - } - } - window::Action::FetchSize(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { - let size = window - .raw - .inner_size() - .to_logical(window.raw.scale_factor()); - - let _ = channel.send(Size::new(size.width, size.height)); - } - } - window::Action::FetchMaximized(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = channel.send(window.raw.is_maximized()); - } - } - window::Action::Maximize(id, maximized) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_maximized(maximized); - } - } - window::Action::FetchMinimized(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = channel.send(window.raw.is_minimized()); - } - } - window::Action::Minimize(id, minimized) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_minimized(minimized); - } - } - window::Action::FetchPosition(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { - let position = window - .raw - .inner_position() - .map(|position| { - let position = position - .to_logical::<f32>(window.raw.scale_factor()); - - Point::new(position.x, position.y) - }) - .ok(); - - let _ = channel.send(position); - } - } - 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, channel) => { - if let Some(window) = window_manager.get_mut(id) { - let mode = if window.raw.is_visible().unwrap_or(true) { - conversion::mode(window.raw.fullscreen()) - } else { - core::window::Mode::Hidden - }; - - let _ = channel.send(mode); - } - } - window::Action::ToggleMaximize(id) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_maximized(!window.raw.is_maximized()); - } - } - window::Action::ToggleDecorations(id) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_decorations(!window.raw.is_decorated()); - } - } - window::Action::RequestUserAttention(id, attention_type) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.request_user_attention( - attention_type.map(conversion::user_attention), - ); - } - } - window::Action::GainFocus(id) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.focus_window(); - } - } - window::Action::ChangeLevel(id, level) => { - if let Some(window) = window_manager.get_mut(id) { - window - .raw - .set_window_level(conversion::window_level(level)); - } - } - window::Action::ShowSystemMenu(id) => { - if let Some(window) = window_manager.get_mut(id) { - if let mouse::Cursor::Available(point) = - window.state.cursor() - { - window.raw.show_window_menu( - winit::dpi::LogicalPosition { - x: point.x, - y: point.y, - }, - ); - } - } - } - window::Action::FetchRawId(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = channel.send(window.raw.id().into()); - } - } - window::Action::RunWithHandle(id, f) => { - use window::raw_window_handle::HasWindowHandle; - - if let Some(handle) = window_manager - .get_mut(id) - .and_then(|window| window.raw.window_handle().ok()) - { - f(handle); - } - } - window::Action::Screenshot(id, channel) => { - if let Some(window) = window_manager.get_mut(id) { - let bytes = compositor.screenshot( - &mut window.renderer, - &mut window.surface, - window.state.viewport(), - window.state.background_color(), - &debug.overlay(), - ); - - let _ = channel.send(window::Screenshot::new( - bytes, - window.state.physical_size(), - window.state.viewport().scale_factor(), - )); - } - } - }, - Action::System(action) => match action { - system::Action::QueryInformation(_channel) => { - #[cfg(feature = "system")] - { - let graphics_info = compositor.fetch_information(); - - let _ = std::thread::spawn(move || { - let information = - crate::system::information(graphics_info); - - let _ = _channel.send(information); - }); - } - } - }, - Action::Widget(operation) => { - let mut current_operation = Some(operation); - - while let Some(mut operation) = current_operation.take() { - for (id, ui) in interfaces.iter_mut() { - if let Some(window) = window_manager.get_mut(*id) { - ui.operate(&window.renderer, operation.as_mut()); - } - } - - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(()) => {} - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } - } - } - } - Action::LoadFont { bytes, channel } => { - // TODO: Error handling (?) - compositor.load_font(bytes.clone()); - - let _ = channel.send(Ok(())); - } - } -} - -/// Build the user interface for every window. -pub fn build_user_interfaces<'a, A: Application, C>( - application: &'a A, - debug: &mut Debug, - window_manager: &mut WindowManager<A, C>, - mut cached_user_interfaces: FxHashMap<window::Id, user_interface::Cache>, -) -> FxHashMap<window::Id, UserInterface<'a, A::Message, A::Theme, A::Renderer>> -where - C: Compositor<Renderer = A::Renderer>, - A::Theme: DefaultStyle, -{ - 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::keyboard::ModifiersState, -) -> bool { - match event { - #[cfg(target_os = "macos")] - winit::event::WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: winit::keyboard::Key::Character(c), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } if c == "q" && _modifiers.super_key() => true, - _ => false, - } -} diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs deleted file mode 100644 index dfd8e696..00000000 --- a/winit/src/multi_window/state.rs +++ /dev/null @@ -1,239 +0,0 @@ -use crate::conversion; -use crate::core::{mouse, window}; -use crate::core::{Color, Size}; -use crate::graphics::Viewport; -use crate::multi_window::{self, Application}; -use std::fmt::{Debug, Formatter}; - -use winit::event::{Touch, WindowEvent}; -use winit::window::Window; - -/// The state of a multi-windowed [`Application`]. -pub struct State<A: Application> -where - A::Theme: multi_window::DefaultStyle, -{ - title: String, - scale_factor: f64, - viewport: Viewport, - viewport_version: u64, - cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, - modifiers: winit::keyboard::ModifiersState, - theme: A::Theme, - appearance: multi_window::Appearance, -} - -impl<A: Application> Debug for State<A> -where - A::Theme: multi_window::DefaultStyle, -{ - 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::Theme: multi_window::DefaultStyle, -{ - /// 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 = application.style(&theme); - - 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::keyboard::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::keyboard::ModifiersState { - self.modifiers - } - - /// Returns the current theme of the [`State`]. - pub fn theme(&self) -> &A::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, - .. - } => { - let size = self.viewport.physical_size(); - - 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.state(); - } - #[cfg(feature = "debug")] - WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: - winit::keyboard::Key::Named( - winit::keyboard::NamedKey::F12, - ), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } => _debug.toggle(), - _ => {} - } - } - - /// 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 = application.style(&self.theme); - } -} diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs deleted file mode 100644 index 57a7dc7e..00000000 --- a/winit/src/multi_window/window_manager.rs +++ /dev/null @@ -1,157 +0,0 @@ -use crate::core::mouse; -use crate::core::window::Id; -use crate::core::{Point, Size}; -use crate::graphics::Compositor; -use crate::multi_window::{Application, DefaultStyle, State}; - -use std::collections::BTreeMap; -use std::sync::Arc; -use winit::monitor::MonitorHandle; - -#[allow(missing_debug_implementations)] -pub struct WindowManager<A, C> -where - A: Application, - C: Compositor<Renderer = A::Renderer>, - A::Theme: DefaultStyle, -{ - 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::Theme: DefaultStyle, -{ - pub fn new() -> Self { - Self { - aliases: BTreeMap::new(), - entries: BTreeMap::new(), - } - } - - pub fn insert( - &mut self, - id: Id, - window: Arc<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.clone(), - 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::None, - }, - ); - - 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::Theme: DefaultStyle, -{ - fn default() -> Self { - Self::new() - } -} - -#[allow(missing_debug_implementations)] -pub struct Window<A, C> -where - A: Application, - C: Compositor<Renderer = A::Renderer>, - A::Theme: DefaultStyle, -{ - pub raw: Arc<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::Theme: DefaultStyle, -{ - 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/program.rs b/winit/src/program.rs new file mode 100644 index 00000000..28cd8e52 --- /dev/null +++ b/winit/src/program.rs @@ -0,0 +1,1356 @@ +//! Create interactive, native cross-platform applications for WGPU. +mod state; +mod window_manager; + +pub use state::State; + +use crate::conversion; +use crate::core; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::operation; +use crate::core::window; +use crate::core::{Color, Element, Point, Size, Theme}; +use crate::futures::futures::channel::mpsc; +use crate::futures::futures::channel::oneshot; +use crate::futures::futures::executor; +use crate::futures::futures::task; +use crate::futures::futures::{Future, StreamExt}; +use crate::futures::subscription::{self, Subscription}; +use crate::futures::{Executor, Runtime}; +use crate::graphics; +use crate::graphics::{compositor, Compositor}; +use crate::runtime::user_interface::{self, UserInterface}; +use crate::runtime::Debug; +use crate::runtime::{self, Action, Task}; +use crate::{Clipboard, Error, Proxy, Settings}; + +use window_manager::WindowManager; + +use rustc_hash::FxHashMap; +use std::mem::ManuallyDrop; +use std::sync::Arc; +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. +/// +/// A [`Program`] can execute asynchronous actions by returning a +/// [`Task`] in some of its methods. +/// +/// When using a [`Program`] with the `debug` feature enabled, a debug view +/// can be toggled by pressing `F12`. +pub trait Program +where + Self: Sized, + Self::Theme: DefaultStyle, +{ + /// The type of __messages__ your [`Program`] will produce. + type Message: std::fmt::Debug + Send; + + /// The theme used to draw the [`Program`]. + type Theme; + + /// The [`Executor`] that will run commands and subscriptions. + /// + /// The [default executor] can be a good starting point! + /// + /// [`Executor`]: Self::Executor + /// [default executor]: crate::futures::backend::default::Executor + type Executor: Executor; + + /// The graphics backend to use to draw the [`Program`]. + type Renderer: core::Renderer + core::text::Renderer; + + /// The data needed to initialize your [`Program`]. + type Flags; + + /// Initializes the [`Program`] with the flags provided to + /// [`run`] as part of the [`Settings`]. + /// + /// Here is where you should return the initial state of your app. + /// + /// Additionally, you can return a [`Task`] if you need to perform some + /// async action in the background on startup. This is useful if you want to + /// load state from a file, perform an initial HTTP request, etc. + fn new(flags: Self::Flags) -> (Self, Task<Self::Message>); + + /// Returns the current title of the [`Program`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self, window: window::Id) -> String; + + /// Handles a __message__ and updates the state of the [`Program`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Task`] returned will be executed immediately in the background by the + /// runtime. + fn update(&mut self, message: Self::Message) -> Task<Self::Message>; + + /// Returns the widgets to display in the [`Program`] for the `window`. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; + + /// Returns the current `Theme` of the [`Program`]. + fn theme(&self, window: window::Id) -> Self::Theme; + + /// Returns the `Style` variation of the `Theme`. + fn style(&self, theme: &Self::Theme) -> Appearance { + theme.default_style() + } + + /// 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 [`Program`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + #[allow(unused_variables)] + fn scale_factor(&self, window: window::Id) -> f64 { + 1.0 + } +} + +/// The appearance of a program. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Appearance { + /// The background [`Color`] of the application. + pub background_color: Color, + + /// The default text [`Color`] of the application. + pub text_color: Color, +} + +/// The default style of a [`Program`]. +pub trait DefaultStyle { + /// Returns the default style of a [`Program`]. + fn default_style(&self) -> Appearance; +} + +impl DefaultStyle for Theme { + fn default_style(&self) -> Appearance { + default(self) + } +} + +/// The default [`Appearance`] of a [`Program`] with the built-in [`Theme`]. +pub fn default(theme: &Theme) -> Appearance { + let palette = theme.extended_palette(); + + Appearance { + background_color: palette.background.base.color, + text_color: palette.background.base.text, + } +} +/// Runs a [`Program`] with an executor, compositor, and the provided +/// settings. +pub fn run<P, C>( + settings: Settings, + graphics_settings: graphics::Settings, + window_settings: Option<window::Settings>, + flags: P::Flags, +) -> Result<(), Error> +where + P: Program + 'static, + C: Compositor<Renderer = P::Renderer> + 'static, + P::Theme: DefaultStyle, +{ + use winit::event_loop::EventLoop; + + let mut debug = Debug::new(); + debug.startup_started(); + + let event_loop = EventLoop::with_user_event() + .build() + .expect("Create event loop"); + + let (proxy, worker) = Proxy::new(event_loop.create_proxy()); + + let mut runtime = { + let executor = + P::Executor::new().map_err(Error::ExecutorCreationFailed)?; + executor.spawn(worker); + + Runtime::new(executor, proxy.clone()) + }; + + let (application, task) = runtime.enter(|| P::new(flags)); + + if let Some(stream) = task.into_stream() { + runtime.run(stream); + } + + let (boot_sender, boot_receiver) = oneshot::channel(); + let (event_sender, event_receiver) = mpsc::unbounded(); + let (control_sender, control_receiver) = mpsc::unbounded(); + + let instance = Box::pin(run_instance::<P, C>( + application, + runtime, + proxy, + debug, + boot_receiver, + event_receiver, + control_sender, + )); + + let context = task::Context::from_waker(task::noop_waker_ref()); + + struct Runner<Message: 'static, F, C> { + instance: std::pin::Pin<Box<F>>, + context: task::Context<'static>, + id: Option<String>, + boot: Option<BootConfig<C>>, + sender: mpsc::UnboundedSender<Event<Message>>, + receiver: mpsc::UnboundedReceiver<Control>, + error: Option<Error>, + } + + struct BootConfig<C> { + sender: oneshot::Sender<Boot<C>>, + window_settings: Option<window::Settings>, + graphics_settings: graphics::Settings, + } + + let mut runner = Runner { + instance, + context, + id: settings.id, + boot: Some(BootConfig { + sender: boot_sender, + window_settings, + graphics_settings, + }), + sender: event_sender, + receiver: control_receiver, + error: None, + }; + + impl<Message, F, C> winit::application::ApplicationHandler<Message> + for Runner<Message, F, C> + where + F: Future<Output = ()>, + C: Compositor, + { + fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { + let Some(BootConfig { + sender, + window_settings, + graphics_settings, + }) = self.boot.take() + else { + return; + }; + + let window = match event_loop.create_window( + winit::window::WindowAttributes::default().with_visible(false), + ) { + Ok(window) => Arc::new(window), + Err(error) => { + self.error = Some(Error::WindowCreationFailed(error)); + event_loop.exit(); + return; + } + }; + + let clipboard = Clipboard::connect(&window); + + let finish_boot = async move { + let compositor = + C::new(graphics_settings, window.clone()).await?; + + sender + .send(Boot { + compositor, + clipboard, + window_settings, + }) + .ok() + .expect("Send boot event"); + + Ok::<_, graphics::Error>(()) + }; + + if let Err(error) = executor::block_on(finish_boot) { + self.error = Some(Error::GraphicsCreationFailed(error)); + event_loop.exit(); + } + } + + fn new_events( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + cause: winit::event::StartCause, + ) { + if self.boot.is_some() { + return; + } + + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::NewEvents(cause)), + ); + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + window_id: winit::window::WindowId, + event: winit::event::WindowEvent, + ) { + #[cfg(target_os = "windows")] + let is_move_or_resize = matches!( + event, + winit::event::WindowEvent::Resized(_) + | winit::event::WindowEvent::Moved(_) + ); + + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::WindowEvent { + window_id, + event, + }), + ); + + // TODO: Remove when unnecessary + // On Windows, we emulate an `AboutToWait` event after every `Resized` event + // since the event loop does not resume during resize interaction. + // More details: https://github.com/rust-windowing/winit/issues/3272 + #[cfg(target_os = "windows")] + { + if is_move_or_resize { + self.process_event( + event_loop, + Event::EventLoopAwakened( + winit::event::Event::AboutToWait, + ), + ); + } + } + } + + fn user_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + message: Message, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::UserEvent( + message, + )), + ); + } + + fn about_to_wait( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::AboutToWait), + ); + } + } + + impl<Message, F, C> Runner<Message, F, C> + where + F: Future<Output = ()>, + C: Compositor, + { + fn process_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + event: Event<Message>, + ) { + if event_loop.exiting() { + return; + } + + self.sender.start_send(event).expect("Send event"); + + loop { + let poll = self.instance.as_mut().poll(&mut self.context); + + match poll { + task::Poll::Pending => match self.receiver.try_next() { + Ok(Some(control)) => match control { + Control::ChangeFlow(flow) => { + use winit::event_loop::ControlFlow; + + match (event_loop.control_flow(), flow) { + ( + ControlFlow::WaitUntil(current), + ControlFlow::WaitUntil(new), + ) if new < current => {} + ( + ControlFlow::WaitUntil(target), + ControlFlow::Wait, + ) if target > Instant::now() => {} + _ => { + event_loop.set_control_flow(flow); + } + } + } + Control::CreateWindow { + id, + settings, + title, + monitor, + } => { + let exit_on_close_request = + settings.exit_on_close_request; + + let window = event_loop + .create_window( + conversion::window_attributes( + settings, + &title, + monitor + .or(event_loop + .primary_monitor()), + self.id.clone(), + ), + ) + .expect("Create window"); + + self.process_event( + event_loop, + Event::WindowCreated { + id, + window, + exit_on_close_request, + }, + ); + } + Control::Exit => { + event_loop.exit(); + } + }, + _ => { + break; + } + }, + task::Poll::Ready(_) => { + event_loop.exit(); + break; + } + }; + } + } + } + + let _ = event_loop.run_app(&mut runner); + + Ok(()) +} + +struct Boot<C> { + compositor: C, + clipboard: Clipboard, + window_settings: Option<window::Settings>, +} + +enum Event<Message: 'static> { + WindowCreated { + id: window::Id, + window: winit::window::Window, + exit_on_close_request: bool, + }, + EventLoopAwakened(winit::event::Event<Message>), +} + +enum Control { + ChangeFlow(winit::event_loop::ControlFlow), + Exit, + CreateWindow { + id: window::Id, + settings: window::Settings, + title: String, + monitor: Option<winit::monitor::MonitorHandle>, + }, +} + +async fn run_instance<P, C>( + mut program: P, + mut runtime: Runtime<P::Executor, Proxy<P::Message>, Action<P::Message>>, + mut proxy: Proxy<P::Message>, + mut debug: Debug, + mut boot: oneshot::Receiver<Boot<C>>, + mut event_receiver: mpsc::UnboundedReceiver<Event<Action<P::Message>>>, + mut control_sender: mpsc::UnboundedSender<Control>, +) where + P: Program + 'static, + C: Compositor<Renderer = P::Renderer> + 'static, + P::Theme: DefaultStyle, +{ + use winit::event; + use winit::event_loop::ControlFlow; + + let Boot { + mut compositor, + mut clipboard, + window_settings, + } = boot.try_recv().ok().flatten().expect("Receive boot"); + + let mut window_manager = WindowManager::new(); + + let mut events = Vec::new(); + let mut messages = Vec::new(); + let mut actions = 0; + + let mut ui_caches = FxHashMap::default(); + let mut user_interfaces = ManuallyDrop::new(build_user_interfaces( + &program, + &mut debug, + &mut window_manager, + FxHashMap::from_iter([( + window::Id::MAIN, + user_interface::Cache::default(), + )]), + )); + + runtime.track(program.subscription().map(Action::Output).into_recipes()); + + let is_daemon = window_settings.is_none(); + + if let Some(window_settings) = window_settings { + let (sender, _receiver) = oneshot::channel(); + + proxy.send_action(Action::Window(runtime::window::Action::Open( + window::Id::unique(), + window_settings, + sender, + ))); + } + + 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, + Arc::new(window), + &program, + &mut compositor, + exit_on_close_request, + ); + + let logical_size = window.state.logical_size(); + + let _ = user_interfaces.insert( + id, + build_user_interface( + &program, + user_interface::Cache::default(), + &mut window.renderer, + logical_size, + &mut debug, + id, + ), + ); + let _ = ui_caches.insert(id, user_interface::Cache::default()); + + events.push(( + id, + core::Event::Window(window::Event::Opened { + position: window.position(), + size: window.size(), + }), + )); + } + Event::EventLoopAwakened(event) => { + match event { + event::Event::NewEvents( + event::StartCause::Init + | event::StartCause::ResumeTimeReached { .. }, + ) => { + for (_id, window) in window_manager.iter_mut() { + // TODO once widgets can request to be redrawn, we can avoid always requesting a + // redraw + window.raw.request_redraw(); + } + } + event::Event::PlatformSpecific( + event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + ), + ) => { + runtime.broadcast( + subscription::Event::PlatformSpecific( + subscription::PlatformSpecific::MacOS( + subscription::MacOS::ReceivedUrl(url), + ), + ), + ); + } + event::Event::UserEvent(action) => { + run_action( + action, + &program, + &mut compositor, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut debug, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + ); + actions += 1; + } + event::Event::WindowEvent { + window_id: id, + event: event::WindowEvent::RedrawRequested, + .. + } => { + let Some((id, window)) = + window_manager.get_mut_alias(id) + else { + continue; + }; + + // 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( + window::Event::RedrawRequested(Instant::now()), + ); + + let cursor = window.state.cursor(); + + let ui = user_interfaces + .get_mut(&id) + .expect("Get user interface"); + + let (ui_state, _) = ui.update( + &[redraw_event.clone()], + cursor, + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + debug.draw_started(); + let new_mouse_interaction = ui.draw( + &mut window.renderer, + window.state.theme(), + &renderer::Style { + text_color: window.state.text_color(), + }, + cursor, + ); + debug.draw_finished(); + + if new_mouse_interaction != window.mouse_interaction { + window.raw.set_cursor( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); + + window.mouse_interaction = new_mouse_interaction; + } + + runtime.broadcast(subscription::Event::Interaction { + window: id, + event: redraw_event, + status: core::event::Status::Ignored, + }); + + 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 => { + window.raw.request_redraw(); + + ControlFlow::Wait + } + window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => ControlFlow::Wait, + }, + )); + + let physical_size = window.state.physical_size(); + + if physical_size.width == 0 || physical_size.height == 0 + { + continue; + } + + if window.viewport_version + != window.state.viewport_version() + { + let logical_size = window.state.logical_size(); + + debug.layout_started(); + let ui = user_interfaces + .remove(&id) + .expect("Remove user interface"); + + let _ = user_interfaces.insert( + id, + ui.relayout(logical_size, &mut window.renderer), + ); + debug.layout_finished(); + + 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(), + ); + debug.draw_finished(); + + if new_mouse_interaction != window.mouse_interaction + { + window.raw.set_cursor( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); + + window.mouse_interaction = + new_mouse_interaction; + } + + compositor.configure_surface( + &mut window.surface, + physical_size.width, + physical_size.height, + ); + + window.viewport_version = + window.state.viewport_version(); + } + + debug.render_started(); + 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, + } => { + if !is_daemon + && matches!( + window_event, + winit::event::WindowEvent::Destroyed + ) + && window_manager.is_empty() + { + break 'main; + } + + 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(( + id, + core::Event::Window(window::Event::Closed), + )); + } else { + window.state.update( + &window.raw, + &window_event, + &mut debug, + ); + + if let Some(event) = conversion::window_event( + window_event, + window.state.scale_factor(), + window.state.modifiers(), + ) { + events.push((id, event)); + } + } + } + event::Event::AboutToWait => { + if events.is_empty() && messages.is_empty() { + continue; + } + + debug.event_processing_started(); + let mut uis_stale = false; + + for (id, window) in window_manager.iter_mut() { + let mut window_events = vec![]; + + events.retain(|(window_id, event)| { + if *window_id == id { + window_events.push(event.clone()); + false + } else { + true + } + }); + + if window_events.is_empty() && messages.is_empty() { + continue; + } + + let (ui_state, statuses) = user_interfaces + .get_mut(&id) + .expect("Get user interface") + .update( + &window_events, + window.state.cursor(), + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + window.raw.request_redraw(); + + 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( + subscription::Event::Interaction { + window: id, + event, + status, + }, + ); + } + } + + for (id, event) in events.drain(..) { + runtime.broadcast( + subscription::Event::Interaction { + window: id, + event, + status: core::event::Status::Ignored, + }, + ); + } + + debug.event_processing_finished(); + + // TODO mw application update returns which window IDs to update + if !messages.is_empty() || uis_stale { + let cached_interfaces: FxHashMap< + window::Id, + user_interface::Cache, + > = ManuallyDrop::into_inner(user_interfaces) + .drain() + .map(|(id, ui)| (id, ui.into_cache())) + .collect(); + + // Update application + update( + &mut program, + &mut runtime, + &mut debug, + &mut messages, + ); + + // 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( + &program, + id, + &window.raw, + ); + + // TODO once widgets can request to be redrawn, we can avoid always requesting a + // redraw + window.raw.request_redraw(); + } + + // rebuild UIs with the synchronized states + user_interfaces = + ManuallyDrop::new(build_user_interfaces( + &program, + &mut debug, + &mut window_manager, + cached_interfaces, + )); + + if actions > 0 { + proxy.free_slots(actions); + actions = 0; + } + } + } + _ => {} + } + } + } + } + + let _ = ManuallyDrop::into_inner(user_interfaces); +} + +/// Builds a window's [`UserInterface`] for the [`Program`]. +fn build_user_interface<'a, P: Program>( + application: &'a P, + cache: user_interface::Cache, + renderer: &mut P::Renderer, + size: Size, + debug: &mut Debug, + id: window::Id, +) -> UserInterface<'a, P::Message, P::Theme, P::Renderer> +where + P::Theme: DefaultStyle, +{ + 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 +} + +fn update<P: Program, E: Executor>( + application: &mut P, + runtime: &mut Runtime<E, Proxy<P::Message>, Action<P::Message>>, + debug: &mut Debug, + messages: &mut Vec<P::Message>, +) where + P::Theme: DefaultStyle, +{ + for message in messages.drain(..) { + debug.log_message(&message); + debug.update_started(); + + let task = runtime.enter(|| application.update(message)); + debug.update_finished(); + + if let Some(stream) = task.into_stream() { + runtime.run(stream); + } + } + + let subscription = application.subscription(); + runtime.track(subscription.map(Action::Output).into_recipes()); +} + +fn run_action<P, C>( + action: Action<P::Message>, + application: &P, + compositor: &mut C, + messages: &mut Vec<P::Message>, + clipboard: &mut Clipboard, + control_sender: &mut mpsc::UnboundedSender<Control>, + debug: &mut Debug, + interfaces: &mut FxHashMap< + window::Id, + UserInterface<'_, P::Message, P::Theme, P::Renderer>, + >, + window_manager: &mut WindowManager<P, C>, + ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>, +) where + P: Program, + C: Compositor<Renderer = P::Renderer> + 'static, + P::Theme: DefaultStyle, +{ + use crate::runtime::clipboard; + use crate::runtime::system; + use crate::runtime::window; + + match action { + Action::Output(message) => { + messages.push(message); + } + Action::Clipboard(action) => match action { + clipboard::Action::Read { target, channel } => { + let _ = channel.send(clipboard.read(target)); + } + clipboard::Action::Write { target, contents } => { + clipboard.write(target, contents); + } + }, + Action::Window(action) => match action { + window::Action::Open(id, settings, channel) => { + let monitor = window_manager.last_monitor(); + + control_sender + .start_send(Control::CreateWindow { + id, + settings, + title: application.title(id), + monitor, + }) + .expect("Send control action"); + + let _ = channel.send(id); + } + window::Action::Close(id) => { + let _ = window_manager.remove(id); + let _ = ui_caches.remove(&id); + } + 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) { + let _ = window.raw.request_inner_size( + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + }, + ); + } + } + window::Action::FetchSize(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let size = window + .raw + .inner_size() + .to_logical(window.raw.scale_factor()); + + let _ = channel.send(Size::new(size.width, size.height)); + } + } + window::Action::FetchMaximized(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.is_maximized()); + } + } + window::Action::Maximize(id, maximized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(maximized); + } + } + window::Action::FetchMinimized(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.is_minimized()); + } + } + window::Action::Minimize(id, minimized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_minimized(minimized); + } + } + window::Action::FetchPosition(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let position = window + .raw + .inner_position() + .map(|position| { + let position = position + .to_logical::<f32>(window.raw.scale_factor()); + + Point::new(position.x, position.y) + }) + .ok(); + + let _ = channel.send(position); + } + } + 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, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let mode = if window.raw.is_visible().unwrap_or(true) { + conversion::mode(window.raw.fullscreen()) + } else { + core::window::Mode::Hidden + }; + + let _ = channel.send(mode); + } + } + window::Action::ToggleMaximize(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(!window.raw.is_maximized()); + } + } + window::Action::ToggleDecorations(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_decorations(!window.raw.is_decorated()); + } + } + window::Action::RequestUserAttention(id, attention_type) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.request_user_attention( + attention_type.map(conversion::user_attention), + ); + } + } + window::Action::GainFocus(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.focus_window(); + } + } + window::Action::ChangeLevel(id, level) => { + if let Some(window) = window_manager.get_mut(id) { + window + .raw + .set_window_level(conversion::window_level(level)); + } + } + window::Action::ShowSystemMenu(id) => { + if let Some(window) = window_manager.get_mut(id) { + if let mouse::Cursor::Available(point) = + window.state.cursor() + { + window.raw.show_window_menu( + winit::dpi::LogicalPosition { + x: point.x, + y: point.y, + }, + ); + } + } + } + window::Action::FetchRawId(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.id().into()); + } + } + window::Action::RunWithHandle(id, f) => { + use window::raw_window_handle::HasWindowHandle; + + if let Some(handle) = window_manager + .get_mut(id) + .and_then(|window| window.raw.window_handle().ok()) + { + f(handle); + } + } + window::Action::Screenshot(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let bytes = compositor.screenshot( + &mut window.renderer, + &mut window.surface, + window.state.viewport(), + window.state.background_color(), + &debug.overlay(), + ); + + let _ = channel.send(window::Screenshot::new( + bytes, + window.state.physical_size(), + window.state.viewport().scale_factor(), + )); + } + } + }, + Action::System(action) => match action { + system::Action::QueryInformation(_channel) => { + #[cfg(feature = "system")] + { + let graphics_info = compositor.fetch_information(); + + let _ = std::thread::spawn(move || { + let information = + crate::system::information(graphics_info); + + let _ = _channel.send(information); + }); + } + } + }, + Action::Widget(operation) => { + let mut current_operation = Some(operation); + + while let Some(mut operation) = current_operation.take() { + for (id, ui) in interfaces.iter_mut() { + if let Some(window) = window_manager.get_mut(*id) { + ui.operate(&window.renderer, operation.as_mut()); + } + } + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(()) => {} + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + } + } + } + Action::LoadFont { bytes, channel } => { + // TODO: Error handling (?) + compositor.load_font(bytes.clone()); + + let _ = channel.send(Ok(())); + } + Action::Exit => { + control_sender + .start_send(Control::Exit) + .expect("Send control action"); + } + } +} + +/// Build the user interface for every window. +pub fn build_user_interfaces<'a, P: Program, C>( + application: &'a P, + debug: &mut Debug, + window_manager: &mut WindowManager<P, C>, + mut cached_user_interfaces: FxHashMap<window::Id, user_interface::Cache>, +) -> FxHashMap<window::Id, UserInterface<'a, P::Message, P::Theme, P::Renderer>> +where + C: Compositor<Renderer = P::Renderer>, + P::Theme: DefaultStyle, +{ + 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 a [`Program`] to +/// exit. +pub fn user_force_quit( + event: &winit::event::WindowEvent, + _modifiers: winit::keyboard::ModifiersState, +) -> bool { + match event { + #[cfg(target_os = "macos")] + winit::event::WindowEvent::KeyboardInput { + event: + winit::event::KeyEvent { + logical_key: winit::keyboard::Key::Character(c), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } if c == "q" && _modifiers.super_key() => true, + _ => false, + } +} diff --git a/winit/src/program/state.rs b/winit/src/program/state.rs new file mode 100644 index 00000000..a7fa2788 --- /dev/null +++ b/winit/src/program/state.rs @@ -0,0 +1,239 @@ +use crate::conversion; +use crate::core::{mouse, window}; +use crate::core::{Color, Size}; +use crate::graphics::Viewport; +use crate::program::{self, Program}; +use std::fmt::{Debug, Formatter}; + +use winit::event::{Touch, WindowEvent}; +use winit::window::Window; + +/// The state of a multi-windowed [`Program`]. +pub struct State<P: Program> +where + P::Theme: program::DefaultStyle, +{ + title: String, + scale_factor: f64, + viewport: Viewport, + viewport_version: u64, + cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, + modifiers: winit::keyboard::ModifiersState, + theme: P::Theme, + appearance: program::Appearance, +} + +impl<P: Program> Debug for State<P> +where + P::Theme: program::DefaultStyle, +{ + 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<P: Program> State<P> +where + P::Theme: program::DefaultStyle, +{ + /// Creates a new [`State`] for the provided [`Program`]'s `window`. + pub fn new( + application: &P, + 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 = application.style(&theme); + + 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::keyboard::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::keyboard::ModifiersState { + self.modifiers + } + + /// Returns the current theme of the [`State`]. + pub fn theme(&self) -> &P::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, + .. + } => { + let size = self.viewport.physical_size(); + + 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.state(); + } + #[cfg(feature = "debug")] + WindowEvent::KeyboardInput { + event: + winit::event::KeyEvent { + logical_key: + winit::keyboard::Key::Named( + winit::keyboard::NamedKey::F12, + ), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } => _debug.toggle(), + _ => {} + } + } + + /// Synchronizes the [`State`] with its [`Program`] and its respective + /// window. + /// + /// Normally, a [`Program`] should be synchronized with its [`State`] + /// and window after calling [`State::update`]. + pub fn synchronize( + &mut self, + application: &P, + 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 = application.style(&self.theme); + } +} diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs new file mode 100644 index 00000000..fcbf79f6 --- /dev/null +++ b/winit/src/program/window_manager.rs @@ -0,0 +1,157 @@ +use crate::core::mouse; +use crate::core::window::Id; +use crate::core::{Point, Size}; +use crate::graphics::Compositor; +use crate::program::{DefaultStyle, Program, State}; + +use std::collections::BTreeMap; +use std::sync::Arc; +use winit::monitor::MonitorHandle; + +#[allow(missing_debug_implementations)] +pub struct WindowManager<P, C> +where + P: Program, + C: Compositor<Renderer = P::Renderer>, + P::Theme: DefaultStyle, +{ + aliases: BTreeMap<winit::window::WindowId, Id>, + entries: BTreeMap<Id, Window<P, C>>, +} + +impl<P, C> WindowManager<P, C> +where + P: Program, + C: Compositor<Renderer = P::Renderer>, + P::Theme: DefaultStyle, +{ + pub fn new() -> Self { + Self { + aliases: BTreeMap::new(), + entries: BTreeMap::new(), + } + } + + pub fn insert( + &mut self, + id: Id, + window: Arc<winit::window::Window>, + application: &P, + compositor: &mut C, + exit_on_close_request: bool, + ) -> &mut Window<P, 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.clone(), + 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::None, + }, + ); + + 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<P, C>)> { + self.entries.iter_mut().map(|(k, v)| (*k, v)) + } + + pub fn get_mut(&mut self, id: Id) -> Option<&mut Window<P, C>> { + self.entries.get_mut(&id) + } + + pub fn get_mut_alias( + &mut self, + id: winit::window::WindowId, + ) -> Option<(Id, &mut Window<P, 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<P, C>> { + let window = self.entries.remove(&id)?; + let _ = self.aliases.remove(&window.raw.id()); + + Some(window) + } +} + +impl<P, C> Default for WindowManager<P, C> +where + P: Program, + C: Compositor<Renderer = P::Renderer>, + P::Theme: DefaultStyle, +{ + fn default() -> Self { + Self::new() + } +} + +#[allow(missing_debug_implementations)] +pub struct Window<P, C> +where + P: Program, + C: Compositor<Renderer = P::Renderer>, + P::Theme: DefaultStyle, +{ + pub raw: Arc<winit::window::Window>, + pub state: State<P>, + pub viewport_version: u64, + pub exit_on_close_request: bool, + pub mouse_interaction: mouse::Interaction, + pub surface: C::Surface, + pub renderer: P::Renderer, +} + +impl<P, C> Window<P, C> +where + P: Program, + C: Compositor<Renderer = P::Renderer>, + P::Theme: DefaultStyle, +{ + 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/proxy.rs b/winit/src/proxy.rs index 0ab61375..d8ad8b3f 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -78,11 +78,22 @@ impl<T: 'static> Proxy<T> { /// Note: This skips the backpressure mechanism with an unbounded /// channel. Use sparingly! pub fn send(&mut self, value: T) + where + T: std::fmt::Debug, + { + self.send_action(Action::Output(value)); + } + + /// Sends an action to the event loop. + /// + /// Note: This skips the backpressure mechanism with an unbounded + /// channel. Use sparingly! + pub fn send_action(&mut self, action: Action<T>) where T: std::fmt::Debug, { self.raw - .send_event(Action::Output(value)) + .send_event(action) .expect("Send message to event loop"); } diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 2e541128..78368a04 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -1,25 +1,15 @@ //! Configure your application. -use crate::core::window; - use std::borrow::Cow; /// The settings of an application. #[derive(Debug, Clone, Default)] -pub struct Settings<Flags> { +pub struct Settings { /// The identifier of the application. /// /// If provided, this identifier may be used to identify the application or /// communicate with it through the windowing system. pub id: Option<String>, - /// The [`window::Settings`]. - pub window: window::Settings, - - /// The data needed to initialize an [`Application`]. - /// - /// [`Application`]: crate::Application - pub flags: Flags, - /// The fonts to load on boot. pub fonts: Vec<Cow<'static, [u8]>>, } -- cgit