From 83aa3109d31eb7ab09c40530ef4b7d5f3e370fd4 Mon Sep 17 00:00:00 2001 From: Konkitoman <51934166+konkitoman@users.noreply.github.com> Date: Thu, 16 Nov 2023 12:25:05 +0200 Subject: [PATCH] Multiple viewports/windows (#3172) * Closes #1044 --- (new PR description written by @emilk) ## Overview This PR introduces the concept of `Viewports`, which on the native eframe backend corresponds to native OS windows. You can spawn a new viewport using `Context::show_viewport` and `Cotext::show_viewport_immediate`. These needs to be called every frame the viewport should be visible. This is implemented by the native `eframe` backend, but not the web one. ## Viewport classes The viewports form a tree of parent-child relationships. There are different classes of viewports. ### Root vieport The root viewport is the original viewport, and cannot be closed without closing the application. ### Deferred viewports These are created with `Context::show_viewport`. Deferred viewports take a closure that is called by the integration at a later time, perhaps multiple times. Deferred viewports are repainted independenantly of the parent viewport. This means communication with them need to done via channels, or `Arc/Mutex`. This is the most performant type of child viewport, though a bit more cumbersome to work with compared to immediate viewports. ### Immediate viewports These are created with `Context::show_viewport_immediate`. Immediate viewports take a `FnOnce` closure, similar to other egui functions, and is called immediately. This makes communication with them much simpler than with deferred viewports, but this simplicity comes at a cost: whenever tha parent viewports needs to be repainted, so will the child viewport, and vice versa. This means that if you have `N` viewports you are poentially doing `N` times as much CPU work. However, if all your viewports are showing animations, and thus are repainting constantly anyway, this doesn't matter. In short: immediate viewports are simpler to use, but can waste a lot of CPU time. ### Embedded viewports These are not real, independenant viewports, but is a fallback mode for when the integration does not support real viewports. In your callback is called with `ViewportClass::Embedded` it means you need to create an `egui::Window` to wrap your ui in, which will then be embedded in the parent viewport, unable to escape it. ## Using the viewports Only one viewport is active at any one time, identified wth `Context::viewport_id`. You can send commands to other viewports using `Context::send_viewport_command_to`. There is an example in . ## For integrations There are several changes relevant to integrations. * There is a [`crate::RawInput::viewport`] with information about the current viewport. * The repaint callback set by `Context::set_request_repaint_callback` now points to which viewport should be repainted. * `Context::run` now returns a list of viewports in `FullOutput` which should result in their own independant windows * There is a new `Context::set_immediate_viewport_renderer` for setting up the immediate viewport integration * If you support viewports, you need to call `Context::set_embed_viewports(false)`, or all new viewports will be embedded (the default behavior). ## Future work * Make it easy to wrap child viewports in the same chrome as `egui::Window` * Automatically show embedded viewports using `egui::Window` * Use the new `ViewportBuilder` in `eframe::NativeOptions` * Automatically position new viewport windows (they currently cover each other) * Add a `Context` method for listing all existing viewports Find more at https://github.com/emilk/egui/issues/3556 ---
Outdated PR description by @konkitoman ## Inspiration - Godot because the app always work desktop or single_window because of embedding - Dear ImGui viewport system ## What is a Viewport A Viewport is a egui isolated component! Can be used by the egui integration to create native windows! When you create a Viewport is possible that the backend do not supports that! So you need to check if the Viewport was created or you are in the normal egui context! This is how you can do that: ```rust if ctx.viewport_id() != ctx.parent_viewport_id() { // In here you add the code for the viewport context, like egui::CentralPanel::default().show(ctx, |ui|{ ui.label("This is in a native window!"); }); }else{ // In here you add the code for when viewport cannot be created! // You cannot use CentralPanel in here because you will override the app CentralPanel egui::Window::new("Virtual Viewport").show(ctx, |ui|{ ui.label("This is without a native window!\nThis is in a embedded viewport"); }); } ``` This PR do not support for drag and drop between Viewports! After this PR is accepted i will begin work to intregrate the Viewport system in `egui::Window`! The `egui::Window` i want to behave the same on desktop and web The `egui::Window` will be like Godot Window ## Changes and new These are only public structs and functions!
## New - `egui::ViewportId` - `egui::ViewportBuilder` This is like winit WindowBuilder - `egui::ViewportCommand` With this you can set any winit property on a viewport, when is a native window! - `egui::Context::new` - `egui::Context::create_viewport` - `egui::Context::create_viewport_sync` - `egui::Context::viewport_id` - `egui::Context::parent_viewport_id` - `egui::Context::viewport_id_pair` - `egui::Context::set_render_sync_callback` - `egui::Context::is_desktop` - `egui::Context::force_embedding` - `egui::Context::set_force_embedding` - `egui::Context::viewport_command` - `egui::Context::send_viewport_command_to` - `egui::Context::input_for` - `egui::Context::input_mut_for` - `egui::Context::frame_nr_for` - `egui::Context::request_repaint_for` - `egui::Context::request_repaint_after_for` - `egui::Context::requested_repaint_last_frame` - `egui::Context::requested_repaint_last_frame_for` - `egui::Context::requested_repaint` - `egui::Context::requested_repaint_for` - `egui::Context::inner_rect` - `egui::Context::outer_rect` - `egui::InputState::inner_rect` - `egui::InputState::outer_rect` - `egui::WindowEvent`
## Changes - `egui::Context::run` Now needs the viewport that we want to render! - `egui::Context::begin_frame` Now needs the viewport that we want to render! - `egui::Context::tessellate` Now needs the viewport that we want to render! - `egui::FullOutput` ```diff - repaint_after + viewports + viewport_commands ``` - `egui::RawInput` ```diff + inner_rect + outer_rect ``` - `egui::Event` ```diff + WindowEvent ```
### Async Viewport Async means that is independent from other viewports! Is created by `egui::Context::create_viewport` To be used you will need to wrap your state in `Arc>` Look at viewports example to understand how to use it! ### Sync Viewport Sync means that is dependent on his parent! Is created by `egui::Context::create_viewport_sync` This will pause the parent then render itself the resumes his parent! #### :warning: This currently will make the fps/2 for every sync viewport ### Common #### :warning: Attention You will need to do this when you render your content ```rust ctx.create_viewport(ViewportBuilder::new("Simple Viewport"), | ctx | { let content = |ui: &mut egui::Ui|{ ui.label("Content"); }; // This will make the content a popup if cannot create a native window if ctx.viewport_id() != ctx.parent_viewport_id() { egui::CentralPanel::default().show(ctx, content); } else { egui::Area::new("Simple Viewport").show(ctx, |ui| { egui::Frame::popup(ui.style()).show(ui, content); }); }; }); ```` ## What you need to know as egui user ### If you are using eframe You don't need to change anything! ### If you have a manual implementation Now `egui::run` or `egui::begin` and `egui::tessellate` will need the current viewport id! You cannot create a `ViewportId` only `ViewportId::MAIN` If you make a single window app you will set the viewport id to be `egui::ViewportId::MAIN` or see the `examples/pure_glow` If you want to have multiples window support look at `crates/eframe` glow or wgpu implementations! ## If you want to try this - cargo run -p viewports ## This before was wanted to change This will probably be in feature PR's ### egui::Window To create a native window when embedded was set to false You can try that in viewports example before: [78a0ae8](https://github.com/emilk/egui/pull/3172/commits/78a0ae879e68621543b9ca700ece3d9456eee8a4) ### egui popups, context_menu, tooltip To be a native window
--------- Co-authored-by: Konkitoman Co-authored-by: Emil Ernerfeldt Co-authored-by: Pablo Sichert --- Cargo.lock | 17 + crates/eframe/src/epi/mod.rs | 6 +- crates/eframe/src/lib.rs | 7 +- crates/eframe/src/native/epi_integration.rs | 242 +- crates/eframe/src/native/run.rs | 2659 ++++++++++++++----- crates/eframe/src/web/app_runner.rs | 18 +- crates/eframe/src/web/events.rs | 5 +- crates/eframe/src/web/web_painter_wgpu.rs | 11 +- crates/egui-wgpu/Cargo.toml | 1 + crates/egui-wgpu/src/winit.rs | 167 +- crates/egui-winit/Cargo.toml | 4 +- crates/egui-winit/src/clipboard.rs | 4 + crates/egui-winit/src/lib.rs | 366 ++- crates/egui-winit/src/window_settings.rs | 31 +- crates/egui/src/containers/area.rs | 12 +- crates/egui/src/containers/combo_box.rs | 2 +- crates/egui/src/containers/panel.rs | 4 +- crates/egui/src/containers/popup.rs | 2 +- crates/egui/src/containers/window.rs | 19 +- crates/egui/src/context.rs | 938 +++++-- crates/egui/src/data/input.rs | 51 +- crates/egui/src/data/output.rs | 52 +- crates/egui/src/id.rs | 2 +- crates/egui/src/input_state.rs | 1 + crates/egui/src/layers.rs | 6 +- crates/egui/src/lib.rs | 8 +- crates/egui/src/load.rs | 4 +- crates/egui/src/memory.rs | 143 +- crates/egui/src/viewport.rs | 804 ++++++ crates/egui_demo_lib/benches/benchmark.rs | 4 +- crates/egui_demo_lib/src/lib.rs | 4 +- crates/egui_glow/examples/pure_glow.rs | 39 +- crates/egui_glow/src/lib.rs | 2 +- crates/egui_glow/src/painter.rs | 31 +- crates/egui_glow/src/winit.rs | 63 +- crates/epaint/src/image.rs | 9 + examples/multiple_viewports/Cargo.toml | 15 + examples/multiple_viewports/README.md | 7 + examples/multiple_viewports/src/main.rs | 102 + examples/test_viewports/Cargo.toml | 17 + examples/test_viewports/README.md | 3 + examples/test_viewports/src/main.rs | 476 ++++ scripts/check.sh | 2 +- 43 files changed, 5087 insertions(+), 1273 deletions(-) create mode 100644 crates/egui/src/viewport.rs create mode 100644 examples/multiple_viewports/Cargo.toml create mode 100644 examples/multiple_viewports/README.md create mode 100644 examples/multiple_viewports/src/main.rs create mode 100644 examples/test_viewports/Cargo.toml create mode 100644 examples/test_viewports/README.md create mode 100644 examples/test_viewports/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index db8dd8999..fdaa5e915 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1274,6 +1274,7 @@ version = "0.23.0" dependencies = [ "bytemuck", "document-features", + "egui", "epaint", "log", "puffin", @@ -2554,6 +2555,14 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "multiple_viewports" +version = "0.1.0" +dependencies = [ + "eframe", + "env_logger", +] + [[package]] name = "naga" version = "0.14.0" @@ -3795,6 +3804,14 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test_viewports" +version = "0.1.0" +dependencies = [ + "eframe", + "env_logger", +] + [[package]] name = "textwrap" version = "0.16.0" diff --git a/crates/eframe/src/epi/mod.rs b/crates/eframe/src/epi/mod.rs index db0d4686d..aff7c7d38 100644 --- a/crates/eframe/src/epi/mod.rs +++ b/crates/eframe/src/epi/mod.rs @@ -44,7 +44,7 @@ pub type EventLoopBuilderHook = Box) /// done by `eframe`. #[cfg(not(target_arch = "wasm32"))] #[cfg(any(feature = "glow", feature = "wgpu"))] -pub type WindowBuilderHook = Box WindowBuilder>; +pub type WindowBuilderHook = Box egui::ViewportBuilder>; /// This is how your app is created. /// @@ -120,6 +120,10 @@ pub trait App { /// The [`egui::Context`] can be cloned and saved if you like. /// /// To force a repaint, call [`egui::Context::request_repaint`] at any time (e.g. from another thread). + /// + /// This is called for the root viewport ([`egui::ViewportId::ROOT`]). + /// Use [`egui::Context::show_viewport`] to spawn additional viewports (windows). + /// (A "viewport" in egui means an native OS window). fn update(&mut self, ctx: &egui::Context, frame: &mut Frame); /// Get a handle to the app. diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 2c36d4287..2edc06ab2 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -313,6 +313,11 @@ pub enum Error { #[error("Found no glutin configs matching the template: {0:?}. Error: {1:?}")] NoGlutinConfigs(glutin::config::ConfigTemplate, Box), + /// An error from [`glutin`] when using [`glow`]. + #[cfg(feature = "glow")] + #[error("egui_glow: {0}")] + OpenGL(#[from] egui_glow::PainterError), + /// An error from [`wgpu`]. #[cfg(feature = "wgpu")] #[error("WGPU error: {0}")] @@ -320,7 +325,7 @@ pub enum Error { } /// Short for `Result`. -pub type Result = std::result::Result; +pub type Result = std::result::Result; // --------------------------------------------------------------------------- diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 357e39848..a984258e8 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -1,15 +1,10 @@ -use winit::event_loop::EventLoopWindowTarget; +use std::time::Instant; -#[cfg(target_os = "macos")] -use winit::platform::macos::WindowBuilderExtMacOS as _; +use winit::event_loop::EventLoopWindowTarget; use raw_window_handle::{HasRawDisplayHandle as _, HasRawWindowHandle as _}; -#[cfg(feature = "accesskit")] -use egui::accesskit; -use egui::NumExt as _; -#[cfg(feature = "accesskit")] -use egui_winit::accesskit_winit; +use egui::{DeferredViewportUiCallback, NumExt as _, ViewportBuilder, ViewportId, ViewportIdPair}; use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings}; use crate::{epi, Theme, WindowInfo}; @@ -22,13 +17,6 @@ pub struct WindowState { pub maximized: bool, } -pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize { - winit::dpi::LogicalSize { - width: points.x as f64, - height: points.y as f64, - } -} - pub fn read_window_info( window: &winit::window::Window, pixels_per_point: f32, @@ -77,7 +65,7 @@ pub fn window_builder( title: &str, native_options: &mut epi::NativeOptions, window_settings: Option, -) -> winit::window::WindowBuilder { +) -> ViewportBuilder { let epi::NativeOptions { maximized, decorated, @@ -97,24 +85,29 @@ pub fn window_builder( .. } = native_options; - let window_icon = icon_data.clone().and_then(load_icon); - - let mut window_builder = winit::window::WindowBuilder::new() + let mut viewport_builder = egui::ViewportBuilder::default() .with_title(title) .with_decorations(*decorated) - .with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None))) + .with_fullscreen(*fullscreen) .with_maximized(*maximized) .with_resizable(*resizable) .with_transparent(*transparent) - .with_window_icon(window_icon) .with_active(*active) // Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279 // We must also keep the window hidden until AccessKit is initialized. .with_visible(false); + if let Some(icon_data) = icon_data { + viewport_builder = + viewport_builder.with_window_icon(egui::ColorImage::from_rgba_premultiplied( + [icon_data.width as usize, icon_data.height as usize], + &icon_data.rgba, + )); + } + #[cfg(target_os = "macos")] if *fullsize_content { - window_builder = window_builder + viewport_builder = viewport_builder .with_title_hidden(true) .with_titlebar_transparent(true) .with_fullsize_content_view(true); @@ -122,21 +115,20 @@ pub fn window_builder( #[cfg(all(feature = "wayland", target_os = "linux"))] { - use winit::platform::wayland::WindowBuilderExtWayland as _; - match &native_options.app_id { - Some(app_id) => window_builder = window_builder.with_name(app_id, ""), - None => window_builder = window_builder.with_name(title, ""), - } + viewport_builder = match &native_options.app_id { + Some(app_id) => viewport_builder.with_name(app_id, ""), + None => viewport_builder.with_name(title, ""), + }; } if let Some(min_size) = *min_window_size { - window_builder = window_builder.with_min_inner_size(points_to_size(min_size)); + viewport_builder = viewport_builder.with_min_inner_size(min_size); } if let Some(max_size) = *max_window_size { - window_builder = window_builder.with_max_inner_size(points_to_size(max_size)); + viewport_builder = viewport_builder.with_max_inner_size(max_size); } - window_builder = window_builder_drag_and_drop(window_builder, *drag_and_drop_support); + viewport_builder = viewport_builder.with_drag_and_drop(*drag_and_drop_support); // Always use the default window size / position on iOS. Trying to restore the previous position // causes the window to be shown too small. @@ -147,20 +139,17 @@ pub fn window_builder( window_settings.clamp_size_to_sane_values(largest_monitor_point_size(event_loop)); window_settings.clamp_position_to_monitors(event_loop); - window_builder = window_settings.initialize_window_builder(window_builder); + viewport_builder = window_settings.initialize_viewport_builder(viewport_builder); window_settings.inner_size_points() } else { if let Some(pos) = *initial_window_pos { - window_builder = window_builder.with_position(winit::dpi::LogicalPosition { - x: pos.x as f64, - y: pos.y as f64, - }); + viewport_builder = viewport_builder.with_position(pos); } if let Some(initial_window_size) = *initial_window_size { let initial_window_size = initial_window_size.at_most(largest_monitor_point_size(event_loop)); - window_builder = window_builder.with_inner_size(points_to_size(initial_window_size)); + viewport_builder = viewport_builder.with_inner_size(initial_window_size); } *initial_window_size @@ -169,19 +158,19 @@ pub fn window_builder( #[cfg(not(target_os = "ios"))] if *centered { if let Some(monitor) = event_loop.available_monitors().next() { - let monitor_size = monitor.size().to_logical::(monitor.scale_factor()); + let monitor_size = monitor.size().to_logical::(monitor.scale_factor()); let inner_size = inner_size_points.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 }); if monitor_size.width > 0.0 && monitor_size.height > 0.0 { - let x = (monitor_size.width - inner_size.x as f64) / 2.0; - let y = (monitor_size.height - inner_size.y as f64) / 2.0; - window_builder = window_builder.with_position(winit::dpi::LogicalPosition { x, y }); + let x = (monitor_size.width - inner_size.x) / 2.0; + let y = (monitor_size.height - inner_size.y) / 2.0; + viewport_builder = viewport_builder.with_position([x, y]); } } } match std::mem::take(&mut native_options.window_builder) { - Some(hook) => hook(window_builder), - None => window_builder, + Some(hook) => hook(viewport_builder), + None => viewport_builder, } } @@ -219,34 +208,14 @@ fn largest_monitor_point_size(event_loop: &EventLoopWindowTarget) -> egui: } } -fn load_icon(icon_data: epi::IconData) -> Option { - winit::window::Icon::from_rgba(icon_data.rgba, icon_data.width, icon_data.height).ok() -} - -#[cfg(target_os = "windows")] -fn window_builder_drag_and_drop( - window_builder: winit::window::WindowBuilder, - enable: bool, -) -> winit::window::WindowBuilder { - use winit::platform::windows::WindowBuilderExtWindows as _; - window_builder.with_drag_and_drop(enable) -} - -#[cfg(not(target_os = "windows"))] -fn window_builder_drag_and_drop( - window_builder: winit::window::WindowBuilder, - _enable: bool, -) -> winit::window::WindowBuilder { - // drag and drop can only be disabled on windows - window_builder -} - pub fn handle_app_output( window: &winit::window::Window, current_pixels_per_point: f32, app_output: epi::backend::AppOutput, window_state: &mut WindowState, ) { + crate::profile_function!(); + let epi::backend::AppOutput { close: _, window_size, @@ -294,7 +263,7 @@ pub fn handle_app_output( } if drag_window { - let _ = window.drag_window(); + window.drag_window().ok(); } if let Some(always_on_top) = always_on_top { @@ -346,10 +315,11 @@ pub fn create_storage(_app_name: &str) -> Option> { /// Everything needed to make a winit-based integration for [`epi`]. pub struct EpiIntegration { pub frame: epi::Frame, - last_auto_save: std::time::Instant, + last_auto_save: Instant, + pub beginning: Instant, + pub frame_start: Instant, pub egui_ctx: egui::Context, pending_full_output: egui::FullOutput, - egui_winit: egui_winit::State, /// When set, it is time to close the native window. close: bool, @@ -364,18 +334,18 @@ pub struct EpiIntegration { impl EpiIntegration { #[allow(clippy::too_many_arguments)] - pub fn new( - event_loop: &EventLoopWindowTarget, - max_texture_side: usize, + pub fn new( window: &winit::window::Window, system_theme: Option, app_name: &str, native_options: &crate::NativeOptions, storage: Option>, + is_desktop: bool, #[cfg(feature = "glow")] gl: Option>, #[cfg(feature = "wgpu")] wgpu_render_state: Option, ) -> Self { let egui_ctx = egui::Context::default(); + egui_ctx.set_embed_viewports(!is_desktop); let memory = load_egui_memory(storage.as_deref()).unwrap_or_default(); egui_ctx.memory_mut(|mem| *mem = memory); @@ -408,10 +378,6 @@ impl EpiIntegration { raw_window_handle: window.raw_window_handle(), }; - let mut egui_winit = egui_winit::State::new(event_loop); - egui_winit.set_max_texture_side(max_texture_side); - egui_winit.set_pixels_per_point(native_pixels_per_point); - let app_icon_setter = super::app_icon::AppTitleIconSetter::new( app_name.to_owned(), native_options.icon_data.clone(), @@ -419,9 +385,8 @@ impl EpiIntegration { Self { frame, - last_auto_save: std::time::Instant::now(), + last_auto_save: Instant::now(), egui_ctx, - egui_winit, pending_full_output: Default::default(), close: false, can_drag_window: false, @@ -430,34 +395,47 @@ impl EpiIntegration { #[cfg(feature = "persistence")] persist_window: native_options.persist_window, app_icon_setter, + beginning: Instant::now(), + frame_start: Instant::now(), } } #[cfg(feature = "accesskit")] - pub fn init_accesskit + Send>( + pub fn init_accesskit + Send>( &mut self, + egui_winit: &mut egui_winit::State, window: &winit::window::Window, event_loop_proxy: winit::event_loop::EventLoopProxy, ) { + crate::profile_function!(); + let egui_ctx = self.egui_ctx.clone(); - self.egui_winit - .init_accesskit(window, event_loop_proxy, move || { - // This function is called when an accessibility client - // (e.g. screen reader) makes its first request. If we got here, - // we know that an accessibility tree is actually wanted. - egui_ctx.enable_accesskit(); - // Enqueue a repaint so we'll receive a full tree update soon. - egui_ctx.request_repaint(); - egui_ctx.accesskit_placeholder_tree_update() - }); + egui_winit.init_accesskit(window, event_loop_proxy, move || { + // This function is called when an accessibility client + // (e.g. screen reader) makes its first request. If we got here, + // we know that an accessibility tree is actually wanted. + egui_ctx.enable_accesskit(); + // Enqueue a repaint so we'll receive a full tree update soon. + egui_ctx.request_repaint(); + egui_ctx.accesskit_placeholder_tree_update() + }); } - pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) { + pub fn warm_up( + &mut self, + app: &mut dyn epi::App, + window: &winit::window::Window, + egui_winit: &mut egui_winit::State, + ) { crate::profile_function!(); let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone()); self.egui_ctx .memory_mut(|mem| mem.set_everything_is_visible(true)); - let full_output = self.update(app, window); + + let raw_input = egui_winit.take_egui_input(window, ViewportIdPair::ROOT); + self.pre_update(window); + let full_output = self.update(app, None, raw_input); + self.post_update(app, window); self.pending_full_output.append(full_output); // Handle it next frame self.egui_ctx.memory_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge. self.egui_ctx.clear_animations(); @@ -472,6 +450,8 @@ impl EpiIntegration { &mut self, app: &mut dyn epi::App, event: &winit::event::WindowEvent<'_>, + egui_winit: &mut egui_winit::State, + viewport_id: ViewportId, ) -> EventResponse { crate::profile_function!(); @@ -480,7 +460,7 @@ impl EpiIntegration { match event { WindowEvent::CloseRequested => { log::debug!("Received WindowEvent::CloseRequested"); - self.close = app.on_close_event(); + self.close = app.on_close_event() && viewport_id == ViewportId::ROOT; log::debug!("App::on_close_event returned {}", self.close); } WindowEvent::Destroyed => { @@ -503,37 +483,47 @@ impl EpiIntegration { _ => {} } - self.egui_winit.on_event(&self.egui_ctx, event) + egui_winit.on_event(&self.egui_ctx, event) } - #[cfg(feature = "accesskit")] - pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) { - self.egui_winit.on_accesskit_action_request(request); + pub fn pre_update(&mut self, window: &winit::window::Window) { + self.frame_start = Instant::now(); + + self.app_icon_setter.update(); + + self.frame.info.window_info = + read_window_info(window, self.egui_ctx.pixels_per_point(), &self.window_state); } + /// Run user code - this can create immediate viewports, so hold no locks over this! + /// + /// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::update`]. pub fn update( &mut self, app: &mut dyn epi::App, - window: &winit::window::Window, + viewport_ui_cb: Option<&DeferredViewportUiCallback>, + mut raw_input: egui::RawInput, ) -> egui::FullOutput { - let frame_start = std::time::Instant::now(); + raw_input.time = Some(self.beginning.elapsed().as_secs_f64()); - self.app_icon_setter.update(); - - self.frame.info.window_info = - read_window_info(window, self.egui_ctx.pixels_per_point(), &self.window_state); - let raw_input = self.egui_winit.take_egui_input(window); - - // Run user code: let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { - crate::profile_scope!("App::update"); - app.update(egui_ctx, &mut self.frame); + if let Some(viewport_ui_cb) = viewport_ui_cb { + // Child viewport + crate::profile_scope!("viewport_callback"); + viewport_ui_cb(egui_ctx); + } else { + // Root viewport + crate::profile_scope!("App::update"); + app.update(egui_ctx, &mut self.frame); + } }); self.pending_full_output.append(full_output); - let full_output = std::mem::take(&mut self.pending_full_output); + std::mem::take(&mut self.pending_full_output) + } - { + pub fn post_update(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) { + let app_output = { let mut app_output = self.frame.take_app_output(); app_output.drag_window &= self.can_drag_window; // Necessary on Windows; see https://github.com/emilk/egui/pull/1108 self.can_drag_window = false; @@ -546,29 +536,30 @@ impl EpiIntegration { if self.frame.output.attention.is_some() { self.frame.output.attention = None; } - handle_app_output( - window, - self.egui_ctx.pixels_per_point(), - app_output, - &mut self.window_state, - ); - } + app_output + }; - let frame_time = frame_start.elapsed().as_secs_f64() as f32; - self.frame.info.cpu_usage = Some(frame_time); + handle_app_output( + window, + self.egui_ctx.pixels_per_point(), + app_output, + &mut self.window_state, + ); - full_output + let frame_time = self.frame_start.elapsed().as_secs_f64() as f32; + self.frame.info.cpu_usage = Some(frame_time); } pub fn post_rendering(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) { + crate::profile_function!(); let inner_size = window.inner_size(); let window_size_px = [inner_size.width, inner_size.height]; - app.post_rendering(window_size_px, &self.frame); } pub fn post_present(&mut self, window: &winit::window::Window) { if let Some(visible) = self.frame.output.visible.take() { + crate::profile_scope!("window.set_visible"); window.set_visible(visible); } } @@ -576,19 +567,24 @@ impl EpiIntegration { pub fn handle_platform_output( &mut self, window: &winit::window::Window, + viewport_id: ViewportId, platform_output: egui::PlatformOutput, + egui_winit: &mut egui_winit::State, ) { - self.egui_winit - .handle_platform_output(window, &self.egui_ctx, platform_output); + egui_winit.handle_platform_output(window, viewport_id, &self.egui_ctx, platform_output); } // ------------------------------------------------------------------------ // Persistence stuff: - pub fn maybe_autosave(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) { - let now = std::time::Instant::now(); + pub fn maybe_autosave( + &mut self, + app: &mut dyn epi::App, + window: Option<&winit::window::Window>, + ) { + let now = Instant::now(); if now - self.last_auto_save > app.auto_save_interval() { - self.save(app, Some(window)); + self.save(app, window); self.last_auto_save = now; } } diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index cb85a303b..3b67a21a1 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -1,13 +1,20 @@ //! Note that this file contains two similar paths - one for [`glow`], one for [`wgpu`]. //! When making changes to one you often also want to apply it to the other. +//! +//! This is also very complex code, and not very pretty. +//! There is a bunch of improvements we could do, +//! like removing a bunch of `unwraps`. -use std::time::Instant; +use std::{cell::RefCell, rc::Rc, sync::Arc, time::Instant}; use raw_window_handle::{HasRawDisplayHandle as _, HasRawWindowHandle as _}; -use winit::event_loop::{ - ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget, +use winit::{ + event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget}, + window::{Window, WindowId}, }; +use egui::{epaint::ahash::HashMap, ViewportBuilder, ViewportId}; + #[cfg(feature = "accesskit")] use egui_winit::accesskit_winit; use egui_winit::winit; @@ -18,11 +25,24 @@ use super::epi_integration::{self, EpiIntegration}; // ---------------------------------------------------------------------------- +pub const IS_DESKTOP: bool = cfg!(any( + target_os = "freebsd", + target_os = "linux", + target_os = "macos", + target_os = "openbsd", + target_os = "windows", +)); + +// ---------------------------------------------------------------------------- + /// The custom even `eframe` uses with the [`winit`] event loop. #[derive(Debug)] pub enum UserEvent { /// A repaint is requested. RequestRepaint { + /// What to repaint. + viewport_id: ViewportId, + /// When to repaint. when: Instant, @@ -56,31 +76,33 @@ enum EventResult { /// /// `RepaintNow` creates a new frame synchronously, and should therefore /// only be used for extremely urgent repaints. - RepaintNow, + RepaintNow(WindowId), /// Queues a repaint for once the event loop handles its next redraw. Exists /// so that multiple input events can be handled in one frame. Does not /// cause any delay like `RepaintNow`. - RepaintNext, + RepaintNext(WindowId), - RepaintAt(Instant), + RepaintAt(WindowId, Instant), Exit, } trait WinitApp { /// The current frame number, as reported by egui. - fn frame_nr(&self) -> u64; + fn frame_nr(&self, viewport_id: ViewportId) -> u64; - fn is_focused(&self) -> bool; + fn is_focused(&self, window_id: WindowId) -> bool; fn integration(&self) -> Option<&EpiIntegration>; - fn window(&self) -> Option<&winit::window::Window>; + fn window(&self, window_id: WindowId) -> Option>; + + fn window_id_from_viewport_id(&self, id: ViewportId) -> Option; fn save_and_destroy(&mut self); - fn run_ui_and_paint(&mut self) -> EventResult; + fn run_ui_and_paint(&mut self, window_id: WindowId) -> EventResult; fn on_event( &mut self, @@ -118,7 +140,6 @@ fn with_event_loop( mut native_options: epi::NativeOptions, f: impl FnOnce(&mut EventLoop, NativeOptions) -> R, ) -> R { - use std::cell::RefCell; thread_local!(static EVENT_LOOP: RefCell>> = RefCell::new(None)); EVENT_LOOP.with(|event_loop| { @@ -140,7 +161,8 @@ fn run_and_return( log::debug!("Entering the winit event loop (run_return)…"); - let mut next_repaint_time = Instant::now(); + // When to repaint what window + let mut windows_next_repaint_times = HashMap::default(); let mut returned_result = Ok(()); @@ -157,22 +179,24 @@ fn run_and_return( return; } - // Platform-dependent event handlers to workaround a winit bug - // See: https://github.com/rust-windowing/winit/issues/987 - // See: https://github.com/rust-windowing/winit/issues/1619 - winit::event::Event::RedrawEventsCleared if cfg!(target_os = "windows") => { - next_repaint_time = extremely_far_future(); - winit_app.run_ui_and_paint() - } - winit::event::Event::RedrawRequested(_) if !cfg!(target_os = "windows") => { - next_repaint_time = extremely_far_future(); - winit_app.run_ui_and_paint() + winit::event::Event::RedrawRequested(window_id) => { + windows_next_repaint_times.remove(window_id); + winit_app.run_ui_and_paint(*window_id) } - winit::event::Event::UserEvent(UserEvent::RequestRepaint { when, frame_nr }) => { - if winit_app.frame_nr() == *frame_nr { + winit::event::Event::UserEvent(UserEvent::RequestRepaint { + when, + frame_nr, + viewport_id, + }) => { + let current_frame_nr = winit_app.frame_nr(*viewport_id); + if current_frame_nr == *frame_nr || current_frame_nr == *frame_nr + 1 { log::trace!("UserEvent::RequestRepaint scheduling repaint at {when:?}"); - EventResult::RepaintAt(*when) + if let Some(window_id) = winit_app.window_id_from_viewport_id(*viewport_id) { + EventResult::RepaintAt(window_id, *when) + } else { + EventResult::Wait + } } else { log::trace!("Got outdated UserEvent::RequestRepaint"); EventResult::Wait // old request - we've already repainted @@ -186,19 +210,10 @@ fn run_and_return( EventResult::Wait } - winit::event::Event::WindowEvent { window_id, .. } - if winit_app.window().is_none() - || *window_id != winit_app.window().unwrap().id() => - { - // This can happen if we close a window, and then reopen a new one, - // or if we have multiple windows open. - EventResult::Wait - } - event => match winit_app.on_event(event_loop, event) { Ok(event_result) => event_result, Err(err) => { - log::error!("Exiting because of error: {err:?} on event {event:?}"); + log::error!("Exiting because of error: {err} during event {event:?}"); returned_result = Err(err); EventResult::Exit } @@ -206,24 +221,32 @@ fn run_and_return( }; match event_result { - EventResult::Wait => {} - EventResult::RepaintNow => { - log::trace!("Repaint caused by winit::Event: {:?}", event); + EventResult::Wait => { + control_flow.set_wait(); + } + EventResult::RepaintNow(window_id) => { + log::trace!("Repaint caused by {}", short_event_description(&event)); if cfg!(target_os = "windows") { // Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280 - next_repaint_time = extremely_far_future(); - winit_app.run_ui_and_paint(); + windows_next_repaint_times.remove(&window_id); + + winit_app.run_ui_and_paint(window_id); } else { // Fix for https://github.com/emilk/egui/issues/2425 - next_repaint_time = Instant::now(); + windows_next_repaint_times.insert(window_id, Instant::now()); } } - EventResult::RepaintNext => { - log::trace!("Repaint caused by winit::Event: {:?}", event); - next_repaint_time = Instant::now(); + EventResult::RepaintNext(window_id) => { + log::trace!("Repaint caused by {}", short_event_description(&event)); + windows_next_repaint_times.insert(window_id, Instant::now()); } - EventResult::RepaintAt(repaint_time) => { - next_repaint_time = next_repaint_time.min(repaint_time); + EventResult::RepaintAt(window_id, repaint_time) => { + windows_next_repaint_times.insert( + window_id, + windows_next_repaint_times + .get(&window_id) + .map_or(repaint_time, |last| (*last).min(repaint_time)), + ); } EventResult::Exit => { log::debug!("Asking to exit event loop…"); @@ -233,19 +256,38 @@ fn run_and_return( } } - *control_flow = if next_repaint_time <= Instant::now() { - if let Some(window) = winit_app.window() { - log::trace!("request_redraw"); - window.request_redraw(); - } - next_repaint_time = extremely_far_future(); - ControlFlow::Poll - } else { + let mut next_repaint_time = windows_next_repaint_times.values().min().copied(); + + // This is for not duplicating redraw requests + use winit::event::Event; + if matches!( + event, + Event::RedrawEventsCleared | Event::RedrawRequested(_) | Event::Resumed + ) { + windows_next_repaint_times.retain(|window_id, repaint_time| { + if Instant::now() < *repaint_time { + return true; + }; + + next_repaint_time = None; + control_flow.set_poll(); + + if let Some(window) = winit_app.window(*window_id) { + log::trace!("request_redraw for {window_id:?}"); + window.request_redraw(); + true + } else { + false + } + }); + } + + if let Some(next_repaint_time) = next_repaint_time { let time_until_next = next_repaint_time.saturating_duration_since(Instant::now()); if time_until_next < std::time::Duration::from_secs(10_000) { log::trace!("WaitUntil {time_until_next:?}"); } - ControlFlow::WaitUntil(next_repaint_time) + control_flow.set_wait_until(next_repaint_time); }; }); @@ -271,66 +313,83 @@ fn run_and_return( fn run_and_exit(event_loop: EventLoop, mut winit_app: impl WinitApp + 'static) -> ! { log::debug!("Entering the winit event loop (run)…"); - let mut next_repaint_time = Instant::now(); + // When to repaint what window + let mut windows_next_repaint_times = HashMap::default(); event_loop.run(move |event, event_loop, control_flow| { crate::profile_scope!("winit_event", short_event_description(&event)); - let event_result = match event { + let event_result = match &event { winit::event::Event::LoopDestroyed => { log::debug!("Received Event::LoopDestroyed"); EventResult::Exit } - // Platform-dependent event handlers to workaround a winit bug - // See: https://github.com/rust-windowing/winit/issues/987 - // See: https://github.com/rust-windowing/winit/issues/1619 - winit::event::Event::RedrawEventsCleared if cfg!(target_os = "windows") => { - next_repaint_time = extremely_far_future(); - winit_app.run_ui_and_paint() - } - winit::event::Event::RedrawRequested(_) if !cfg!(target_os = "windows") => { - next_repaint_time = extremely_far_future(); - winit_app.run_ui_and_paint() + winit::event::Event::RedrawRequested(window_id) => { + windows_next_repaint_times.remove(window_id); + winit_app.run_ui_and_paint(*window_id) } - winit::event::Event::UserEvent(UserEvent::RequestRepaint { when, frame_nr }) => { - if winit_app.frame_nr() == frame_nr { - EventResult::RepaintAt(when) + winit::event::Event::UserEvent(UserEvent::RequestRepaint { + when, + frame_nr, + viewport_id, + }) => { + let current_frame_nr = winit_app.frame_nr(*viewport_id); + if current_frame_nr == *frame_nr || current_frame_nr == *frame_nr + 1 { + if let Some(window_id) = winit_app.window_id_from_viewport_id(*viewport_id) { + EventResult::RepaintAt(window_id, *when) + } else { + EventResult::Wait + } } else { + log::trace!("Got outdated UserEvent::RequestRepaint"); EventResult::Wait // old request - we've already repainted } } winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached { .. - }) => EventResult::Wait, // We just woke up to check next_repaint_time + }) => { + log::trace!("Woke up to check next_repaint_time"); + EventResult::Wait + } - event => match winit_app.on_event(event_loop, &event) { + event => match winit_app.on_event(event_loop, event) { Ok(event_result) => event_result, Err(err) => { - panic!("eframe encountered a fatal error: {err}"); + panic!("eframe encountered a fatal error: {err} during event {event:?}"); } }, }; match event_result { - EventResult::Wait => {} - EventResult::RepaintNow => { + EventResult::Wait => { + control_flow.set_wait(); + } + EventResult::RepaintNow(window_id) => { + log::trace!("Repaint caused by {}", short_event_description(&event)); if cfg!(target_os = "windows") { // Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280 - next_repaint_time = extremely_far_future(); - winit_app.run_ui_and_paint(); + windows_next_repaint_times.remove(&window_id); + + winit_app.run_ui_and_paint(window_id); } else { // Fix for https://github.com/emilk/egui/issues/2425 - next_repaint_time = Instant::now(); + windows_next_repaint_times.insert(window_id, Instant::now()); } } - EventResult::RepaintNext => { - next_repaint_time = Instant::now(); + EventResult::RepaintNext(window_id) => { + log::trace!("Repaint caused by {}", short_event_description(&event)); + windows_next_repaint_times.insert(window_id, Instant::now()); } - EventResult::RepaintAt(repaint_time) => { - next_repaint_time = next_repaint_time.min(repaint_time); + EventResult::RepaintAt(window_id, repaint_time) => { + windows_next_repaint_times.insert( + window_id, + windows_next_repaint_times + .get(&window_id) + .map_or(repaint_time, |last| (*last).min(repaint_time)), + ); } EventResult::Exit => { log::debug!("Quitting - saving app state…"); @@ -340,19 +399,49 @@ fn run_and_exit(event_loop: EventLoop, mut winit_app: impl WinitApp + } } - *control_flow = if next_repaint_time <= Instant::now() { - if let Some(window) = winit_app.window() { - window.request_redraw(); + let mut next_repaint_time = windows_next_repaint_times.values().min().copied(); + + // This is for not duplicating redraw requests + use winit::event::Event; + if matches!( + event, + Event::RedrawEventsCleared | Event::RedrawRequested(_) | Event::Resumed + ) { + windows_next_repaint_times.retain(|window_id, repaint_time| { + if Instant::now() < *repaint_time { + return true; + } + + next_repaint_time = None; + control_flow.set_poll(); + + if let Some(window) = winit_app.window(*window_id) { + log::trace!("request_redraw for {window_id:?}"); + window.request_redraw(); + true + } else { + false + } + }); + } + + if let Some(next_repaint_time) = next_repaint_time { + let time_until_next = next_repaint_time.saturating_duration_since(Instant::now()); + if time_until_next < std::time::Duration::from_secs(10_000) { + log::trace!("WaitUntil {time_until_next:?}"); } - next_repaint_time = extremely_far_future(); - ControlFlow::Poll - } else { + // WaitUntil seems to not work on iOS #[cfg(target_os = "ios")] - if let Some(window) = winit_app.window() { - window.request_redraw(); - } - ControlFlow::WaitUntil(next_repaint_time) + winit_app + .get_window_winit_id(ViewportId::ROOT) + .map(|window_id| { + winit_app + .window(window_id) + .map(|window| window.request_redraw()) + }); + + control_flow.set_wait_until(next_repaint_time); }; }) } @@ -361,15 +450,18 @@ fn run_and_exit(event_loop: EventLoop, mut winit_app: impl WinitApp + /// Run an egui app #[cfg(feature = "glow")] mod glow_integration { - use std::sync::Arc; - - use egui::NumExt as _; use glutin::{ display::GetGlDisplay, prelude::{GlDisplay, NotCurrentGlContextSurfaceAccessor, PossiblyCurrentGlContext}, surface::GlSurface, }; + use egui::{ + epaint::ahash::HashMap, DeferredViewportUiCallback, ImmediateViewport, NumExt as _, + ViewportClass, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput, + }; + use egui_winit::{create_winit_window_builder, process_viewport_commands, EventResponse}; + use super::*; // Note: that the current Glutin API design tightly couples the GL context with @@ -383,19 +475,322 @@ mod glow_integration { // There is work in progress to improve the Glutin API so it has a separate Surface // API that would allow us to just destroy a Window/Surface when suspending, see: // https://github.com/rust-windowing/glutin/pull/1435 - // /// State that is initialized when the application is first starts running via /// a Resumed event. On Android this ensures that any graphics state is only /// initialized once the application has an associated `SurfaceView`. struct GlowWinitRunning { - gl: Arc, - painter: egui_glow::Painter, integration: epi_integration::EpiIntegration, app: Box, - // Conceptually this will be split out eventually so that the rest of the state - // can be persistent. - gl_window: GlutinWindowContext, + + // These needs to be shared with the immediate viewport renderer, hence the Rc/Arc/RefCells: + glutin: Rc>, + painter: Rc>, + } + + impl GlowWinitRunning { + fn run_ui_and_paint( + &mut self, + window_id: WindowId, + focused_viewport: Option, + ) -> EventResult { + let Some(viewport_id) = self + .glutin + .borrow() + .viewport_from_window + .get(&window_id) + .copied() + else { + return EventResult::Wait; + }; + + #[cfg(feature = "puffin")] + puffin::GlobalProfiler::lock().new_frame(); + crate::profile_scope!("frame"); + + { + let glutin = self.glutin.borrow(); + let viewport = &glutin.viewports[&viewport_id]; + let is_immediate = viewport.viewport_ui_cb.is_none(); + if is_immediate && viewport_id != ViewportId::ROOT { + // This will only happen if this is an immediate viewport. + // That means that the viewport cannot be rendered by itself and needs his parent to be rendered. + if let Some(parent_viewport) = glutin.viewports.get(&viewport.ids.parent) { + if let Some(window) = parent_viewport.window.as_ref() { + return EventResult::RepaintNext(window.id()); + } + } + return EventResult::Wait; + } + } + + let (raw_input, viewport_ui_cb) = { + let mut glutin = self.glutin.borrow_mut(); + let viewport = glutin.viewports.get_mut(&viewport_id).unwrap(); + let window = viewport.window.as_ref().unwrap(); + + let egui_winit = viewport.egui_winit.as_mut().unwrap(); + let raw_input = egui_winit.take_egui_input(window, viewport.ids); + + self.integration.pre_update(window); + + (raw_input, viewport.viewport_ui_cb.clone()) + }; + + // ------------------------------------------------------------ + // The update function, which could call immediate viewports, + // so make sure we don't hold any locks here required by the immediate viewports rendeer. + + let full_output = + self.integration + .update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input); + + // ------------------------------------------------------------ + + let Self { + integration, + app, + glutin, + painter, + .. + } = self; + + let mut glutin = glutin.borrow_mut(); + let mut painter = painter.borrow_mut(); + + let egui::FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = full_output; + + let GlutinWindowContext { + viewports, + current_gl_context, + .. + } = &mut *glutin; + + let viewport = viewports.get_mut(&viewport_id).unwrap(); + let window = viewport.window.as_ref().unwrap(); + let gl_surface = viewport.gl_surface.as_ref().unwrap(); + let egui_winit = viewport.egui_winit.as_mut().unwrap(); + + integration.post_update(app.as_mut(), window); + integration.handle_platform_output(window, viewport_id, platform_output, egui_winit); + + let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); + + *current_gl_context = Some( + current_gl_context + .take() + .unwrap() + .make_not_current() + .unwrap() + .make_current(gl_surface) + .unwrap(), + ); + + let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); + + painter.clear( + screen_size_in_pixels, + app.clear_color(&integration.egui_ctx.style().visuals), + ); + + painter.paint_and_update_textures( + screen_size_in_pixels, + pixels_per_point, + &clipped_primitives, + &textures_delta, + ); + + { + let screenshot_requested = &mut integration.frame.output.screenshot_requested; + if *screenshot_requested { + *screenshot_requested = false; + let screenshot = painter.read_screen_rgba(screen_size_in_pixels); + integration.frame.screenshot.set(Some(screenshot)); + } + integration.post_rendering(app.as_mut(), window); + } + + { + crate::profile_scope!("swap_buffers"); + if let Err(err) = gl_surface.swap_buffers( + current_gl_context + .as_ref() + .expect("failed to get current context to swap buffers"), + ) { + log::error!("swap_buffers failed: {err}"); + } + } + + integration.post_present(window); + + // give it time to settle: + #[cfg(feature = "__screenshot")] + if integration.egui_ctx.frame_nr() == 2 { + if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") { + save_screeshot_and_exit(&path, &painter, screen_size_in_pixels); + } + } + + integration.maybe_autosave(app.as_mut(), Some(window)); + + if window.is_minimized() == Some(true) { + // On Mac, a minimized Window uses up all CPU: + // https://github.com/emilk/egui/issues/325 + crate::profile_scope!("minimized_sleep"); + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + glutin.handle_viewport_output(viewport_output, focused_viewport); + + if integration.should_close() { + EventResult::Exit + } else { + EventResult::Wait + } + } + + fn on_window_event( + &mut self, + window_id: WindowId, + event: &winit::event::WindowEvent<'_>, + focused_viewport: &mut Option, + ) -> EventResult { + let viewport_id = self + .glutin + .borrow() + .viewport_from_window + .get(&window_id) + .copied(); + + // On Windows, if a window is resized by the user, it should repaint synchronously, inside the + // event handler. + // + // If this is not done, the compositor will assume that the window does not want to redraw, + // and continue ahead. + // + // In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver + // new frames to the compositor in time. + // + // The flickering is technically glutin or glow's fault, but we should be responding properly + // to resizes anyway, as doing so avoids dropping frames. + // + // See: https://github.com/emilk/egui/issues/903 + let mut repaint_asap = false; + + match event { + winit::event::WindowEvent::Focused(new_focused) => { + *focused_viewport = new_focused.then(|| viewport_id).flatten(); + } + + winit::event::WindowEvent::Resized(physical_size) => { + // Resize with 0 width and height is used by winit to signal a minimize event on Windows. + // See: https://github.com/rust-windowing/winit/issues/208 + // This solves an issue where the app would panic when minimizing on Windows. + if 0 < physical_size.width && 0 < physical_size.height { + if let Some(viewport_id) = viewport_id { + repaint_asap = true; + self.glutin.borrow_mut().resize(viewport_id, *physical_size); + } + } + } + + winit::event::WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { + if let Some(viewport_id) = viewport_id { + repaint_asap = true; + self.glutin + .borrow_mut() + .resize(viewport_id, **new_inner_size); + } + } + + winit::event::WindowEvent::CloseRequested => { + let is_root = viewport_id == Some(ViewportId::ROOT); + if is_root && self.integration.should_close() { + log::debug!("Received WindowEvent::CloseRequested"); + return EventResult::Exit; + } + } + _ => {} + } + + let event_response = 'res: { + if let Some(viewport_id) = viewport_id { + let mut glutin = self.glutin.borrow_mut(); + if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) { + break 'res self.integration.on_event( + self.app.as_mut(), + event, + viewport.egui_winit.as_mut().unwrap(), + viewport.ids.this, + ); + } + } + + EventResponse { + consumed: false, + repaint: false, + } + }; + + if self.integration.should_close() { + EventResult::Exit + } else if event_response.repaint { + if repaint_asap { + EventResult::RepaintNow(window_id) + } else { + EventResult::RepaintNext(window_id) + } + } else { + EventResult::Wait + } + } + } + + #[cfg(feature = "__screenshot")] + fn save_screeshot_and_exit( + path: &str, + painter: &egui_glow::Painter, + screen_size_in_pixels: [u32; 2], + ) { + assert!( + path.ends_with(".png"), + "Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}" + ); + let screenshot = painter.read_screen_rgba(screen_size_in_pixels); + image::save_buffer( + path, + screenshot.as_raw(), + screenshot.width() as u32, + screenshot.height() as u32, + image::ColorType::Rgba8, + ) + .unwrap_or_else(|err| { + panic!("Failed to save screenshot to {path:?}: {err}"); + }); + eprintln!("Screenshot saved to {path:?}."); + + #[allow(clippy::exit)] + std::process::exit(0); + } + + struct Viewport { + ids: ViewportIdPair, + class: ViewportClass, + builder: ViewportBuilder, + + /// The user-callback that shows the ui. + /// None for immediate viewports. + viewport_ui_cb: Option>, + + gl_surface: Option>, + window: Option>, + egui_winit: Option, } /// This struct will contain both persistent and temporary glutin state. @@ -412,26 +807,31 @@ mod glow_integration { /// The setup is divided between the `new` fn and `on_resume` fn. we can just assume that `on_resume` is a continuation of /// `new` fn on all platforms. only on android, do we get multiple resumed events because app can be suspended. struct GlutinWindowContext { - builder: winit::window::WindowBuilder, swap_interval: glutin::surface::SwapInterval, gl_config: glutin::config::Config, + + max_texture_side: Option, + current_gl_context: Option, - gl_surface: Option>, not_current_gl_context: Option, - window: Option, + + viewports: ViewportIdMap, + viewport_from_window: HashMap, + window_from_viewport: ViewportIdMap, } impl GlutinWindowContext { - /// There is a lot of complexity with opengl creation, so prefer extensive logging to get all the help we can to debug issues. - /// #[allow(unsafe_code)] unsafe fn new( - winit_window_builder: winit::window::WindowBuilder, + viewport_builder: ViewportBuilder, native_options: &epi::NativeOptions, event_loop: &EventLoopWindowTarget, ) -> Result { crate::profile_function!(); + // There is a lot of complexity with opengl creation, + // so prefer extensive logging to get all the help we can to debug issues. + use glutin::prelude::*; // convert native options to glutin options let hardware_acceleration = match native_options.hardware_acceleration { @@ -468,16 +868,13 @@ mod glow_integration { config_template_builder }; - log::debug!( - "trying to create glutin Display with config: {:?}", - &config_template_builder - ); + log::debug!("trying to create glutin Display with config: {config_template_builder:?}"); // Create GL display. This may probably create a window too on most platforms. Definitely on `MS windows`. Never on Android. let display_builder = glutin_winit::DisplayBuilder::new() // we might want to expose this option to users in the future. maybe using an env var or using native_options. .with_preference(glutin_winit::ApiPrefence::FallbackEgl) // https://github.com/emilk/egui/issues/2520#issuecomment-1367841150 - .with_window_builder(Some(winit_window_builder.clone())); + .with_window_builder(Some(create_winit_window_builder(&viewport_builder))); let (window, gl_config) = { crate::profile_scope!("DisplayBuilder::build"); @@ -491,8 +888,7 @@ mod glow_integration { "failed to find a matching configuration for creating glutin config", ); log::debug!( - "using the first config from config picker closure. config: {:?}", - &config + "using the first config from config picker closure. config: {config:?}" ); config }, @@ -509,10 +905,7 @@ mod glow_integration { gl_display.supported_features() ); let raw_window_handle = window.as_ref().map(|w| w.raw_window_handle()); - log::debug!( - "creating gl context using raw window handle: {:?}", - raw_window_handle - ); + log::debug!("creating gl context using raw window handle: {raw_window_handle:?}"); // create gl context. if core context cannot be created, try gl es context as fallback. let context_attributes = @@ -531,8 +924,8 @@ mod glow_integration { let gl_context = match gl_context_result { Ok(it) => it, Err(err) => { - log::warn!("failed to create context using default context attributes {context_attributes:?} due to error: {err}"); - log::debug!("retrying with fallback context attributes: {fallback_context_attributes:?}"); + log::warn!("Failed to create context using default context attributes {context_attributes:?} due to error: {err}"); + log::debug!("Retrying with fallback context attributes: {fallback_context_attributes:?}"); gl_config .display() .create_context(&gl_config, &fallback_context_attributes)? @@ -540,19 +933,46 @@ mod glow_integration { }; let not_current_gl_context = Some(gl_context); + let mut viewport_from_window = HashMap::default(); + let mut window_from_viewport = ViewportIdMap::default(); + if let Some(window) = &window { + viewport_from_window.insert(window.id(), ViewportId::ROOT); + window_from_viewport.insert(ViewportId::ROOT, window.id()); + } + + let mut viewports = ViewportIdMap::default(); + viewports.insert( + ViewportId::ROOT, + Viewport { + ids: ViewportIdPair::ROOT, + class: ViewportClass::Root, + builder: viewport_builder, + viewport_ui_cb: None, + gl_surface: None, + window: window.map(Rc::new), + egui_winit: None, + }, + ); + // the fun part with opengl gl is that we never know whether there is an error. the context creation might have failed, but // it could keep working until we try to make surface current or swap buffers or something else. future glutin improvements might // help us start from scratch again if we fail context creation and go back to preferEgl or try with different config etc.. // https://github.com/emilk/egui/pull/2541#issuecomment-1370767582 - Ok(GlutinWindowContext { - builder: winit_window_builder, + + let mut slf = GlutinWindowContext { swap_interval, gl_config, current_gl_context: None, - window, - gl_surface: None, not_current_gl_context, - }) + viewports, + viewport_from_window, + max_texture_side: None, + window_from_viewport, + }; + + slf.on_resume(event_loop)?; + + Ok(slf) } /// This will be run after `new`. on android, it might be called multiple times over the course of the app's lifetime. @@ -563,63 +983,116 @@ mod glow_integration { /// 4. make surface and context current. /// /// we presently assume that we will - #[allow(unsafe_code)] fn on_resume(&mut self, event_loop: &EventLoopWindowTarget) -> Result<()> { crate::profile_function!(); - if self.gl_surface.is_some() { - log::warn!("on_resume called even thought we already have a surface. early return"); - return Ok(()); + let viewports: Vec = self + .viewports + .iter() + .filter(|(_, viewport)| viewport.gl_surface.is_none()) + .map(|(id, _)| *id) + .collect(); + + for viewport_id in viewports { + self.init_viewport(viewport_id, event_loop)?; } - log::debug!("running on_resume fn."); - // make sure we have a window or create one. - let window = self.window.take().unwrap_or_else(|| { - log::debug!("window doesn't exist yet. creating one now with finalize_window"); - glutin_winit::finalize_window(event_loop, self.builder.clone(), &self.gl_config) - .expect("failed to finalize glutin window") - }); - // surface attributes - let (width, height): (u32, u32) = window.inner_size().into(); - let width = std::num::NonZeroU32::new(width.at_least(1)).unwrap(); - let height = std::num::NonZeroU32::new(height.at_least(1)).unwrap(); - let surface_attributes = - glutin::surface::SurfaceAttributesBuilder::::new() - .build(window.raw_window_handle(), width, height); - log::debug!( - "creating surface with attributes: {:?}", - &surface_attributes - ); - // create surface - let gl_surface = unsafe { - self.gl_config - .display() - .create_window_surface(&self.gl_config, &surface_attributes)? + Ok(()) + } + + #[allow(unsafe_code)] + pub(crate) fn init_viewport( + &mut self, + viewport_id: ViewportId, + event_loop: &EventLoopWindowTarget, + ) -> Result<()> { + crate::profile_function!(); + + let viewport = self + .viewports + .get_mut(&viewport_id) + .expect("viewport doesn't exist"); + + let window = if let Some(window) = &mut viewport.window { + window + } else { + log::trace!("Window doesn't exist yet. Creating one now with finalize_window"); + viewport + .window + .insert(Rc::new(glutin_winit::finalize_window( + event_loop, + create_winit_window_builder(&viewport.builder), + &self.gl_config, + )?)) }; - log::debug!("surface created successfully: {gl_surface:?}.making context current"); - // make surface and context current. - let not_current_gl_context = self - .not_current_gl_context - .take() - .expect("failed to get not current context after resume event. impossible!"); - let current_gl_context = not_current_gl_context.make_current(&gl_surface)?; - // try setting swap interval. but its not absolutely necessary, so don't panic on failure. - log::debug!("made context current. setting swap interval for surface"); - if let Err(e) = gl_surface.set_swap_interval(¤t_gl_context, self.swap_interval) { - log::error!("failed to set swap interval due to error: {e:?}"); - } - // we will reach this point only once in most platforms except android. - // create window/surface/make context current once and just use them forever. - self.gl_surface = Some(gl_surface); - self.current_gl_context = Some(current_gl_context); - self.window = Some(window); + + { + // surface attributes + let (width_px, height_px): (u32, u32) = window.inner_size().into(); + let width_px = std::num::NonZeroU32::new(width_px.at_least(1)).unwrap(); + let height_px = std::num::NonZeroU32::new(height_px.at_least(1)).unwrap(); + let surface_attributes = glutin::surface::SurfaceAttributesBuilder::< + glutin::surface::WindowSurface, + >::new() + .build(window.raw_window_handle(), width_px, height_px); + + log::trace!("creating surface with attributes: {surface_attributes:?}"); + let gl_surface = unsafe { + self.gl_config + .display() + .create_window_surface(&self.gl_config, &surface_attributes)? + }; + + log::trace!("surface created successfully: {gl_surface:?}. making context current"); + + let not_current_gl_context = + if let Some(not_current_context) = self.not_current_gl_context.take() { + not_current_context + } else { + self.current_gl_context + .take() + .unwrap() + .make_not_current() + .unwrap() + }; + let current_gl_context = not_current_gl_context.make_current(&gl_surface)?; + + // try setting swap interval. but its not absolutely necessary, so don't panic on failure. + log::trace!("made context current. setting swap interval for surface"); + if let Err(err) = + gl_surface.set_swap_interval(¤t_gl_context, self.swap_interval) + { + log::warn!("Failed to set swap interval due to error: {err}"); + } + + // we will reach this point only once in most platforms except android. + // create window/surface/make context current once and just use them forever. + + viewport.egui_winit.get_or_insert_with(|| { + egui_winit::State::new( + event_loop, + Some(window.scale_factor() as f32), + self.max_texture_side, + ) + }); + + viewport.gl_surface = Some(gl_surface); + self.current_gl_context = Some(current_gl_context); + self.viewport_from_window + .insert(window.id(), viewport.ids.this); + self.window_from_viewport + .insert(viewport.ids.this, window.id()); + } + Ok(()) } /// only applies for android. but we basically drop surface + window and make context not current fn on_suspend(&mut self) -> Result<()> { log::debug!("received suspend event. dropping window and surface"); - self.gl_surface.take(); - self.window.take(); + for viewport in self.viewports.values_mut() { + viewport.gl_surface = None; + viewport.window = None; + } if let Some(current) = self.current_gl_context.take() { log::debug!("context is current, so making it non-current"); self.not_current_gl_context = Some(current.make_not_current()?); @@ -629,52 +1102,185 @@ mod glow_integration { Ok(()) } - fn window(&self) -> &winit::window::Window { - self.window.as_ref().expect("winit window doesn't exist") + fn viewport(&self, viewport_id: ViewportId) -> &Viewport { + self.viewports + .get(&viewport_id) + .expect("viewport doesn't exist") } - fn resize(&self, physical_size: winit::dpi::PhysicalSize) { - let width = std::num::NonZeroU32::new(physical_size.width.at_least(1)).unwrap(); - let height = std::num::NonZeroU32::new(physical_size.height.at_least(1)).unwrap(); - self.gl_surface - .as_ref() - .expect("failed to get surface to resize") - .resize( - self.current_gl_context - .as_ref() - .expect("failed to get current context to resize surface"), - width, - height, - ); + fn viewport_mut(&mut self, viewport_id: ViewportId) -> &mut Viewport { + self.viewports + .get_mut(&viewport_id) + .expect("viewport doesn't exist") } - fn swap_buffers(&self) -> glutin::error::Result<()> { - self.gl_surface - .as_ref() - .expect("failed to get surface to swap buffers") - .swap_buffers( - self.current_gl_context - .as_ref() - .expect("failed to get current context to swap buffers"), - ) + fn window(&self, viewport_id: ViewportId) -> Rc { + self.viewport(viewport_id) + .window + .clone() + .expect("winit window doesn't exist") + } + + fn resize( + &mut self, + viewport_id: ViewportId, + physical_size: winit::dpi::PhysicalSize, + ) { + let width_px = std::num::NonZeroU32::new(physical_size.width.at_least(1)).unwrap(); + let height_px = std::num::NonZeroU32::new(physical_size.height.at_least(1)).unwrap(); + + if let Some(viewport) = self.viewports.get(&viewport_id) { + if let Some(gl_surface) = &viewport.gl_surface { + self.current_gl_context = Some( + self.current_gl_context + .take() + .unwrap() + .make_not_current() + .unwrap() + .make_current(gl_surface) + .unwrap(), + ); + gl_surface.resize( + self.current_gl_context + .as_ref() + .expect("failed to get current context to resize surface"), + width_px, + height_px, + ); + } + } } fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void { self.gl_config.display().get_proc_address(addr) } - } - - struct GlowWinitApp { - repaint_proxy: Arc>>, - app_name: String, - native_options: epi::NativeOptions, - running: Option, + + fn handle_viewport_output( + &mut self, + viewport_output: ViewportIdMap, + focused_viewport: Option, + ) { + crate::profile_function!(); + + let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); + + for ( + viewport_id, + ViewportOutput { + parent, + class, + builder, + viewport_ui_cb, + commands, + repaint_delay: _, // ignored - we listened to the repaint callback instead + }, + ) in viewport_output + { + let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent); + + initialize_or_update_viewport( + &mut self.viewports, + ids, + class, + builder, + viewport_ui_cb, + focused_viewport, + ); + + if let Some(viewport) = self.viewports.get(&viewport_id) { + if let Some(window) = &viewport.window { + let is_viewport_focused = focused_viewport == Some(viewport_id); + egui_winit::process_viewport_commands( + commands, + window, + is_viewport_focused, + ); + } + } + } + + // GC old viewports + self.viewports + .retain(|id, _| active_viewports_ids.contains(id)); + self.viewport_from_window + .retain(|_, id| active_viewports_ids.contains(id)); + self.window_from_viewport + .retain(|id, _| active_viewports_ids.contains(id)); + } + } + + fn initialize_or_update_viewport( + viewports: &mut ViewportIdMap, + ids: ViewportIdPair, + class: ViewportClass, + mut builder: ViewportBuilder, + viewport_ui_cb: Option>, + focused_viewport: Option, + ) -> &mut Viewport { + crate::profile_function!(); + + if builder.icon.is_none() { + // Inherit icon from parent + builder.icon = viewports + .get_mut(&ids.parent) + .and_then(|vp| vp.builder.icon.clone()); + } + + match viewports.entry(ids.this) { + std::collections::hash_map::Entry::Vacant(entry) => { + // New viewport: + log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title); + entry.insert(Viewport { + ids, + class, + builder, + viewport_ui_cb, + window: None, + egui_winit: None, + gl_surface: None, + }) + } + + std::collections::hash_map::Entry::Occupied(mut entry) => { + // Patch an existing viewport: + let viewport = entry.get_mut(); + + viewport.ids.parent = ids.parent; + viewport.class = class; + viewport.viewport_ui_cb = viewport_ui_cb; + + let (delta_commands, recreate) = viewport.builder.patch(&builder); + + if recreate { + log::debug!( + "Recreating window for viewport {:?} ({:?})", + ids.this, + builder.title + ); + viewport.window = None; + viewport.egui_winit = None; + } else if let Some(window) = &viewport.window { + let is_viewport_focused = focused_viewport == Some(ids.this); + process_viewport_commands(delta_commands, window, is_viewport_focused); + } + + entry.into_mut() + } + } + } + + struct GlowWinitApp { + repaint_proxy: Arc>>, + app_name: String, + native_options: epi::NativeOptions, + running: Option, // Note that since this `AppCreator` is FnOnce we are currently unable to support // re-initializing the `GlowWinitRunning` state on Android if the application // suspends and resumes. app_creator: Option, - is_focused: bool, + + focused_viewport: Option, } impl GlowWinitApp { @@ -691,7 +1297,7 @@ mod glow_integration { native_options, running: None, app_creator: Some(app_creator), - is_focused: true, + focused_viewport: Some(ViewportId::ROOT), } } @@ -701,41 +1307,52 @@ mod glow_integration { storage: Option<&dyn epi::Storage>, title: &str, native_options: &mut NativeOptions, - ) -> Result<(GlutinWindowContext, glow::Context)> { + ) -> Result<(GlutinWindowContext, egui_glow::Painter)> { crate::profile_function!(); let window_settings = epi_integration::load_window_settings(storage); let winit_window_builder = epi_integration::window_builder(event_loop, title, native_options, window_settings); + let mut glutin_window_context = unsafe { GlutinWindowContext::new(winit_window_builder, native_options, event_loop)? }; + + // Creates the window - must come before we create our glow context glutin_window_context.on_resume(event_loop)?; - if let Some(window) = &glutin_window_context.window { - epi_integration::apply_native_options_to_window( - window, - native_options, - window_settings, - ); + if let Some(viewport) = glutin_window_context.viewports.get(&ViewportId::ROOT) { + if let Some(window) = &viewport.window { + epi_integration::apply_native_options_to_window( + window, + native_options, + window_settings, + ); + } } let gl = unsafe { crate::profile_scope!("glow::Context::from_loader_function"); - glow::Context::from_loader_function(|s| { + Arc::new(glow::Context::from_loader_function(|s| { let s = std::ffi::CString::new(s) .expect("failed to construct C string from string for gl proc address"); glutin_window_context.get_proc_address(&s) - }) + })) }; - Ok((glutin_window_context, gl)) + let painter = egui_glow::Painter::new(gl, "", native_options.shader_version)?; + + Ok((glutin_window_context, painter)) } - fn init_run_state(&mut self, event_loop: &EventLoopWindowTarget) -> Result<()> { + fn init_run_state( + &mut self, + event_loop: &EventLoopWindowTarget, + ) -> Result<&mut GlowWinitRunning> { crate::profile_function!(); + let storage = epi_integration::create_storage( self.native_options .app_id @@ -743,41 +1360,35 @@ mod glow_integration { .unwrap_or(&self.app_name), ); - let (gl_window, gl) = Self::create_glutin_windowed_context( + let (mut glutin, painter) = Self::create_glutin_windowed_context( event_loop, storage.as_deref(), &self.app_name, &mut self.native_options, )?; - let gl = Arc::new(gl); + let gl = painter.gl().clone(); - let painter = - egui_glow::Painter::new(gl.clone(), "", self.native_options.shader_version) - .unwrap_or_else(|err| panic!("An OpenGL error occurred: {err}\n")); + let max_texture_side = painter.max_texture_side(); + glutin.max_texture_side = Some(max_texture_side); + for viewport in glutin.viewports.values_mut() { + if let Some(egui_winit) = viewport.egui_winit.as_mut() { + egui_winit.set_max_texture_side(max_texture_side); + } + } + + let system_theme = system_theme(&glutin.window(ViewportId::ROOT), &self.native_options); - let system_theme = system_theme(gl_window.window(), &self.native_options); let mut integration = epi_integration::EpiIntegration::new( - event_loop, - painter.max_texture_side(), - gl_window.window(), + &glutin.window(ViewportId::ROOT), system_theme, &self.app_name, &self.native_options, storage, + IS_DESKTOP, Some(gl.clone()), #[cfg(feature = "wgpu")] None, ); - #[cfg(feature = "accesskit")] - { - integration.init_accesskit(gl_window.window(), self.repaint_proxy.lock().clone()); - } - let theme = system_theme.unwrap_or(self.native_options.default_theme); - integration.egui_ctx.set_visuals(theme.egui_visuals()); - - if self.native_options.mouse_passthrough { - gl_window.window().set_cursor_hittest(false).unwrap(); - } { let event_loop_proxy = self.repaint_proxy.clone(); @@ -785,194 +1396,311 @@ mod glow_integration { .egui_ctx .set_request_repaint_callback(move |info| { log::trace!("request_repaint_callback: {info:?}"); - let when = Instant::now() + info.after; + let when = Instant::now() + info.delay; let frame_nr = info.current_frame_nr; event_loop_proxy .lock() - .send_event(UserEvent::RequestRepaint { when, frame_nr }) + .send_event(UserEvent::RequestRepaint { + viewport_id: info.viewport_id, + when, + frame_nr, + }) .ok(); }); } + #[cfg(feature = "accesskit")] + { + let event_loop_proxy = self.repaint_proxy.lock().clone(); + let viewport = glutin.viewports.get_mut(&ViewportId::ROOT).unwrap(); + if let Viewport { + window: Some(window), + egui_winit: Some(egui_winit), + .. + } = viewport + { + integration.init_accesskit(egui_winit, window, event_loop_proxy); + } + } + + let theme = system_theme.unwrap_or(self.native_options.default_theme); + integration.egui_ctx.set_visuals(theme.egui_visuals()); + + if self.native_options.mouse_passthrough { + if let Err(err) = glutin.window(ViewportId::ROOT).set_cursor_hittest(false) { + log::warn!("set_cursor_hittest(false) failed: {err}"); + } + } + let app_creator = std::mem::take(&mut self.app_creator) .expect("Single-use AppCreator has unexpectedly already been taken"); - let mut app = app_creator(&epi::CreationContext { - egui_ctx: integration.egui_ctx.clone(), - integration_info: integration.frame.info().clone(), - storage: integration.frame.storage(), - gl: Some(gl.clone()), - #[cfg(feature = "wgpu")] - wgpu_render_state: None, - raw_display_handle: gl_window.window().raw_display_handle(), - raw_window_handle: gl_window.window().raw_window_handle(), - }); - if app.warm_up_enabled() { - integration.warm_up(app.as_mut(), gl_window.window()); + let app = { + let window = glutin.window(ViewportId::ROOT); + let mut app = app_creator(&epi::CreationContext { + egui_ctx: integration.egui_ctx.clone(), + integration_info: integration.frame.info().clone(), + storage: integration.frame.storage(), + gl: Some(gl), + #[cfg(feature = "wgpu")] + wgpu_render_state: None, + raw_display_handle: window.raw_display_handle(), + raw_window_handle: window.raw_window_handle(), + }); + + if app.warm_up_enabled() { + let viewport = glutin.viewport_mut(ViewportId::ROOT); + integration.warm_up( + app.as_mut(), + &window, + viewport.egui_winit.as_mut().unwrap(), + ); + } + + app + }; + + let glutin = Rc::new(RefCell::new(glutin)); + let painter = Rc::new(RefCell::new(painter)); + + { + // Create weak pointers so that we don't keep + // state alive for too long. + let glutin = Rc::downgrade(&glutin); + let painter = Rc::downgrade(&painter); + let beginning = integration.beginning; + + let event_loop: *const EventLoopWindowTarget = event_loop; + + egui::Context::set_immediate_viewport_renderer( + move |egui_ctx, immediate_viewport| { + if let (Some(glutin), Some(painter)) = (glutin.upgrade(), painter.upgrade()) + { + // SAFETY: the event loop lives longer than + // the Rc:s we just upgraded above. + #[allow(unsafe_code)] + let event_loop = unsafe { event_loop.as_ref().unwrap() }; + + render_immediate_viewport( + event_loop, + egui_ctx, + &glutin, + &painter, + beginning, + immediate_viewport, + ); + } else { + log::warn!("render_sync_callback called after window closed"); + } + }, + ); } - self.running = Some(GlowWinitRunning { - gl_window, - gl, + Ok(self.running.insert(GlowWinitRunning { + glutin, painter, integration, app, - }); - - Ok(()) + })) } } - impl WinitApp for GlowWinitApp { - fn frame_nr(&self) -> u64 { - self.running - .as_ref() - .map_or(0, |r| r.integration.egui_ctx.frame_nr()) - } - - fn is_focused(&self) -> bool { - self.is_focused - } + /// This is called (via a callback) by user code to render immediate viewports, + /// i.e. viewport that are directly nested inside a parent viewport. + fn render_immediate_viewport( + event_loop: &EventLoopWindowTarget, + egui_ctx: &egui::Context, + glutin: &RefCell, + painter: &RefCell, + beginning: Instant, + immediate_viewport: ImmediateViewport<'_>, + ) { + crate::profile_function!(); + + let ImmediateViewport { + ids, + builder, + viewport_ui_cb, + } = immediate_viewport; - fn integration(&self) -> Option<&EpiIntegration> { - self.running.as_ref().map(|r| &r.integration) - } + { + let mut glutin = glutin.borrow_mut(); - fn window(&self) -> Option<&winit::window::Window> { - self.running.as_ref().map(|r| r.gl_window.window()) - } + let viewport = initialize_or_update_viewport( + &mut glutin.viewports, + ids, + ViewportClass::Immediate, + builder, + None, + None, + ); - fn save_and_destroy(&mut self) { - if let Some(mut running) = self.running.take() { - crate::profile_function!(); - running - .integration - .save(running.app.as_mut(), running.gl_window.window.as_ref()); - running.app.on_exit(Some(&running.gl)); - running.painter.destroy(); + if viewport.gl_surface.is_none() { + glutin.init_viewport(ids.this, event_loop).expect( + "Failed to initialize window in egui::Context::show_viewport_immediate", + ); } } - fn run_ui_and_paint(&mut self) -> EventResult { - let Some(running) = &mut self.running else { - return EventResult::Wait; + let input = { + let mut glutin = glutin.borrow_mut(); + + let Some(viewport) = glutin.viewports.get_mut(&ids.this) else { + return; }; - if running.gl_window.window.is_none() { - return EventResult::Wait; - } + let Some(winit_state) = &mut viewport.egui_winit else { + return; + }; + let Some(window) = &viewport.window else { + return; + }; - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); - crate::profile_scope!("frame"); + let mut input = winit_state.take_egui_input(window, ids); + input.time = Some(beginning.elapsed().as_secs_f64()); + input + }; - let GlowWinitRunning { - gl_window, - gl, - app, - integration, - painter, - } = running; + // --------------------------------------------------- + // Call the user ui-code, which could re-entrantly call this function again! + // No locks may be hold while calling this function. + + let egui::FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = egui_ctx.run(input, |ctx| { + viewport_ui_cb(ctx); + }); - let window = gl_window.window(); + // --------------------------------------------------- - let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); + let mut glutin = glutin.borrow_mut(); - egui_glow::painter::clear( - gl, - screen_size_in_pixels, - app.clear_color(&integration.egui_ctx.style().visuals), - ); + let GlutinWindowContext { + current_gl_context, + viewports, + .. + } = &mut *glutin; - let egui::FullOutput { - platform_output, - repaint_after, - textures_delta, - shapes, - } = integration.update(app.as_mut(), window); + let Some(viewport) = viewports.get_mut(&ids.this) else { + return; + }; - integration.handle_platform_output(window, platform_output); + let Some(winit_state) = &mut viewport.egui_winit else { + return; + }; + let (Some(window), Some(gl_surface)) = (&viewport.window, &viewport.gl_surface) else { + return; + }; - let clipped_primitives = { - crate::profile_scope!("tessellate"); - integration.egui_ctx.tessellate(shapes) - }; + let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); - painter.paint_and_update_textures( - screen_size_in_pixels, - integration.egui_ctx.pixels_per_point(), - &clipped_primitives, - &textures_delta, - ); + let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); - let screenshot_requested = &mut integration.frame.output.screenshot_requested; + let mut painter = painter.borrow_mut(); - if *screenshot_requested { - *screenshot_requested = false; - let screenshot = painter.read_screen_rgba(screen_size_in_pixels); - integration.frame.screenshot.set(Some(screenshot)); - } + *current_gl_context = Some( + current_gl_context + .take() + .unwrap() + .make_not_current() + .unwrap() + .make_current(gl_surface) + .unwrap(), + ); - integration.post_rendering(app.as_mut(), window); + let current_gl_context = current_gl_context.as_ref().unwrap(); - { - crate::profile_scope!("swap_buffers"); - gl_window.swap_buffers().unwrap(); + if !gl_surface.is_current(current_gl_context) { + log::error!("egui::show_viewport_immediate: viewport {:?} ({:?}) is not created in main thread, try to use wgpu!", viewport.ids.this, viewport.builder.title); + } + + let gl = &painter.gl().clone(); + egui_glow::painter::clear(gl, screen_size_in_pixels, [0.0, 0.0, 0.0, 0.0]); + + painter.paint_and_update_textures( + screen_size_in_pixels, + pixels_per_point, + &clipped_primitives, + &textures_delta, + ); + + { + crate::profile_scope!("swap_buffers"); + if let Err(err) = gl_surface.swap_buffers(current_gl_context) { + log::error!("swap_buffers failed: {err}"); } + } - integration.post_present(window); + winit_state.handle_platform_output(window, ids.this, egui_ctx, platform_output); - #[cfg(feature = "__screenshot")] - // give it time to settle: - if integration.egui_ctx.frame_nr() == 2 { - if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") { - assert!( - path.ends_with(".png"), - "Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}" - ); - let screenshot = painter.read_screen_rgba(screen_size_in_pixels); - image::save_buffer( - &path, - screenshot.as_raw(), - screenshot.width() as u32, - screenshot.height() as u32, - image::ColorType::Rgba8, - ) - .unwrap_or_else(|err| { - panic!("Failed to save screenshot to {path:?}: {err}"); - }); - eprintln!("Screenshot saved to {path:?}."); - std::process::exit(0); + let focused_viewport = None; // TODO + glutin.handle_viewport_output(viewport_output, focused_viewport); + } + + impl WinitApp for GlowWinitApp { + fn frame_nr(&self, viewport_id: ViewportId) -> u64 { + self.running + .as_ref() + .map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id)) + } + + fn is_focused(&self, window_id: WindowId) -> bool { + if let Some(focused_viewport) = self.focused_viewport { + if let Some(running) = &self.running { + if let Some(window_id) = + running.glutin.borrow().viewport_from_window.get(&window_id) + { + return focused_viewport == *window_id; + } } } + false + } - let control_flow = if integration.should_close() { - EventResult::Exit - } else if repaint_after.is_zero() { - EventResult::RepaintNext - } else if let Some(repaint_after_instant) = - std::time::Instant::now().checked_add(repaint_after) - { - // if repaint_after is something huge and can't be added to Instant, - // we will use `ControlFlow::Wait` instead. - // technically, this might lead to some weird corner cases where the user *WANTS* - // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own - // egui backend impl i guess. - EventResult::RepaintAt(repaint_after_instant) + fn integration(&self) -> Option<&EpiIntegration> { + self.running.as_ref().map(|r| &r.integration) + } + + fn window(&self, window_id: WindowId) -> Option> { + let running = self.running.as_ref()?; + let glutin = running.glutin.borrow(); + let viewport_id = *glutin.viewport_from_window.get(&window_id)?; + if let Some(viewport) = glutin.viewports.get(&viewport_id) { + viewport.window.clone() } else { - EventResult::Wait - }; + None + } + } - integration.maybe_autosave(app.as_mut(), window); + fn window_id_from_viewport_id(&self, id: ViewportId) -> Option { + self.running + .as_ref() + .and_then(|r| r.glutin.borrow().window_from_viewport.get(&id).copied()) + } - if window.is_minimized() == Some(true) { - // On Mac, a minimized Window uses up all CPU: - // https://github.com/emilk/egui/issues/325 - crate::profile_scope!("minimized_sleep"); - std::thread::sleep(std::time::Duration::from_millis(10)); + fn save_and_destroy(&mut self) { + if let Some(mut running) = self.running.take() { + crate::profile_function!(); + + running.integration.save( + running.app.as_mut(), + Some(&running.glutin.borrow().window(ViewportId::ROOT)), + ); + running.app.on_exit(Some(running.painter.borrow().gl())); + running.painter.borrow_mut().destroy(); } + } - control_flow + fn run_ui_and_paint(&mut self, window_id: WindowId) -> EventResult { + if let Some(running) = &mut self.running { + running.run_ui_and_paint(window_id, self.focused_viewport) + } else { + EventResult::Wait + } } fn on_event( @@ -984,88 +1712,44 @@ mod glow_integration { Ok(match event { winit::event::Event::Resumed => { - // first resume event. - // we can actually move this outside of event loop. - // and just run the on_resume fn of gl_window - if self.running.is_none() { - self.init_run_state(event_loop)?; - } else { + let running = if let Some(running) = &mut self.running { // not the first resume event. create whatever you need. - self.running - .as_mut() - .unwrap() - .gl_window - .on_resume(event_loop)?; - } - EventResult::RepaintNow + running.glutin.borrow_mut().on_resume(event_loop)?; + running + } else { + // first resume event. + // we can actually move this outside of event loop. + // and just run the on_resume fn of gl_window + self.init_run_state(event_loop)? + }; + let window_id = running + .glutin + .borrow() + .window_from_viewport + .get(&ViewportId::ROOT) + .copied(); + EventResult::RepaintNow(window_id.unwrap()) } - winit::event::Event::Suspended => { - self.running.as_mut().unwrap().gl_window.on_suspend()?; + winit::event::Event::Suspended => { + if let Some(running) = &mut self.running { + running.glutin.borrow_mut().on_suspend()?; + } EventResult::Wait } - winit::event::Event::WindowEvent { event, .. } => { - if let Some(running) = &mut self.running { - // On Windows, if a window is resized by the user, it should repaint synchronously, inside the - // event handler. - // - // If this is not done, the compositor will assume that the window does not want to redraw, - // and continue ahead. - // - // In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver - // new frames to the compositor in time. - // - // The flickering is technically glutin or glow's fault, but we should be responding properly - // to resizes anyway, as doing so avoids dropping frames. - // - // See: https://github.com/emilk/egui/issues/903 - let mut repaint_asap = false; - - match &event { - winit::event::WindowEvent::Focused(new_focused) => { - self.is_focused = *new_focused; - } - winit::event::WindowEvent::Resized(physical_size) => { - repaint_asap = true; - - // Resize with 0 width and height is used by winit to signal a minimize event on Windows. - // See: https://github.com/rust-windowing/winit/issues/208 - // This solves an issue where the app would panic when minimizing on Windows. - if 0 < physical_size.width && 0 < physical_size.height { - running.gl_window.resize(*physical_size); - } - } - winit::event::WindowEvent::ScaleFactorChanged { - new_inner_size, - .. - } => { - repaint_asap = true; - running.gl_window.resize(**new_inner_size); - } - winit::event::WindowEvent::CloseRequested - if running.integration.should_close() => - { - log::debug!("Received WindowEvent::CloseRequested"); - return Ok(EventResult::Exit); - } - _ => {} + winit::event::Event::MainEventsCleared => { + if let Some(running) = &self.running { + if let Err(err) = running.glutin.borrow_mut().on_resume(event_loop) { + log::warn!("on_resume failed {err}"); } + } + EventResult::Wait + } - let event_response = - running.integration.on_event(running.app.as_mut(), event); - - if running.integration.should_close() { - EventResult::Exit - } else if event_response.repaint { - if repaint_asap { - EventResult::RepaintNow - } else { - EventResult::RepaintNext - } - } else { - EventResult::Wait - } + winit::event::Event::WindowEvent { event, window_id } => { + if let Some(running) = &mut self.running { + running.on_window_event(*window_id, event, &mut self.focused_viewport) } else { EventResult::Wait } @@ -1073,16 +1757,23 @@ mod glow_integration { #[cfg(feature = "accesskit")] winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( - accesskit_winit::ActionRequestEvent { request, .. }, + accesskit_winit::ActionRequestEvent { request, window_id }, )) => { - if let Some(running) = &mut self.running { - crate::profile_scope!("on_accesskit_action_request"); - running - .integration - .on_accesskit_action_request(request.clone()); + if let Some(running) = &self.running { + let mut glutin = running.glutin.borrow_mut(); + if let Some(viewport_id) = + glutin.viewport_from_window.get(window_id).copied() + { + if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) { + if let Some(egui_winit) = &mut viewport.egui_winit { + crate::profile_scope!("on_accesskit_action_request"); + egui_winit.on_accesskit_action_request(request.clone()); + } + } + } // As a form of user input, accessibility actions should // lead to a repaint. - EventResult::RepaintNext + EventResult::RepaintNext(*window_id) } else { EventResult::Wait } @@ -1099,23 +1790,16 @@ mod glow_integration { ) -> Result<()> { #[cfg(not(target_os = "ios"))] if native_options.run_and_return { - with_event_loop(native_options, |event_loop, native_options| { + return with_event_loop(native_options, |event_loop, native_options| { let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator); run_and_return(event_loop, glow_eframe) - }) - } else { - let event_loop = create_event_loop(&mut native_options); - let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator); - run_and_exit(event_loop, glow_eframe); + }); } - #[cfg(target_os = "ios")] - { - let event_loop = create_event_loop(&mut native_options); - let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator); - run_and_exit(event_loop, glow_eframe); - } + let event_loop = create_event_loop(&mut native_options); + let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator); + run_and_exit(event_loop, glow_eframe); } } @@ -1125,32 +1809,106 @@ pub use glow_integration::run_glow; #[cfg(feature = "wgpu")] mod wgpu_integration { - use std::sync::Arc; - use parking_lot::Mutex; + use egui::{ + DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportClass, ViewportIdMap, + ViewportIdPair, ViewportIdSet, ViewportOutput, + }; + use egui_winit::{create_winit_window_builder, process_viewport_commands}; + use super::*; - /// State that is initialized when the application is first starts running via - /// a Resumed event. On Android this ensures that any graphics state is only - /// initialized once the application has an associated `SurfaceView`. - struct WgpuWinitRunning { - painter: egui_wgpu::winit::Painter, - integration: epi_integration::EpiIntegration, - app: Box, - } + pub struct Viewport { + ids: ViewportIdPair, - struct WgpuWinitApp { - repaint_proxy: Arc>>, - app_name: String, - native_options: epi::NativeOptions, - app_creator: Option, - running: Option, + class: ViewportClass, + + builder: ViewportBuilder, + + /// `None` for sync viewports. + viewport_ui_cb: Option>, /// Window surface state that's initialized when the app starts running via a Resumed event /// and on Android will also be destroyed if the application is paused. - window: Option, - is_focused: bool, + window: Option>, + + /// `window` and `egui_winit` are initialized together. + egui_winit: Option, + } + + impl Viewport { + fn init_window( + &mut self, + windows_id: &mut HashMap, + painter: &mut egui_wgpu::winit::Painter, + event_loop: &EventLoopWindowTarget, + ) { + crate::profile_function!(); + + let viewport_id = self.ids.this; + + match create_winit_window_builder(&self.builder).build(event_loop) { + Ok(new_window) => { + windows_id.insert(new_window.id(), viewport_id); + + if let Err(err) = + pollster::block_on(painter.set_window(viewport_id, Some(&new_window))) + { + log::error!("on set_window: viewport_id {viewport_id:?} {err}"); + } + + self.egui_winit = Some(egui_winit::State::new( + event_loop, + Some(new_window.scale_factor() as f32), + painter.max_texture_side(), + )); + + self.window = Some(Rc::new(new_window)); + } + Err(err) => { + log::error!("Failed to create window: {err}"); + } + } + } + } + + pub type Viewports = ViewportIdMap; + + /// Everything needed by the immediate viewport renderer. + /// + /// Wrapped in an `Rc>` so it can be re-entrantly shared via a weak-pointer. + pub struct SharedState { + viewports: Viewports, + painter: egui_wgpu::winit::Painter, + viewport_from_window: HashMap, + } + + /// State that is initialized when the application is first starts running via + /// a Resumed event. On Android this ensures that any graphics state is only + /// initialized once the application has an associated `SurfaceView`. + struct WgpuWinitRunning { + integration: epi_integration::EpiIntegration, + + /// The users application. + app: Box, + + /// Wrapped in an `Rc>` so it can be re-entrantly shared via a weak-pointer. + shared: Rc>, + } + + struct WgpuWinitApp { + repaint_proxy: Arc>>, + app_name: String, + native_options: epi::NativeOptions, + + /// Set at initialization, then taken and set to `None` in `init_run_state`. + app_creator: Option, + + /// Set when we are actually up and running. + running: Option, + + focused_viewport: Option, } impl WgpuWinitApp { @@ -1161,6 +1919,7 @@ mod wgpu_integration { app_creator: epi::AppCreator, ) -> Self { crate::profile_function!(); + #[cfg(feature = "__screenshot")] assert!( std::env::var("EFRAME_SCREENSHOT_TO").is_err(), @@ -1172,54 +1931,35 @@ mod wgpu_integration { app_name: app_name.to_owned(), native_options, running: None, - window: None, app_creator: Some(app_creator), - is_focused: true, + focused_viewport: Some(ViewportId::ROOT), } } - fn create_window( - event_loop: &EventLoopWindowTarget, - storage: Option<&dyn epi::Storage>, - title: &str, - native_options: &mut NativeOptions, - ) -> std::result::Result { - crate::profile_function!(); - - let window_settings = epi_integration::load_window_settings(storage); - let window_builder = - epi_integration::window_builder(event_loop, title, native_options, window_settings); - let window = { - crate::profile_scope!("WindowBuilder::build"); - window_builder.build(event_loop)? + fn build_windows(&mut self, event_loop: &EventLoopWindowTarget) { + let Some(running) = &mut self.running else { + return; }; - epi_integration::apply_native_options_to_window( - &window, - native_options, - window_settings, - ); - Ok(window) - } + let mut shared = running.shared.borrow_mut(); + let SharedState { + viewports, + painter, + viewport_from_window, + } = &mut *shared; - #[allow(unsafe_code)] - fn set_window( - &mut self, - window: winit::window::Window, - ) -> std::result::Result<(), egui_wgpu::WgpuError> { - self.window = Some(window); - if let Some(running) = &mut self.running { - crate::profile_function!(); - pollster::block_on(running.painter.set_window(self.window.as_ref()))?; + for viewport in viewports.values_mut() { + if viewport.window.is_none() { + viewport.init_window(viewport_from_window, painter, event_loop); + } } - Ok(()) } - #[allow(unsafe_code)] #[cfg(target_os = "android")] - fn drop_window(&mut self) -> std::result::Result<(), egui_wgpu::WgpuError> { - self.window = None; + fn drop_window(&mut self) -> Result<(), egui_wgpu::WgpuError> { if let Some(running) = &mut self.running { - pollster::block_on(running.painter.set_window(None))?; + let mut shared = running.shared.borrow_mut(); + shared.viewports.remove(&ViewportId::ROOT); + pollster::block_on(shared.painter.set_window(ViewportId::ROOT, None))?; } Ok(()) } @@ -1228,8 +1968,9 @@ mod wgpu_integration { &mut self, event_loop: &EventLoopWindowTarget, storage: Option>, - window: winit::window::Window, - ) -> std::result::Result<(), egui_wgpu::WgpuError> { + window: Window, + builder: ViewportBuilder, + ) -> Result<&mut WgpuWinitRunning, egui_wgpu::WgpuError> { crate::profile_function!(); #[allow(unsafe_code, unused_mut, unused_unsafe)] @@ -1242,45 +1983,58 @@ mod wgpu_integration { ), self.native_options.transparent, ); - pollster::block_on(painter.set_window(Some(&window)))?; + pollster::block_on(painter.set_window(ViewportId::ROOT, Some(&window)))?; let wgpu_render_state = painter.render_state(); let system_theme = system_theme(&window, &self.native_options); let mut integration = epi_integration::EpiIntegration::new( - event_loop, - painter.max_texture_side().unwrap_or(2048), &window, system_theme, &self.app_name, &self.native_options, storage, + IS_DESKTOP, #[cfg(feature = "glow")] None, wgpu_render_state.clone(), ); - #[cfg(feature = "accesskit")] - { - integration.init_accesskit(&window, self.repaint_proxy.lock().clone()); - } - let theme = system_theme.unwrap_or(self.native_options.default_theme); - integration.egui_ctx.set_visuals(theme.egui_visuals()); { let event_loop_proxy = self.repaint_proxy.clone(); + integration .egui_ctx .set_request_repaint_callback(move |info| { log::trace!("request_repaint_callback: {info:?}"); - let when = Instant::now() + info.after; + let when = Instant::now() + info.delay; let frame_nr = info.current_frame_nr; + event_loop_proxy .lock() - .send_event(UserEvent::RequestRepaint { when, frame_nr }) + .send_event(UserEvent::RequestRepaint { + when, + frame_nr, + viewport_id: info.viewport_id, + }) .ok(); }); } + let mut egui_winit = egui_winit::State::new( + event_loop, + Some(window.scale_factor() as f32), + painter.max_texture_side(), + ); + + #[cfg(feature = "accesskit")] + { + let event_loop_proxy = self.repaint_proxy.lock().clone(); + integration.init_accesskit(&mut egui_winit, &window, event_loop_proxy); + } + let theme = system_theme.unwrap_or(self.native_options.default_theme); + integration.egui_ctx.set_visuals(theme.egui_visuals()); + let app_creator = std::mem::take(&mut self.app_creator) .expect("Single-use AppCreator has unexpectedly already been taken"); let cc = epi::CreationContext { @@ -1299,127 +2053,247 @@ mod wgpu_integration { }; if app.warm_up_enabled() { - integration.warm_up(app.as_mut(), &window); + integration.warm_up(app.as_mut(), &window, &mut egui_winit); } - self.running = Some(WgpuWinitRunning { + let mut viewport_from_window = HashMap::default(); + viewport_from_window.insert(window.id(), ViewportId::ROOT); + + let mut viewports = Viewports::default(); + viewports.insert( + ViewportId::ROOT, + Viewport { + ids: ViewportIdPair::ROOT, + class: ViewportClass::Root, + builder, + viewport_ui_cb: None, + window: Some(Rc::new(window)), + egui_winit: Some(egui_winit), + }, + ); + + let shared = Rc::new(RefCell::new(SharedState { + viewport_from_window, + viewports, painter, + })); + + { + // Create a weak pointer so that we don't keep state alive for too long. + let shared = Rc::downgrade(&shared); + let beginning = integration.beginning; + + let event_loop: *const EventLoopWindowTarget = event_loop; + + egui::Context::set_immediate_viewport_renderer( + move |egui_ctx, immediate_viewport| { + if let Some(shared) = shared.upgrade() { + // SAFETY: the event loop lives longer than + // the Rc:s we just upgraded above. + #[allow(unsafe_code)] + let event_loop = unsafe { event_loop.as_ref().unwrap() }; + + render_immediate_viewport( + event_loop, + egui_ctx, + beginning, + &shared, + immediate_viewport, + ); + } else { + log::warn!("render_sync_callback called after window closed"); + } + }, + ); + } + + Ok(self.running.insert(WgpuWinitRunning { integration, app, - }); - self.window = Some(window); + shared, + })) + } + } - Ok(()) + fn create_window( + event_loop: &EventLoopWindowTarget, + storage: Option<&dyn epi::Storage>, + title: &str, + native_options: &mut NativeOptions, + ) -> Result<(Window, ViewportBuilder), winit::error::OsError> { + crate::profile_function!(); + + let window_settings = epi_integration::load_window_settings(storage); + let window_builder = + epi_integration::window_builder(event_loop, title, native_options, window_settings); + let window = { + crate::profile_scope!("WindowBuilder::build"); + create_winit_window_builder(&window_builder).build(event_loop)? + }; + epi_integration::apply_native_options_to_window(&window, native_options, window_settings); + Ok((window, window_builder)) + } + + fn render_immediate_viewport( + event_loop: &EventLoopWindowTarget, + egui_ctx: &egui::Context, + beginning: Instant, + shared: &RefCell, + immediate_viewport: ImmediateViewport<'_>, + ) { + crate::profile_function!(); + + let ImmediateViewport { + ids, + builder, + viewport_ui_cb, + } = immediate_viewport; + + let input = { + let SharedState { + viewports, + painter, + viewport_from_window, + } = &mut *shared.borrow_mut(); + let viewport = initialize_or_update_viewport( + viewports, + ids, + ViewportClass::Immediate, + builder, + None, + None, + ); + + if viewport.window.is_none() { + viewport.init_window(viewport_from_window, painter, event_loop); + } + + let (Some(window), Some(winit_state)) = (&viewport.window, &mut viewport.egui_winit) + else { + return; + }; + + let mut input = winit_state.take_egui_input(window, ids); + input.time = Some(beginning.elapsed().as_secs_f64()); + input + }; + + // ------------------------------------------ + + // Run the user code, which could re-entrantly call this function again (!). + // Make sure no locks are held during this call. + let egui::FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = egui_ctx.run(input, |ctx| { + viewport_ui_cb(ctx); + }); + + // ------------------------------------------ + + let mut shared = shared.borrow_mut(); + let SharedState { + viewports, painter, .. + } = &mut *shared; + + let Some(viewport) = viewports.get_mut(&ids.this) else { + return; + }; + let Some(winit_state) = &mut viewport.egui_winit else { + return; + }; + let Some(window) = &viewport.window else { + return; + }; + + if let Err(err) = pollster::block_on(painter.set_window(ids.this, Some(window))) { + log::error!( + "when rendering viewport_id={:?}, set_window Error {err}", + ids.this + ); } + + let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); + painter.paint_and_update_textures( + ids.this, + pixels_per_point, + [0.0, 0.0, 0.0, 0.0], + &clipped_primitives, + &textures_delta, + false, + ); + + winit_state.handle_platform_output(window, ids.this, egui_ctx, platform_output); + + let focused_viewport = None; // TODO + handle_viewport_output(viewport_output, viewports, focused_viewport); } impl WinitApp for WgpuWinitApp { - fn frame_nr(&self) -> u64 { + fn frame_nr(&self, viewport_id: ViewportId) -> u64 { self.running .as_ref() - .map_or(0, |r| r.integration.egui_ctx.frame_nr()) + .map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id)) } - fn is_focused(&self) -> bool { - self.is_focused + fn is_focused(&self, window_id: WindowId) -> bool { + let viewport_id = self.running.as_ref().and_then(|r| { + r.shared + .borrow() + .viewport_from_window + .get(&window_id) + .copied() + }); + + self.focused_viewport.is_some() && self.focused_viewport == viewport_id } fn integration(&self) -> Option<&EpiIntegration> { self.running.as_ref().map(|r| &r.integration) } - fn window(&self) -> Option<&winit::window::Window> { - self.window.as_ref() + fn window(&self, window_id: WindowId) -> Option> { + self.running + .as_ref() + .and_then(|r| { + let shared = r.shared.borrow(); + shared + .viewport_from_window + .get(&window_id) + .and_then(|id| shared.viewports.get(id).map(|v| v.window.clone())) + }) + .flatten() + } + + fn window_id_from_viewport_id(&self, id: ViewportId) -> Option { + Some( + self.running + .as_ref()? + .shared + .borrow() + .viewports + .get(&id)? + .window + .as_ref()? + .id(), + ) } fn save_and_destroy(&mut self) { if let Some(mut running) = self.running.take() { - crate::profile_function!(); - running - .integration - .save(running.app.as_mut(), self.window.as_ref()); - - #[cfg(feature = "glow")] - running.app.on_exit(None); - - #[cfg(not(feature = "glow"))] - running.app.on_exit(); - - running.painter.destroy(); + running.save_and_destroy(); } } - fn run_ui_and_paint(&mut self) -> EventResult { - let (Some(running), Some(window)) = (&mut self.running, &self.window) else { - return EventResult::Wait; - }; - - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); - crate::profile_scope!("frame"); - - let WgpuWinitRunning { - app, - integration, - painter, - } = running; - - let egui::FullOutput { - platform_output, - repaint_after, - textures_delta, - shapes, - } = integration.update(app.as_mut(), window); - - integration.handle_platform_output(window, platform_output); - - let clipped_primitives = { - crate::profile_scope!("tessellate"); - integration.egui_ctx.tessellate(shapes) - }; - - let screenshot_requested = &mut integration.frame.output.screenshot_requested; - - let screenshot = painter.paint_and_update_textures( - integration.egui_ctx.pixels_per_point(), - app.clear_color(&integration.egui_ctx.style().visuals), - &clipped_primitives, - &textures_delta, - *screenshot_requested, - ); - *screenshot_requested = false; - integration.frame.screenshot.set(screenshot); - - integration.post_rendering(app.as_mut(), window); - integration.post_present(window); - - let control_flow = if integration.should_close() { - EventResult::Exit - } else if repaint_after.is_zero() { - EventResult::RepaintNext - } else if let Some(repaint_after_instant) = - std::time::Instant::now().checked_add(repaint_after) - { - // if repaint_after is something huge and can't be added to Instant, - // we will use `ControlFlow::Wait` instead. - // technically, this might lead to some weird corner cases where the user *WANTS* - // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own - // egui backend impl i guess. - EventResult::RepaintAt(repaint_after_instant) + fn run_ui_and_paint(&mut self, window_id: WindowId) -> EventResult { + if let Some(running) = &mut self.running { + running.run_ui_and_paint(window_id, self.focused_viewport) } else { EventResult::Wait - }; - - integration.maybe_autosave(app.as_mut(), window); - - if window.is_minimized() == Some(true) { - // On Mac, a minimized Window uses up all CPU: - // https://github.com/emilk/egui/issues/325 - crate::profile_scope!("minimized_sleep"); - std::thread::sleep(std::time::Duration::from_millis(10)); } - - control_flow } fn on_event( @@ -1429,18 +2303,26 @@ mod wgpu_integration { ) -> Result { crate::profile_function!(); + self.build_windows(event_loop); + Ok(match event { winit::event::Event::Resumed => { - if let Some(running) = &self.running { - if self.window.is_none() { - let window = Self::create_window( + let running = if let Some(running) = &self.running { + if !running + .shared + .borrow() + .viewports + .contains_key(&ViewportId::ROOT) + { + create_window( event_loop, running.integration.frame.storage(), &self.app_name, &mut self.native_options, )?; - self.set_window(window)?; + running.set_window(ViewportId::ROOT)?; } + running } else { let storage = epi_integration::create_storage( self.native_options @@ -1448,102 +2330,60 @@ mod wgpu_integration { .as_ref() .unwrap_or(&self.app_name), ); - let window = Self::create_window( + let (window, builder) = create_window( event_loop, storage.as_deref(), &self.app_name, &mut self.native_options, )?; - self.init_run_state(event_loop, storage, window)?; - } - EventResult::RepaintNow + self.init_run_state(event_loop, storage, window, builder)? + }; + + EventResult::RepaintNow( + running.shared.borrow().viewports[&ViewportId::ROOT] + .window + .as_ref() + .unwrap() + .id(), + ) } + winit::event::Event::Suspended => { #[cfg(target_os = "android")] self.drop_window()?; EventResult::Wait } - winit::event::Event::WindowEvent { event, .. } => { + winit::event::Event::WindowEvent { event, window_id } => { if let Some(running) = &mut self.running { - // On Windows, if a window is resized by the user, it should repaint synchronously, inside the - // event handler. - // - // If this is not done, the compositor will assume that the window does not want to redraw, - // and continue ahead. - // - // In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver - // new frames to the compositor in time. - // - // The flickering is technically glutin or glow's fault, but we should be responding properly - // to resizes anyway, as doing so avoids dropping frames. - // - // See: https://github.com/emilk/egui/issues/903 - let mut repaint_asap = false; - - match &event { - winit::event::WindowEvent::Focused(new_focused) => { - self.is_focused = *new_focused; - } - winit::event::WindowEvent::Resized(physical_size) => { - repaint_asap = true; - - // Resize with 0 width and height is used by winit to signal a minimize event on Windows. - // See: https://github.com/rust-windowing/winit/issues/208 - // This solves an issue where the app would panic when minimizing on Windows. - if 0 < physical_size.width && 0 < physical_size.height { - running.painter.on_window_resized( - physical_size.width, - physical_size.height, - ); - } - } - winit::event::WindowEvent::ScaleFactorChanged { - new_inner_size, - .. - } => { - repaint_asap = true; - running - .painter - .on_window_resized(new_inner_size.width, new_inner_size.height); - } - winit::event::WindowEvent::CloseRequested - if running.integration.should_close() => - { - log::debug!("Received WindowEvent::CloseRequested"); - return Ok(EventResult::Exit); - } - _ => {} - }; - - let event_response = - running.integration.on_event(running.app.as_mut(), event); - if running.integration.should_close() { - EventResult::Exit - } else if event_response.repaint { - if repaint_asap { - EventResult::RepaintNow - } else { - EventResult::RepaintNext - } - } else { - EventResult::Wait - } + running.on_window_event(*window_id, event, &mut self.focused_viewport) } else { EventResult::Wait } } + #[cfg(feature = "accesskit")] winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( - accesskit_winit::ActionRequestEvent { request, .. }, + accesskit_winit::ActionRequestEvent { request, window_id }, )) => { if let Some(running) = &mut self.running { - running - .integration - .on_accesskit_action_request(request.clone()); + let mut shared_lock = running.shared.borrow_mut(); + let SharedState { + viewport_from_window, + viewports, + .. + } = &mut *shared_lock; + if let Some(viewport) = viewport_from_window + .get(window_id) + .and_then(|id| viewports.get_mut(id)) + { + if let Some(egui_winit) = &mut viewport.egui_winit { + egui_winit.on_accesskit_action_request(request.clone()); + } + } // As a form of user input, accessibility actions should // lead to a repaint. - EventResult::RepaintNext + EventResult::RepaintNext(*window_id) } else { EventResult::Wait } @@ -1553,6 +2393,398 @@ mod wgpu_integration { } } + impl WgpuWinitRunning { + fn set_window(&self, id: ViewportId) -> Result<(), egui_wgpu::WgpuError> { + crate::profile_function!(); + let mut shared = self.shared.borrow_mut(); + let SharedState { + viewports, painter, .. + } = &mut *shared; + if let Some(Viewport { window, .. }) = viewports.get(&id) { + return pollster::block_on(painter.set_window(id, window.as_deref())); + } + Ok(()) + } + + fn save_and_destroy(&mut self) { + crate::profile_function!(); + + let mut shared = self.shared.borrow_mut(); + if let Some(Viewport { window, .. }) = shared.viewports.get(&ViewportId::ROOT) { + self.integration.save(self.app.as_mut(), window.as_deref()); + } + + #[cfg(feature = "glow")] + self.app.on_exit(None); + + #[cfg(not(feature = "glow"))] + self.app.on_exit(); + + shared.painter.destroy(); + } + + /// This is called both for the root viewport, and all deferred viewports + fn run_ui_and_paint( + &mut self, + window_id: WindowId, + focused_viewport: Option, + ) -> EventResult { + let Some(viewport_id) = self + .shared + .borrow() + .viewport_from_window + .get(&window_id) + .copied() + else { + return EventResult::Wait; + }; + + #[cfg(feature = "puffin")] + puffin::GlobalProfiler::lock().new_frame(); + + crate::profile_scope!("frame"); + + let WgpuWinitRunning { + app, + integration, + shared, + } = self; + + let (viewport_ui_cb, raw_input) = { + let mut shared_lock = shared.borrow_mut(); + + let SharedState { + viewports, painter, .. + } = &mut *shared_lock; + + let Some(viewport) = viewports.get(&viewport_id) else { + return EventResult::Wait; + }; + + if viewport_id != ViewportId::ROOT && viewport.viewport_ui_cb.is_none() { + // This will only happen if this is an immediate viewport. + // That means that the viewport cannot be rendered by itself and needs his parent to be rendered. + if let Some(viewport) = viewports.get(&viewport.ids.parent) { + if let Some(window) = viewport.window.as_ref() { + return EventResult::RepaintNext(window.id()); + } + } + return EventResult::Wait; + } + + let Some(viewport) = viewports.get_mut(&viewport_id) else { + return EventResult::Wait; + }; + + let Viewport { + ids, + viewport_ui_cb, + window, + egui_winit, + .. + } = viewport; + + let Some(window) = window else { + return EventResult::Wait; + }; + + if let Err(err) = pollster::block_on(painter.set_window(viewport_id, Some(window))) + { + log::warn!("Failed to set window: {err}"); + } + + let raw_input = egui_winit.as_mut().unwrap().take_egui_input( + window, + ViewportIdPair::from_self_and_parent(viewport_id, ids.parent), + ); + + integration.pre_update(window); + + (viewport_ui_cb.clone(), raw_input) + }; + + // ------------------------------------------------------------ + + // Runs the update, which could call immediate viewports, + // so make sure we hold no locks here! + let full_output = + integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input); + + // ------------------------------------------------------------ + + let mut shared = shared.borrow_mut(); + + let SharedState { + viewports, + painter, + viewport_from_window, + } = &mut *shared; + + let Some(viewport) = viewports.get_mut(&viewport_id) else { + return EventResult::Wait; + }; + + let Viewport { + window: Some(window), + egui_winit: Some(egui_winit), + .. + } = viewport + else { + return EventResult::Wait; + }; + + integration.post_update(app.as_mut(), window); + + let FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = full_output; + + integration.handle_platform_output(window, viewport_id, platform_output, egui_winit); + + { + let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); + + let screenshot_requested = &mut integration.frame.output.screenshot_requested; + let screenshot = painter.paint_and_update_textures( + viewport_id, + pixels_per_point, + app.clear_color(&integration.egui_ctx.style().visuals), + &clipped_primitives, + &textures_delta, + *screenshot_requested, + ); + *screenshot_requested = false; + integration.frame.screenshot.set(screenshot); + } + + integration.post_rendering(app.as_mut(), window); + integration.post_present(window); + + let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); + + handle_viewport_output(viewport_output, viewports, focused_viewport); + + // Prune dead viewports: + viewports.retain(|id, _| active_viewports_ids.contains(id)); + viewport_from_window.retain(|_, id| active_viewports_ids.contains(id)); + painter.gc_viewports(&active_viewports_ids); + + let window = viewport_from_window + .get(&window_id) + .and_then(|id| viewports.get(id)) + .and_then(|vp| vp.window.as_ref()); + + integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref())); + + if let Some(window) = window { + if window.is_minimized() == Some(true) { + // On Mac, a minimized Window uses up all CPU: + // https://github.com/emilk/egui/issues/325 + crate::profile_scope!("minimized_sleep"); + std::thread::sleep(std::time::Duration::from_millis(10)); + } + } + + if integration.should_close() { + EventResult::Exit + } else { + EventResult::Wait + } + } + + fn on_window_event( + &mut self, + window_id: WindowId, + event: &winit::event::WindowEvent<'_>, + focused_viewport: &mut Option, + ) -> EventResult { + let Self { + integration, + app, + shared, + } = self; + let mut shared = shared.borrow_mut(); + + let viewport_id = shared.viewport_from_window.get(&window_id).copied(); + + // On Windows, if a window is resized by the user, it should repaint synchronously, inside the + // event handler. + // + // If this is not done, the compositor will assume that the window does not want to redraw, + // and continue ahead. + // + // In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver + // new frames to the compositor in time. + // + // The flickering is technically glutin or glow's fault, but we should be responding properly + // to resizes anyway, as doing so avoids dropping frames. + // + // See: https://github.com/emilk/egui/issues/903 + let mut repaint_asap = false; + + match event { + winit::event::WindowEvent::Focused(new_focused) => { + *focused_viewport = new_focused.then(|| viewport_id).flatten(); + } + winit::event::WindowEvent::Resized(physical_size) => { + // Resize with 0 width and height is used by winit to signal a minimize event on Windows. + // See: https://github.com/rust-windowing/winit/issues/208 + // This solves an issue where the app would panic when minimizing on Windows. + if let Some(viewport_id) = viewport_id { + use std::num::NonZeroU32; + if let (Some(width), Some(height)) = ( + NonZeroU32::new(physical_size.width), + NonZeroU32::new(physical_size.height), + ) { + repaint_asap = true; + shared.painter.on_window_resized(viewport_id, width, height); + } + } + } + winit::event::WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { + use std::num::NonZeroU32; + if let (Some(width), Some(height), Some(viewport_id)) = ( + NonZeroU32::new(new_inner_size.width), + NonZeroU32::new(new_inner_size.height), + shared.viewport_from_window.get(&window_id).copied(), + ) { + repaint_asap = true; + shared.painter.on_window_resized(viewport_id, width, height); + } + } + winit::event::WindowEvent::CloseRequested if integration.should_close() => { + log::debug!("Received WindowEvent::CloseRequested"); + return EventResult::Exit; + } + _ => {} + }; + + let event_response = viewport_id.and_then(|viewport_id| { + shared.viewports.get_mut(&viewport_id).and_then(|viewport| { + viewport.egui_winit.as_mut().map(|egui_winit| { + integration.on_event(app.as_mut(), event, egui_winit, viewport_id) + }) + }) + }); + + if integration.should_close() { + EventResult::Exit + } else if let Some(event_response) = event_response { + if event_response.repaint { + if repaint_asap { + EventResult::RepaintNow(window_id) + } else { + EventResult::RepaintNext(window_id) + } + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } + } + + /// Add new viewports, and update existing ones: + fn handle_viewport_output( + viewport_output: ViewportIdMap, + viewports: &mut ViewportIdMap, + focused_viewport: Option, + ) { + for ( + viewport_id, + ViewportOutput { + parent, + class, + builder, + viewport_ui_cb, + commands, + repaint_delay: _, // ignored - we listened to the repaint callback instead + }, + ) in viewport_output + { + let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent); + + initialize_or_update_viewport( + viewports, + ids, + class, + builder, + viewport_ui_cb, + focused_viewport, + ); + + if let Some(window) = viewports + .get(&viewport_id) + .and_then(|vp| vp.window.as_ref()) + { + let is_viewport_focused = focused_viewport == Some(viewport_id); + egui_winit::process_viewport_commands(commands, window, is_viewport_focused); + } + } + } + + fn initialize_or_update_viewport( + viewports: &mut Viewports, + ids: ViewportIdPair, + class: ViewportClass, + mut builder: ViewportBuilder, + viewport_ui_cb: Option>, + focused_viewport: Option, + ) -> &mut Viewport { + if builder.icon.is_none() { + // Inherit icon from parent + builder.icon = viewports + .get_mut(&ids.parent) + .and_then(|vp| vp.builder.icon.clone()); + } + + match viewports.entry(ids.this) { + std::collections::hash_map::Entry::Vacant(entry) => { + // New viewport: + log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title); + entry.insert(Viewport { + ids, + class, + builder, + viewport_ui_cb, + window: None, + egui_winit: None, + }) + } + + std::collections::hash_map::Entry::Occupied(mut entry) => { + // Patch an existing viewport: + let viewport = entry.get_mut(); + + viewport.class = class; + viewport.ids.parent = ids.parent; + viewport.viewport_ui_cb = viewport_ui_cb; + + let (delta_commands, recreate) = viewport.builder.patch(&builder); + + if recreate { + log::debug!( + "Recreating window for viewport {:?} ({:?})", + ids.this, + builder.title + ); + viewport.window = None; + viewport.egui_winit = None; + } else if let Some(window) = &viewport.window { + let is_viewport_focused = focused_viewport == Some(ids.this); + process_viewport_commands(delta_commands, window, is_viewport_focused); + } + + entry.into_mut() + } + } + } + pub fn run_wgpu( app_name: &str, mut native_options: epi::NativeOptions, @@ -1560,23 +2792,16 @@ mod wgpu_integration { ) -> Result<()> { #[cfg(not(target_os = "ios"))] if native_options.run_and_return { - with_event_loop(native_options, |event_loop, native_options| { + return with_event_loop(native_options, |event_loop, native_options| { let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator); run_and_return(event_loop, wgpu_eframe) - }) - } else { - let event_loop = create_event_loop(&mut native_options); - let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator); - run_and_exit(event_loop, wgpu_eframe); + }); } - #[cfg(target_os = "ios")] - { - let event_loop = create_event_loop(&mut native_options); - let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator); - run_and_exit(event_loop, wgpu_eframe); - } + let event_loop = create_event_loop(&mut native_options); + let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator); + run_and_exit(event_loop, wgpu_eframe); } } @@ -1585,7 +2810,7 @@ pub use wgpu_integration::run_wgpu; // ---------------------------------------------------------------------------- -fn system_theme(window: &winit::window::Window, options: &NativeOptions) -> Option { +fn system_theme(window: &Window, options: &NativeOptions) -> Option { if options.follow_system_theme { window .theme() @@ -1595,12 +2820,6 @@ fn system_theme(window: &winit::window::Window, options: &NativeOptions) -> Opti } } -// ---------------------------------------------------------------------------- - -fn extremely_far_future() -> std::time::Instant { - std::time::Instant::now() + std::time::Duration::from_secs(10_000_000_000) -} - // For the puffin profiler! #[allow(dead_code)] // Only used for profiling fn short_event_description(event: &winit::event::Event<'_, UserEvent>) -> &'static str { diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index d8646df2b..6c93924f8 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -94,7 +94,7 @@ impl AppRunner { { let needs_repaint = needs_repaint.clone(); egui_ctx.set_request_repaint_callback(move |info| { - needs_repaint.repaint_after(info.after.as_secs_f64()); + needs_repaint.repaint_after(info.delay.as_secs_f64()); }); } @@ -170,10 +170,8 @@ impl AppRunner { self.painter.destroy(); } - /// Returns how long to wait until the next repaint. - /// /// Call [`Self::paint`] later to paint - pub fn logic(&mut self) -> (std::time::Duration, Vec) { + pub fn logic(&mut self) -> Vec { let frame_start = now_sec(); super::resize_canvas_to_screen_size(self.canvas_id(), self.web_options.max_size_points); @@ -185,14 +183,20 @@ impl AppRunner { }); let egui::FullOutput { platform_output, - repaint_after, textures_delta, shapes, + pixels_per_point, + viewport_output, } = full_output; + if viewport_output.len() > 1 { + log::warn!("Multiple viewports not yet supported on the web"); + } + // TODO(emilk): handle some of the command in `viewport_output`, like setting the title and icon? + self.handle_platform_output(platform_output); self.textures_delta.append(textures_delta); - let clipped_primitives = self.egui_ctx.tessellate(shapes); + let clipped_primitives = self.egui_ctx.tessellate(shapes, pixels_per_point); { let app_output = self.frame.take_app_output(); @@ -201,7 +205,7 @@ impl AppRunner { self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32); - (repaint_after, clipped_primitives) + clipped_primitives } /// Paint the results of the last call to [`Self::logic`]. diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index ea5a5bf08..907d7f8e6 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -19,11 +19,8 @@ fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> { fn paint_if_needed(runner: &mut AppRunner) -> Result<(), JsValue> { if runner.needs_repaint.when_to_repaint() <= now_sec() { runner.needs_repaint.clear(); - let (repaint_after, clipped_primitives) = runner.logic(); + let clipped_primitives = runner.logic(); runner.paint(&clipped_primitives)?; - runner - .needs_repaint - .repaint_after(repaint_after.as_secs_f64()); runner.auto_save_if_needed(); } Ok(()) diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index 798efdba7..d743b52c8 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -208,8 +208,7 @@ impl WebPainter for WebPainterWgpu { let frame = match self.surface.get_current_texture() { Ok(frame) => frame, - #[allow(clippy::single_match_else)] - Err(e) => match (*self.on_surface_error)(e) { + Err(err) => match (*self.on_surface_error)(err) { SurfaceErrorAction::RecreateSurface => { self.surface .configure(&render_state.device, &self.surface_configuration); @@ -271,11 +270,9 @@ impl WebPainter for WebPainterWgpu { } // Submit the commands: both the main buffer and user-defined ones. - render_state.queue.submit( - user_cmd_bufs - .into_iter() - .chain(std::iter::once(encoder.finish())), - ); + render_state + .queue + .submit(user_cmd_bufs.into_iter().chain([encoder.finish()])); if let Some(frame) = frame { frame.present(); diff --git a/crates/egui-wgpu/Cargo.toml b/crates/egui-wgpu/Cargo.toml index 06bc638b3..3b7547cb2 100644 --- a/crates/egui-wgpu/Cargo.toml +++ b/crates/egui-wgpu/Cargo.toml @@ -36,6 +36,7 @@ winit = ["dep:winit"] [dependencies] +egui = { version = "0.23.0", path = "../egui", default-features = false } epaint = { version = "0.23.0", path = "../epaint", default-features = false, features = [ "bytemuck", ] } diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 91eb1a435..68803a24b 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -1,4 +1,6 @@ -use std::sync::Arc; +use std::{num::NonZeroU32, sync::Arc}; + +use egui::{ViewportId, ViewportIdMap, ViewportIdSet}; use crate::{renderer, RenderState, SurfaceErrorAction, WgpuConfiguration}; @@ -77,13 +79,15 @@ pub struct Painter { msaa_samples: u32, support_transparent_backbuffer: bool, depth_format: Option, - depth_texture_view: Option, - msaa_texture_view: Option, screen_capture_state: Option, instance: wgpu::Instance, render_state: Option, - surface_state: Option, + + // Per viewport/window: + depth_texture_view: ViewportIdMap, + msaa_texture_view: ViewportIdMap, + surfaces: ViewportIdMap, } impl Painter { @@ -115,13 +119,14 @@ impl Painter { msaa_samples, support_transparent_backbuffer, depth_format, - depth_texture_view: None, screen_capture_state: None, instance, render_state: None, - surface_state: None, - msaa_texture_view: None, + + depth_texture_view: Default::default(), + surfaces: Default::default(), + msaa_texture_view: Default::default(), } } @@ -180,11 +185,14 @@ impl Painter { /// If the provided wgpu configuration does not match an available device. pub async fn set_window( &mut self, + viewport_id: ViewportId, window: Option<&winit::window::Window>, ) -> Result<(), crate::WgpuError> { crate::profile_function!(); - match window { - Some(window) => { + + if let Some(window) = window { + let size = window.inner_size(); + if self.surfaces.get(&viewport_id).is_none() { let surface = unsafe { self.instance.create_surface(&window)? }; let render_state = if let Some(render_state) = &self.render_state { @@ -223,19 +231,30 @@ impl Painter { let supports_screenshot = !matches!(render_state.adapter.get_info().backend, wgpu::Backend::Gl); - let size = window.inner_size(); - self.surface_state = Some(SurfaceState { - surface, - width: size.width, - height: size.height, - alpha_mode, - supports_screenshot, - }); - self.resize_and_generate_depth_texture_view_and_msaa_view(size.width, size.height); - } - None => { - self.surface_state = None; + self.surfaces.insert( + viewport_id, + SurfaceState { + surface, + width: size.width, + height: size.height, + alpha_mode, + supports_screenshot, + }, + ); } + + let Some(width) = NonZeroU32::new(size.width) else { + log::debug!("The window width was zero; skipping generate textures"); + return Ok(()); + }; + let Some(height) = NonZeroU32::new(size.height) else { + log::debug!("The window height was zero; skipping generate textures"); + return Ok(()); + }; + self.resize_and_generate_depth_texture_view_and_msaa_view(viewport_id, width, height); + } else { + log::warn!("All surfaces was deleted!"); + self.surfaces.clear(); } Ok(()) } @@ -253,51 +272,61 @@ impl Painter { fn resize_and_generate_depth_texture_view_and_msaa_view( &mut self, - width_in_pixels: u32, - height_in_pixels: u32, + viewport_id: ViewportId, + width_in_pixels: NonZeroU32, + height_in_pixels: NonZeroU32, ) { crate::profile_function!(); + + let width = width_in_pixels.get(); + let height = height_in_pixels.get(); + let render_state = self.render_state.as_ref().unwrap(); - let surface_state = self.surface_state.as_mut().unwrap(); + let surface_state = self.surfaces.get_mut(&viewport_id).unwrap(); - surface_state.width = width_in_pixels; - surface_state.height = height_in_pixels; + surface_state.width = width; + surface_state.height = height; Self::configure_surface(surface_state, render_state, self.configuration.present_mode); - self.depth_texture_view = self.depth_format.map(|depth_format| { - render_state - .device - .create_texture(&wgpu::TextureDescriptor { - label: Some("egui_depth_texture"), - size: wgpu::Extent3d { - width: width_in_pixels, - height: height_in_pixels, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: self.msaa_samples, - dimension: wgpu::TextureDimension::D2, - format: depth_format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[depth_format], - }) - .create_view(&wgpu::TextureViewDescriptor::default()) - }); + if let Some(depth_format) = self.depth_format { + self.depth_texture_view.insert( + viewport_id, + render_state + .device + .create_texture(&wgpu::TextureDescriptor { + label: Some("egui_depth_texture"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: self.msaa_samples, + dimension: wgpu::TextureDimension::D2, + format: depth_format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[depth_format], + }) + .create_view(&wgpu::TextureViewDescriptor::default()), + ); + } - self.msaa_texture_view = (self.msaa_samples > 1) + if let Some(render_state) = (self.msaa_samples > 1) .then_some(self.render_state.as_ref()) .flatten() - .map(|render_state| { - let texture_format = render_state.target_format; + { + let texture_format = render_state.target_format; + self.msaa_texture_view.insert( + viewport_id, render_state .device .create_texture(&wgpu::TextureDescriptor { label: Some("egui_msaa_texture"), size: wgpu::Extent3d { - width: width_in_pixels, - height: height_in_pixels, + width, + height, depth_or_array_layers: 1, }, mip_level_count: 1, @@ -307,14 +336,22 @@ impl Painter { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, view_formats: &[texture_format], }) - .create_view(&wgpu::TextureViewDescriptor::default()) - }); + .create_view(&wgpu::TextureViewDescriptor::default()), + ); + }; } - pub fn on_window_resized(&mut self, width_in_pixels: u32, height_in_pixels: u32) { + pub fn on_window_resized( + &mut self, + viewport_id: ViewportId, + width_in_pixels: NonZeroU32, + height_in_pixels: NonZeroU32, + ) { crate::profile_function!(); - if self.surface_state.is_some() { + + if self.surfaces.contains_key(&viewport_id) { self.resize_and_generate_depth_texture_view_and_msaa_view( + viewport_id, width_in_pixels, height_in_pixels, ); @@ -425,6 +462,7 @@ impl Painter { // Returns a vector with the frame's pixel data if it was requested. pub fn paint_and_update_textures( &mut self, + viewport_id: ViewportId, pixels_per_point: f32, clear_color: [f32; 4], clipped_primitives: &[epaint::ClippedPrimitive], @@ -434,7 +472,7 @@ impl Painter { crate::profile_function!(); let render_state = self.render_state.as_mut()?; - let surface_state = self.surface_state.as_ref()?; + let surface_state = self.surfaces.get(&viewport_id)?; let output_frame = { crate::profile_scope!("get_current_texture"); @@ -444,8 +482,7 @@ impl Painter { let output_frame = match output_frame { Ok(frame) => frame, - #[allow(clippy::single_match_else)] - Err(e) => match (*self.configuration.on_surface_error)(e) { + Err(err) => match (*self.configuration.on_surface_error)(err) { SurfaceErrorAction::RecreateSurface => { Self::configure_surface( surface_state, @@ -521,7 +558,7 @@ impl Painter { }; let (view, resolve_target) = (self.msaa_samples > 1) - .then_some(self.msaa_texture_view.as_ref()) + .then_some(self.msaa_texture_view.get(&viewport_id)) .flatten() .map_or((&frame_view, None), |texture_view| { (texture_view, Some(&frame_view)) @@ -542,7 +579,7 @@ impl Painter { store: wgpu::StoreOp::Store, }, })], - depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| { + depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| { wgpu::RenderPassDepthStencilAttachment { view, depth_ops: Some(wgpu::Operations { @@ -578,7 +615,7 @@ impl Painter { crate::profile_scope!("Queue::submit"); render_state .queue - .submit(user_cmd_bufs.into_iter().chain(std::iter::once(encoded))); + .submit(user_cmd_bufs.into_iter().chain([encoded])); }; let screenshot = if capture { @@ -595,6 +632,14 @@ impl Painter { screenshot } + pub fn gc_viewports(&mut self, active_viewports: &ViewportIdSet) { + self.surfaces.retain(|id, _| active_viewports.contains(id)); + self.depth_texture_view + .retain(|id, _| active_viewports.contains(id)); + self.msaa_texture_view + .retain(|id, _| active_viewports.contains(id)); + } + #[allow(clippy::unused_self)] pub fn destroy(&mut self) { // TODO(emilk): something here? diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index 0c8434850..8c5550def 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -49,10 +49,10 @@ puffin = ["dep:puffin", "egui/puffin"] serde = ["egui/serde", "dep:serde"] ## Enables Wayland support. -wayland = ["winit/wayland"] +wayland = ["winit/wayland", "bytemuck"] ## Enables compiling for x11. -x11 = ["winit/x11"] +x11 = ["winit/x11", "bytemuck"] [dependencies] egui = { version = "0.23.0", path = "../egui", default-features = false, features = [ diff --git a/crates/egui-winit/src/clipboard.rs b/crates/egui-winit/src/clipboard.rs index 65d1636ca..bd0fa511e 100644 --- a/crates/egui-winit/src/clipboard.rs +++ b/crates/egui-winit/src/clipboard.rs @@ -112,6 +112,8 @@ impl Clipboard { #[cfg(all(feature = "arboard", not(target_os = "android")))] fn init_arboard() -> Option { + crate::profile_function!(); + log::debug!("Initializing arboard clipboard…"); match arboard::Clipboard::new() { Ok(clipboard) => Some(clipboard), @@ -135,6 +137,8 @@ fn init_arboard() -> Option { fn init_smithay_clipboard( _display_target: &dyn HasRawDisplayHandle, ) -> Option { + crate::profile_function!(); + use raw_window_handle::RawDisplayHandle; if let RawDisplayHandle::Wayland(display) = _display_target.raw_display_handle() { log::debug!("Initializing smithay clipboard…"); diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index f9feae9a4..52b7e7182 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -14,6 +14,7 @@ pub use accesskit_winit; pub use egui; #[cfg(feature = "accesskit")] use egui::accesskit; +use egui::{Pos2, Rect, Vec2, ViewportBuilder, ViewportCommand, ViewportId, ViewportIdPair}; pub use winit; pub mod clipboard; @@ -51,7 +52,9 @@ pub struct EventResponse { // ---------------------------------------------------------------------------- -/// Handles the integration between egui and winit. +/// Handles the integration between egui and a winit Window. +/// +/// Instantiate one of these per viewport/window. pub struct State { start_time: web_time::Instant, egui_input: egui::RawInput, @@ -86,13 +89,19 @@ pub struct State { impl State { /// Construct a new instance - pub fn new(display_target: &dyn HasRawDisplayHandle) -> Self { + pub fn new( + display_target: &dyn HasRawDisplayHandle, + native_pixels_per_point: Option, + max_texture_side: Option, + ) -> Self { + crate::profile_function!(); + let egui_input = egui::RawInput { focused: false, // winit will tell us when we have focus ..Default::default() }; - Self { + let mut slf = Self { start_time: web_time::Instant::now(), egui_input, pointer_pos_in_points: None, @@ -111,7 +120,14 @@ impl State { accesskit: None, allow_ime: false, + }; + if let Some(native_pixels_per_point) = native_pixels_per_point { + slf.set_pixels_per_point(native_pixels_per_point); + } + if let Some(max_texture_side) = max_texture_side { + slf.set_max_texture_side(max_texture_side); } + slf } #[cfg(feature = "accesskit")] @@ -121,6 +137,7 @@ impl State { event_loop_proxy: winit::event_loop::EventLoopProxy, initial_tree_update_factory: impl 'static + FnOnce() -> accesskit::TreeUpdate + Send, ) { + crate::profile_function!(); self.accesskit = Some(accesskit_winit::Adapter::new( window, initial_tree_update_factory, @@ -163,22 +180,81 @@ impl State { /// Prepare for a new frame by extracting the accumulated input, /// as well as setting [the time](egui::RawInput::time) and [screen rectangle](egui::RawInput::screen_rect). - pub fn take_egui_input(&mut self, window: &winit::window::Window) -> egui::RawInput { + pub fn take_egui_input( + &mut self, + window: &winit::window::Window, + ids: ViewportIdPair, + ) -> egui::RawInput { + crate::profile_function!(); + let pixels_per_point = self.pixels_per_point(); self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64()); + // TODO remove this in winit 0.29 + // This hack make the window outer_position and size to be valid, X11 Only + // That was happending because winit get the window state before the compositor adds decorations! + #[cfg(all(feature = "x11", target_os = "linux"))] + window.set_maximized(window.is_maximized()); + // On Windows, a minimized window will have 0 width and height. // See: https://github.com/rust-windowing/winit/issues/208 // This solves an issue where egui window positions would be changed when minimizing on Windows. let screen_size_in_pixels = screen_size_in_pixels(window); let screen_size_in_points = screen_size_in_pixels / pixels_per_point; - self.egui_input.screen_rect = - if screen_size_in_points.x > 0.0 && screen_size_in_points.y > 0.0 { - Some(egui::Rect::from_min_size( - egui::Pos2::ZERO, - screen_size_in_points, - )) + + self.egui_input.screen_rect = (screen_size_in_points.x > 0.0 + && screen_size_in_points.y > 0.0) + .then(|| Rect::from_min_size(Pos2::ZERO, screen_size_in_points)); + + let has_a_position = match window.is_minimized() { + None | Some(true) => false, + Some(false) => true, + }; + + let inner_pos_px = if has_a_position { + window + .inner_position() + .map(|pos| Pos2::new(pos.x as f32, pos.y as f32)) + .ok() + } else { + None + }; + + let outer_pos_px = if has_a_position { + window + .outer_position() + .map(|pos| Pos2::new(pos.x as f32, pos.y as f32)) + .ok() + } else { + None + }; + + let inner_size_px = if has_a_position { + let size = window.inner_size(); + Some(Vec2::new(size.width as f32, size.height as f32)) + } else { + None + }; + + let outer_size_px = if has_a_position { + let size = window.outer_size(); + Some(Vec2::new(size.width as f32, size.height as f32)) + } else { + None + }; + + self.egui_input.viewport.ids = ids; + self.egui_input.viewport.inner_rect_px = + if let (Some(pos), Some(size)) = (inner_pos_px, inner_size_px) { + Some(Rect::from_min_size(pos, size)) + } else { + None + }; + + self.egui_input.viewport.outer_rect_px = + if let (Some(pos), Some(size)) = (outer_pos_px, outer_size_px) { + Some(Rect::from_min_size(pos, size)) } else { None }; @@ -376,11 +452,18 @@ impl State { } // Things that may require repaint: - WindowEvent::CloseRequested - | WindowEvent::CursorEntered { .. } + WindowEvent::CloseRequested => { + self.egui_input.viewport.close_requested = true; + EventResponse { + consumed: true, + repaint: true, + } + } + WindowEvent::CursorEntered { .. } | WindowEvent::Destroyed | WindowEvent::Occluded(_) | WindowEvent::Resized(_) + | WindowEvent::Moved(_) | WindowEvent::ThemeChanged(_) | WindowEvent::TouchpadPressure { .. } => EventResponse { repaint: true, @@ -389,7 +472,6 @@ impl State { // Things we completely ignore: WindowEvent::AxisMotion { .. } - | WindowEvent::Moved(_) | WindowEvent::SmartMagnify { .. } | WindowEvent::TouchpadRotate { .. } => EventResponse { repaint: false, @@ -643,20 +725,24 @@ impl State { pub fn handle_platform_output( &mut self, window: &winit::window::Window, + viewport_id: ViewportId, egui_ctx: &egui::Context, platform_output: egui::PlatformOutput, ) { + crate::profile_function!(); + let egui::PlatformOutput { cursor_icon, open_url, copied_text, - events: _, // handled above + events: _, // handled elsewhere mutable_text_under_cursor: _, // only used in eframe web text_cursor_pos, #[cfg(feature = "accesskit")] accesskit_update, } = platform_output; - self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI + + self.current_pixels_per_point = egui_ctx.input_for(viewport_id, |i| i.pixels_per_point); // someone can have changed it to scale the UI self.set_cursor_icon(window, cursor_icon); @@ -905,6 +991,252 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option, + window: &winit::window::Window, + is_viewport_focused: bool, +) { + crate::profile_function!(); + + use winit::window::ResizeDirection; + + for command in commands { + match command { + egui::ViewportCommand::StartDrag => { + // if this is not checked on x11 the input will be permanently taken until the app is killed! + if is_viewport_focused { + if let Err(err) = window.drag_window() { + log::warn!("{command:?}: {err}"); + } + } + } + egui::ViewportCommand::InnerSize(size) => { + let width = size.x.max(1.0); + let height = size.y.max(1.0); + window.set_inner_size(LogicalSize::new(width, height)); + } + egui::ViewportCommand::BeginResize(direction) => { + if let Err(err) = window.drag_resize_window(match direction { + egui::viewport::ResizeDirection::North => ResizeDirection::North, + egui::viewport::ResizeDirection::South => ResizeDirection::South, + egui::viewport::ResizeDirection::West => ResizeDirection::West, + egui::viewport::ResizeDirection::NorthEast => ResizeDirection::NorthEast, + egui::viewport::ResizeDirection::SouthEast => ResizeDirection::SouthEast, + egui::viewport::ResizeDirection::NorthWest => ResizeDirection::NorthWest, + egui::viewport::ResizeDirection::SouthWest => ResizeDirection::SouthWest, + }) { + log::warn!("{command:?}: {err}"); + } + } + ViewportCommand::Title(title) => window.set_title(&title), + ViewportCommand::Transparent(v) => window.set_transparent(v), + ViewportCommand::Visible(v) => window.set_visible(v), + ViewportCommand::OuterPosition(pos) => { + window.set_outer_position(LogicalPosition::new(pos.x, pos.y)); + } + ViewportCommand::MinInnerSize(s) => { + window.set_min_inner_size( + (s.is_finite() && s != Vec2::ZERO).then_some(LogicalSize::new(s.x, s.y)), + ); + } + ViewportCommand::MaxInnerSize(s) => { + window.set_max_inner_size( + (s.is_finite() && s != Vec2::INFINITY).then_some(LogicalSize::new(s.x, s.y)), + ); + } + ViewportCommand::ResizeIncrements(s) => { + window.set_resize_increments(s.map(|s| LogicalSize::new(s.x, s.y))); + } + ViewportCommand::Resizable(v) => window.set_resizable(v), + ViewportCommand::EnableButtons { + close, + minimized, + maximize, + } => window.set_enabled_buttons( + if close { + WindowButtons::CLOSE + } else { + WindowButtons::empty() + } | if minimized { + WindowButtons::MINIMIZE + } else { + WindowButtons::empty() + } | if maximize { + WindowButtons::MAXIMIZE + } else { + WindowButtons::empty() + }, + ), + ViewportCommand::Minimized(v) => window.set_minimized(v), + ViewportCommand::Maximized(v) => window.set_maximized(v), + ViewportCommand::Fullscreen(v) => { + window.set_fullscreen(v.then_some(winit::window::Fullscreen::Borderless(None))); + } + ViewportCommand::Decorations(v) => window.set_decorations(v), + ViewportCommand::WindowLevel(l) => window.set_window_level(match l { + egui::viewport::WindowLevel::AlwaysOnBottom => WindowLevel::AlwaysOnBottom, + egui::viewport::WindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop, + egui::viewport::WindowLevel::Normal => WindowLevel::Normal, + }), + ViewportCommand::WindowIcon(icon) => { + window.set_window_icon(icon.map(|icon| { + winit::window::Icon::from_rgba( + icon.as_raw().to_owned(), + icon.width() as u32, + icon.height() as u32, + ) + .expect("Invalid ICON data!") + })); + } + ViewportCommand::IMEPosition(pos) => { + window.set_ime_position(LogicalPosition::new(pos.x, pos.y)); + } + ViewportCommand::IMEAllowed(v) => window.set_ime_allowed(v), + ViewportCommand::IMEPurpose(p) => window.set_ime_purpose(match p { + egui::viewport::IMEPurpose::Password => winit::window::ImePurpose::Password, + egui::viewport::IMEPurpose::Terminal => winit::window::ImePurpose::Terminal, + egui::viewport::IMEPurpose::Normal => winit::window::ImePurpose::Normal, + }), + ViewportCommand::RequestUserAttention(a) => { + window.request_user_attention(a.map(|a| match a { + egui::viewport::UserAttentionType::Critical => { + winit::window::UserAttentionType::Critical + } + egui::viewport::UserAttentionType::Informational => { + winit::window::UserAttentionType::Informational + } + })); + } + ViewportCommand::SetTheme(t) => window.set_theme(match t { + egui::SystemTheme::Light => Some(winit::window::Theme::Light), + egui::SystemTheme::Dark => Some(winit::window::Theme::Dark), + egui::SystemTheme::SystemDefault => None, + }), + ViewportCommand::ContentProtected(v) => window.set_content_protected(v), + ViewportCommand::CursorPosition(pos) => { + if let Err(err) = window.set_cursor_position(LogicalPosition::new(pos.x, pos.y)) { + log::warn!("{command:?}: {err}"); + } + } + ViewportCommand::CursorGrab(o) => { + if let Err(err) = window.set_cursor_grab(match o { + egui::viewport::CursorGrab::None => CursorGrabMode::None, + egui::viewport::CursorGrab::Confined => CursorGrabMode::Confined, + egui::viewport::CursorGrab::Locked => CursorGrabMode::Locked, + }) { + log::warn!("{command:?}: {err}"); + } + } + ViewportCommand::CursorVisible(v) => window.set_cursor_visible(v), + ViewportCommand::CursorHitTest(v) => { + if let Err(err) = window.set_cursor_hittest(v) { + log::warn!("{command:?}: {err}"); + } + } + } + } +} + +pub fn create_winit_window_builder(builder: &ViewportBuilder) -> winit::window::WindowBuilder { + crate::profile_function!(); + + let mut window_builder = winit::window::WindowBuilder::new() + .with_title( + builder + .title + .clone() + .unwrap_or_else(|| "egui window".to_owned()), + ) + .with_transparent(builder.transparent.unwrap_or(false)) + .with_decorations(builder.decorations.unwrap_or(true)) + .with_resizable(builder.resizable.unwrap_or(true)) + .with_visible(builder.visible.unwrap_or(true)) + .with_maximized(builder.maximized.unwrap_or(false)) + .with_fullscreen( + builder + .fullscreen + .and_then(|e| e.then_some(winit::window::Fullscreen::Borderless(None))), + ) + .with_enabled_buttons({ + let mut buttons = WindowButtons::empty(); + if builder.minimize_button.unwrap_or(true) { + buttons |= WindowButtons::MINIMIZE; + } + if builder.maximize_button.unwrap_or(true) { + buttons |= WindowButtons::MAXIMIZE; + } + if builder.close_button.unwrap_or(true) { + buttons |= WindowButtons::CLOSE; + } + buttons + }) + .with_active(builder.active.unwrap_or(true)); + + if let Some(inner_size) = builder.inner_size { + window_builder = window_builder + .with_inner_size(winit::dpi::LogicalSize::new(inner_size.x, inner_size.y)); + } + + if let Some(min_inner_size) = builder.min_inner_size { + window_builder = window_builder.with_min_inner_size(winit::dpi::LogicalSize::new( + min_inner_size.x, + min_inner_size.y, + )); + } + + if let Some(max_inner_size) = builder.max_inner_size { + window_builder = window_builder.with_max_inner_size(winit::dpi::LogicalSize::new( + max_inner_size.x, + max_inner_size.y, + )); + } + + if let Some(position) = builder.position { + window_builder = + window_builder.with_position(winit::dpi::LogicalPosition::new(position.x, position.y)); + } + + if let Some(icon) = builder.icon.clone() { + window_builder = window_builder.with_window_icon(Some( + winit::window::Icon::from_rgba( + icon.as_raw().to_owned(), + icon.width() as u32, + icon.height() as u32, + ) + .expect("Invalid Icon Data!"), + )); + } + + #[cfg(all(feature = "wayland", target_os = "linux"))] + if let Some(name) = builder.name.clone() { + use winit::platform::wayland::WindowBuilderExtWayland as _; + window_builder = window_builder.with_name(name.0, name.1); + } + + #[cfg(target_os = "windows")] + if let Some(enable) = builder.drag_and_drop { + use winit::platform::windows::WindowBuilderExtWindows as _; + window_builder = window_builder.with_drag_and_drop(enable); + } + + #[cfg(target_os = "macos")] + { + use winit::platform::macos::WindowBuilderExtMacOS as _; + window_builder = window_builder + .with_title_hidden(builder.title_hidden.unwrap_or(false)) + .with_titlebar_transparent(builder.titlebar_transparent.unwrap_or(false)) + .with_fullsize_content_view(builder.fullsize_content_view.unwrap_or(false)); + } + + // TODO: implement `ViewportBuilder::hittest` + // Is not implemented because winit in his current state will not allow to set cursor_hittest on a `WindowBuilder` + + window_builder +} + // --------------------------------------------------------------------------- mod profiling_scopes { @@ -934,3 +1266,7 @@ mod profiling_scopes { #[allow(unused_imports)] pub(crate) use profiling_scopes::*; +use winit::{ + dpi::{LogicalPosition, LogicalSize}, + window::{CursorGrabMode, WindowButtons, WindowLevel}, +}; diff --git a/crates/egui-winit/src/window_settings.rs b/crates/egui-winit/src/window_settings.rs index 985ac423b..021b4c3d7 100644 --- a/crates/egui-winit/src/window_settings.rs +++ b/crates/egui-winit/src/window_settings.rs @@ -1,3 +1,5 @@ +use egui::ViewportBuilder; + /// Can be used to store native window settings (position and size). #[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -46,10 +48,10 @@ impl WindowSettings { self.inner_size_points } - pub fn initialize_window_builder( + pub fn initialize_viewport_builder( &self, - mut window: winit::window::WindowBuilder, - ) -> winit::window::WindowBuilder { + mut viewport_builder: ViewportBuilder, + ) -> ViewportBuilder { // `WindowBuilder::with_position` expects inner position in Macos, and outer position elsewhere // See [`winit::window::WindowBuilder::with_position`] for details. let pos_px = if cfg!(target_os = "macos") { @@ -57,26 +59,17 @@ impl WindowSettings { } else { self.outer_position_pixels }; - if let Some(pos_px) = pos_px { - window = window.with_position(winit::dpi::PhysicalPosition { - x: pos_px.x as f64, - y: pos_px.y as f64, - }); + if let Some(pos) = pos_px { + viewport_builder = viewport_builder.with_position(pos); } if let Some(inner_size_points) = self.inner_size_points { - window - .with_inner_size(winit::dpi::LogicalSize { - width: inner_size_points.x as f64, - height: inner_size_points.y as f64, - }) - .with_fullscreen( - self.fullscreen - .then_some(winit::window::Fullscreen::Borderless(None)), - ) - } else { - window + viewport_builder = viewport_builder + .with_inner_size(inner_size_points) + .with_fullscreen(self.fullscreen); } + + viewport_builder } pub fn initialize_window(&self, window: &winit::window::Window) { diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 4f603717e..cf078c861 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -259,7 +259,7 @@ impl Area { let layer_id = LayerId::new(order, id); - let state = ctx.memory(|mem| mem.areas.get(id).copied()); + let state = ctx.memory(|mem| mem.areas().get(id).copied()); let is_new = state.is_none(); if is_new { ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place @@ -307,9 +307,9 @@ impl Area { if (move_response.dragged() || move_response.clicked()) || pointer_pressed_on_area(ctx, layer_id) - || !ctx.memory(|m| m.areas.visible_last_frame(&layer_id)) + || !ctx.memory(|m| m.areas().visible_last_frame(&layer_id)) { - ctx.memory_mut(|m| m.areas.move_to_top(layer_id)); + ctx.memory_mut(|m| m.areas_mut().move_to_top(layer_id)); ctx.request_repaint(); } @@ -353,7 +353,7 @@ impl Area { } let layer_id = LayerId::new(self.order, self.id); - let area_rect = ctx.memory(|mem| mem.areas.get(self.id).map(|area| area.rect())); + let area_rect = ctx.memory(|mem| mem.areas().get(self.id).map(|area| area.rect())); if let Some(area_rect) = area_rect { let clip_rect = ctx.available_rect(); let painter = Painter::new(ctx.clone(), layer_id, clip_rect); @@ -441,7 +441,7 @@ impl Prepared { state.size = content_ui.min_size(); - ctx.memory_mut(|m| m.areas.set_state(layer_id, state)); + ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state)); move_response } @@ -458,7 +458,7 @@ fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool { fn automatic_area_position(ctx: &Context) -> Pos2 { let mut existing: Vec = ctx.memory(|mem| { - mem.areas + mem.areas() .visible_windows() .into_iter() .map(State::rect) diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index e29b12b4a..e840a8e12 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -244,7 +244,7 @@ fn combo_box_dyn<'c, R>( let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id)); - let popup_height = ui.memory(|m| m.areas.get(popup_id).map_or(100.0, |state| state.size.y)); + let popup_height = ui.memory(|m| m.areas().get(popup_id).map_or(100.0, |state| state.size.y)); let above_or_below = if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index f5267e0f0..ffc073920 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -702,9 +702,9 @@ impl TopBottomPanel { if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down()) && mouse_over_resize_line { - ui.memory_mut(|mem| mem.interaction.drag_id = Some(resize_id)); + ui.memory_mut(|mem| mem.interaction_mut().drag_id = Some(resize_id)); } - is_resizing = ui.memory(|mem| mem.interaction.drag_id == Some(resize_id)); + is_resizing = ui.memory(|mem| mem.interaction().drag_id == Some(resize_id)); if is_resizing { let height = (pointer.y - side.side_y(panel_rect)).abs(); let height = diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index a82c82ee2..8b92a945c 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -280,7 +280,7 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool { if *individual_id == tooltip_id { let area_id = common_id.with(count); let layer_id = LayerId::new(Order::Tooltip, area_id); - if ctx.memory(|mem| mem.areas.visible_last_frame(&layer_id)) { + if ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id)) { return true; } } diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 14e878ad9..64de28e20 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -24,6 +24,9 @@ use super::*; /// ``` /// /// The previous rectangle used by this window can be obtained through [`crate::Memory::area_rect()`]. +/// +/// Note that this is NOT a native OS window. +/// To create a new native OS window, use [`crate::Context::show_viewport`]. #[must_use = "You should call .show()"] pub struct Window<'open> { title: WidgetText, @@ -585,7 +588,7 @@ fn interact( } } - ctx.memory_mut(|mem| mem.areas.move_to_top(area_layer_id)); + ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id)); Some(window_interaction) } @@ -638,31 +641,31 @@ fn window_interaction( rect: Rect, ) -> Option { { - let drag_id = ctx.memory(|mem| mem.interaction.drag_id); + let drag_id = ctx.memory(|mem| mem.interaction().drag_id); if drag_id.is_some() && drag_id != Some(id) { return None; } } - let mut window_interaction = ctx.memory(|mem| mem.window_interaction); + let mut window_interaction = ctx.memory(|mem| mem.window_interaction()); if window_interaction.is_none() { if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) { hover_window_interaction.set_cursor(ctx); if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) { ctx.memory_mut(|mem| { - mem.interaction.drag_id = Some(id); - mem.interaction.drag_is_window = true; + mem.interaction_mut().drag_id = Some(id); + mem.interaction_mut().drag_is_window = true; window_interaction = Some(hover_window_interaction); - mem.window_interaction = window_interaction; + mem.set_window_interaction(window_interaction); }); } } } if let Some(window_interaction) = window_interaction { - let is_active = ctx.memory_mut(|mem| mem.interaction.drag_id == Some(id)); + let is_active = ctx.memory_mut(|mem| mem.interaction().drag_id == Some(id)); if is_active && window_interaction.area_layer_id == area_layer_id { return Some(window_interaction); @@ -690,7 +693,7 @@ fn resize_hover( } } - if ctx.memory(|mem| mem.interaction.drag_interest) { + if ctx.memory(|mem| mem.interaction().drag_interest) { // Another widget will become active if we drag here return None; } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 168489752..9dc28a543 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1,24 +1,35 @@ #![warn(missing_docs)] // Let's keep `Context` well-documented. -use std::borrow::Cow; -use std::sync::Arc; +use std::{borrow::Cow, cell::RefCell, sync::Arc, time::Duration}; + +use ahash::HashMap; +use epaint::{mutex::*, stats::*, text::Fonts, TessellationOptions, *}; -use crate::load::Bytes; -use crate::load::SizedTexture; use crate::{ - animation_manager::AnimationManager, data::output::PlatformOutput, frame_state::FrameState, - input_state::*, layers::GraphicLayers, load::Loaders, memory::Options, os::OperatingSystem, - output::FullOutput, util::IdTypeMap, TextureHandle, *, + animation_manager::AnimationManager, + data::output::PlatformOutput, + frame_state::FrameState, + input_state::*, + layers::GraphicLayers, + load::{Bytes, Loaders, SizedTexture}, + memory::Options, + os::OperatingSystem, + output::FullOutput, + util::IdTypeMap, + viewport::ViewportClass, + TextureHandle, ViewportCommand, *, }; -use epaint::{mutex::*, stats::*, text::Fonts, TessellationOptions, *}; /// Information given to the backend about when it is time to repaint the ui. /// /// This is given in the callback set by [`Context::set_request_repaint_callback`]. #[derive(Clone, Copy, Debug)] pub struct RequestRepaintInfo { + /// This is used to specify what viewport that should repaint. + pub viewport_id: ViewportId, + /// Repaint after this duration. If zero, repaint as soon as possible. - pub after: std::time::Duration, + pub delay: Duration, /// The current frame number. /// @@ -29,6 +40,12 @@ pub struct RequestRepaintInfo { // ---------------------------------------------------------------------------- +thread_local! { + static IMMEDIATE_VIEWPORT_RENDERER: RefCell>> = Default::default(); +} + +// ---------------------------------------------------------------------------- + struct WrappedTextureManager(Arc>); impl Default for WrappedTextureManager { @@ -49,89 +66,128 @@ impl Default for WrappedTextureManager { // ---------------------------------------------------------------------------- -/// Logic related to repainting the ui. -struct Repaint { - /// The current frame number. +/// Repaint-logic +impl ContextImpl { + fn request_repaint(&mut self, viewport_id: ViewportId) { + self.request_repaint_after(Duration::ZERO, viewport_id); + } + + fn request_repaint_after(&mut self, delay: Duration, viewport_id: ViewportId) { + let mut viewport = self.viewports.entry(viewport_id).or_default(); + + // Each request results in two repaints, just to give some things time to settle. + // This solves some corner-cases of missing repaints on frame-delayed responses. + viewport.repaint.outstanding = 1; + + if let Some(callback) = &self.request_repaint_callback { + // We save some CPU time by only calling the callback if we need to. + // If the new delay is greater or equal to the previous lowest, + // it means we have already called the callback, and don't need to do it again. + if delay < viewport.repaint.repaint_delay { + viewport.repaint.repaint_delay = delay; + + (callback)(RequestRepaintInfo { + viewport_id, + delay, + current_frame_nr: viewport.repaint.frame_nr, + }); + } + } + } + + #[must_use] + fn requested_repaint_last_frame(&self, viewport_id: &ViewportId) -> bool { + self.viewports + .get(viewport_id) + .map_or(false, |v| v.repaint.requested_last_frame) + } + + #[must_use] + fn has_requested_repaint(&self, viewport_id: &ViewportId) -> bool { + self.viewports.get(viewport_id).map_or(false, |v| { + 0 < v.repaint.outstanding || v.repaint.repaint_delay < Duration::MAX + }) + } +} + +// ---------------------------------------------------------------------------- + +/// State stored per viewport +#[derive(Default)] +struct ViewportState { + /// The type of viewport. /// - /// Incremented at the end of each frame. + /// This will never be [`ViewportClass::Embedded`], + /// since those don't result in real viewports. + class: ViewportClass, + + /// The latest delta + builder: ViewportBuilder, + + /// The user-code that shows the GUI, used for deferred viewports. + /// + /// `None` for immediate viewports. + viewport_ui_cb: Option>, + + input: InputState, + + /// State that is collected during a frame and then cleared + frame_state: FrameState, + + /// Has this viewport been updated this frame? + used: bool, + + /// Written to during the frame. + layer_rects_this_frame: HashMap>, + + /// Read + layer_rects_prev_frame: HashMap>, + + /// State related to repaint scheduling. + repaint: ViewportRepaintInfo, + + // ---------------------- + // The output of a frame: + graphics: GraphicLayers, + // Most of the things in `PlatformOutput` are not actually viewport dependent. + output: PlatformOutput, + commands: Vec, +} + +/// Per-viewport state related to repaint scheduling. +struct ViewportRepaintInfo { + /// Monotonically increasing counter. frame_nr: u64, - /// The duration backend will poll for new events, before forcing another egui update - /// even if there's no new events. + /// The duration which the backend will poll for new events + /// before forcing another egui update, even if there's no new events. /// /// Also used to suppress multiple calls to the repaint callback during the same frame. - repaint_after: std::time::Duration, + /// + /// This is also returned in [`crate::ViewportOutput`]. + repaint_delay: Duration, - /// While positive, keep requesting repaints. Decrement at the end of each frame. - repaint_requests: u32, - request_repaint_callback: Option>, + /// While positive, keep requesting repaints. Decrement at the start of each frame. + outstanding: u8, - requested_repaint_last_frame: bool, + /// Did we? + requested_last_frame: bool, } -impl Default for Repaint { +impl Default for ViewportRepaintInfo { fn default() -> Self { Self { frame_nr: 0, - repaint_after: std::time::Duration::from_millis(100), - // Start with painting an extra frame to compensate for some widgets - // that take two frames before they "settle": - repaint_requests: 1, - request_repaint_callback: None, - requested_repaint_last_frame: false, - } - } -} -impl Repaint { - fn request_repaint(&mut self) { - self.request_repaint_after(std::time::Duration::ZERO); - } - - fn request_repaint_after(&mut self, after: std::time::Duration) { - if after == std::time::Duration::ZERO { - // Do a few extra frames to let things settle. - // This is a bit of a hack, and we don't support it for `repaint_after` callbacks yet. - self.repaint_requests = 2; - } + // We haven't scheduled a repaint yet. + repaint_delay: Duration::MAX, - // We only re-call the callback if we get a lower duration, - // otherwise it's already been covered by the previous callback. - if after < self.repaint_after { - self.repaint_after = after; + // Let's run a couple of frames at the start, because why not. + outstanding: 1, - if let Some(callback) = &self.request_repaint_callback { - let info = RequestRepaintInfo { - after, - current_frame_nr: self.frame_nr, - }; - (callback)(info); - } + requested_last_frame: false, } } - - fn start_frame(&mut self) { - // We are repainting; no need to reschedule a repaint unless the user asks for it again. - self.repaint_after = std::time::Duration::MAX; - } - - // returns how long to wait until repaint - fn end_frame(&mut self) -> std::time::Duration { - // if repaint_requests is greater than zero. just set the duration to zero for immediate - // repaint. if there's no repaint requests, then we can use the actual repaint_after instead. - let repaint_after = if self.repaint_requests > 0 { - self.repaint_requests -= 1; - std::time::Duration::ZERO - } else { - self.repaint_after - }; - self.repaint_after = std::time::Duration::MAX; - - self.requested_repaint_last_frame = repaint_after.is_zero(); - self.frame_nr += 1; - - repaint_after - } } // ---------------------------------------------------------------------------- @@ -146,24 +202,20 @@ struct ContextImpl { os: OperatingSystem, - input: InputState, + /// How deeply nested are we? + viewport_stack: Vec, - /// State that is collected during a frame and then cleared - frame_state: FrameState, - - // The output of a frame: - graphics: GraphicLayers, - output: PlatformOutput, + /// What is the last viewport rendered? + last_viewport: ViewportId, paint_stats: PaintStats, - repaint: Repaint, + request_repaint_callback: Option>, - /// Written to during the frame. - layer_rects_this_frame: ahash::HashMap>, + viewport_parents: ViewportIdMap, + viewports: ViewportIdMap, - /// Read - layer_rects_prev_frame: ahash::HashMap>, + embed_viewports: bool, #[cfg(feature = "accesskit")] is_accesskit_enabled: bool, @@ -175,33 +227,57 @@ struct ContextImpl { impl ContextImpl { fn begin_frame_mut(&mut self, mut new_raw_input: RawInput) { - self.repaint.start_frame(); - - if let Some(new_pixels_per_point) = self.memory.new_pixels_per_point.take() { - new_raw_input.pixels_per_point = Some(new_pixels_per_point); + let ids = new_raw_input.viewport.ids; + let viewport_id = ids.this; + self.viewport_stack.push(ids); + let viewport = self.viewports.entry(viewport_id).or_default(); + + if viewport.repaint.outstanding == 0 { + // We are repainting now, so we can wait a while for the next repaint. + viewport.repaint.repaint_delay = Duration::MAX; + } else { + viewport.repaint.repaint_delay = Duration::ZERO; + viewport.repaint.outstanding -= 1; + if let Some(callback) = &self.request_repaint_callback { + (callback)(RequestRepaintInfo { + viewport_id, + delay: Duration::ZERO, + current_frame_nr: viewport.repaint.frame_nr, + }); + } + } - // This is a bit hacky, but is required to avoid jitter: - let ratio = self.input.pixels_per_point / new_pixels_per_point; - let mut rect = self.input.screen_rect; - rect.min = (ratio * rect.min.to_vec2()).to_pos2(); - rect.max = (ratio * rect.max.to_vec2()).to_pos2(); - new_raw_input.screen_rect = Some(rect); + if let Some(new_pixels_per_point) = self.memory.override_pixels_per_point { + if viewport.input.pixels_per_point != new_pixels_per_point { + new_raw_input.pixels_per_point = Some(new_pixels_per_point); + + let input = &viewport.input; + // This is a bit hacky, but is required to avoid jitter: + let ratio = input.pixels_per_point / new_pixels_per_point; + let mut rect = input.screen_rect; + rect.min = (ratio * rect.min.to_vec2()).to_pos2(); + rect.max = (ratio * rect.max.to_vec2()).to_pos2(); + new_raw_input.screen_rect = Some(rect); + } } - self.layer_rects_prev_frame = std::mem::take(&mut self.layer_rects_this_frame); + viewport.layer_rects_prev_frame = std::mem::take(&mut viewport.layer_rects_this_frame); - self.memory.begin_frame(&self.input, &new_raw_input); + let all_viewport_ids: ViewportIdSet = self.all_viewport_ids(); - self.input = std::mem::take(&mut self.input) - .begin_frame(new_raw_input, self.repaint.requested_repaint_last_frame); + let viewport = self.viewports.entry(self.viewport_id()).or_default(); - self.frame_state.begin_frame(&self.input); + self.memory + .begin_frame(&viewport.input, &new_raw_input, &all_viewport_ids); - self.update_fonts_mut(); + viewport.input = std::mem::take(&mut viewport.input) + .begin_frame(new_raw_input, viewport.repaint.requested_last_frame); + + viewport.frame_state.begin_frame(&viewport.input); // Ensure we register the background area so panels and background ui can catch clicks: - let screen_rect = self.input.screen_rect(); - self.memory.areas.set_state( + let screen_rect = viewport.input.screen_rect(); + self.memory.areas_mut().set_state( LayerId::background(), containers::area::State { pivot_pos: screen_rect.left_top(), @@ -217,24 +293,26 @@ impl ContextImpl { use crate::frame_state::AccessKitFrameState; let id = crate::accesskit_root_id(); let mut builder = accesskit::NodeBuilder::new(accesskit::Role::Window); - builder.set_transform(accesskit::Affine::scale( - self.input.pixels_per_point().into(), - )); + let pixels_per_point = viewport.input.pixels_per_point(); + builder.set_transform(accesskit::Affine::scale(pixels_per_point.into())); let mut node_builders = IdMap::default(); node_builders.insert(id, builder); - self.frame_state.accesskit_state = Some(AccessKitFrameState { + viewport.frame_state.accesskit_state = Some(AccessKitFrameState { node_builders, parent_stack: vec![id], }); } + + self.update_fonts_mut(); } /// Load fonts unless already loaded. fn update_fonts_mut(&mut self) { crate::profile_function!(); - let pixels_per_point = self.input.pixels_per_point(); - let max_texture_side = self.input.max_texture_side; + let input = &self.viewport().input; + let pixels_per_point = input.pixels_per_point(); + let max_texture_side = input.max_texture_side; if let Some(font_definitions) = self.memory.new_font_definitions.take() { crate::profile_scope!("Fonts::new"); @@ -265,7 +343,12 @@ impl ContextImpl { #[cfg(feature = "accesskit")] fn accesskit_node_builder(&mut self, id: Id) -> &mut accesskit::NodeBuilder { - let state = self.frame_state.accesskit_state.as_mut().unwrap(); + let state = self + .viewport() + .frame_state + .accesskit_state + .as_mut() + .unwrap(); let builders = &mut state.node_builders; if let std::collections::hash_map::Entry::Vacant(entry) = builders.entry(id) { entry.insert(Default::default()); @@ -275,6 +358,41 @@ impl ContextImpl { } builders.get_mut(&id).unwrap() } + + /// Return the `ViewportId` of the current viewport. + /// + /// For the root viewport this will return [`ViewportId::ROOT`]. + pub(crate) fn viewport_id(&self) -> ViewportId { + self.viewport_stack.last().copied().unwrap_or_default().this + } + + /// Return the `ViewportId` of his parent. + /// + /// For the root viewport this will return [`ViewportId::ROOT`]. + pub(crate) fn parent_viewport_id(&self) -> ViewportId { + self.viewport_stack + .last() + .copied() + .unwrap_or_default() + .parent + } + + fn all_viewport_ids(&self) -> ViewportIdSet { + self.viewports + .keys() + .copied() + .chain([ViewportId::ROOT]) + .collect() + } + + /// The current active viewport + fn viewport(&mut self) -> &mut ViewportState { + self.viewports.entry(self.viewport_id()).or_default() + } + + fn viewport_for(&mut self, viewport_id: ViewportId) -> &mut ViewportState { + self.viewports.entry(viewport_id).or_default() + } } // ---------------------------------------------------------------------------- @@ -325,7 +443,7 @@ impl ContextImpl { /// }); /// }); /// handle_platform_output(full_output.platform_output); -/// let clipped_primitives = ctx.tessellate(full_output.shapes); // create triangles to paint +/// let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point); /// paint(full_output.textures_delta, clipped_primitives); /// } /// ``` @@ -346,7 +464,13 @@ impl std::cmp::PartialEq for Context { impl Default for Context { fn default() -> Self { - Self(Arc::new(RwLock::new(ContextImpl::default()))) + let s = Self(Arc::new(RwLock::new(ContextImpl::default()))); + + s.write(|ctx| { + ctx.embed_viewports = true; + }); + + s } } @@ -386,6 +510,7 @@ impl Context { #[must_use] pub fn run(&self, new_input: RawInput, run_ui: impl FnOnce(&Context)) -> FullOutput { crate::profile_function!(); + self.begin_frame(new_input); run_ui(self); self.end_frame() @@ -410,6 +535,7 @@ impl Context { /// ``` pub fn begin_frame(&self, new_input: RawInput) { crate::profile_function!(); + self.write(|ctx| ctx.begin_frame_mut(new_input)); } } @@ -434,13 +560,25 @@ impl Context { /// ``` #[inline] pub fn input(&self, reader: impl FnOnce(&InputState) -> R) -> R { - self.read(move |ctx| reader(&ctx.input)) + self.input_for(self.viewport_id(), reader) + } + + /// This will create a `InputState::default()` if there is no input state for that viewport + #[inline] + pub fn input_for(&self, id: ViewportId, reader: impl FnOnce(&InputState) -> R) -> R { + self.write(move |ctx| reader(&ctx.viewport_for(id).input)) } /// Read-write access to [`InputState`]. #[inline] pub fn input_mut(&self, writer: impl FnOnce(&mut InputState) -> R) -> R { - self.write(move |ctx| writer(&mut ctx.input)) + self.input_mut_for(self.viewport_id(), writer) + } + + /// This will create a `InputState::default()` if there is no input state for that viewport + #[inline] + pub fn input_mut_for(&self, id: ViewportId, writer: impl FnOnce(&mut InputState) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.viewport_for(id).input)) } /// Read-only access to [`Memory`]. @@ -470,7 +608,7 @@ impl Context { /// Read-write access to [`GraphicLayers`], where painted [`crate::Shape`]s are written to. #[inline] pub(crate) fn graphics_mut(&self, writer: impl FnOnce(&mut GraphicLayers) -> R) -> R { - self.write(move |ctx| writer(&mut ctx.graphics)) + self.write(move |ctx| writer(&mut ctx.viewport().graphics)) } /// Read-only access to [`PlatformOutput`]. @@ -483,25 +621,25 @@ impl Context { /// ``` #[inline] pub fn output(&self, reader: impl FnOnce(&PlatformOutput) -> R) -> R { - self.read(move |ctx| reader(&ctx.output)) + self.write(move |ctx| reader(&ctx.viewport().output)) } /// Read-write access to [`PlatformOutput`]. #[inline] pub fn output_mut(&self, writer: impl FnOnce(&mut PlatformOutput) -> R) -> R { - self.write(move |ctx| writer(&mut ctx.output)) + self.write(move |ctx| writer(&mut ctx.viewport().output)) } /// Read-only access to [`FrameState`]. #[inline] pub(crate) fn frame_state(&self, reader: impl FnOnce(&FrameState) -> R) -> R { - self.read(move |ctx| reader(&ctx.frame_state)) + self.write(move |ctx| reader(&ctx.viewport().frame_state)) } /// Read-write access to [`FrameState`]. #[inline] pub(crate) fn frame_state_mut(&self, writer: impl FnOnce(&mut FrameState) -> R) -> R { - self.write(move |ctx| writer(&mut ctx.frame_state)) + self.write(move |ctx| writer(&mut ctx.viewport().frame_state)) } /// Read-only access to [`Fonts`]. @@ -676,15 +814,18 @@ impl Context { let mut show_blocking_widget = None; self.write(|ctx| { - ctx.layer_rects_this_frame + let viewport = ctx.viewport(); + + viewport + .layer_rects_this_frame .entry(layer_id) .or_default() .push((id, interact_rect)); if hovered { - let pointer_pos = ctx.input.pointer.interact_pos(); + let pointer_pos = viewport.input.pointer.interact_pos(); if let Some(pointer_pos) = pointer_pos { - if let Some(rects) = ctx.layer_rects_prev_frame.get(&layer_id) { + if let Some(rects) = viewport.layer_rects_prev_frame.get(&layer_id) { for &(prev_id, prev_rect) in rects.iter().rev() { if prev_id == id { break; // there is no other interactive widget covering us at the pointer position. @@ -778,8 +919,8 @@ impl Context { let clicked_elsewhere = response.clicked_elsewhere(); self.write(|ctx| { + let input = &ctx.viewports.entry(ctx.viewport_id()).or_default().input; let memory = &mut ctx.memory; - let input = &mut ctx.input; if sense.focusable { memory.interested_in_focus(id); @@ -803,21 +944,25 @@ impl Context { } if sense.click || sense.drag { - memory.interaction.click_interest |= hovered && sense.click; - memory.interaction.drag_interest |= hovered && sense.drag; + let interaction = memory.interaction_mut(); + + interaction.click_interest |= hovered && sense.click; + interaction.drag_interest |= hovered && sense.drag; - response.dragged = memory.interaction.drag_id == Some(id); + response.dragged = interaction.drag_id == Some(id); response.is_pointer_button_down_on = - memory.interaction.click_id == Some(id) || response.dragged; + interaction.click_id == Some(id) || response.dragged; for pointer_event in &input.pointer.pointer_events { match pointer_event { PointerEvent::Moved(_) => {} PointerEvent::Pressed { .. } => { if hovered { - if sense.click && memory.interaction.click_id.is_none() { + let interaction = memory.interaction_mut(); + + if sense.click && interaction.click_id.is_none() { // potential start of a click - memory.interaction.click_id = Some(id); + interaction.click_id = Some(id); response.is_pointer_button_down_on = true; } @@ -827,13 +972,12 @@ impl Context { // This is needed because we do window interaction first (to prevent frame delay), // and then do content layout. if sense.drag - && (memory.interaction.drag_id.is_none() - || memory.interaction.drag_is_window) + && (interaction.drag_id.is_none() || interaction.drag_is_window) { // potential start of a drag - memory.interaction.drag_id = Some(id); - memory.interaction.drag_is_window = false; - memory.window_interaction = None; // HACK: stop moving windows (if any) + interaction.drag_id = Some(id); + interaction.drag_is_window = false; + memory.set_window_interaction(None); // HACK: stop moving windows (if any) response.is_pointer_button_down_on = true; response.dragged = true; } @@ -980,13 +1124,22 @@ impl Context { } } - /// The current frame number. + /// The current frame number for the current viewport. /// /// Starts at zero, and is incremented at the end of [`Self::run`] or by [`Self::end_frame`]. /// /// Between calls to [`Self::run`], this is the frame number of the coming frame. pub fn frame_nr(&self) -> u64 { - self.read(|ctx| ctx.repaint.frame_nr) + self.frame_nr_for(self.viewport_id()) + } + + /// The current frame number. + /// + /// Starts at zero, and is incremented at the end of [`Self::run`] or by [`Self::end_frame`]. + /// + /// Between calls to [`Self::run`], this is the frame number of the coming frame. + pub fn frame_nr_for(&self, id: ViewportId) -> u64 { + self.read(|ctx| ctx.viewports.get(&id).map_or(0, |v| v.repaint.frame_nr)) } /// Call this if there is need to repaint the UI, i.e. if you are showing an animation. @@ -997,9 +1150,24 @@ impl Context { /// If called from outside the UI thread, the UI thread will wake up and run, /// provided the egui integration has set that up via [`Self::set_request_repaint_callback`] /// (this will work on `eframe`). + /// + /// This will repaint the current viewport pub fn request_repaint(&self) { - // request two frames of repaint, just to cover some corner cases (frame delays): - self.write(|ctx| ctx.repaint.request_repaint()); + self.request_repaint_of(self.viewport_id()); + } + + /// Call this if there is need to repaint the UI, i.e. if you are showing an animation. + /// + /// If this is called at least once in a frame, then there will be another frame right after this. + /// Call as many times as you wish, only one repaint will be issued. + /// + /// If called from outside the UI thread, the UI thread will wake up and run, + /// provided the egui integration has set that up via [`Self::set_request_repaint_callback`] + /// (this will work on `eframe`). + /// + /// This will repaint the specified viewport + pub fn request_repaint_of(&self, id: ViewportId) { + self.write(|ctx| ctx.request_repaint(id)); } /// Request repaint after at most the specified duration elapses. @@ -1021,8 +1189,37 @@ impl Context { /// and call this function, to make sure that you are displaying the latest updated time, but /// not wasting resources on needless repaints within the same second. /// - /// NOTE: only works if called before `Context::end_frame()`. to force egui to update, - /// use `Context::request_repaint()` instead. + /// ### Quirk: + /// Duration begins at the next frame. lets say for example that its a very inefficient app + /// and takes 500 milliseconds per frame at 2 fps. The widget / user might want a repaint in + /// next 500 milliseconds. Now, app takes 1000 ms per frame (1 fps) because the backend event + /// timeout takes 500 milliseconds AFTER the vsync swap buffer. + /// So, its not that we are requesting repaint within X duration. We are rather timing out + /// during app idle time where we are not receiving any new input events. + /// + /// This repaints the current viewport + pub fn request_repaint_after(&self, duration: Duration) { + self.request_repaint_after_for(duration, self.viewport_id()); + } + + /// Request repaint after at most the specified duration elapses. + /// + /// The backend can chose to repaint sooner, for instance if some other code called + /// this method with a lower duration, or if new events arrived. + /// + /// The function can be multiple times, but only the *smallest* duration will be considered. + /// So, if the function is called two times with `1 second` and `2 seconds`, egui will repaint + /// after `1 second` + /// + /// This is primarily useful for applications who would like to save battery by avoiding wasted + /// redraws when the app is not in focus. But sometimes the GUI of the app might become stale + /// and outdated if it is not updated for too long. + /// + /// Lets say, something like a stop watch widget that displays the time in seconds. You would waste + /// resources repainting multiple times within the same second (when you have no input), + /// just calculate the difference of duration between current time and next second change, + /// and call this function, to make sure that you are displaying the latest updated time, but + /// not wasting resources on needless repaints within the same second. /// /// ### Quirk: /// Duration begins at the next frame. lets say for example that its a very inefficient app @@ -1031,12 +1228,37 @@ impl Context { /// timeout takes 500 milliseconds AFTER the vsync swap buffer. /// So, its not that we are requesting repaint within X duration. We are rather timing out /// during app idle time where we are not receiving any new input events. - pub fn request_repaint_after(&self, duration: std::time::Duration) { - // Maybe we can check if duration is ZERO, and call self.request_repaint()? - self.write(|ctx| ctx.repaint.request_repaint_after(duration)); + /// + /// This repaints the specified viewport + pub fn request_repaint_after_for(&self, duration: Duration, id: ViewportId) { + self.write(|ctx| ctx.request_repaint_after(duration, id)); + } + + /// Was a repaint requested last frame for the current viewport? + #[must_use] + pub fn requested_repaint_last_frame(&self) -> bool { + self.requested_repaint_last_frame_for(&self.viewport_id()) } - /// For integrations: this callback will be called when an egui user calls [`Self::request_repaint`]. + /// Was a repaint requested last frame for the given viewport? + #[must_use] + pub fn requested_repaint_last_frame_for(&self, viewport_id: &ViewportId) -> bool { + self.read(|ctx| ctx.requested_repaint_last_frame(viewport_id)) + } + + /// Has a repaint been requested for the current viewport? + #[must_use] + pub fn has_requested_repaint(&self) -> bool { + self.has_requested_repaint_for(&self.viewport_id()) + } + + /// Has a repaint been requested for the given viewport? + #[must_use] + pub fn has_requested_repaint_for(&self, viewport_id: &ViewportId) -> bool { + self.read(|ctx| ctx.has_requested_repaint(viewport_id)) + } + + /// For integrations: this callback will be called when an egui user calls [`Self::request_repaint`] or [`Self::request_repaint_after`]. /// /// This lets you wake up a sleeping UI thread. /// @@ -1046,7 +1268,7 @@ impl Context { callback: impl Fn(RequestRepaintInfo) + Send + Sync + 'static, ) { let callback = Box::new(callback); - self.write(|ctx| ctx.repaint.request_repaint_callback = Some(callback)); + self.write(|ctx| ctx.request_repaint_callback = Some(callback)); } /// Tell `egui` which fonts to use. @@ -1123,8 +1345,12 @@ impl Context { /// For instance, when using `eframe` on web, the browsers native zoom level will always be used. pub fn set_pixels_per_point(&self, pixels_per_point: f32) { if pixels_per_point != self.pixels_per_point() { - self.request_repaint(); - self.memory_mut(|mem| mem.new_pixels_per_point = Some(pixels_per_point)); + self.write(|ctx| { + ctx.memory.override_pixels_per_point = Some(pixels_per_point); + for id in ctx.all_viewport_ids() { + ctx.request_repaint(id); + } + }); } } @@ -1265,48 +1491,53 @@ impl Context { #[must_use] pub fn end_frame(&self) -> FullOutput { crate::profile_function!(); - if self.input(|i| i.wants_repaint()) { - self.request_repaint(); - } - - let textures_delta = self.write(|ctx| { - ctx.memory.end_frame(&ctx.input, &ctx.frame_state.used_ids); + self.write(|ctx| ctx.end_frame()) + } +} - let font_image_delta = ctx.fonts.as_ref().unwrap().font_image_delta(); - if let Some(font_image_delta) = font_image_delta { - ctx.tex_manager - .0 - .write() - .set(TextureId::default(), font_image_delta); - } +impl ContextImpl { + fn end_frame(&mut self) -> FullOutput { + let ended_viewport_id = self.viewport_id(); + let viewport = self.viewports.entry(ended_viewport_id).or_default(); + let pixels_per_point = viewport.input.pixels_per_point; + + viewport.repaint.frame_nr += 1; + + self.memory + .end_frame(&viewport.input, &viewport.frame_state.used_ids); + + let font_image_delta = self.fonts.as_ref().unwrap().font_image_delta(); + if let Some(font_image_delta) = font_image_delta { + self.tex_manager + .0 + .write() + .set(TextureId::default(), font_image_delta); + } - ctx.tex_manager.0.write().take_delta() - }); + let textures_delta = self.tex_manager.0.write().take_delta(); #[cfg_attr(not(feature = "accesskit"), allow(unused_mut))] - let mut platform_output: PlatformOutput = self.output_mut(std::mem::take); + let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output); #[cfg(feature = "accesskit")] { crate::profile_scope!("accesskit"); - let state = self.frame_state_mut(|fs| fs.accesskit_state.take()); + let state = viewport.frame_state.accesskit_state.take(); if let Some(state) = state { let root_id = crate::accesskit_root_id().accesskit_id(); - let nodes = self.write(|ctx| { + let nodes = { state .node_builders .into_iter() .map(|(id, builder)| { ( id.accesskit_id(), - builder.build(&mut ctx.accesskit_node_classes), + builder.build(&mut self.accesskit_node_classes), ) }) .collect() - }); - let focus_id = self - .memory(|mem| mem.focus()) - .map_or(root_id, |id| id.accesskit_id()); + }; + let focus_id = self.memory.focus().map_or(root_id, |id| id.accesskit_id()); platform_output.accesskit_update = Some(accesskit::TreeUpdate { nodes, tree: Some(accesskit::Tree::new(root_id)), @@ -1315,32 +1546,124 @@ impl Context { } } - let repaint_after = self.write(|ctx| ctx.repaint.end_frame()); - let shapes = self.drain_paint_lists(); + let shapes = viewport.graphics.drain(self.memory.areas().order()); + + if viewport.input.wants_repaint() { + self.request_repaint(ended_viewport_id); + } + + // ------------------- + + let all_viewport_ids = self.all_viewport_ids(); + + self.last_viewport = ended_viewport_id; + + self.viewports.retain(|&id, viewport| { + let parent = *self.viewport_parents.entry(id).or_default(); + + if !all_viewport_ids.contains(&parent) { + #[cfg(feature = "log")] + log::debug!( + "Removing viewport {:?} ({:?}): the parent is gone", + id, + viewport.builder.title + ); + + return false; + } + + let is_our_child = parent == ended_viewport_id && id != ViewportId::ROOT; + if is_our_child { + if !viewport.used { + #[cfg(feature = "log")] + log::debug!( + "Removing viewport {:?} ({:?}): it was never used this frame", + id, + viewport.builder.title + ); + + return false; // Only keep children that have been updated this frame + } + + viewport.used = false; // reset so we can check again next frame + } + + true + }); + + // If we are an immediate viewport, this will resume the previous viewport. + self.viewport_stack.pop(); + + // The last viewport is not necessarily the root viewport, + // just the top _immediate_ viewport. + let is_last = self.viewport_stack.is_empty(); + + let viewport_output = self + .viewports + .iter_mut() + .map(|(&id, viewport)| { + let parent = *self.viewport_parents.entry(id).or_default(); + let commands = if is_last { + // Let the primary immediate viewport handle the commands of its children too. + // This can make things easier for the backend, as otherwise we may get commands + // that affect a viewport while its egui logic is running. + std::mem::take(&mut viewport.commands) + } else { + vec![] + }; + + ( + id, + ViewportOutput { + parent, + class: viewport.class, + builder: viewport.builder.clone(), + viewport_ui_cb: viewport.viewport_ui_cb.clone(), + commands, + repaint_delay: viewport.repaint.repaint_delay, + }, + ) + }) + .collect(); + + if is_last { + // Remove dead viewports: + self.viewports.retain(|id, _| all_viewport_ids.contains(id)); + self.viewport_parents + .retain(|id, _| all_viewport_ids.contains(id)); + } else { + let viewport_id = self.viewport_id(); + self.memory.set_viewport_id(viewport_id); + } FullOutput { platform_output, - repaint_after, textures_delta, shapes, + pixels_per_point, + viewport_output, } } +} - fn drain_paint_lists(&self) -> Vec { - crate::profile_function!(); - self.write(|ctx| ctx.graphics.drain(ctx.memory.areas.order()).collect()) - } - +impl Context { /// Tessellate the given shapes into triangle meshes. - pub fn tessellate(&self, shapes: Vec) -> Vec { + /// + /// `pixels_per_point` is used for feathering (anti-aliasing). + /// For this you can use [`FullOutput::pixels_per_point`], [`Self::pixels_per_point`], + /// or whatever is appropriate for your viewport. + pub fn tessellate( + &self, + shapes: Vec, + pixels_per_point: f32, + ) -> Vec { crate::profile_function!(); + // A tempting optimization is to reuse the tessellation from last frame if the // shapes are the same, but just comparing the shapes takes about 50% of the time // it takes to tessellate them, so it is not a worth optimization. - // here we expect that we are the only user of context, since frame is ended self.write(|ctx| { - let pixels_per_point = ctx.input.pixels_per_point(); let tessellation_options = ctx.memory.options.tessellation_options; let texture_atlas = ctx .fonts @@ -1385,9 +1708,9 @@ impl Context { /// How much space is used by panels and windows. pub fn used_rect(&self) -> Rect { - self.read(|ctx| { - let mut used = ctx.frame_state.used_by_panels; - for window in ctx.memory.areas.visible_windows() { + self.write(|ctx| { + let mut used = ctx.viewport().frame_state.used_by_panels; + for window in ctx.memory.areas().visible_windows() { used = used.union(window.rect()); } used @@ -1436,12 +1759,12 @@ impl Context { /// /// NOTE: this will return `false` if the pointer is just hovering over an egui area. pub fn is_using_pointer(&self) -> bool { - self.memory(|m| m.interaction.is_using_pointer()) + self.memory(|m| m.interaction().is_using_pointer()) } /// If `true`, egui is currently listening on text input (e.g. typing text in a [`TextEdit`]). pub fn wants_keyboard_input(&self) -> bool { - self.memory(|m| m.interaction.focus.focused().is_some()) + self.memory(|m| m.interaction().focus.focused().is_some()) } /// Highlight this widget, to make it look like it is hovered, even if it isn't. @@ -1515,7 +1838,7 @@ impl Context { /// /// [`Area`]:s and [`Window`]:s also do this automatically when being clicked on or interacted with. pub fn move_to_top(&self, layer_id: LayerId) { - self.memory_mut(|mem| mem.areas.move_to_top(layer_id)); + self.memory_mut(|mem| mem.areas_mut().move_to_top(layer_id)); } pub(crate) fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool { @@ -1563,8 +1886,12 @@ impl Context { /// Like [`Self::animate_bool`] but allows you to control the animation time. pub fn animate_bool_with_time(&self, id: Id, target_value: bool, animation_time: f32) -> f32 { let animated_value = self.write(|ctx| { - ctx.animation_manager - .animate_bool(&ctx.input, animation_time, id, target_value) + ctx.animation_manager.animate_bool( + &ctx.viewports.entry(ctx.viewport_id()).or_default().input, + animation_time, + id, + target_value, + ) }); let animation_in_progress = 0.0 < animated_value && animated_value < 1.0; if animation_in_progress { @@ -1579,8 +1906,12 @@ impl Context { /// When it is called with a new value, it linearly interpolates to it in the given time. pub fn animate_value_with_time(&self, id: Id, target_value: f32, animation_time: f32) -> f32 { let animated_value = self.write(|ctx| { - ctx.animation_manager - .animate_value(&ctx.input, animation_time, id, target_value) + ctx.animation_manager.animate_value( + &ctx.viewports.entry(ctx.viewport_id()).or_default().input, + animation_time, + id, + target_value, + ) }); let animation_in_progress = animated_value != target_value; if animation_in_progress { @@ -1637,7 +1968,7 @@ impl Context { .on_hover_text("Is egui currently listening for text input?"); ui.label(format!( "Keyboard focus widget: {}", - self.memory(|m| m.interaction.focus.focused()) + self.memory(|m| m.interaction().focus.focused()) .as_ref() .map(Id::short_debug_format) .unwrap_or_default() @@ -1768,20 +2099,20 @@ impl Context { ui.horizontal(|ui| { ui.label(format!( "{} areas (panels, windows, popups, …)", - self.memory(|mem| mem.areas.count()) + self.memory(|mem| mem.areas().count()) )); if ui.button("Reset").clicked() { - self.memory_mut(|mem| mem.areas = Default::default()); + self.memory_mut(|mem| *mem.areas_mut() = Default::default()); } }); ui.indent("areas", |ui| { ui.label("Visible areas, ordered back to front."); ui.label("Hover to highlight"); - let layers_ids: Vec = self.memory(|mem| mem.areas.order().to_vec()); + let layers_ids: Vec = self.memory(|mem| mem.areas().order().to_vec()); for layer_id in layers_ids { - let area = self.memory(|mem| mem.areas.get(layer_id.id).copied()); + let area = self.memory(|mem| mem.areas().get(layer_id.id).copied()); if let Some(area) = area { - let is_visible = self.memory(|mem| mem.areas.is_visible(&layer_id)); + let is_visible = self.memory(|mem| mem.areas().is_visible(&layer_id)); if !is_visible { continue; } @@ -1844,7 +2175,7 @@ impl Context { ui.label("NOTE: the position of this window cannot be reset from within itself."); ui.collapsing("Interaction", |ui| { - let interaction = self.memory(|mem| mem.interaction.clone()); + let interaction = self.memory(|mem| mem.interaction().clone()); interaction.ui(ui); }); } @@ -1904,7 +2235,8 @@ impl Context { writer: impl FnOnce(&mut accesskit::NodeBuilder) -> R, ) -> Option { self.write(|ctx| { - ctx.frame_state + ctx.viewport() + .frame_state .accesskit_state .is_some() .then(|| ctx.accesskit_node_builder(id)) @@ -1931,6 +2263,8 @@ impl Context { /// to get a full tree update after running [`Context::enable_accesskit`]. #[cfg(feature = "accesskit")] pub fn accesskit_placeholder_tree_update(&self) -> accesskit::TreeUpdate { + crate::profile_function!(); + use accesskit::{NodeBuilder, Role, Tree, TreeUpdate}; let root_id = crate::accesskit_root_id().accesskit_id(); @@ -2160,6 +2494,208 @@ impl Context { } } +/// ## Viewports +impl Context { + /// Return the `ViewportId` of the current viewport. + /// + /// If this is the root viewport, this will return [`ViewportId::ROOT`]. + /// + /// Don't use this outside of `Self::run`, or after `Self::end_frame`. + pub fn viewport_id(&self) -> ViewportId { + self.read(|ctx| ctx.viewport_id()) + } + + /// Return the `ViewportId` of his parent. + /// + /// If this is the root viewport, this will return [`ViewportId::ROOT`]. + /// + /// Don't use this outside of `Self::run`, or after `Self::end_frame`. + pub fn parent_viewport_id(&self) -> ViewportId { + self.read(|ctx| ctx.parent_viewport_id()) + } + + /// For integrations: Set this to render a sync viewport. + /// + /// This will only be set the callback for the current thread, + /// which most likely should be the main thread. + /// + /// When an immediate viewport is created with [`Self::show_viewport_immediate`] it will be rendered by this function. + /// + /// When called, the integration need to: + /// * Check if there already is a window for this viewport id, and if not open one + /// * Set the window attributes (position, size, …) based on [`ImmediateViewport::builder`]. + /// * Call [`Context::run`] with [`ImmediateViewport::viewport_ui_cb`]. + /// * Handle the output from [`Context::run`], including rendering + #[allow(clippy::unused_self)] + pub fn set_immediate_viewport_renderer( + callback: impl for<'a> Fn(&Context, ImmediateViewport<'a>) + 'static, + ) { + let callback = Box::new(callback); + IMMEDIATE_VIEWPORT_RENDERER.with(|render_sync| { + render_sync.replace(Some(callback)); + }); + } + + /// If `true`, [`Self::show_viewport`] and [`Self::show_viewport_immediate`] will + /// embed the new viewports inside the existing one, instead of spawning a new native window. + /// + /// `eframe` sets this to `false` on supported platforms, but the default value is `true`. + pub fn embed_viewports(&self) -> bool { + self.read(|ctx| ctx.embed_viewports) + } + + /// If `true`, [`Self::show_viewport`] and [`Self::show_viewport_immediate`] will + /// embed the new viewports inside the existing one, instead of spawning a new native window. + /// + /// `eframe` sets this to `false` on supported platforms, but the default value is `true`. + pub fn set_embed_viewports(&self, value: bool) { + self.write(|ctx| ctx.embed_viewports = value); + } + + /// Send a command to the current viewport. + /// + /// This lets you affect the current viewport, e.g. resizing the window. + pub fn send_viewport_command(&self, command: ViewportCommand) { + self.send_viewport_command_to(self.viewport_id(), command); + } + + /// Send a command to a speicfic viewport. + /// + /// This lets you affect another viewport, e.g. resizing its window. + pub fn send_viewport_command_to(&self, id: ViewportId, command: ViewportCommand) { + self.write(|ctx| ctx.viewport_for(id).commands.push(command)); + } + + /// This creates a new native window, if possible. + /// + /// The given id must be unique for each viewport. + /// + /// You need to call this each frame when the child viewport should exist. + /// + /// The given callback will be called whenever the child viewport needs repainting, + /// e.g. on an event or when [`Self::request_repaint`] is called. + /// This means it may be called multiple times, for instance while the + /// parent viewport (the caller) is sleeping but the child viewport is animating. + /// + /// You will need to wrap your viewport state in an `Arc>` or `Arc>`. + /// When this is called again with the same id in `ViewportBuilder` the render function for that viewport will be updated. + /// + /// You can also use [`Self::show_viewport_immediate`], which uses a simpler `FnOnce` + /// with no need for `Send` or `Sync`. The downside is that it will require + /// the parent viewport (the caller) to repaint anytime the child is repainted, + /// and vice versa. + /// + /// If [`Context::embed_viewports`] is `true` (e.g. if the current egui + /// backend does not support multiple viewports), the given callback + /// will be called immediately, embedding the new viewport in the current one. + /// You can check this with the [`ViewportClass`] given in the callback. + /// If you find [`ViewportClass::Embedded`], you need to create a new [`crate::Window`] for you content. + /// + /// See [`crate::viewport`] for more information about viewports. + pub fn show_viewport( + &self, + new_viewport_id: ViewportId, + viewport_builder: ViewportBuilder, + viewport_ui_cb: impl Fn(&Context, ViewportClass) + Send + Sync + 'static, + ) { + crate::profile_function!(); + + if self.embed_viewports() { + viewport_ui_cb(self, ViewportClass::Embedded); + } else { + self.write(|ctx| { + ctx.viewport_parents + .insert(new_viewport_id, ctx.viewport_id()); + + let mut viewport = ctx.viewports.entry(new_viewport_id).or_default(); + viewport.class = ViewportClass::Deferred; + viewport.builder = viewport_builder; + viewport.used = true; + viewport.viewport_ui_cb = Some(Arc::new(move |ctx| { + (viewport_ui_cb)(ctx, ViewportClass::Deferred); + })); + }); + } + } + + /// This creates a new native window, if possible. + /// + /// This is the easier type of viewport to use, but it is less performant + /// at it requires both parent and child to repaint if any one of them needs repainting, + /// which efficvely produce double work for two viewports, and triple work for three viewports, etc. + /// To avoid this, use [`Self::show_viewport`] instead. + /// + /// The given id must be unique for each viewport. + /// + /// You need to call this each frame when the child viewport should exist. + /// + /// The given ui function will be called immediately. + /// This may only be called on the main thread. + /// This call will pause the current viewport and render the child viewport in its own window. + /// This means that the child viewport will not be repainted when the parent viewport is repainted, and vice versa. + /// + /// If [`Context::embed_viewports`] is `true` (e.g. if the current egui + /// backend does not support multiple viewports), the given callback + /// will be called immediately, embedding the new viewport in the current one. + /// You can check this with the [`ViewportClass`] given in the callback. + /// If you find [`ViewportClass::Embedded`], you need to create a new [`crate::Window`] for you content. + /// + /// See [`crate::viewport`] for more information about viewports. + pub fn show_viewport_immediate( + &self, + new_viewport_id: ViewportId, + builder: ViewportBuilder, + viewport_ui_cb: impl FnOnce(&Context, ViewportClass) -> T, + ) -> T { + crate::profile_function!(); + + if self.embed_viewports() { + return viewport_ui_cb(self, ViewportClass::Embedded); + } + + IMMEDIATE_VIEWPORT_RENDERER.with(|immediate_viewport_renderer| { + let immediate_viewport_renderer = immediate_viewport_renderer.borrow(); + let Some(immediate_viewport_renderer) = immediate_viewport_renderer.as_ref() else { + // This egui backend does not support multiple viewports. + return viewport_ui_cb(self, ViewportClass::Embedded); + }; + + let ids = self.write(|ctx| { + let parent_viewport_id = ctx.viewport_id(); + + ctx.viewport_parents + .insert(new_viewport_id, parent_viewport_id); + + let mut viewport = ctx.viewports.entry(new_viewport_id).or_default(); + viewport.builder = builder.clone(); + viewport.used = true; + viewport.viewport_ui_cb = None; // it is immediate + + ViewportIdPair::from_self_and_parent(new_viewport_id, parent_viewport_id) + }); + + let mut out = None; + { + let out = &mut out; + + let viewport = ImmediateViewport { + ids, + builder, + viewport_ui_cb: Box::new(move |context| { + *out = Some(viewport_ui_cb(context, ViewportClass::Immediate)); + }), + }; + + immediate_viewport_renderer(self, viewport); + } + + out.expect( + "egui backend is implemented incorrectly - the user callback was never called", + ) + }) + } +} + #[test] fn context_impl_send_sync() { fn assert_send_sync() {} diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 4ed234c05..b08732536 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -1,6 +1,6 @@ //! The input needed by egui. -use crate::emath::*; +use crate::{emath::*, ViewportIdPair}; /// What the integrations provides to egui at the start of each frame. /// @@ -13,6 +13,9 @@ use crate::emath::*; #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct RawInput { + /// Information about the viwport the input is part of. + pub viewport: ViewportInfo, + /// Position and size of the area that egui should use, in points. /// Usually you would set this to /// @@ -72,6 +75,7 @@ pub struct RawInput { impl Default for RawInput { fn default() -> Self { Self { + viewport: ViewportInfo::default(), screen_rect: None, pixels_per_point: None, max_texture_side: None, @@ -93,6 +97,7 @@ impl RawInput { /// * [`Self::dropped_files`] is moved. pub fn take(&mut self) -> RawInput { RawInput { + viewport: self.viewport.take(), screen_rect: self.screen_rect.take(), pixels_per_point: self.pixels_per_point.take(), max_texture_side: self.max_texture_side.take(), @@ -109,6 +114,7 @@ impl RawInput { /// Add on new input. pub fn append(&mut self, newer: Self) { let Self { + viewport, screen_rect, pixels_per_point, max_texture_side, @@ -121,6 +127,7 @@ impl RawInput { focused, } = newer; + self.viewport = viewport; self.screen_rect = screen_rect.or(self.screen_rect); self.pixels_per_point = pixels_per_point.or(self.pixels_per_point); self.max_texture_side = max_texture_side.or(self.max_texture_side); @@ -134,6 +141,46 @@ impl RawInput { } } +/// Information about the current viewport, +/// given as input each frame. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct ViewportInfo { + /// Id of us and our parent. + pub ids: ViewportIdPair, + + /// Viewport inner position and size, only the drowable area + /// unit = physical pixels + pub inner_rect_px: Option, + + /// Viewport outer position and size, drowable area + decorations + /// unit = physical pixels + pub outer_rect_px: Option, + + /// The user requested the viewport should close, + /// e.g. by pressing the close button in the window decoration. + pub close_requested: bool, +} + +impl ViewportInfo { + pub fn take(&mut self) -> Self { + core::mem::take(self) + } + + pub fn ui(&self, ui: &mut crate::Ui) { + let Self { + ids, + inner_rect_px, + outer_rect_px, + close_requested, + } = self; + ui.label(format!("ids: {ids:?}")); + ui.label(format!("inner_rect_px: {inner_rect_px:?}")); + ui.label(format!("outer_rect_px: {outer_rect_px:?}")); + ui.label(format!("close_requested: {close_requested:?}")); + } +} + /// A file about to be dropped into egui. #[derive(Clone, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -943,8 +990,10 @@ impl RawInput { hovered_files, dropped_files, focused, + viewport, } = self; + viewport.ui(ui); ui.label(format!("screen_rect: {screen_rect:?} points")); ui.label(format!("pixels_per_point: {pixels_per_point:?}")) .on_hover_text( diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index 2e60db0d7..2e94b5f99 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -1,25 +1,15 @@ //! All the data egui returns to the backend at the end of each frame. -use crate::WidgetType; +use crate::{ViewportIdMap, ViewportOutput, WidgetType}; /// What egui emits each frame from [`crate::Context::run`]. /// /// The backend should use this. -#[derive(Clone, Default, PartialEq)] +#[derive(Clone, Default)] pub struct FullOutput { /// Non-rendering related output. pub platform_output: PlatformOutput, - /// If `Duration::is_zero()`, egui is requesting immediate repaint (i.e. on the next frame). - /// - /// This happens for instance when there is an animation, or if a user has called `Context::request_repaint()`. - /// - /// If `Duration` is greater than zero, egui wants to be repainted at or before the specified - /// duration elapses. when in reactive mode, egui spends forever waiting for input and only then, - /// will it repaint itself. this can be used to make sure that backend will only wait for a - /// specified amount of time, and repaint egui without any new input. - pub repaint_after: std::time::Duration, - /// Texture changes since last frame (including the font texture). /// /// The backend needs to apply [`crate::TexturesDelta::set`] _before_ painting, @@ -30,6 +20,14 @@ pub struct FullOutput { /// /// You can use [`crate::Context::tessellate`] to turn this into triangles. pub shapes: Vec, + + /// The number of physical pixels per logical ui point, for the viewport that was updated. + /// + /// You can pass this to [`crate::Context::tessellate`] together with [`Self::shapes`]. + pub pixels_per_point: f32, + + /// All the active viewports, including the root. + pub viewport_output: ViewportIdMap, } impl FullOutput { @@ -37,15 +35,27 @@ impl FullOutput { pub fn append(&mut self, newer: Self) { let Self { platform_output, - repaint_after, textures_delta, shapes, + pixels_per_point, + viewport_output: viewports, } = newer; self.platform_output.append(platform_output); - self.repaint_after = repaint_after; // if the last frame doesn't need a repaint, then we don't need to repaint self.textures_delta.append(textures_delta); self.shapes = shapes; // Only paint the latest + self.pixels_per_point = pixels_per_point; // Use latest + + for (id, new_viewport) in viewports { + match self.viewport_output.entry(id) { + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(new_viewport); + } + std::collections::hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().append(new_viewport); + } + } + } } } @@ -88,6 +98,9 @@ pub struct PlatformOutput { /// Iff `Some`, the user is editing text. pub text_cursor_pos: Option, + /// The difference in the widget tree since last frame. + /// + /// NOTE: this needs to be per-viewport. #[cfg(feature = "accesskit")] pub accesskit_update: Option, } @@ -626,16 +639,15 @@ impl WidgetInfo { } if typ == &WidgetType::TextEdit { - let text; - if let Some(text_value) = text_value { + let text = if let Some(text_value) = text_value { if text_value.is_empty() { - text = "blank".into(); + "blank".into() } else { - text = text_value.to_string(); + text_value.to_string() } } else { - text = "blank".into(); - } + "blank".into() + }; description = format!("{text}: {description}"); } diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index 9caa3bafd..e588271d9 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -42,7 +42,7 @@ impl Id { Self(0) } - pub(crate) fn background() -> Self { + pub(crate) const fn background() -> Self { Self(1) } diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index 47b8544f2..ac03c1ccd 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -150,6 +150,7 @@ impl InputState { requested_repaint_last_frame: bool, ) -> InputState { crate::profile_function!(); + let time = new.time.unwrap_or(self.time + new.predicted_dt as f64); let unstable_dt = (time - self.time) as f32; diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 83e7fdeb6..34cfdae05 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -170,7 +170,9 @@ impl GraphicLayers { .or_default() } - pub fn drain(&mut self, area_order: &[LayerId]) -> impl ExactSizeIterator { + pub fn drain(&mut self, area_order: &[LayerId]) -> Vec { + crate::profile_function!(); + let mut all_shapes: Vec<_> = Default::default(); for &order in &Order::ALL { @@ -196,6 +198,6 @@ impl GraphicLayers { } } - all_shapes.into_iter() + all_shapes } } diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 13108416e..760475fb9 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -96,6 +96,10 @@ //! # }); //! ``` //! +//! ## Viewports +//! Some egui backends support multiple _viewports_, which is what egui calls the native OS windows it resides in. +//! See [`crate::viewport`] for more information. +//! //! ## Coordinate system //! The left-top corner of the screen is `(0.0, 0.0)`, //! with X increasing to the right and Y increasing downwards. @@ -134,7 +138,7 @@ //! }); //! }); //! handle_platform_output(full_output.platform_output); -//! let clipped_primitives = ctx.tessellate(full_output.shapes); // create triangles to paint +//! let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point); //! paint(full_output.textures_delta, clipped_primitives); //! } //! ``` @@ -357,6 +361,7 @@ mod sense; pub mod style; mod ui; pub mod util; +pub mod viewport; pub mod widget_text; pub mod widgets; @@ -417,6 +422,7 @@ pub use { style::{FontSelection, Margin, Style, TextStyle, Visuals}, text::{Galley, TextFormat}, ui::Ui, + viewport::*, widget_text::{RichText, WidgetText}, widgets::*, }; diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index 717383d31..f1c61a644 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -58,7 +58,7 @@ mod texture_loader; use std::borrow::Cow; use std::fmt::Debug; use std::ops::Deref; -use std::{error::Error as StdError, fmt::Display, sync::Arc}; +use std::{fmt::Display, sync::Arc}; use ahash::HashMap; @@ -118,7 +118,7 @@ impl Display for LoadError { } } -impl StdError for LoadError {} +impl std::error::Error for LoadError {} pub type Result = std::result::Result; diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index 908d9f7bb..17135760d 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -2,7 +2,12 @@ use epaint::{emath::Rangef, vec2, Vec2}; -use crate::{area, window, EventFilter, Id, IdMap, InputState, LayerId, Pos2, Rect, Style}; +use crate::{ + area, + window::{self, WindowInteraction}, + EventFilter, Id, IdMap, InputState, LayerId, Pos2, Rect, Style, ViewportId, ViewportIdMap, + ViewportIdSet, +}; // ---------------------------------------------------------------------------- @@ -15,7 +20,7 @@ use crate::{area, window, EventFilter, Id, IdMap, InputState, LayerId, Pos2, Rec /// For this you need to enable the `persistence`. /// /// If you want to store data for your widgets, you should look at [`Memory::data`] -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "persistence", serde(default))] pub struct Memory { @@ -68,23 +73,19 @@ pub struct Memory { // ------------------------------------------ /// new scale that will be applied at the start of the next frame #[cfg_attr(feature = "persistence", serde(skip))] - pub(crate) new_pixels_per_point: Option, + pub(crate) override_pixels_per_point: Option, /// new fonts that will be applied at the start of the next frame #[cfg_attr(feature = "persistence", serde(skip))] pub(crate) new_font_definitions: Option, + // Current active viewport #[cfg_attr(feature = "persistence", serde(skip))] - pub(crate) interaction: Interaction, - - #[cfg_attr(feature = "persistence", serde(skip))] - pub(crate) window_interaction: Option, + pub(crate) viewport_id: ViewportId, #[cfg_attr(feature = "persistence", serde(skip))] pub(crate) drag_value: crate::widgets::drag_value::MonoState, - pub(crate) areas: Areas, - /// Which popup-window is open (if any)? /// Could be a combo box, color picker, menu etc. #[cfg_attr(feature = "persistence", serde(skip))] @@ -92,6 +93,38 @@ pub struct Memory { #[cfg_attr(feature = "persistence", serde(skip))] everything_is_visible: bool, + + // ------------------------------------------------- + // Per-viewport: + areas: ViewportIdMap, + + #[cfg_attr(feature = "persistence", serde(skip))] + pub(crate) interactions: ViewportIdMap, + + #[cfg_attr(feature = "persistence", serde(skip))] + window_interactions: ViewportIdMap, +} + +impl Default for Memory { + fn default() -> Self { + let mut slf = Self { + options: Default::default(), + data: Default::default(), + caches: Default::default(), + override_pixels_per_point: Default::default(), + new_font_definitions: Default::default(), + interactions: Default::default(), + viewport_id: Default::default(), + window_interactions: Default::default(), + drag_value: Default::default(), + areas: Default::default(), + popup: Default::default(), + everything_is_visible: Default::default(), + }; + slf.interactions.entry(slf.viewport_id).or_default(); + slf.areas.entry(slf.viewport_id).or_default(); + slf + } } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -516,34 +549,63 @@ impl Memory { &mut self, prev_input: &crate::input_state::InputState, new_input: &crate::data::input::RawInput, + viewports: &ViewportIdSet, ) { crate::profile_function!(); - self.interaction.begin_frame(prev_input, new_input); + + // Cleanup + self.interactions.retain(|id, _| viewports.contains(id)); + self.areas.retain(|id, _| viewports.contains(id)); + self.window_interactions + .retain(|id, _| viewports.contains(id)); + + self.viewport_id = new_input.viewport.ids.this; + self.interactions + .entry(self.viewport_id) + .or_default() + .begin_frame(prev_input, new_input); + self.areas.entry(self.viewport_id).or_default(); if !prev_input.pointer.any_down() { - self.window_interaction = None; + self.window_interactions.remove(&self.viewport_id); } } pub(crate) fn end_frame(&mut self, input: &InputState, used_ids: &IdMap) { self.caches.update(); - self.areas.end_frame(); - self.interaction.focus.end_frame(used_ids); + self.areas_mut().end_frame(); + self.interaction_mut().focus.end_frame(used_ids); self.drag_value.end_frame(input); } + pub(crate) fn set_viewport_id(&mut self, viewport_id: ViewportId) { + self.viewport_id = viewport_id; + } + + /// Access memory of the [`Area`](crate::containers::area::Area)s, such as `Window`s. + pub fn areas(&self) -> &Areas { + self.areas + .get(&self.viewport_id) + .expect("Memory broken: no area for the current viewport") + } + + /// Access memory of the [`Area`](crate::containers::area::Area)s, such as `Window`s. + pub fn areas_mut(&mut self) -> &mut Areas { + self.areas.entry(self.viewport_id).or_default() + } + /// Top-most layer at the given position. pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option { - self.areas.layer_id_at(pos, resize_interact_radius_side) + self.areas().layer_id_at(pos, resize_interact_radius_side) } /// An iterator over all layers. Back-to-front. Top is last. pub fn layer_ids(&self) -> impl ExactSizeIterator + '_ { - self.areas.order().iter().copied() + self.areas().order().iter().copied() } pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool { - self.interaction.focus.id_previous_frame == Some(id) + self.interaction().focus.id_previous_frame == Some(id) } /// True if the given widget had keyboard focus last frame, but not this one. @@ -564,12 +626,12 @@ impl Memory { /// from the window and back. #[inline(always)] pub fn has_focus(&self, id: Id) -> bool { - self.interaction.focus.focused() == Some(id) + self.interaction().focus.focused() == Some(id) } /// Which widget has keyboard focus? pub fn focus(&self) -> Option { - self.interaction.focus.focused() + self.interaction().focus.focused() } /// Set an event filter for a widget. @@ -580,7 +642,7 @@ impl Memory { /// You must first give focus to the widget before calling this. pub fn set_focus_lock_filter(&mut self, id: Id, event_filter: EventFilter) { if self.had_focus_last_frame(id) && self.has_focus(id) { - if let Some(focused) = &mut self.interaction.focus.focused_widget { + if let Some(focused) = &mut self.interaction_mut().focus.focused_widget { if focused.id == id { focused.filter = event_filter; } @@ -607,15 +669,16 @@ impl Memory { /// See also [`crate::Response::request_focus`]. #[inline(always)] pub fn request_focus(&mut self, id: Id) { - self.interaction.focus.focused_widget = Some(FocusWidget::new(id)); + self.interaction_mut().focus.focused_widget = Some(FocusWidget::new(id)); } /// Surrender keyboard focus for a specific widget. /// See also [`crate::Response::surrender_focus`]. #[inline(always)] pub fn surrender_focus(&mut self, id: Id) { - if self.interaction.focus.focused() == Some(id) { - self.interaction.focus.focused_widget = None; + let interaction = self.interaction_mut(); + if interaction.focus.focused() == Some(id) { + interaction.focus.focused_widget = None; } } @@ -628,37 +691,37 @@ impl Memory { /// and rendered correctly in a single frame. #[inline(always)] pub fn interested_in_focus(&mut self, id: Id) { - self.interaction.focus.interested_in_focus(id); + self.interaction_mut().focus.interested_in_focus(id); } /// Stop editing of active [`TextEdit`](crate::TextEdit) (if any). #[inline(always)] pub fn stop_text_input(&mut self) { - self.interaction.focus.focused_widget = None; + self.interaction_mut().focus.focused_widget = None; } /// Is any widget being dragged? #[inline(always)] pub fn is_anything_being_dragged(&self) -> bool { - self.interaction.drag_id.is_some() + self.interaction().drag_id.is_some() } /// Is this specific widget being dragged? #[inline(always)] pub fn is_being_dragged(&self, id: Id) -> bool { - self.interaction.drag_id == Some(id) + self.interaction().drag_id == Some(id) } /// Set which widget is being dragged. #[inline(always)] pub fn set_dragged_id(&mut self, id: Id) { - self.interaction.drag_id = Some(id); + self.interaction_mut().drag_id = Some(id); } /// Stop dragging any widget. #[inline(always)] pub fn stop_dragging(&mut self) { - self.interaction.drag_id = None; + self.interaction_mut().drag_id = None; } /// Forget window positions, sizes etc. @@ -669,7 +732,29 @@ impl Memory { /// Obtain the previous rectangle of an area. pub fn area_rect(&self, id: impl Into) -> Option { - self.areas.get(id.into()).map(|state| state.rect()) + self.areas().get(id.into()).map(|state| state.rect()) + } + + pub(crate) fn window_interaction(&self) -> Option { + self.window_interactions.get(&self.viewport_id).copied() + } + + pub(crate) fn set_window_interaction(&mut self, wi: Option) { + if let Some(wi) = wi { + self.window_interactions.insert(self.viewport_id, wi); + } else { + self.window_interactions.remove(&self.viewport_id); + } + } + + pub(crate) fn interaction(&self) -> &Interaction { + self.interactions + .get(&self.viewport_id) + .expect("Failed to get interaction") + } + + pub(crate) fn interaction_mut(&mut self) -> &mut Interaction { + self.interactions.entry(self.viewport_id).or_default() } } diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs new file mode 100644 index 000000000..c3fa7cec0 --- /dev/null +++ b/crates/egui/src/viewport.rs @@ -0,0 +1,804 @@ +//! egui supports multiple viewports, corresponding to multiple native windows. +//! +//! Not all egui backends support multiple viewports, but `eframe` native does +//! (but not on web). +//! +//! You can spawn a new viewport using [`Context::show_viewport`] and [`Context::show_viewport_immediate`]. +//! These needs to be called every frame the viewport should be visible. +//! +//! This is implemented by the native `eframe` backend, but not the web one. +//! +//! ## Viewport classes +//! The viewports form a tree of parent-child relationships. +//! +//! There are different classes of viewports. +//! +//! ### Root viewport +//! The root viewport is the original viewport, and cannot be closed without closing the application. +//! +//! ### Deferred viewports +//! These are created with [`Context::show_viewport`]. +//! Deferred viewports take a closure that is called by the integration at a later time, perhaps multiple times. +//! Deferred viewports are repainted independenantly of the parent viewport. +//! This means communication with them need to done via channels, or `Arc/Mutex`. +//! +//! This is the most performant type of child viewport, though a bit more cumbersome to work with compared to immediate viewports. +//! +//! ### Immediate viewports +//! These are created with [`Context::show_viewport_immediate`]. +//! Immediate viewports take a `FnOnce` closure, similar to other egui functions, and is called immediately. +//! This makes communication with them much simpler than with deferred viewports, but this simplicity comes at a cost: whenever the parent viewports needs to be repainted, so will the child viewport, and vice versa. +//! This means that if you have `N` viewports you are potentially doing `N` times as much CPU work. However, if all your viewports are showing animations, and thus are repainting constantly anyway, this doesn't matter. +//! +//! In short: immediate viewports are simpler to use, but can waste a lot of CPU time. +//! +//! ### Embedded viewports +//! These are not real, independenant viewports, but is a fallback mode for when the integration does not support real viewports. In your callback is called with [`ViewportClass::Embedded`] it means you need to create an [`crate::Window`] to wrap your ui in, which will then be embedded in the parent viewport, unable to escape it. +//! +//! +//! ## Using the viewports +//! Only one viewport is active at any one time, identified with [`Context::viewport_id`]. +//! You can send commands to other viewports using [`Context::send_viewport_command_to`]. +//! +//! There is an example in . +//! +//! ## For integrations +//! * There is a [`crate::RawInput::viewport`] with information about the current viewport. +//! * The repaint callback set by [`Context::set_request_repaint_callback`] points to which viewport should be repainted. +//! * [`crate::FullOutput::viewport_output`] is a list of viewports which should result in their own independent windows. +//! * To support immediate viewports you need to call [`Context::set_immediate_viewport_renderer`]. +//! * If you support viewports, you need to call [`Context::set_embed_viewports`] with `false`, or all new viewports will be embedded (the default behavior). +//! +//! ## Future work +//! There are several more things related to viewports that we want to add. +//! Read more at . + +use std::sync::Arc; + +use epaint::{ColorImage, Pos2, Vec2}; + +use crate::{Context, Id}; + +// ---------------------------------------------------------------------------- + +/// The different types of viewports supported by egui. +#[derive(Clone, Copy, Default, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ViewportClass { + /// The root viewport; i.e. the original window. + #[default] + Root, + + /// A viewport run independently from the parent viewport. + /// + /// This is the preferred type of viewport from a performance perspective. + /// + /// Create these with [`crate::Context::show_viewport`]. + Deferred, + + /// A viewport run inside the parent viewport. + /// + /// This is the easier type of viewport to use, but it is less performant + /// at it requires both parent and child to repaint if any one of them needs repainting, + /// which efficvely produce double work for two viewports, and triple work for three viewports, etc. + /// + /// Create these with [`crate::Context::show_viewport_immediate`]. + Immediate, + + /// The fallback, when the egui integration doesn't support viewports, + /// or [`crate::Context::embed_viewports`] is set to `true`. + Embedded, +} + +// ---------------------------------------------------------------------------- + +/// A unique identifier of a viewport. +/// +/// This is returned by [`Context::viewport_id`] and [`Context::parent_viewport_id`]. +#[derive(Clone, Copy, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct ViewportId(pub Id); + +impl Default for ViewportId { + #[inline] + fn default() -> Self { + Self::ROOT + } +} + +impl std::fmt::Debug for ViewportId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.short_debug_format().fmt(f) + } +} + +impl ViewportId { + /// The `ViewportId` of the root viewport. + pub const ROOT: Self = Self(Id::NULL); + + #[inline] + pub fn from_hash_of(source: impl std::hash::Hash) -> Self { + Self(Id::new(source)) + } +} + +impl From for Id { + #[inline] + fn from(id: ViewportId) -> Self { + id.0 + } +} + +impl nohash_hasher::IsEnabled for ViewportId {} + +/// A fast hash set of [`ViewportId`]. +pub type ViewportIdSet = nohash_hasher::IntSet; + +/// A fast hash map from [`ViewportId`] to `T`. +pub type ViewportIdMap = nohash_hasher::IntMap; + +// ---------------------------------------------------------------------------- + +/// A pair of [`ViewportId`], used to identify a viewport and its parent. +#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct ViewportIdPair { + pub this: ViewportId, + pub parent: ViewportId, +} + +impl Default for ViewportIdPair { + #[inline] + fn default() -> Self { + Self::ROOT + } +} + +impl ViewportIdPair { + /// The `ViewportIdPair` of the root viewport, which is its own parent. + pub const ROOT: Self = Self { + this: ViewportId::ROOT, + parent: ViewportId::ROOT, + }; + + #[inline] + pub fn from_self_and_parent(this: ViewportId, parent: ViewportId) -> Self { + Self { this, parent } + } +} + +/// The user-code that shows the ui in the viewport, used for deferred viewports. +pub type DeferredViewportUiCallback = dyn Fn(&Context) + Sync + Send; + +/// Render the given viewport, calling the given ui callback. +pub type ImmediateViewportRendererCallback = dyn for<'a> Fn(&Context, ImmediateViewport<'a>); + +/// Control the building of a new egui viewport (i.e. native window). +/// +/// The fields are public, but you should use the builder pattern to set them, +/// and that's where you'll find the documentation too. +/// +/// Since egui is immediate mode, `ViewportBuilder` is accumulative in nature. +/// Setting any option to `None` means "keep the current value", +/// or "Use the default" if it is the first call. +/// +/// The default values are implementation defined, so you may want to explicitly +/// configure the size of the window, and what buttons are shown. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[allow(clippy::option_option)] +pub struct ViewportBuilder { + /// The title of the vieweport. + /// `eframe` will use this as the title of the native window. + pub title: Option, + + /// This is wayland only. See [`Self::with_name`]. + pub name: Option<(String, String)>, + + pub position: Option, + pub inner_size: Option, + pub min_inner_size: Option, + pub max_inner_size: Option, + + pub fullscreen: Option, + pub maximized: Option, + pub resizable: Option, + pub transparent: Option, + pub decorations: Option, + pub icon: Option>, + pub active: Option, + pub visible: Option, + pub title_hidden: Option, + pub titlebar_transparent: Option, + pub fullsize_content_view: Option, + pub drag_and_drop: Option, + + pub close_button: Option, + pub minimize_button: Option, + pub maximize_button: Option, + + pub hittest: Option, +} + +impl ViewportBuilder { + /// Sets the initial title of the window in the title bar. + /// + /// Look at winit for more details + #[inline] + pub fn with_title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); + self + } + + /// Sets whether the window should have a border, a title bar, etc. + /// + /// The default is `true`. + /// + /// Look at winit for more details + #[inline] + pub fn with_decorations(mut self, decorations: bool) -> Self { + self.decorations = Some(decorations); + self + } + + /// Sets whether the window should be put into fullscreen upon creation. + /// + /// The default is `None`. + /// + /// Look at winit for more details + /// This will use borderless + #[inline] + pub fn with_fullscreen(mut self, fullscreen: bool) -> Self { + self.fullscreen = Some(fullscreen); + self + } + + /// Request that the window is maximized upon creation. + /// + /// The default is `false`. + /// + /// Look at winit for more details + #[inline] + pub fn with_maximized(mut self, maximized: bool) -> Self { + self.maximized = Some(maximized); + self + } + + /// Sets whether the window is resizable or not. + /// + /// The default is `true`. + /// + /// Look at winit for more details + #[inline] + pub fn with_resizable(mut self, resizable: bool) -> Self { + self.resizable = Some(resizable); + self + } + + /// Sets whether the background of the window should be transparent. + /// + /// If this is `true`, writing colors with alpha values different than + /// `1.0` will produce a transparent window. On some platforms this + /// is more of a hint for the system and you'd still have the alpha + /// buffer. + /// + /// The default is `false`. + /// If this is not working is because the graphic context dozen't support transparency, + /// you will need to set the transparency in the eframe! + #[inline] + pub fn with_transparent(mut self, transparent: bool) -> Self { + self.transparent = Some(transparent); + self + } + + /// The icon needs to be wrapped in Arc because will be cloned every frame + #[inline] + pub fn with_window_icon(mut self, icon: impl Into>) -> Self { + self.icon = Some(icon.into()); + self + } + + /// Whether the window will be initially focused or not. + /// + /// The window should be assumed as not focused by default + /// + /// ## Platform-specific: + /// + /// **Android / iOS / X11 / Wayland / Orbital:** Unsupported. + /// + /// Look at winit for more details + #[inline] + pub fn with_active(mut self, active: bool) -> Self { + self.active = Some(active); + self + } + + /// Sets whether the window will be initially visible or hidden. + /// + /// The default is to show the window. + /// + /// Look at winit for more details + #[inline] + pub fn with_visible(mut self, visible: bool) -> Self { + self.visible = Some(visible); + self + } + + /// Hides the window title. + /// + /// Mac Os only. + #[inline] + pub fn with_title_hidden(mut self, title_hidden: bool) -> Self { + self.title_hidden = Some(title_hidden); + self + } + + /// Makes the titlebar transparent and allows the content to appear behind it. + /// + /// Mac Os only. + #[inline] + pub fn with_titlebar_transparent(mut self, value: bool) -> Self { + self.titlebar_transparent = Some(value); + self + } + + /// Makes the window content appear behind the titlebar. + /// + /// Mac Os only. + #[inline] + pub fn with_fullsize_content_view(mut self, value: bool) -> Self { + self.fullsize_content_view = Some(value); + self + } + + /// Requests the window to be of specific dimensions. + /// + /// If this is not set, some platform-specific dimensions will be used. + /// + /// Should be bigger then 0 + /// Look at winit for more details + #[inline] + pub fn with_inner_size(mut self, size: impl Into) -> Self { + self.inner_size = Some(size.into()); + self + } + + /// Sets the minimum dimensions a window can have. + /// + /// If this is not set, the window will have no minimum dimensions (aside + /// from reserved). + /// + /// Should be bigger then 0 + /// Look at winit for more details + #[inline] + pub fn with_min_inner_size(mut self, size: impl Into) -> Self { + self.min_inner_size = Some(size.into()); + self + } + + /// Sets the maximum dimensions a window can have. + /// + /// If this is not set, the window will have no maximum or will be set to + /// the primary monitor's dimensions by the platform. + /// + /// Should be bigger then 0 + /// Look at winit for more details + #[inline] + pub fn with_max_inner_size(mut self, size: impl Into) -> Self { + self.max_inner_size = Some(size.into()); + self + } + + /// X11 not working! + #[inline] + pub fn with_close_button(mut self, value: bool) -> Self { + self.close_button = Some(value); + self + } + + /// X11 not working! + #[inline] + pub fn with_minimize_button(mut self, value: bool) -> Self { + self.minimize_button = Some(value); + self + } + + /// X11 not working! + #[inline] + pub fn with_maximize_button(mut self, value: bool) -> Self { + self.maximize_button = Some(value); + self + } + + /// This currently only work on windows to be disabled! + #[inline] + pub fn with_drag_and_drop(mut self, value: bool) -> Self { + self.drag_and_drop = Some(value); + self + } + + /// This will probably not work as expected! + #[inline] + pub fn with_position(mut self, pos: impl Into) -> Self { + self.position = Some(pos.into()); + self + } + + /// This is wayland only! + /// Build window with the given name. + /// + /// The `general` name sets an application ID, which should match the `.desktop` + /// file distributed with your program. The `instance` is a `no-op`. + /// + /// For details about application ID conventions, see the + /// [Desktop Entry Spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id) + #[inline] + pub fn with_name(mut self, id: impl Into, instance: impl Into) -> Self { + self.name = Some((id.into(), instance.into())); + self + } + + /// Is not implemented for winit + /// You should use `ViewportCommand::CursorHitTest` if you want to set this! + #[deprecated] + #[inline] + pub fn with_hittest(mut self, value: bool) -> Self { + self.hittest = Some(value); + self + } + + /// Update this `ViewportBuilder` with a delta, + /// returning a list of commands and a bool intdicating if the window needs to be recreated. + pub fn patch(&mut self, new: &ViewportBuilder) -> (Vec, bool) { + let mut commands = Vec::new(); + + if let Some(new_title) = &new.title { + if Some(new_title) != self.title.as_ref() { + self.title = Some(new_title.clone()); + commands.push(ViewportCommand::Title(new_title.clone())); + } + } + + if let Some(new_position) = new.position { + if Some(new_position) != self.position { + self.position = Some(new_position); + commands.push(ViewportCommand::OuterPosition(new_position)); + } + } + + if let Some(new_inner_size) = new.inner_size { + if Some(new_inner_size) != self.inner_size { + self.inner_size = Some(new_inner_size); + commands.push(ViewportCommand::InnerSize(new_inner_size)); + } + } + + if let Some(new_min_inner_size) = new.min_inner_size { + if Some(new_min_inner_size) != self.min_inner_size { + self.min_inner_size = Some(new_min_inner_size); + commands.push(ViewportCommand::MinInnerSize(new_min_inner_size)); + } + } + + if let Some(new_max_inner_size) = new.max_inner_size { + if Some(new_max_inner_size) != self.max_inner_size { + self.max_inner_size = Some(new_max_inner_size); + commands.push(ViewportCommand::MaxInnerSize(new_max_inner_size)); + } + } + + if let Some(new_fullscreen) = new.fullscreen { + if Some(new_fullscreen) != self.fullscreen { + self.fullscreen = Some(new_fullscreen); + commands.push(ViewportCommand::Fullscreen(new_fullscreen)); + } + } + + if let Some(new_maximized) = new.maximized { + if Some(new_maximized) != self.maximized { + self.maximized = Some(new_maximized); + commands.push(ViewportCommand::Maximized(new_maximized)); + } + } + + if let Some(new_resizable) = new.resizable { + if Some(new_resizable) != self.resizable { + self.resizable = Some(new_resizable); + commands.push(ViewportCommand::Resizable(new_resizable)); + } + } + + if let Some(new_transparent) = new.transparent { + if Some(new_transparent) != self.transparent { + self.transparent = Some(new_transparent); + commands.push(ViewportCommand::Transparent(new_transparent)); + } + } + + if let Some(new_decorations) = new.decorations { + if Some(new_decorations) != self.decorations { + self.decorations = Some(new_decorations); + commands.push(ViewportCommand::Decorations(new_decorations)); + } + } + + if let Some(new_icon) = &new.icon { + let is_new = match &self.icon { + Some(existing) => !Arc::ptr_eq(new_icon, existing), + None => true, + }; + + if is_new { + commands.push(ViewportCommand::WindowIcon(Some(new_icon.clone()))); + self.icon = Some(new_icon.clone()); + } + } + + if let Some(new_visible) = new.visible { + if Some(new_visible) != self.active { + self.visible = Some(new_visible); + commands.push(ViewportCommand::Visible(new_visible)); + } + } + + if let Some(new_hittest) = new.hittest { + if Some(new_hittest) != self.hittest { + self.hittest = Some(new_hittest); + commands.push(ViewportCommand::CursorHitTest(new_hittest)); + } + } + + // TODO: Implement compare for windows buttons + + let mut recreate_window = false; + + if let Some(new_active) = new.active { + if Some(new_active) != self.active { + self.active = Some(new_active); + recreate_window = true; + } + } + + if let Some(new_close_button) = new.close_button { + if Some(new_close_button) != self.close_button { + self.close_button = Some(new_close_button); + recreate_window = true; + } + } + + if let Some(new_minimize_button) = new.minimize_button { + if Some(new_minimize_button) != self.minimize_button { + self.minimize_button = Some(new_minimize_button); + recreate_window = true; + } + } + + if let Some(new_maximized_button) = new.maximize_button { + if Some(new_maximized_button) != self.maximize_button { + self.maximize_button = Some(new_maximized_button); + recreate_window = true; + } + } + + if let Some(new_title_hidden) = new.title_hidden { + if Some(new_title_hidden) != self.title_hidden { + self.title_hidden = Some(new_title_hidden); + recreate_window = true; + } + } + + if let Some(new_titlebar_transparent) = new.titlebar_transparent { + if Some(new_titlebar_transparent) != self.titlebar_transparent { + self.titlebar_transparent = Some(new_titlebar_transparent); + recreate_window = true; + } + } + + if let Some(new_fullsize_content_view) = new.fullsize_content_view { + if Some(new_fullsize_content_view) != self.fullsize_content_view { + self.fullsize_content_view = Some(new_fullsize_content_view); + recreate_window = true; + } + } + + (commands, recreate_window) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum WindowLevel { + Normal, + AlwaysOnBottom, + AlwaysOnTop, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum IMEPurpose { + Normal, + Password, + Terminal, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum SystemTheme { + Light, + Dark, + SystemDefault, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum CursorGrab { + None, + Confined, + Locked, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum UserAttentionType { + Informational, + Critical, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ResizeDirection { + North, + South, + West, + NorthEast, + SouthEast, + NorthWest, + SouthWest, +} + +/// You can send a [`ViewportCommand`] to the viewport with [`Context::send_viewport_command`]. +/// +/// All coordinates are in logical points. +/// +/// This is essentially a way to diff [`ViewportBuilder`]. +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ViewportCommand { + /// Set the title + Title(String), + + /// Turn the window transparent or not. + Transparent(bool), + + /// Set the visibility of the window. + Visible(bool), + + /// Moves the window with the left mouse button until the button is released. + /// + /// There's no guarantee that this will work unless the left mouse button was pressed + /// immediately before this function is called. + StartDrag, + + /// Set the outer position of the viewport, i.e. moves the window. + OuterPosition(Pos2), + + /// Should be bigger then 0 + InnerSize(Vec2), + + /// Should be bigger then 0 + MinInnerSize(Vec2), + + /// Should be bigger then 0 + MaxInnerSize(Vec2), + + /// Should be bigger then 0 + ResizeIncrements(Option), + + /// Begin resizing the viewport with the left mouse button until the button is released. + /// + /// There's no guarantee that this will work unless the left mouse button was pressed + /// immediately before this function is called. + BeginResize(ResizeDirection), + + /// Can the window be resized? + Resizable(bool), + + /// Set which window buttons are enabled + EnableButtons { + close: bool, + minimized: bool, + maximize: bool, + }, + Minimized(bool), + Maximized(bool), + Fullscreen(bool), + + /// Show window decorations, i.e. the chrome around the content + /// with the title bar, close buttons, resize handles, etc. + Decorations(bool), + + WindowLevel(WindowLevel), + WindowIcon(Option>), + + IMEPosition(Pos2), + IMEAllowed(bool), + IMEPurpose(IMEPurpose), + + RequestUserAttention(Option), + + SetTheme(SystemTheme), + + ContentProtected(bool), + + /// Will probably not work as expected! + CursorPosition(Pos2), + + CursorGrab(CursorGrab), + + CursorVisible(bool), + + CursorHitTest(bool), +} + +/// Describes a viewport, i.e. a native window. +#[derive(Clone)] +pub struct ViewportOutput { + /// Id of our parent viewport. + pub parent: ViewportId, + + /// What type of viewport are we? + /// + /// This will never be [`ViewportClass::Embedded`], + /// since those don't result in real viewports. + pub class: ViewportClass, + + /// The window attrbiutes such as title, position, size, etc. + pub builder: ViewportBuilder, + + /// The user-code that shows the GUI, used for deferred viewports. + /// + /// `None` for immediate viewports and the ROOT viewport. + pub viewport_ui_cb: Option>, + + /// Commands to change the viewport, e.g. window title and size. + pub commands: Vec, + + /// Schedulare a repaint of this viewport after this delay. + /// + /// It is preferably to instead install a [`Context::set_request_repaint_callback`], + /// but if you haven't, you can use this instead. + /// + /// If the duration is zero, schedule a repaint immediately. + pub repaint_delay: std::time::Duration, +} + +impl ViewportOutput { + /// Add on new output. + pub fn append(&mut self, newer: Self) { + let Self { + parent, + class, + builder, + viewport_ui_cb, + mut commands, + repaint_delay, + } = newer; + + self.parent = parent; + self.class = class; + self.builder.patch(&builder); + self.viewport_ui_cb = viewport_ui_cb; + self.commands.append(&mut commands); + self.repaint_delay = self.repaint_delay.min(repaint_delay); + } +} + +/// Viewport for immediate rendering. +pub struct ImmediateViewport<'a> { + /// Id of us and our parent. + pub ids: ViewportIdPair, + + pub builder: ViewportBuilder, + + /// The user-code that shows the GUI. + pub viewport_ui_cb: Box, +} diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index dcc55ea6e..1baaefd7e 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -16,7 +16,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { let full_output = ctx.run(RawInput::default(), |ctx| { demo_windows.ui(ctx); }); - ctx.tessellate(full_output.shapes) + ctx.tessellate(full_output.shapes, full_output.pixels_per_point) }); }); @@ -32,7 +32,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { demo_windows.ui(ctx); }); c.bench_function("demo_only_tessellate", |b| { - b.iter(|| ctx.tessellate(full_output.shapes.clone())); + b.iter(|| ctx.tessellate(full_output.shapes.clone(), full_output.pixels_per_point)); }); } diff --git a/crates/egui_demo_lib/src/lib.rs b/crates/egui_demo_lib/src/lib.rs index 9d7eaa536..893413f35 100644 --- a/crates/egui_demo_lib/src/lib.rs +++ b/crates/egui_demo_lib/src/lib.rs @@ -77,7 +77,7 @@ fn test_egui_e2e() { let full_output = ctx.run(raw_input.clone(), |ctx| { demo_windows.ui(ctx); }); - let clipped_primitives = ctx.tessellate(full_output.shapes); + let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point); assert!(!clipped_primitives.is_empty()); } } @@ -96,7 +96,7 @@ fn test_egui_zero_window_size() { let full_output = ctx.run(raw_input.clone(), |ctx| { demo_windows.ui(ctx); }); - let clipped_primitives = ctx.tessellate(full_output.shapes); + let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point); assert!( clipped_primitives.is_empty(), "There should be nothing to show, has at least one primitive with clip_rect: {:?}", diff --git a/crates/egui_glow/examples/pure_glow.rs b/crates/egui_glow/examples/pure_glow.rs index 008c3dad3..6392cd302 100644 --- a/crates/egui_glow/examples/pure_glow.rs +++ b/crates/egui_glow/examples/pure_glow.rs @@ -17,7 +17,7 @@ impl GlutinWindowContext { // refactor this function to use `glutin-winit` crate eventually. // preferably add android support at the same time. #[allow(unsafe_code)] - unsafe fn new(event_loop: &winit::event_loop::EventLoopWindowTarget<()>) -> Self { + unsafe fn new(event_loop: &winit::event_loop::EventLoopWindowTarget) -> Self { use egui::NumExt; use glutin::context::NotCurrentGlContextSurfaceAccessor; use glutin::display::GetGlDisplay; @@ -142,20 +142,37 @@ impl GlutinWindowContext { } } +#[derive(Debug)] +pub enum UserEvent { + Redraw(std::time::Duration), +} + fn main() { let mut clear_color = [0.1, 0.1, 0.1]; - let event_loop = winit::event_loop::EventLoopBuilder::with_user_event().build(); + let event_loop = winit::event_loop::EventLoopBuilder::::with_user_event().build(); let (gl_window, gl) = create_display(&event_loop); let gl = std::sync::Arc::new(gl); - let mut egui_glow = egui_glow::EguiGlow::new(&event_loop, gl.clone(), None); + let mut egui_glow = egui_glow::EguiGlow::new(&event_loop, gl.clone(), None, None); + + let event_loop_proxy = egui::mutex::Mutex::new(event_loop.create_proxy()); + egui_glow + .egui_ctx + .set_request_repaint_callback(move |info| { + event_loop_proxy + .lock() + .send_event(UserEvent::Redraw(info.delay)) + .expect("Cannot send event"); + }); + + let mut repaint_delay = std::time::Duration::MAX; event_loop.run(move |event, _, control_flow| { let mut redraw = || { let mut quit = false; - let repaint_after = egui_glow.run(gl_window.window(), |egui_ctx| { + egui_glow.run(gl_window.window(), |egui_ctx| { egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| { ui.heading("Hello World!"); if ui.button("Quit").clicked() { @@ -167,13 +184,13 @@ fn main() { *control_flow = if quit { winit::event_loop::ControlFlow::Exit - } else if repaint_after.is_zero() { + } else if repaint_delay.is_zero() { gl_window.window().request_redraw(); winit::event_loop::ControlFlow::Poll - } else if let Some(repaint_after_instant) = - std::time::Instant::now().checked_add(repaint_after) + } else if let Some(repaint_delay_instant) = + std::time::Instant::now().checked_add(repaint_delay) { - winit::event_loop::ControlFlow::WaitUntil(repaint_after_instant) + winit::event_loop::ControlFlow::WaitUntil(repaint_delay_instant) } else { winit::event_loop::ControlFlow::Wait }; @@ -224,6 +241,10 @@ fn main() { gl_window.window().request_redraw(); } } + + winit::event::Event::UserEvent(UserEvent::Redraw(delay)) => { + repaint_delay = delay; + } winit::event::Event::LoopDestroyed => { egui_glow.destroy(); } @@ -239,7 +260,7 @@ fn main() { } fn create_display( - event_loop: &winit::event_loop::EventLoopWindowTarget<()>, + event_loop: &winit::event_loop::EventLoopWindowTarget, ) -> (GlutinWindowContext, glow::Context) { let glutin_window_context = unsafe { GlutinWindowContext::new(event_loop) }; let gl = unsafe { diff --git a/crates/egui_glow/src/lib.rs b/crates/egui_glow/src/lib.rs index e53819523..b12d2ff5f 100644 --- a/crates/egui_glow/src/lib.rs +++ b/crates/egui_glow/src/lib.rs @@ -13,7 +13,7 @@ pub mod painter; pub use glow; -pub use painter::{CallbackFn, Painter}; +pub use painter::{CallbackFn, Painter, PainterError}; mod misc_util; mod shader_version; mod vao; diff --git a/crates/egui_glow/src/painter.rs b/crates/egui_glow/src/painter.rs index c04dd927e..2b1034010 100644 --- a/crates/egui_glow/src/painter.rs +++ b/crates/egui_glow/src/painter.rs @@ -34,6 +34,24 @@ impl TextureFilterExt for egui::TextureFilter { } } +#[derive(Debug)] +pub struct PainterError(String); + +impl std::error::Error for PainterError {} + +impl std::fmt::Display for PainterError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "OpenGL: {}", self.0) + } +} + +impl From for PainterError { + #[inline] + fn from(value: String) -> Self { + Self(value) + } +} + /// An OpenGL painter using [`glow`]. /// /// This is responsible for painting egui and managing egui textures. @@ -103,7 +121,7 @@ impl Painter { gl: Arc, shader_prefix: &str, shader_version: Option, - ) -> Result { + ) -> Result { crate::profile_function!(); crate::check_for_gl_error_even_in_release!(&gl, "before Painter::new"); @@ -121,7 +139,7 @@ impl Painter { if gl.version().major < 2 { // this checks on desktop that we are not using opengl 1.1 microsoft sw rendering context. // ShaderVersion::get fn will segfault due to SHADING_LANGUAGE_VERSION (added in gl2.0) - return Err("egui_glow requires opengl 2.0+. ".to_owned()); + return Err(PainterError("egui_glow requires opengl 2.0+. ".to_owned())); } let max_texture_side = unsafe { gl.get_parameter_i32(glow::MAX_TEXTURE_SIZE) } as usize; @@ -305,6 +323,10 @@ impl Painter { (width_in_pixels, height_in_pixels) } + pub fn clear(&self, screen_size_in_pixels: [u32; 2], clear_color: [f32; 4]) { + clear(&self.gl, screen_size_in_pixels, clear_color); + } + /// You are expected to have cleared the color buffer before calling this. pub fn paint_and_update_textures( &mut self, @@ -314,6 +336,7 @@ impl Painter { textures_delta: &egui::TexturesDelta, ) { crate::profile_function!(); + for (id, image_delta) in &textures_delta.set { self.set_texture(*id, image_delta); } @@ -621,6 +644,8 @@ impl Painter { } pub fn read_screen_rgba(&self, [w, h]: [u32; 2]) -> egui::ColorImage { + crate::profile_function!(); + let mut pixels = vec![0_u8; (w * h * 4) as usize]; unsafe { self.gl.read_pixels( @@ -644,6 +669,8 @@ impl Painter { } pub fn read_screen_rgb(&self, [w, h]: [u32; 2]) -> Vec { + crate::profile_function!(); + let mut pixels = vec![0_u8; (w * h * 3) as usize]; unsafe { self.gl.read_pixels( diff --git a/crates/egui_glow/src/winit.rs b/crates/egui_glow/src/winit.rs index 3185c7d03..ad47d6ebc 100644 --- a/crates/egui_glow/src/winit.rs +++ b/crates/egui_glow/src/winit.rs @@ -1,15 +1,20 @@ -use crate::shader_version::ShaderVersion; pub use egui_winit; -use egui_winit::winit; pub use egui_winit::EventResponse; +use egui::{ViewportId, ViewportIdPair, ViewportOutput}; +use egui_winit::winit; + +use crate::shader_version::ShaderVersion; + /// Use [`egui`] from a [`glow`] app based on [`winit`]. pub struct EguiGlow { pub egui_ctx: egui::Context, pub egui_winit: egui_winit::State, pub painter: crate::Painter, + // output from the last update: shapes: Vec, + pixels_per_point: f32, textures_delta: egui::TexturesDelta, } @@ -19,6 +24,7 @@ impl EguiGlow { event_loop: &winit::event_loop::EventLoopWindowTarget, gl: std::sync::Arc, shader_version: Option, + native_pixels_per_point: Option, ) -> Self { let painter = crate::Painter::new(gl, "", shader_version) .map_err(|err| { @@ -26,11 +32,19 @@ impl EguiGlow { }) .unwrap(); + let egui_winit = egui_winit::State::new( + event_loop, + native_pixels_per_point, + Some(painter.max_texture_side()), + ); + let pixels_per_point = egui_winit.pixels_per_point(); + Self { egui_ctx: Default::default(), - egui_winit: egui_winit::State::new(event_loop), + egui_winit, painter, shapes: Default::default(), + pixels_per_point, textures_delta: Default::default(), } } @@ -39,28 +53,37 @@ impl EguiGlow { self.egui_winit.on_event(&self.egui_ctx, event) } - /// Returns the `Duration` of the timeout after which egui should be repainted even if there's no new events. - /// /// Call [`Self::paint`] later to paint. - pub fn run( - &mut self, - window: &winit::window::Window, - run_ui: impl FnMut(&egui::Context), - ) -> std::time::Duration { - let raw_input = self.egui_winit.take_egui_input(window); + pub fn run(&mut self, window: &winit::window::Window, run_ui: impl FnMut(&egui::Context)) { + let raw_input = self + .egui_winit + .take_egui_input(window, ViewportIdPair::ROOT); + let egui::FullOutput { platform_output, - repaint_after, textures_delta, shapes, + pixels_per_point, + viewport_output, } = self.egui_ctx.run(raw_input, run_ui); - self.egui_winit - .handle_platform_output(window, &self.egui_ctx, platform_output); + if viewport_output.len() > 1 { + log::warn!("Multiple viewports not yet supported by EguiGlow"); + } + for (_, ViewportOutput { commands, .. }) in viewport_output { + egui_winit::process_viewport_commands(commands, window, true); + } + + self.egui_winit.handle_platform_output( + window, + ViewportId::ROOT, + &self.egui_ctx, + platform_output, + ); self.shapes = shapes; + self.pixels_per_point = pixels_per_point; self.textures_delta.append(textures_delta); - repaint_after } /// Paint the results of the last call to [`Self::run`]. @@ -72,13 +95,11 @@ impl EguiGlow { self.painter.set_texture(id, &image_delta); } - let clipped_primitives = self.egui_ctx.tessellate(shapes); + let pixels_per_point = self.pixels_per_point; + let clipped_primitives = self.egui_ctx.tessellate(shapes, pixels_per_point); let dimensions: [u32; 2] = window.inner_size().into(); - self.painter.paint_primitives( - dimensions, - self.egui_ctx.pixels_per_point(), - &clipped_primitives, - ); + self.painter + .paint_primitives(dimensions, pixels_per_point, &clipped_primitives); for id in textures_delta.free.drain(..) { self.painter.free_texture(id); diff --git a/crates/epaint/src/image.rs b/crates/epaint/src/image.rs index 0a40fe4a0..446f3fcce 100644 --- a/crates/epaint/src/image.rs +++ b/crates/epaint/src/image.rs @@ -238,6 +238,15 @@ impl From> for ImageData { } } +impl std::fmt::Debug for ColorImage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ColorImage") + .field("size", &self.size) + .field("pixel-count", &self.pixels.len()) + .finish_non_exhaustive() + } +} + // ---------------------------------------------------------------------------- /// A single-channel image designed for the font texture. diff --git a/examples/multiple_viewports/Cargo.toml b/examples/multiple_viewports/Cargo.toml new file mode 100644 index 000000000..fb0fa3371 --- /dev/null +++ b/examples/multiple_viewports/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "multiple_viewports" +version = "0.1.0" +authors = ["Emil Ernerfeldt "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.70" +publish = false + + +[dependencies] +eframe = { path = "../../crates/eframe", features = [ + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } +env_logger = "0.10" diff --git a/examples/multiple_viewports/README.md b/examples/multiple_viewports/README.md new file mode 100644 index 000000000..1ce0a2b98 --- /dev/null +++ b/examples/multiple_viewports/README.md @@ -0,0 +1,7 @@ +Example how to show multiple viewports (native windows) can be created in `egui` when using the `eframe` backend. + +```sh +cargo run -p multiple_viewports +``` + +For a more advanced example, see [../test_viewports]. diff --git a/examples/multiple_viewports/src/main.rs b/examples/multiple_viewports/src/main.rs new file mode 100644 index 000000000..d49125bcc --- /dev/null +++ b/examples/multiple_viewports/src/main.rs @@ -0,0 +1,102 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release + +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +use eframe::egui; + +fn main() -> Result<(), eframe::Error> { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let options = eframe::NativeOptions { + initial_window_size: Some(egui::vec2(320.0, 240.0)), + ..Default::default() + }; + eframe::run_native( + "Multiple viewports", + options, + Box::new(|_cc| Box::::default()), + ) +} + +#[derive(Default)] +struct MyApp { + /// Immediate viewports are show immediately, so passing state to/from them is easy. + /// The downside is that their painting is linked with the parent viewport: + /// if either needs repainting, they are both repainted. + show_immediate_viewport: bool, + + /// Deferred viewports run independent of the parent viewport, which can save + /// CPU if only some of the viewports require repainting. + /// However, this requires passing state with `Arc` and locks. + show_deferred_viewport: Arc, +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.label("Hello from the root viewport"); + + ui.checkbox( + &mut self.show_immediate_viewport, + "Show immediate child viewport", + ); + + let mut show_deferred_viewport = self.show_deferred_viewport.load(Ordering::Relaxed); + ui.checkbox(&mut show_deferred_viewport, "Show deferred child viewport"); + self.show_deferred_viewport + .store(show_deferred_viewport, Ordering::Relaxed); + }); + + if self.show_immediate_viewport { + ctx.show_viewport_immediate( + egui::ViewportId::from_hash_of("immediate_viewport"), + egui::ViewportBuilder::default() + .with_title("Immediate Viewport") + .with_inner_size([200.0, 100.0]), + |ctx, class| { + assert!( + class == egui::ViewportClass::Immediate, + "This egui backend doesn't support multiple viewports" + ); + + egui::CentralPanel::default().show(ctx, |ui| { + ui.label("Hello from immediate viewport"); + }); + + if ctx.input(|i| i.raw.viewport.close_requested) { + // Tell parent viewport that we should not show next frame: + self.show_immediate_viewport = false; + ctx.request_repaint(); // make sure there is a next frame + } + }, + ); + } + + if self.show_deferred_viewport.load(Ordering::Relaxed) { + let show_deferred_viewport = self.show_deferred_viewport.clone(); + ctx.show_viewport_immediate( + egui::ViewportId::from_hash_of("deferred_viewport"), + egui::ViewportBuilder::default() + .with_title("Deferred Viewport") + .with_inner_size([200.0, 100.0]), + |ctx, class| { + assert!( + class == egui::ViewportClass::Deferred, + "This egui backend doesn't support multiple viewports" + ); + + egui::CentralPanel::default().show(ctx, |ui| { + ui.label("Hello from deferred viewport"); + }); + if ctx.input(|i| i.raw.viewport.close_requested) { + // Tell parent to close us. + show_deferred_viewport.store(false, Ordering::Relaxed); + ctx.request_repaint(); // make sure there is a next frame + } + }, + ); + } + } +} diff --git a/examples/test_viewports/Cargo.toml b/examples/test_viewports/Cargo.toml new file mode 100644 index 000000000..a7c760699 --- /dev/null +++ b/examples/test_viewports/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "test_viewports" +version = "0.1.0" +authors = ["konkitoman"] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.70" +publish = false + +[features] +wgpu = ["eframe/wgpu"] + +[dependencies] +eframe = { path = "../../crates/eframe", features = [ + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } +env_logger = "0.10" diff --git a/examples/test_viewports/README.md b/examples/test_viewports/README.md new file mode 100644 index 000000000..9e7b4fa6c --- /dev/null +++ b/examples/test_viewports/README.md @@ -0,0 +1,3 @@ +This is a test of the viewports feature of eframe and egui, where we show off using multiple windows. + +For a simple example, see [../multiple_viewports]. diff --git a/examples/test_viewports/src/main.rs b/examples/test_viewports/src/main.rs new file mode 100644 index 000000000..99908cd90 --- /dev/null +++ b/examples/test_viewports/src/main.rs @@ -0,0 +1,476 @@ +use std::sync::Arc; + +use eframe::egui; +use egui::{mutex::RwLock, Id, InnerResponse, ViewportBuilder, ViewportId}; + +// Drag-and-drop between windows is not yet implemented, but if you wanna work on it, enable this: +pub const DRAG_AND_DROP_TEST: bool = false; + +fn main() { + env_logger::init(); // Use `RUST_LOG=debug` to see logs. + + let _ = eframe::run_native( + "Viewports", + eframe::NativeOptions { + #[cfg(feature = "wgpu")] + renderer: eframe::Renderer::Wgpu, + + initial_window_size: Some(egui::Vec2::new(450.0, 400.0)), + ..Default::default() + }, + Box::new(|_| Box::::default()), + ); +} + +pub struct ViewportState { + pub id: ViewportId, + pub visible: bool, + pub immediate: bool, + pub title: String, + pub children: Vec>>, +} + +impl ViewportState { + pub fn new_deferred( + title: &'static str, + children: Vec>>, + ) -> Arc> { + Arc::new(RwLock::new(Self { + id: ViewportId::from_hash_of(title), + visible: false, + immediate: false, + title: title.into(), + children, + })) + } + + pub fn new_immediate( + title: &'static str, + children: Vec>>, + ) -> Arc> { + Arc::new(RwLock::new(Self { + id: ViewportId::from_hash_of(title), + visible: false, + immediate: true, + title: title.into(), + children, + })) + } + + pub fn show(vp_state: Arc>, ctx: &egui::Context) { + if !vp_state.read().visible { + return; + } + let vp_id = vp_state.read().id; + let immediate = vp_state.read().immediate; + let title = vp_state.read().title.clone(); + + let viewport = ViewportBuilder::default() + .with_title(&title) + .with_inner_size([450.0, 400.0]); + + if immediate { + let mut vp_state = vp_state.write(); + ctx.show_viewport_immediate(vp_id, viewport, move |ctx, class| { + show_as_popup(ctx, class, &title, vp_id.into(), |ui: &mut egui::Ui| { + generic_child_ui(ui, &mut vp_state); + }); + }); + } else { + let count = Arc::new(RwLock::new(0)); + ctx.show_viewport(vp_id, viewport, move |ctx, class| { + let mut vp_state = vp_state.write(); + let count = count.clone(); + show_as_popup( + ctx, + class, + &title, + vp_id.into(), + move |ui: &mut egui::Ui| { + let current_count = *count.read(); + ui.label(format!("Callback has been reused {current_count} times")); + *count.write() += 1; + + generic_child_ui(ui, &mut vp_state); + }, + ); + }); + } + } + + pub fn set_visible_recursive(&mut self, visible: bool) { + self.visible = visible; + for child in &self.children { + child.write().set_visible_recursive(true); + } + } +} + +pub struct App { + top: Vec>>, +} + +impl Default for App { + fn default() -> Self { + Self { + top: vec![ + ViewportState::new_deferred( + "Top Deferred Viewport", + vec![ + ViewportState::new_deferred( + "DD: Deferred Viewport in Deferred Viewport", + vec![], + ), + ViewportState::new_immediate( + "DS: Immediate Viewport in Deferred Viewport", + vec![], + ), + ], + ), + ViewportState::new_immediate( + "Top Immediate Viewport", + vec![ + ViewportState::new_deferred( + "SD: Deferred Viewport in Immediate Viewport", + vec![], + ), + ViewportState::new_immediate( + "SS: Immediate Viewport in Immediate Viewport", + vec![], + ), + ], + ), + ], + } + } +} + +impl eframe::App for App { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("Root viewport"); + { + let mut embed_viewports = ctx.embed_viewports(); + ui.checkbox(&mut embed_viewports, "Embed all viewports"); + if ui.button("Open all viewports").clicked() { + for viewport in &self.top { + viewport.write().set_visible_recursive(true); + } + } + ctx.set_embed_viewports(embed_viewports); + } + + generic_ui(ui, &self.top); + }); + } +} + +/// This will make the content as a popup if cannot has his own native window +fn show_as_popup( + ctx: &egui::Context, + class: egui::ViewportClass, + title: &str, + id: Id, + content: impl FnOnce(&mut egui::Ui), +) { + if class == egui::ViewportClass::Embedded { + // Not a real viewport + egui::Window::new(title).id(id).show(ctx, content); + } else { + egui::CentralPanel::default().show(ctx, |ui| ui.push_id(id, content)); + } +} + +fn generic_child_ui(ui: &mut egui::Ui, vp_state: &mut ViewportState) { + ui.horizontal(|ui| { + ui.label("Title:"); + if ui.text_edit_singleline(&mut vp_state.title).changed() { + // Title changes happen at the parent level: + ui.ctx().request_repaint_of(ui.ctx().parent_viewport_id()); + } + }); + + generic_ui(ui, &vp_state.children); +} + +fn generic_ui(ui: &mut egui::Ui, children: &[Arc>]) { + let container_id = ui.id(); + + let ctx = ui.ctx().clone(); + ui.label(format!( + "Frame nr: {} (this increases when this viewport is being rendered)", + ctx.frame_nr() + )); + ui.horizontal(|ui| { + let mut show_spinner = + ui.data_mut(|data| *data.get_temp_mut_or(container_id.with("show_spinner"), false)); + ui.checkbox(&mut show_spinner, "Show Spinner (forces repaint)"); + if show_spinner { + ui.spinner(); + } + ui.data_mut(|data| data.insert_temp(container_id.with("show_spinner"), show_spinner)); + }); + + ui.add_space(8.0); + + ui.label(format!("Viewport Id: {:?}", ctx.viewport_id())); + ui.label(format!( + "Parent Viewport Id: {:?}", + ctx.parent_viewport_id() + )); + + ui.add_space(8.0); + + if let Some(inner_rect) = ctx.input(|i| i.raw.viewport.inner_rect_px) { + ui.label(format!( + "Inner Rect: Pos: {:?}, Size: {:?}", + inner_rect.min, + inner_rect.size() + )); + } + if let Some(outer_rect) = ctx.input(|i| i.raw.viewport.outer_rect_px) { + ui.label(format!( + "Outer Rect: Pos: {:?}, Size: {:?}", + outer_rect.min, + outer_rect.size() + )); + } + + let tmp_pixels_per_point = ctx.pixels_per_point(); + let mut pixels_per_point = ui.data_mut(|data| { + *data.get_temp_mut_or(container_id.with("pixels_per_point"), tmp_pixels_per_point) + }); + let res = ui.add( + egui::DragValue::new(&mut pixels_per_point) + .prefix("Pixels per Point: ") + .speed(0.1) + .clamp_range(0.5..=4.0), + ); + if res.drag_released() { + ctx.set_pixels_per_point(pixels_per_point); + } + if res.dragged() { + ui.data_mut(|data| { + data.insert_temp(container_id.with("pixels_per_point"), pixels_per_point); + }); + } else { + ui.data_mut(|data| { + data.insert_temp(container_id.with("pixels_per_point"), tmp_pixels_per_point); + }); + } + egui::gui_zoom::zoom_with_keyboard_shortcuts(&ctx, None); + + if ctx.viewport_id() != ctx.parent_viewport_id() { + let parent = ctx.parent_viewport_id(); + if ui.button("Set parent pos 0,0").clicked() { + ctx.send_viewport_command_to( + parent, + egui::ViewportCommand::OuterPosition(egui::pos2(0.0, 0.0)), + ); + } + } + + if DRAG_AND_DROP_TEST { + drag_and_drop_test(ui); + } + + if !children.is_empty() { + ui.separator(); + + ui.heading("Children:"); + + for child in children { + let visible = { + let mut child_lock = child.write(); + let ViewportState { visible, title, .. } = &mut *child_lock; + ui.checkbox(visible, title.as_str()); + *visible + }; + if visible { + ViewportState::show(child.clone(), &ctx); + } + } + } +} + +// ---------------------------------------------------------------------------- +// Drag-and-drop between windows is not yet implemented, but there is some test code for it here: + +fn drag_and_drop_test(ui: &mut egui::Ui) { + use std::collections::HashMap; + use std::sync::OnceLock; + + let container_id = ui.id(); + + const COLS: usize = 2; + static DATA: OnceLock> = OnceLock::new(); + let data = DATA.get_or_init(Default::default); + data.write().init(container_id); + + #[derive(Default)] + struct DragAndDrop { + containers_data: HashMap>>, + data: HashMap, + counter: usize, + is_dragged: Option, + } + + impl DragAndDrop { + fn init(&mut self, container: Id) { + if !self.containers_data.contains_key(&container) { + for i in 0..COLS { + self.insert( + container, + i, + format!("From: {container:?}, and is: {}", self.counter), + ); + } + } + } + + fn insert(&mut self, container: Id, col: usize, value: impl Into) { + assert!(col <= COLS, "The coll should be less then: {COLS}"); + + let value: String = value.into(); + let id = Id::new(format!("%{}% {}", self.counter, &value)); + self.data.insert(id, value); + let viewport_data = self.containers_data.entry(container).or_insert_with(|| { + let mut res = Vec::new(); + res.resize_with(COLS, Default::default); + res + }); + self.counter += 1; + + viewport_data[col].push(id); + } + + fn cols(&self, container: Id, col: usize) -> Vec<(Id, String)> { + assert!(col <= COLS, "The col should be less then: {COLS}"); + let container_data = &self.containers_data[&container]; + container_data[col] + .iter() + .map(|id| (*id, self.data[id].clone())) + .collect() + } + + /// Move element ID to Viewport and col + fn mov(&mut self, to: Id, col: usize) { + let Some(id) = self.is_dragged.take() else { + return; + }; + assert!(col <= COLS, "The col should be less then: {COLS}"); + + // Should be a better way to do this! + for container_data in self.containers_data.values_mut() { + for ids in container_data { + ids.retain(|i| *i != id); + } + } + + if let Some(container_data) = self.containers_data.get_mut(&to) { + container_data[col].push(id); + } + } + + fn dragging(&mut self, id: Id) { + self.is_dragged = Some(id); + } + } + + ui.separator(); + ui.label("Drag and drop:"); + ui.columns(COLS, |ui| { + for col in 0..COLS { + let data = DATA.get().unwrap(); + let ui = &mut ui[col]; + let mut is_dragged = None; + let res = drop_target(ui, |ui| { + ui.set_min_height(60.0); + for (id, value) in data.read().cols(container_id, col) { + drag_source(ui, id, |ui| { + ui.add(egui::Label::new(value).sense(egui::Sense::click())); + if ui.memory(|mem| mem.is_being_dragged(id)) { + is_dragged = Some(id); + } + }); + } + }); + if let Some(id) = is_dragged { + data.write().dragging(id); + } + if res.response.hovered() && ui.input(|i| i.pointer.any_released()) { + data.write().mov(container_id, col); + } + } + }); +} + +// This is taken from crates/egui_demo_lib/src/debo/drag_and_drop.rs +fn drag_source( + ui: &mut egui::Ui, + id: egui::Id, + body: impl FnOnce(&mut egui::Ui) -> R, +) -> InnerResponse { + let is_being_dragged = ui.memory(|mem| mem.is_being_dragged(id)); + + if !is_being_dragged { + let res = ui.scope(body); + + // Check for drags: + let response = ui.interact(res.response.rect, id, egui::Sense::drag()); + if response.hovered() { + ui.ctx().set_cursor_icon(egui::CursorIcon::Grab); + } + res + } else { + ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing); + + // Paint the body to a new layer: + let layer_id = egui::LayerId::new(egui::Order::Tooltip, id); + let res = ui.with_layer_id(layer_id, body); + + if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { + let delta = pointer_pos - res.response.rect.center(); + ui.ctx().translate_layer(layer_id, delta); + } + + res + } +} + +// This is taken from crates/egui_demo_lib/src/debo/drag_and_drop.rs +fn drop_target( + ui: &mut egui::Ui, + body: impl FnOnce(&mut egui::Ui) -> R, +) -> egui::InnerResponse { + let is_being_dragged = ui.memory(|mem| mem.is_anything_being_dragged()); + + let margin = egui::Vec2::splat(ui.visuals().clip_rect_margin); // 3.0 + + let background_id = ui.painter().add(egui::Shape::Noop); + + let available_rect = ui.available_rect_before_wrap(); + let inner_rect = available_rect.shrink2(margin); + let mut content_ui = ui.child_ui(inner_rect, *ui.layout()); + let ret = body(&mut content_ui); + + let outer_rect = + egui::Rect::from_min_max(available_rect.min, content_ui.min_rect().max + margin); + let (rect, response) = ui.allocate_at_least(outer_rect.size(), egui::Sense::hover()); + + let style = if is_being_dragged && response.hovered() { + ui.visuals().widgets.active + } else { + ui.visuals().widgets.inactive + }; + + let fill = style.bg_fill; + let stroke = style.bg_stroke; + + ui.painter().set( + background_id, + egui::epaint::RectShape::new(rect, style.rounding, fill, stroke), + ); + + egui::InnerResponse::new(ret, response) +} diff --git a/scripts/check.sh b/scripts/check.sh index f0491f0d2..4ceb47d49 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -37,7 +37,7 @@ cargo doc --document-private-items --no-deps --all-features (cd crates/egui_extras && cargo check --no-default-features) (cd crates/egui_glow && cargo check --no-default-features) (cd crates/egui-winit && cargo check --no-default-features --features "wayland") -(cd crates/egui-winit && cargo check --no-default-features --features "winit/x11") +(cd crates/egui-winit && cargo check --no-default-features --features "x11") (cd crates/emath && cargo check --no-default-features) (cd crates/epaint && cargo check --no-default-features --release) (cd crates/epaint && cargo check --no-default-features)