mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Add Context::run_hosted_viewport for app-rendered viewports
Adds a way to run a pass for a viewport that the application paints itself, 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 nested-pass machinery already existed for immediate viewports, but two things were missing for an application to use it: * Nothing public sets `ViewportState::used`, so `end_pass` threw the child viewport's state away every pass. Hover, click and drag could never work. * The child appeared in `FullOutput::viewport_output`, so an integration like eframe would open a native window for it. `ViewportClass::Hosted` marks these viewports and keeps them out of `viewport_output`, so the integration never learns they exist. In return the application takes on what a backend normally does: supply the `RawInput`, paint the shapes, apply the texture delta, and act on the platform output. Also derives `Debug` for `ViewportClass`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<T>(
|
||||
&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<T>(
|
||||
&self,
|
||||
new_viewport_id: ViewportId,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
96
crates/egui/tests/hosted_viewport.rs
Normal file
96
crates/egui/tests/hosted_viewport.rs
Normal file
@@ -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<Event>) -> 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();
|
||||
}
|
||||
Reference in New Issue
Block a user