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)