diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 78f6ead11..07da39ce7 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2699,6 +2699,9 @@ impl ContextImpl { let viewport_output = self .viewports .iter_mut() + // Hosted viewports are painted by the application itself, so the integration + // must not learn about them - it would create a window for them. + .filter(|(_, viewport)| viewport.class != ViewportClass::Hosted) .map(|(&id, viewport)| { let parent = *self.viewport_parents.entry(id).or_default(); let commands = if is_last { @@ -4071,6 +4074,106 @@ impl Context { }) } + /// Run a pass for a viewport that _you_ render yourself, e.g. into a texture. + /// + /// This is the building block for embedding one egui UI inside another: a scaled, + /// rotated or blurred sub-UI, a minimap, a live preview, or an off-screen capture. + /// + /// The new viewport shares this [`Context`]'s memory, style, fonts and texture atlas, + /// so it will look and behave like the rest of your app, but it gets its own input, + /// its own hit-testing, its own focus, and its own paint list. + /// + /// You may call this from inside another pass (i.e. from your normal UI code); the + /// current pass is suspended and resumed around it. You must _not_ call it from + /// inside a closure that holds a lock on the [`Context`], such as the ones passed to + /// [`Self::memory_mut`] or [`Self::graphics_mut`]. + /// + /// You need to call this each pass in which the viewport should exist. If you skip a + /// pass, the viewport's state (input, focus, hit-test data) is thrown away. + /// + /// The given `input` is used as-is, except that [`RawInput::viewport_id`] is set for + /// you, and the viewport's [`crate::ViewportInfo::parent`] and, if you left it at + /// `None`, its [`crate::ViewportInfo::native_pixels_per_point`] are inherited from the + /// calling viewport. You will usually want to set at least + /// [`RawInput::screen_rect`] and [`RawInput::events`]. + /// + /// Unlike [`Self::show_viewport_immediate`] and [`Self::show_viewport_deferred`], the + /// egui integration is never told that this viewport exists (see + /// [`ViewportClass::Hosted`]), so no window is created for it. In return, _you_ are + /// responsible for everything a backend would normally do: painting + /// [`FullOutput::shapes`], applying [`FullOutput::textures_delta`], and acting on + /// [`FullOutput::platform_output`]. + /// + /// Note that a [`Self::request_repaint`] from inside the viewport only marks _that_ + /// viewport as needing a repaint, so bridge it to the viewport that owns the window: + /// + /// ``` + /// # let ctx = &egui::Context::default(); + /// # let child_id = egui::ViewportId::from_hash_of("child"); + /// let parent_id = ctx.viewport_id(); + /// let (output, ()) = ctx.run_hosted_viewport( + /// child_id, + /// egui::RawInput { + /// screen_rect: Some(egui::Rect::from_min_size( + /// egui::Pos2::ZERO, + /// egui::vec2(320.0, 240.0), + /// )), + /// ..Default::default() + /// }, + /// |ui| { + /// ui.label("I live in my own viewport"); + /// }, + /// ); + /// if ctx.has_requested_repaint_for(&child_id) { + /// ctx.request_repaint_of(parent_id); + /// } + /// # output.drop_without_applying_deltas(); + /// ``` + /// + /// See [`crate::viewport`] for more information about viewports. + #[must_use] + pub fn run_hosted_viewport( + &self, + viewport_id: ViewportId, + mut input: RawInput, + mut ui_fn: impl FnMut(&mut Ui) -> T, + ) -> (FullOutput, T) { + profiling::function_scope!(); + + let parent_id = self.write(|ctx| { + let parent_id = ctx.viewport_id(); + ctx.viewport_parents.insert(viewport_id, parent_id); + + let viewport = ctx.viewports.entry(viewport_id).or_default(); + viewport.class = ViewportClass::Hosted; + viewport.used = true; + viewport.viewport_ui_cb = None; // we run it right here + + parent_id + }); + + input.viewport_id = viewport_id; + let parent_ppp = self.input_for(parent_id, |i| { + i.raw + .viewports + .get(&parent_id) + .and_then(|info| info.native_pixels_per_point) + }); + let info = input.viewports.entry(viewport_id).or_default(); + info.parent = Some(parent_id); + info.native_pixels_per_point = info.native_pixels_per_point.or(parent_ppp); + + // `run_ui` may run the ui function more than once (see `Self::request_discard`), + // so keep the value from the last pass. + let mut out = None; + let full_output = self.run_ui(input, |ui| { + out = Some(ui_fn(ui)); + }); + + let out = out.expect("Bug in egui: the ui function was never called"); + (full_output, out) + } + fn show_embedded_viewport( &self, new_viewport_id: ViewportId, diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index 962b065a3..d1310daaf 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -77,7 +77,7 @@ use epaint::{Pos2, Vec2}; // ---------------------------------------------------------------------------- /// The different types of viewports supported by egui. -#[derive(Clone, Copy, Default, Hash, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum ViewportClass { /// The root viewport; i.e. the original window. @@ -106,6 +106,17 @@ pub enum ViewportClass { /// If you get this, it is because you are already wrapped in a [`crate::Window`] /// inside of the parent viewport. EmbeddedWindow, + + /// A viewport that the application renders itself, e.g. into a texture. + /// + /// The egui integration is never told about these viewports: they are left out of + /// [`crate::FullOutput::viewport_output`], so no window is created for them and no + /// [`ViewportCommand`] is delivered to them. The application that called + /// [`crate::Context::run_hosted_viewport`] owns the viewport completely: it supplies the + /// [`crate::RawInput`], and it paints the resulting shapes wherever it wants. + /// + /// Create these with [`crate::Context::run_hosted_viewport`]. + Hosted, } // ---------------------------------------------------------------------------- diff --git a/crates/egui/tests/hosted_viewport.rs b/crates/egui/tests/hosted_viewport.rs new file mode 100644 index 000000000..ad1c35db5 --- /dev/null +++ b/crates/egui/tests/hosted_viewport.rs @@ -0,0 +1,96 @@ +//! Tests for [`egui::Context::run_hosted_viewport`]. + +use egui::{Context, Event, Pos2, RawInput, Rect, Sense, ViewportClass, ViewportId, pos2, vec2}; + +const CHILD_SIZE: egui::Vec2 = vec2(100.0, 100.0); + +fn input(size: egui::Vec2, events: Vec) -> RawInput { + RawInput { + screen_rect: Some(Rect::from_min_size(Pos2::ZERO, size)), + events, + ..Default::default() + } +} + +/// The child viewport must keep its state (input, hit-test data, focus) between passes. +/// +/// Hovering only works if the previous pass' widget rects survived, so a widget that +/// reports `hovered()` on the second pass proves the state was not thrown away. +#[test] +fn hosted_viewport_keeps_its_state_between_passes() { + let ctx = Context::default(); + let child_id = ViewportId::from_hash_of("child"); + + let mut hovered = Vec::new(); + + for _ in 0..3 { + let parent_output = ctx.run_ui(input(vec2(300.0, 300.0), vec![]), |_ui| { + let (child_output, ()) = ctx.run_hosted_viewport( + child_id, + input(CHILD_SIZE, vec![Event::PointerMoved(pos2(10.0, 10.0))]), + |ui| { + let response = ui.allocate_response(vec2(50.0, 50.0), Sense::click()); + hovered.push(response.hovered()); + }, + ); + child_output.drop_without_applying_deltas(); + }); + parent_output.drop_without_applying_deltas(); + } + + assert_eq!( + hovered, + vec![false, true, true], + "the hosted viewport lost its state between passes" + ); +} + +/// A hosted viewport is the application's business, so the integration must never see it - +/// otherwise a backend like eframe would open a window for it. +#[test] +fn hosted_viewport_is_hidden_from_the_integration() { + let ctx = Context::default(); + let child_id = ViewportId::from_hash_of("child"); + + let parent_output = ctx.run_ui(input(vec2(300.0, 300.0), vec![]), |_ui| { + let (child_output, ()) = + ctx.run_hosted_viewport(child_id, input(CHILD_SIZE, vec![]), |ui| { + ui.label("hello"); + }); + child_output.drop_without_applying_deltas(); + }); + + assert!( + !parent_output.viewport_output.contains_key(&child_id), + "a hosted viewport must be left out of FullOutput::viewport_output" + ); + assert_eq!( + ctx.viewport_for(child_id, |viewport| viewport.class), + ViewportClass::Hosted + ); + + parent_output.drop_without_applying_deltas(); +} + +/// The child gets its own paint list; its shapes must not leak into the parent's. +#[test] +fn hosted_viewport_shapes_are_separate() { + let ctx = Context::default(); + let child_id = ViewportId::from_hash_of("child"); + + let parent_output = ctx.run_ui(input(vec2(300.0, 300.0), vec![]), |ui| { + ui.label("parent"); + let (child_output, ()) = + ctx.run_hosted_viewport(child_id, input(CHILD_SIZE, vec![]), |ui| { + ui.label("child"); + }); + assert!( + !child_output.shapes.is_empty(), + "the child should have painted something" + ); + child_output.drop_without_applying_deltas(); + }); + + assert!(!parent_output.shapes.is_empty()); + parent_output.drop_without_applying_deltas(); +}