mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 21:30: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
|
let viewport_output = self
|
||||||
.viewports
|
.viewports
|
||||||
.iter_mut()
|
.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)| {
|
.map(|(&id, viewport)| {
|
||||||
let parent = *self.viewport_parents.entry(id).or_default();
|
let parent = *self.viewport_parents.entry(id).or_default();
|
||||||
let commands = if is_last {
|
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>(
|
fn show_embedded_viewport<T>(
|
||||||
&self,
|
&self,
|
||||||
new_viewport_id: ViewportId,
|
new_viewport_id: ViewportId,
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ use epaint::{Pos2, Vec2};
|
|||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// The different types of viewports supported by egui.
|
/// 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))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub enum ViewportClass {
|
pub enum ViewportClass {
|
||||||
/// The root viewport; i.e. the original window.
|
/// 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`]
|
/// If you get this, it is because you are already wrapped in a [`crate::Window`]
|
||||||
/// inside of the parent viewport.
|
/// inside of the parent viewport.
|
||||||
EmbeddedWindow,
|
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