From 26f0c2a222f706db944cf764a17b870d2528ef77 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 16 Nov 2023 09:09:41 +0100 Subject: [PATCH] Classify viewports using `enum ViewportClass` --- crates/eframe/src/native/epi_integration.rs | 4 +- crates/eframe/src/native/run.rs | 46 +++++++++++++++---- crates/egui/src/context.rs | 49 ++++++++++++++------- crates/egui/src/viewport.rs | 43 +++++++++++++++++- examples/multiple_viewports/src/main.rs | 17 +++++-- examples/test_viewports/src/main.rs | 35 ++++++++++----- 6 files changed, 151 insertions(+), 43 deletions(-) diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index d664e95f0..a984258e8 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -4,7 +4,7 @@ use winit::event_loop::EventLoopWindowTarget; use raw_window_handle::{HasRawDisplayHandle as _, HasRawWindowHandle as _}; -use egui::{NumExt as _, ViewportBuilder, ViewportId, ViewportIdPair, ViewportUiCallback}; +use egui::{DeferredViewportUiCallback, NumExt as _, ViewportBuilder, ViewportId, ViewportIdPair}; use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings}; use crate::{epi, Theme, WindowInfo}; @@ -501,7 +501,7 @@ impl EpiIntegration { pub fn update( &mut self, app: &mut dyn epi::App, - viewport_ui_cb: Option<&ViewportUiCallback>, + viewport_ui_cb: Option<&DeferredViewportUiCallback>, mut raw_input: egui::RawInput, ) -> egui::FullOutput { raw_input.time = Some(self.beginning.elapsed().as_secs_f64()); diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 93412341a..3b67a21a1 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -457,8 +457,8 @@ mod glow_integration { }; use egui::{ - epaint::ahash::HashMap, ImmediateViewport, NumExt as _, ViewportIdMap, ViewportIdPair, - ViewportIdSet, ViewportOutput, ViewportUiCallback, + epaint::ahash::HashMap, DeferredViewportUiCallback, ImmediateViewport, NumExt as _, + ViewportClass, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportOutput, }; use egui_winit::{create_winit_window_builder, process_viewport_commands, EventResponse}; @@ -781,11 +781,12 @@ mod glow_integration { struct Viewport { ids: ViewportIdPair, + class: ViewportClass, builder: ViewportBuilder, /// The user-callback that shows the ui. /// None for immediate viewports. - viewport_ui_cb: Option>, + viewport_ui_cb: Option>, gl_surface: Option>, window: Option>, @@ -944,6 +945,7 @@ mod glow_integration { ViewportId::ROOT, Viewport { ids: ViewportIdPair::ROOT, + class: ViewportClass::Root, builder: viewport_builder, viewport_ui_cb: None, gl_surface: None, @@ -1166,6 +1168,7 @@ mod glow_integration { viewport_id, ViewportOutput { parent, + class, builder, viewport_ui_cb, commands, @@ -1178,6 +1181,7 @@ mod glow_integration { initialize_or_update_viewport( &mut self.viewports, ids, + class, builder, viewport_ui_cb, focused_viewport, @@ -1208,6 +1212,7 @@ mod glow_integration { fn initialize_or_update_viewport( viewports: &mut ViewportIdMap, ids: ViewportIdPair, + class: ViewportClass, mut builder: ViewportBuilder, viewport_ui_cb: Option>, focused_viewport: Option, @@ -1227,6 +1232,7 @@ mod glow_integration { log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title); entry.insert(Viewport { ids, + class, builder, viewport_ui_cb, window: None, @@ -1240,6 +1246,7 @@ mod glow_integration { 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); @@ -1519,8 +1526,14 @@ mod glow_integration { { let mut glutin = glutin.borrow_mut(); - let viewport = - initialize_or_update_viewport(&mut glutin.viewports, ids, builder, None, None); + let viewport = initialize_or_update_viewport( + &mut glutin.viewports, + ids, + ViewportClass::Immediate, + builder, + None, + None, + ); if viewport.gl_surface.is_none() { glutin.init_viewport(ids.this, event_loop).expect( @@ -1799,8 +1812,8 @@ mod wgpu_integration { use parking_lot::Mutex; use egui::{ - FullOutput, ImmediateViewport, ViewportIdMap, ViewportIdPair, ViewportIdSet, - ViewportOutput, ViewportUiCallback, + DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportClass, ViewportIdMap, + ViewportIdPair, ViewportIdSet, ViewportOutput, }; use egui_winit::{create_winit_window_builder, process_viewport_commands}; @@ -1809,10 +1822,12 @@ mod wgpu_integration { pub struct Viewport { ids: ViewportIdPair, + class: ViewportClass, + builder: ViewportBuilder, /// `None` for sync viewports. - viewport_ui_cb: Option>, + 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. @@ -2049,6 +2064,7 @@ mod wgpu_integration { ViewportId::ROOT, Viewport { ids: ViewportIdPair::ROOT, + class: ViewportClass::Root, builder, viewport_ui_cb: None, window: Some(Rc::new(window)), @@ -2139,7 +2155,14 @@ mod wgpu_integration { painter, viewport_from_window, } = &mut *shared.borrow_mut(); - let viewport = initialize_or_update_viewport(viewports, ids, builder, None, None); + 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); @@ -2676,6 +2699,7 @@ mod wgpu_integration { viewport_id, ViewportOutput { parent, + class, builder, viewport_ui_cb, commands, @@ -2688,6 +2712,7 @@ mod wgpu_integration { initialize_or_update_viewport( viewports, ids, + class, builder, viewport_ui_cb, focused_viewport, @@ -2706,6 +2731,7 @@ mod wgpu_integration { fn initialize_or_update_viewport( viewports: &mut Viewports, ids: ViewportIdPair, + class: ViewportClass, mut builder: ViewportBuilder, viewport_ui_cb: Option>, focused_viewport: Option, @@ -2723,6 +2749,7 @@ mod wgpu_integration { log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title); entry.insert(Viewport { ids, + class, builder, viewport_ui_cb, window: None, @@ -2734,6 +2761,7 @@ mod wgpu_integration { // Patch an existing viewport: let viewport = entry.get_mut(); + viewport.class = class; viewport.ids.parent = ids.parent; viewport.viewport_ui_cb = viewport_ui_cb; diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 9bded6e16..4babd81c7 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -16,6 +16,7 @@ use crate::{ os::OperatingSystem, output::FullOutput, util::IdTypeMap, + viewport::ViewportClass, TextureHandle, ViewportCommand, *, }; @@ -114,13 +115,19 @@ impl ContextImpl { /// State stored per viewport #[derive(Default)] struct ViewportState { + /// The type of viewport. + /// + /// 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>, + viewport_ui_cb: Option>, input: InputState, @@ -1609,6 +1616,7 @@ impl ContextImpl { id, ViewportOutput { parent, + class: viewport.class, builder: viewport.builder.clone(), viewport_ui_cb: viewport.viewport_ui_cb.clone(), commands, @@ -2577,68 +2585,75 @@ impl Context { /// the parent viewport (the caller) to repaint anytime the child is repainted, /// and vice versa. /// - /// If you use a [`crate::CentralPanel`] you need to check if the viewport is a new window like: - /// `ctx.viewport_id() != ctx.parent_viewport_id` if false you should create a [`crate::Window`]. + /// 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. pub fn show_viewport( &self, new_viewport_id: ViewportId, viewport_builder: ViewportBuilder, - viewport_ui_cb: impl Fn(&Context) + Send + Sync + 'static, + viewport_ui_cb: impl Fn(&Context, ViewportClass) + Send + Sync + 'static, ) { crate::profile_function!(); if self.embed_viewports() { - viewport_ui_cb(self); + 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(Box::new(viewport_ui_cb))); + 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. - /// This can lead to unnecessary repaint. - /// To avoid this, use [`Self::show_viewport`] instead. /// /// 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. - /// - /// If you use a `egui::CentralPanel` you need to check if the viewport is a new window like: - /// `ctx.viewport_id() != ctx.parent_viewport_id` if false you should create a [`crate::Window`]. + /// 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. pub fn show_viewport_immediate( &self, new_viewport_id: ViewportId, builder: ViewportBuilder, - viewport_ui_cb: impl FnOnce(&Context) -> T, + viewport_ui_cb: impl FnOnce(&Context, ViewportClass) -> T, ) -> T { crate::profile_function!(); if self.embed_viewports() { - return viewport_ui_cb(self); + 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); + return viewport_ui_cb(self, ViewportClass::Embedded); }; let ids = self.write(|ctx| { @@ -2662,7 +2677,9 @@ impl Context { let viewport = ImmediateViewport { ids, builder, - viewport_ui_cb: Box::new(move |context| *out = Some(viewport_ui_cb(context))), + viewport_ui_cb: Box::new(move |context| { + *out = Some(viewport_ui_cb(context, ViewportClass::Immediate)); + }), }; immediate_viewport_renderer(self, viewport); diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index 35de10059..8fbecfa44 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -15,6 +15,37 @@ 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`]. @@ -91,7 +122,7 @@ impl ViewportIdPair { } /// The user-code that shows the ui in the viewport, used for deferred viewports. -pub type ViewportUiCallback = dyn Fn(&Context) + Sync + Send; +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>); @@ -668,13 +699,19 @@ 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>, + pub viewport_ui_cb: Option>, /// Commands to change the viewport, e.g. window title and size. pub commands: Vec, @@ -693,6 +730,7 @@ impl ViewportOutput { pub fn append(&mut self, newer: Self) { let Self { parent, + class, builder, viewport_ui_cb, mut commands, @@ -700,6 +738,7 @@ impl ViewportOutput { } = newer; self.parent = parent; + self.class = class; self.builder.patch(&builder); self.viewport_ui_cb = viewport_ui_cb; self.commands.append(&mut commands); diff --git a/examples/multiple_viewports/src/main.rs b/examples/multiple_viewports/src/main.rs index 794e6ecd6..d49125bcc 100644 --- a/examples/multiple_viewports/src/main.rs +++ b/examples/multiple_viewports/src/main.rs @@ -55,10 +55,16 @@ impl eframe::App for MyApp { egui::ViewportBuilder::default() .with_title("Immediate Viewport") .with_inner_size([200.0, 100.0]), - |ctx| { + |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; @@ -75,12 +81,17 @@ impl eframe::App for MyApp { egui::ViewportBuilder::default() .with_title("Deferred Viewport") .with_inner_size([200.0, 100.0]), - |ctx| { + |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 use + // 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/src/main.rs b/examples/test_viewports/src/main.rs index 5bc690a85..8153509f6 100644 --- a/examples/test_viewports/src/main.rs +++ b/examples/test_viewports/src/main.rs @@ -71,23 +71,29 @@ impl ViewportState { if immediate { let mut vp_state = vp_state.write(); - ctx.show_viewport_immediate(vp_id, viewport, move |ctx| { - show_as_popup(ctx, &title, vp_id.into(), |ui: &mut egui::Ui| { + 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| { + ctx.show_viewport(vp_id, viewport, move |ctx, class| { let mut vp_state = vp_state.write(); let count = count.clone(); - show_as_popup(ctx, &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; + 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); - }); + generic_child_ui(ui, &mut vp_state); + }, + ); }); } } @@ -160,8 +166,15 @@ impl eframe::App for App { } /// This will make the content as a popup if cannot has his own native window -fn show_as_popup(ctx: &egui::Context, title: &str, id: Id, content: impl FnOnce(&mut egui::Ui)) { - if ctx.viewport_id() == ctx.parent_viewport_id() { +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));