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 --- 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 +- 11 files changed, 1772 insertions(+), 3041 deletions(-) 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 (limited to 'winit/src') 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); - - /// 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 { - 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( - settings: Settings, - graphics_settings: graphics::Settings, -) -> Result<(), Error> -where - A: Application + 'static, - E: Executor + 'static, - C: Compositor + '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::( - application, - runtime, - proxy, - debug, - boot_receiver, - event_receiver, - control_sender, - settings.fonts, - )); - - let context = task::Context::from_waker(task::noop_waker_ref()); - - struct Runner { - instance: std::pin::Pin>, - context: task::Context<'static>, - boot: Option>, - sender: mpsc::UnboundedSender>>, - receiver: mpsc::UnboundedReceiver, - error: Option, - #[cfg(target_arch = "wasm32")] - is_booted: std::rc::Rc>, - #[cfg(target_arch = "wasm32")] - queued_events: Vec>>, - } - - struct BootConfig { - sender: oneshot::Sender>, - id: Option, - 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 winit::application::ApplicationHandler> - for Runner - where - F: Future, - 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, - ) { - 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 Runner - where - F: Future, - { - fn process_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - event: winit::event::Event>, - ) { - // 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 { - window: Arc, - compositor: C, - should_be_visible: bool, - exit_on_close_request: bool, -} - -async fn run_instance( - mut application: A, - mut runtime: Runtime, Action>, - mut proxy: Proxy, - mut debug: Debug, - mut boot: oneshot::Receiver>, - mut event_receiver: mpsc::UnboundedReceiver< - winit::event::Event>, - >, - mut control_sender: mpsc::UnboundedSender, - fonts: Vec>, -) where - A: Application + 'static, - E: Executor + 'static, - C: Compositor + '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( - application: &mut A, - state: &mut State, - runtime: &mut Runtime, Action>, - debug: &mut Debug, - messages: &mut Vec, - 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( - action: Action, - user_interface: &mut UserInterface<'_, A::Message, A::Theme, C::Renderer>, - compositor: &mut C, - surface: &mut C::Surface, - state: &State, - renderer: &mut A::Renderer, - messages: &mut Vec, - clipboard: &mut Clipboard, - should_exit: &mut bool, - debug: &mut Debug, - window: &winit::window::Window, -) where - A: Application, - C: Compositor + '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::(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 -where - A::Theme: application::DefaultStyle, -{ - title: String, - scale_factor: f64, - viewport: Viewport, - viewport_version: usize, - cursor_position: Option>, - modifiers: winit::keyboard::ModifiersState, - theme: A::Theme, - appearance: application::Appearance, - application: PhantomData, -} - -impl State -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 { - self.viewport.physical_size() - } - - /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`]. - pub fn logical_size(&self) -> Size { - 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); - - /// 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 { - 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( - settings: Settings, - graphics_settings: graphics::Settings, -) -> Result<(), Error> -where - A: Application + 'static, - E: Executor + 'static, - C: Compositor + '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::( - application, - runtime, - proxy, - debug, - boot_receiver, - event_receiver, - control_sender, - )); - - let context = task::Context::from_waker(task::noop_waker_ref()); - - struct Runner { - instance: std::pin::Pin>, - context: task::Context<'static>, - boot: Option>, - sender: mpsc::UnboundedSender>, - receiver: mpsc::UnboundedReceiver, - error: Option, - } - - struct BootConfig { - sender: oneshot::Sender>, - id: Option, - 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 winit::application::ApplicationHandler - for Runner - where - F: Future, - 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 Runner - where - F: Future, - C: Compositor, - { - fn process_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - event: Event, - ) { - if event_loop.exiting() { - return; - } - - self.sender.start_send(event).expect("Send event"); - - loop { - let poll = self.instance.as_mut().poll(&mut self.context); - - match poll { - task::Poll::Pending => match self.receiver.try_next() { - Ok(Some(control)) => match control { - Control::ChangeFlow(flow) => { - use winit::event_loop::ControlFlow; - - match (event_loop.control_flow(), flow) { - ( - ControlFlow::WaitUntil(current), - ControlFlow::WaitUntil(new), - ) if 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 { - window: Arc, - compositor: C, - should_be_visible: bool, - exit_on_close_request: bool, -} - -enum Event { - WindowCreated { - id: window::Id, - window: winit::window::Window, - exit_on_close_request: bool, - }, - EventLoopAwakened(winit::event::Event), -} - -enum Control { - ChangeFlow(winit::event_loop::ControlFlow), - Exit, - CreateWindow { - id: window::Id, - settings: window::Settings, - title: String, - monitor: Option, - }, -} - -async fn run_instance( - mut application: A, - mut runtime: Runtime, Action>, - mut proxy: Proxy, - mut debug: Debug, - mut boot: oneshot::Receiver>, - mut event_receiver: mpsc::UnboundedReceiver>>, - mut control_sender: mpsc::UnboundedSender, -) where - A: Application + 'static, - E: Executor + 'static, - C: Compositor + '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( - application: &mut A, - runtime: &mut Runtime, Action>, - debug: &mut Debug, - messages: &mut Vec, -) 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( - action: Action, - application: &A, - compositor: &mut C, - messages: &mut Vec, - clipboard: &mut Clipboard, - control_sender: &mut mpsc::UnboundedSender, - debug: &mut Debug, - interfaces: &mut FxHashMap< - window::Id, - UserInterface<'_, A::Message, A::Theme, A::Renderer>, - >, - window_manager: &mut WindowManager, - ui_caches: &mut FxHashMap, -) where - A: Application, - C: Compositor + '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::(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, - mut cached_user_interfaces: FxHashMap, -) -> FxHashMap> -where - C: Compositor, - 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 -where - A::Theme: multi_window::DefaultStyle, -{ - title: String, - scale_factor: f64, - viewport: Viewport, - viewport_version: u64, - cursor_position: Option>, - modifiers: winit::keyboard::ModifiersState, - theme: A::Theme, - appearance: multi_window::Appearance, -} - -impl Debug for State -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 State -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 { - self.viewport.physical_size() - } - - /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`]. - pub fn logical_size(&self) -> Size { - 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 -where - A: Application, - C: Compositor, - A::Theme: DefaultStyle, -{ - aliases: BTreeMap, - entries: BTreeMap>, -} - -impl WindowManager -where - A: Application, - C: Compositor, - A::Theme: DefaultStyle, -{ - pub fn new() -> Self { - Self { - aliases: BTreeMap::new(), - entries: BTreeMap::new(), - } - } - - pub fn insert( - &mut self, - id: Id, - window: Arc, - application: &A, - compositor: &mut C, - exit_on_close_request: bool, - ) -> &mut Window { - 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)> { - self.entries.iter_mut().map(|(k, v)| (*k, v)) - } - - pub fn get_mut(&mut self, id: Id) -> Option<&mut Window> { - self.entries.get_mut(&id) - } - - pub fn get_mut_alias( - &mut self, - id: winit::window::WindowId, - ) -> Option<(Id, &mut Window)> { - let id = self.aliases.get(&id).copied()?; - - Some((id, self.get_mut(id)?)) - } - - pub fn last_monitor(&self) -> Option { - self.entries.values().last()?.raw.current_monitor() - } - - pub fn remove(&mut self, id: Id) -> Option> { - let window = self.entries.remove(&id)?; - let _ = self.aliases.remove(&window.raw.id()); - - Some(window) - } -} - -impl Default for WindowManager -where - A: Application, - C: Compositor, - A::Theme: DefaultStyle, -{ - fn default() -> Self { - Self::new() - } -} - -#[allow(missing_debug_implementations)] -pub struct Window -where - A: Application, - C: Compositor, - A::Theme: DefaultStyle, -{ - pub raw: Arc, - pub state: State, - pub viewport_version: u64, - pub exit_on_close_request: bool, - pub mouse_interaction: mouse::Interaction, - pub surface: C::Surface, - pub renderer: A::Renderer, -} - -impl Window -where - A: Application, - C: Compositor, - A::Theme: DefaultStyle, -{ - pub fn position(&self) -> Option { - 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); + + /// Returns the current title of the [`Program`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self, window: window::Id) -> String; + + /// Handles a __message__ and updates the state of the [`Program`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Task`] returned will be executed immediately in the background by the + /// runtime. + fn update(&mut self, message: Self::Message) -> Task; + + /// Returns the widgets to display in the [`Program`] for the `window`. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; + + /// Returns the current `Theme` of the [`Program`]. + fn theme(&self, window: window::Id) -> Self::Theme; + + /// Returns the `Style` variation of the `Theme`. + fn style(&self, theme: &Self::Theme) -> 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 { + 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( + settings: Settings, + graphics_settings: graphics::Settings, + window_settings: Option, + flags: P::Flags, +) -> Result<(), Error> +where + P: Program + 'static, + C: Compositor + '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::( + application, + runtime, + proxy, + debug, + boot_receiver, + event_receiver, + control_sender, + )); + + let context = task::Context::from_waker(task::noop_waker_ref()); + + struct Runner { + instance: std::pin::Pin>, + context: task::Context<'static>, + id: Option, + boot: Option>, + sender: mpsc::UnboundedSender>, + receiver: mpsc::UnboundedReceiver, + error: Option, + } + + struct BootConfig { + sender: oneshot::Sender>, + window_settings: Option, + 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 winit::application::ApplicationHandler + for Runner + where + F: Future, + 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 Runner + where + F: Future, + C: Compositor, + { + fn process_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + event: Event, + ) { + if event_loop.exiting() { + return; + } + + self.sender.start_send(event).expect("Send event"); + + loop { + let poll = self.instance.as_mut().poll(&mut self.context); + + match poll { + task::Poll::Pending => match self.receiver.try_next() { + Ok(Some(control)) => match control { + Control::ChangeFlow(flow) => { + use winit::event_loop::ControlFlow; + + match (event_loop.control_flow(), flow) { + ( + ControlFlow::WaitUntil(current), + ControlFlow::WaitUntil(new), + ) if 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 { + compositor: C, + clipboard: Clipboard, + window_settings: Option, +} + +enum Event { + WindowCreated { + id: window::Id, + window: winit::window::Window, + exit_on_close_request: bool, + }, + EventLoopAwakened(winit::event::Event), +} + +enum Control { + ChangeFlow(winit::event_loop::ControlFlow), + Exit, + CreateWindow { + id: window::Id, + settings: window::Settings, + title: String, + monitor: Option, + }, +} + +async fn run_instance( + mut program: P, + mut runtime: Runtime, Action>, + mut proxy: Proxy, + mut debug: Debug, + mut boot: oneshot::Receiver>, + mut event_receiver: mpsc::UnboundedReceiver>>, + mut control_sender: mpsc::UnboundedSender, +) where + P: Program + 'static, + C: Compositor + '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( + application: &mut P, + runtime: &mut Runtime, Action>, + debug: &mut Debug, + messages: &mut Vec, +) 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( + action: Action, + application: &P, + compositor: &mut C, + messages: &mut Vec, + clipboard: &mut Clipboard, + control_sender: &mut mpsc::UnboundedSender, + debug: &mut Debug, + interfaces: &mut FxHashMap< + window::Id, + UserInterface<'_, P::Message, P::Theme, P::Renderer>, + >, + window_manager: &mut WindowManager, + ui_caches: &mut FxHashMap, +) where + P: Program, + C: Compositor + '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::(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, + mut cached_user_interfaces: FxHashMap, +) -> FxHashMap> +where + C: Compositor, + 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 +where + P::Theme: program::DefaultStyle, +{ + title: String, + scale_factor: f64, + viewport: Viewport, + viewport_version: u64, + cursor_position: Option>, + modifiers: winit::keyboard::ModifiersState, + theme: P::Theme, + appearance: program::Appearance, +} + +impl Debug for State

+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 State

+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 { + self.viewport.physical_size() + } + + /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn logical_size(&self) -> Size { + 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 +where + P: Program, + C: Compositor, + P::Theme: DefaultStyle, +{ + aliases: BTreeMap, + entries: BTreeMap>, +} + +impl WindowManager +where + P: Program, + C: Compositor, + P::Theme: DefaultStyle, +{ + pub fn new() -> Self { + Self { + aliases: BTreeMap::new(), + entries: BTreeMap::new(), + } + } + + pub fn insert( + &mut self, + id: Id, + window: Arc, + application: &P, + compositor: &mut C, + exit_on_close_request: bool, + ) -> &mut Window { + 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)> { + self.entries.iter_mut().map(|(k, v)| (*k, v)) + } + + pub fn get_mut(&mut self, id: Id) -> Option<&mut Window> { + self.entries.get_mut(&id) + } + + pub fn get_mut_alias( + &mut self, + id: winit::window::WindowId, + ) -> Option<(Id, &mut Window)> { + let id = self.aliases.get(&id).copied()?; + + Some((id, self.get_mut(id)?)) + } + + pub fn last_monitor(&self) -> Option { + self.entries.values().last()?.raw.current_monitor() + } + + pub fn remove(&mut self, id: Id) -> Option> { + let window = self.entries.remove(&id)?; + let _ = self.aliases.remove(&window.raw.id()); + + Some(window) + } +} + +impl Default for WindowManager +where + P: Program, + C: Compositor, + P::Theme: DefaultStyle, +{ + fn default() -> Self { + Self::new() + } +} + +#[allow(missing_debug_implementations)] +pub struct Window +where + P: Program, + C: Compositor, + P::Theme: DefaultStyle, +{ + pub raw: Arc, + pub state: State

, + pub viewport_version: u64, + pub exit_on_close_request: bool, + pub mouse_interaction: mouse::Interaction, + pub surface: C::Surface, + pub renderer: P::Renderer, +} + +impl Window +where + P: Program, + C: Compositor, + P::Theme: DefaultStyle, +{ + pub fn position(&self) -> Option { + 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 Proxy { /// 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) 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 { +pub struct Settings { /// The identifier of the application. /// /// If provided, this identifier may be used to identify the application or /// communicate with it through the windowing system. pub id: Option, - /// The [`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>, } -- cgit