mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Untangle logic-only frames: one frame entry point, one output type
Same fixes as the previous commits (run no egui pass at all when
nothing will be shown), but the mechanisms are shared instead of
duplicated:
* `Context::run_frame(input, show_ui, f)` is the one entry point for
integrations: one `FramePhase::Logic` (always, outside any pass),
then one `FramePhase::Ui` per pass (none when `show_ui` is false).
`Context::run_logic` is now a thin wrapper around it, and the
logic-outside-of-pass sequencing lives in egui, not in each backend.
* egui itself buffers the input of pass-less frames
(`ViewportState::pending_raw_input`) and prepends it to the next
pass. This replaces `EpiIntegration::pending_raw_input` and the web
backend's `input.raw.append`, so an integration cannot lose input.
* `FullOutput` is now `{ platform_output, viewport_commands,
pass_output: Option<PassOutput> }`, and `LogicOutput` is gone.
One-shot viewport commands (imperative) are separated from
`ViewportOutput` (which viewports should exist - declarative), so
each backend has exactly one command-handling path, shared by frames
with and without a pass. `pass_output: None` encodes "no pass ran:
paint nothing, leave the viewports alone" in the type.
* The glow/wgpu `!show_ui` early-return blocks are gone: a hidden root
viewport flows through the same tail as a visible one, with the
paint and viewport-structure steps gated on `pass_output`.
Behavioral fixes that fall out:
* `App::logic` now sees the current window state in visible frames
too (it was one frame stale outside the hidden path).
* Auto-save keeps working while a window is minimized or occluded.
* Commands sent to a freshly created viewport apply in the same frame.
* The Wayland resize workaround now also covers commands from
pass-less frames.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -158,10 +158,6 @@ pub struct EpiIntegration {
|
||||
pub egui_ctx: egui::Context,
|
||||
pending_full_output: egui::FullOutput,
|
||||
|
||||
/// Input that we have received, but not yet given to egui,
|
||||
/// because we haven't run any pass since (see [`Self::update_logic_only`]).
|
||||
pending_raw_input: Option<egui::RawInput>,
|
||||
|
||||
/// When set, it is time to close the native window.
|
||||
close: bool,
|
||||
|
||||
@@ -220,7 +216,6 @@ impl EpiIntegration {
|
||||
frame,
|
||||
last_auto_save: Instant::now(),
|
||||
pending_full_output: Default::default(),
|
||||
pending_raw_input: None,
|
||||
close: false,
|
||||
can_drag_window: false,
|
||||
#[cfg(feature = "persistence")]
|
||||
@@ -267,111 +262,60 @@ impl EpiIntegration {
|
||||
|
||||
/// 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::logic`] and [`crate::App::ui`].
|
||||
/// If `viewport_ui_cb` is None, we are in the root viewport and run one app frame:
|
||||
/// [`crate::App::logic`], and, if `show_ui`, [`crate::App::ui`].
|
||||
///
|
||||
/// Only call this when the ui will actually be shown;
|
||||
/// use [`Self::update_logic_only`] otherwise.
|
||||
/// Pass `show_ui: false` when nothing will be shown,
|
||||
/// e.g. because the window is minimized or occluded.
|
||||
/// Then no egui pass is run and all ui state is left untouched:
|
||||
/// the app will find everything where it left it once the window is visible again.
|
||||
/// The returned [`egui::FullOutput`] then has no [`egui::FullOutput::pass_output`].
|
||||
pub fn update(
|
||||
&mut self,
|
||||
app: &mut dyn epi::App,
|
||||
viewport_ui_cb: Option<&DeferredViewportUiCallback>,
|
||||
raw_input: egui::RawInput,
|
||||
mut raw_input: egui::RawInput,
|
||||
show_ui: bool,
|
||||
) -> egui::FullOutput {
|
||||
let raw_input = self.prepare_raw_input(app, raw_input);
|
||||
raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
|
||||
app.raw_input_hook(&self.egui_ctx, &mut raw_input);
|
||||
|
||||
let close_requested = raw_input.viewport().close_requested();
|
||||
|
||||
let is_root_viewport = viewport_ui_cb.is_none();
|
||||
|
||||
if is_root_viewport {
|
||||
// Note that this is _not_ inside the pass below:
|
||||
// `App::logic` may not show any ui, and should not affect any ui state.
|
||||
profiling::scope!("App::logic");
|
||||
app.logic(&self.egui_ctx, &mut self.frame);
|
||||
}
|
||||
|
||||
// Anything `App::logic` asked for (viewport commands etc) is still in the
|
||||
// `Context`, and will come out of the pass we are about to run.
|
||||
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
|
||||
if let Some(viewport_ui_cb) = viewport_ui_cb {
|
||||
// Child viewport
|
||||
let full_output = if let Some(viewport_ui_cb) = viewport_ui_cb {
|
||||
// Child viewport. It has no `App::logic`, so there is no frame to run,
|
||||
// and the backend only calls us when there is something to show:
|
||||
debug_assert!(show_ui, "update called for a hidden child viewport");
|
||||
self.egui_ctx.run_ui(raw_input, |ui| {
|
||||
profiling::scope!("viewport_callback");
|
||||
viewport_ui_cb(ui);
|
||||
} else {
|
||||
profiling::scope!("App::ui");
|
||||
app.ui(ui, &mut self.frame);
|
||||
}
|
||||
});
|
||||
})
|
||||
} else {
|
||||
self.egui_ctx
|
||||
.run_frame(raw_input, show_ui, |phase| match phase {
|
||||
egui::FramePhase::Logic(ctx) => {
|
||||
profiling::scope!("App::logic");
|
||||
app.logic(ctx, &mut self.frame);
|
||||
}
|
||||
egui::FramePhase::Ui(ui) => {
|
||||
profiling::scope!("App::ui");
|
||||
app.ui(ui, &mut self.frame);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
if is_root_viewport && close_requested {
|
||||
let canceled = full_output.viewport_output[&ViewportId::ROOT]
|
||||
.commands
|
||||
.contains(&egui::ViewportCommand::CancelClose);
|
||||
self.handle_close_request(canceled);
|
||||
}
|
||||
|
||||
self.pending_full_output.append(full_output);
|
||||
std::mem::take(&mut self.pending_full_output)
|
||||
}
|
||||
|
||||
/// Let the app tick its logic without showing any ui,
|
||||
/// because the window is minimized or occluded.
|
||||
///
|
||||
/// No egui pass is run, so all ui state is left untouched:
|
||||
/// the app will find everything where it left it once the window is visible again.
|
||||
///
|
||||
/// Only call this for the root viewport: only it has [`crate::App::logic`].
|
||||
pub fn update_logic_only(
|
||||
&mut self,
|
||||
app: &mut dyn epi::App,
|
||||
raw_input: egui::RawInput,
|
||||
) -> egui::LogicOutput {
|
||||
let raw_input = self.prepare_raw_input(app, raw_input);
|
||||
|
||||
let close_requested = raw_input.viewport().close_requested();
|
||||
|
||||
let logic_output = self.egui_ctx.run_logic(&raw_input, |ctx| {
|
||||
profiling::scope!("App::logic");
|
||||
app.logic(ctx, &mut self.frame);
|
||||
});
|
||||
|
||||
// No pass consumed the input, so save it for the next one:
|
||||
match &mut self.pending_raw_input {
|
||||
Some(pending) => pending.append(raw_input),
|
||||
None => self.pending_raw_input = Some(raw_input),
|
||||
}
|
||||
|
||||
if close_requested {
|
||||
let canceled = logic_output
|
||||
let canceled = full_output
|
||||
.viewport_commands
|
||||
.get(&ViewportId::ROOT)
|
||||
.is_some_and(|commands| commands.contains(&egui::ViewportCommand::CancelClose));
|
||||
self.handle_close_request(canceled);
|
||||
}
|
||||
|
||||
logic_output
|
||||
}
|
||||
|
||||
/// Set the time, prepend any input we couldn't give to egui earlier, and run the app hook.
|
||||
fn prepare_raw_input(
|
||||
&mut self,
|
||||
app: &mut dyn epi::App,
|
||||
raw_input: egui::RawInput,
|
||||
) -> egui::RawInput {
|
||||
let mut raw_input = match self.pending_raw_input.take() {
|
||||
Some(mut pending) => {
|
||||
pending.append(raw_input); // The new input wins where they overlap
|
||||
pending
|
||||
}
|
||||
None => raw_input,
|
||||
};
|
||||
|
||||
raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
|
||||
|
||||
app.raw_input_hook(&self.egui_ctx, &mut raw_input);
|
||||
|
||||
raw_input
|
||||
self.pending_full_output.append(full_output);
|
||||
std::mem::take(&mut self.pending_full_output)
|
||||
}
|
||||
|
||||
fn handle_close_request(&mut self, canceled: bool) {
|
||||
|
||||
@@ -40,7 +40,7 @@ use super::{
|
||||
use crate::epaint::textures::TexturesDelta;
|
||||
use crate::{
|
||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
|
||||
native::{epi_integration::EpiIntegration, winit_integration::sleep_if_invisible_or_minimized},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -613,36 +613,10 @@ impl GlowWinitRunning<'_> {
|
||||
(raw_input, viewport_ui_cb, is_visible, show_ui)
|
||||
};
|
||||
|
||||
if !show_ui {
|
||||
// Nothing will be shown, so we run no egui pass at all.
|
||||
// That way all ui state is left untouched, and is still there
|
||||
// when this viewport becomes visible again.
|
||||
let is_root_viewport = viewport_ui_cb.is_none();
|
||||
if is_root_viewport {
|
||||
// The app logic keeps ticking, so it can e.g. ask to be shown again:
|
||||
let egui::LogicOutput {
|
||||
platform_output,
|
||||
viewport_commands,
|
||||
} = self
|
||||
.integration
|
||||
.update_logic_only(self.app.as_mut(), raw_input);
|
||||
|
||||
let mut glutin = self.glutin.borrow_mut();
|
||||
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
|
||||
viewport.info.events.clear(); // they should have been processed
|
||||
if let Some(window) = viewport.window.clone()
|
||||
&& let Some(egui_winit) = viewport.egui_winit.as_mut()
|
||||
{
|
||||
egui_winit.handle_platform_output_with_event_loop(
|
||||
&window,
|
||||
event_loop,
|
||||
platform_output,
|
||||
);
|
||||
}
|
||||
}
|
||||
glutin.handle_viewport_commands(&self.integration.egui_ctx, viewport_commands);
|
||||
}
|
||||
|
||||
if !show_ui && viewport_ui_cb.is_some() {
|
||||
// A hidden child viewport: it has no app logic to tick, and nothing to show,
|
||||
// so there is nothing to do. We run no egui pass at all, so all its ui state
|
||||
// is left untouched, and is still there when it becomes visible again.
|
||||
self.sleep_if_minimized(viewport_id);
|
||||
|
||||
return Ok(if self.integration.should_close() {
|
||||
@@ -700,9 +674,12 @@ impl GlowWinitRunning<'_> {
|
||||
// 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 full_output = self.integration.update(
|
||||
self.app.as_mut(),
|
||||
viewport_ui_cb.as_deref(),
|
||||
raw_input,
|
||||
show_ui,
|
||||
);
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@@ -720,14 +697,13 @@ impl GlowWinitRunning<'_> {
|
||||
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
viewport_output,
|
||||
viewport_commands,
|
||||
pass_output,
|
||||
} = full_output;
|
||||
pending_deltas.append(textures_delta);
|
||||
|
||||
glutin.remove_viewports_not_in(&viewport_output);
|
||||
if let Some(pass_output) = &pass_output {
|
||||
glutin.remove_viewports_not_in(&pass_output.viewport_output);
|
||||
}
|
||||
|
||||
let GlutinWindowContext {
|
||||
viewports,
|
||||
@@ -742,108 +718,113 @@ impl GlowWinitRunning<'_> {
|
||||
|
||||
viewport.info.events.clear(); // they should have been processed
|
||||
let window = viewport.window.clone().unwrap();
|
||||
let gl_surface = viewport.gl_surface.as_ref().unwrap();
|
||||
let egui_winit = viewport.egui_winit.as_mut().unwrap();
|
||||
|
||||
egui_winit.handle_platform_output_with_event_loop(&window, event_loop, platform_output);
|
||||
|
||||
if is_visible {
|
||||
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
|
||||
|
||||
{
|
||||
// We may need to switch contexts again, because of immediate viewports:
|
||||
frame_timer.pause();
|
||||
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
|
||||
frame_timer.resume();
|
||||
}
|
||||
|
||||
let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
|
||||
|
||||
if !clear_before_update {
|
||||
painter.clear(screen_size_in_pixels, clear_color);
|
||||
}
|
||||
|
||||
painter.paint_and_update_textures(
|
||||
screen_size_in_pixels,
|
||||
if let Some(pass_output) = pass_output {
|
||||
let egui::PassOutput {
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
&clipped_primitives,
|
||||
pending_deltas,
|
||||
);
|
||||
viewport_output,
|
||||
} = pass_output;
|
||||
pending_deltas.append(textures_delta);
|
||||
|
||||
{
|
||||
for action in viewport.actions_requested.drain(..) {
|
||||
match action {
|
||||
ActionRequested::Screenshot(user_data) => {
|
||||
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
|
||||
egui_winit
|
||||
.egui_input_mut()
|
||||
.events
|
||||
.push(egui::Event::Screenshot {
|
||||
viewport_id,
|
||||
user_data,
|
||||
image: screenshot.into(),
|
||||
});
|
||||
}
|
||||
ActionRequested::Cut => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
|
||||
}
|
||||
ActionRequested::Copy => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
|
||||
}
|
||||
ActionRequested::Paste => {
|
||||
if let Some(contents) = egui_winit.clipboard_text() {
|
||||
let contents = contents.replace("\r\n", "\n");
|
||||
if !contents.is_empty() {
|
||||
egui_winit
|
||||
.egui_input_mut()
|
||||
.events
|
||||
.push(egui::Event::Paste(contents));
|
||||
if is_visible {
|
||||
let gl_surface = viewport.gl_surface.as_ref().unwrap();
|
||||
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
|
||||
|
||||
{
|
||||
// We may need to switch contexts again, because of immediate viewports:
|
||||
frame_timer.pause();
|
||||
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
|
||||
frame_timer.resume();
|
||||
}
|
||||
|
||||
let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
|
||||
|
||||
if !clear_before_update {
|
||||
painter.clear(screen_size_in_pixels, clear_color);
|
||||
}
|
||||
|
||||
painter.paint_and_update_textures(
|
||||
screen_size_in_pixels,
|
||||
pixels_per_point,
|
||||
&clipped_primitives,
|
||||
pending_deltas,
|
||||
);
|
||||
|
||||
{
|
||||
for action in viewport.actions_requested.drain(..) {
|
||||
match action {
|
||||
ActionRequested::Screenshot(user_data) => {
|
||||
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
|
||||
egui_winit
|
||||
.egui_input_mut()
|
||||
.events
|
||||
.push(egui::Event::Screenshot {
|
||||
viewport_id,
|
||||
user_data,
|
||||
image: screenshot.into(),
|
||||
});
|
||||
}
|
||||
ActionRequested::Cut => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
|
||||
}
|
||||
ActionRequested::Copy => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
|
||||
}
|
||||
ActionRequested::Paste => {
|
||||
if let Some(contents) = egui_winit.clipboard_text() {
|
||||
let contents = contents.replace("\r\n", "\n");
|
||||
if !contents.is_empty() {
|
||||
egui_winit
|
||||
.egui_input_mut()
|
||||
.events
|
||||
.push(egui::Event::Paste(contents));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
integration.post_rendering(&window);
|
||||
}
|
||||
|
||||
integration.post_rendering(&window);
|
||||
{
|
||||
// vsync - don't count as frame-time:
|
||||
frame_timer.pause();
|
||||
profiling::scope!("swap_buffers");
|
||||
let context = current_gl_context.as_ref().ok_or_else(|| {
|
||||
egui_glow::PainterError::from(
|
||||
"failed to get current context to swap buffers".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
gl_surface.swap_buffers(context)?;
|
||||
frame_timer.resume();
|
||||
}
|
||||
|
||||
// give it time to settle:
|
||||
#[cfg(feature = "__screenshot")]
|
||||
if integration.egui_ctx.cumulative_pass_nr() == 2
|
||||
&& let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO")
|
||||
{
|
||||
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// vsync - don't count as frame-time:
|
||||
frame_timer.pause();
|
||||
profiling::scope!("swap_buffers");
|
||||
let context = current_gl_context.as_ref().ok_or_else(|| {
|
||||
egui_glow::PainterError::from(
|
||||
"failed to get current context to swap buffers".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
gl_surface.swap_buffers(context)?;
|
||||
frame_timer.resume();
|
||||
}
|
||||
|
||||
// give it time to settle:
|
||||
#[cfg(feature = "__screenshot")]
|
||||
if integration.egui_ctx.cumulative_pass_nr() == 2
|
||||
&& let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO")
|
||||
{
|
||||
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
|
||||
}
|
||||
glutin.handle_viewport_output(event_loop, &viewport_output);
|
||||
}
|
||||
|
||||
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);
|
||||
glutin.handle_viewport_commands(&integration.egui_ctx, viewport_commands);
|
||||
|
||||
integration.report_frame_time(frame_timer.total_time_sec()); // don't count auto-save time as part of regular frame time
|
||||
|
||||
integration.maybe_autosave(app.as_mut(), Some(&window));
|
||||
|
||||
if is_invisible_or_minimized(&window) {
|
||||
// On Mac, a minimized Window uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/325
|
||||
// On Windows, an invisible window also uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/7776
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
sleep_if_invisible_or_minimized(&window);
|
||||
|
||||
if integration.should_close() {
|
||||
Ok(EventResult::CloseRequested)
|
||||
@@ -852,19 +833,12 @@ impl GlowWinitRunning<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// On Mac, a minimized Window uses up all CPU:
|
||||
/// <https://github.com/emilk/egui/issues/325>
|
||||
///
|
||||
/// On Windows, an invisible window also uses up all CPU:
|
||||
/// <https://github.com/emilk/egui/issues/7776>
|
||||
fn sleep_if_minimized(&self, viewport_id: ViewportId) {
|
||||
let glutin = self.glutin.borrow();
|
||||
if let Some(viewport) = glutin.viewports.get(&viewport_id)
|
||||
&& let Some(window) = viewport.window.as_ref()
|
||||
&& is_invisible_or_minimized(window)
|
||||
{
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
sleep_if_invisible_or_minimized(window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1419,69 +1393,38 @@ impl GlutinWindowContext {
|
||||
|
||||
/// Apply commands to already existing viewports, without creating or removing any.
|
||||
///
|
||||
/// This is for commands that came out of [`egui::Context::run_logic`],
|
||||
/// which knows nothing about which viewports should exist.
|
||||
/// Which viewports should exist is decided by [`Self::handle_viewport_output`];
|
||||
/// commands can also come from a frame that ran no pass
|
||||
/// (see [`egui::Context::run_frame`]), and such a frame knows nothing about
|
||||
/// which viewports should exist.
|
||||
///
|
||||
/// This also flushes any previously deferred commands,
|
||||
/// e.g. those produced by patching a [`ViewportBuilder`].
|
||||
fn handle_viewport_commands(
|
||||
&mut self,
|
||||
egui_ctx: &egui::Context,
|
||||
viewport_commands: egui::OrderedViewportIdMap<Vec<egui::ViewportCommand>>,
|
||||
mut viewport_commands: egui::OrderedViewportIdMap<Vec<egui::ViewportCommand>>,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
for (viewport_id, mut commands) in viewport_commands {
|
||||
let viewport_ids: Vec<ViewportId> = self.viewports.keys().copied().collect();
|
||||
|
||||
for viewport_id in viewport_ids {
|
||||
let Some(viewport) = self.viewports.get_mut(&viewport_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
viewport.deferred_commands.append(&mut commands);
|
||||
|
||||
if let Some(window) = &viewport.window {
|
||||
egui_winit::process_viewport_commands(
|
||||
egui_ctx,
|
||||
&mut viewport.info,
|
||||
std::mem::take(&mut viewport.deferred_commands),
|
||||
window,
|
||||
&mut viewport.actions_requested,
|
||||
);
|
||||
if let Some(mut commands) = viewport_commands.remove(&viewport_id) {
|
||||
viewport.deferred_commands.append(&mut commands);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_viewport_output(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
egui_ctx: &egui::Context,
|
||||
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
for (
|
||||
viewport_id,
|
||||
ViewportOutput {
|
||||
parent,
|
||||
class,
|
||||
builder,
|
||||
viewport_ui_cb,
|
||||
mut commands,
|
||||
repaint_delay: _, // ignored - we listened to the repaint callback instead
|
||||
},
|
||||
) in viewport_output.clone()
|
||||
{
|
||||
let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent);
|
||||
|
||||
let viewport = initialize_or_update_viewport(
|
||||
&mut self.viewports,
|
||||
ids,
|
||||
class,
|
||||
builder,
|
||||
viewport_ui_cb,
|
||||
);
|
||||
if viewport.deferred_commands.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(window) = &viewport.window {
|
||||
let old_inner_size = window.inner_size();
|
||||
|
||||
viewport.deferred_commands.append(&mut commands);
|
||||
|
||||
egui_winit::process_viewport_commands(
|
||||
egui_ctx,
|
||||
&mut viewport.info,
|
||||
@@ -1499,6 +1442,31 @@ impl GlutinWindowContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create, update and remove viewports (native windows), to match `viewport_output`.
|
||||
fn handle_viewport_output(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
for (
|
||||
viewport_id,
|
||||
ViewportOutput {
|
||||
parent,
|
||||
class,
|
||||
builder,
|
||||
viewport_ui_cb,
|
||||
repaint_delay: _, // ignored - we listened to the repaint callback instead
|
||||
},
|
||||
) in viewport_output.clone()
|
||||
{
|
||||
let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent);
|
||||
|
||||
initialize_or_update_viewport(&mut self.viewports, ids, class, builder, viewport_ui_cb);
|
||||
}
|
||||
|
||||
// Create windows for any new viewports:
|
||||
self.initialize_all_windows(event_loop);
|
||||
@@ -1663,13 +1631,17 @@ fn render_immediate_viewport(
|
||||
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
viewport_commands,
|
||||
pass_output,
|
||||
} = egui_ctx.run_ui(input, |ui| {
|
||||
viewport_ui_cb(ui);
|
||||
});
|
||||
let egui::PassOutput {
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
viewport_output,
|
||||
} = egui_ctx.run_ui(input, |ui| {
|
||||
viewport_ui_cb(ui);
|
||||
});
|
||||
} = pass_output.expect("run_ui always runs a pass");
|
||||
|
||||
// ---------------------------------------------------
|
||||
|
||||
@@ -1737,8 +1709,9 @@ fn render_immediate_viewport(
|
||||
egui_winit.handle_platform_output(window, platform_output);
|
||||
|
||||
event_loop_context::with_current_event_loop(|event_loop| {
|
||||
glutin.handle_viewport_output(event_loop, egui_ctx, &viewport_output);
|
||||
glutin.handle_viewport_output(event_loop, &viewport_output);
|
||||
});
|
||||
glutin.handle_viewport_commands(egui_ctx, viewport_commands);
|
||||
}
|
||||
|
||||
#[cfg(feature = "__screenshot")]
|
||||
|
||||
@@ -30,7 +30,7 @@ use crate::{
|
||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||
native::{
|
||||
epi_integration::EpiIntegration,
|
||||
winit_integration::{EventResult, is_invisible_or_minimized},
|
||||
winit_integration::{EventResult, sleep_if_invisible_or_minimized},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -695,40 +695,10 @@ impl WgpuWinitRunning<'_> {
|
||||
(viewport_ui_cb, raw_input, is_visible, show_ui)
|
||||
};
|
||||
|
||||
if !show_ui {
|
||||
// Nothing will be shown, so we run no egui pass at all.
|
||||
// That way all ui state is left untouched, and is still there
|
||||
// when this viewport becomes visible again.
|
||||
let is_root_viewport = viewport_ui_cb.is_none();
|
||||
if is_root_viewport {
|
||||
// The app logic keeps ticking, so it can e.g. ask to be shown again:
|
||||
let egui::LogicOutput {
|
||||
platform_output,
|
||||
viewport_commands,
|
||||
} = integration.update_logic_only(app.as_mut(), raw_input);
|
||||
|
||||
let mut shared_mut = shared.borrow_mut();
|
||||
let SharedState { viewports, .. } = &mut *shared_mut;
|
||||
|
||||
if let Some(viewport) = viewports.get_mut(&viewport_id) {
|
||||
viewport.info.events.clear(); // they should have been processed
|
||||
if let Viewport {
|
||||
window: Some(window),
|
||||
egui_winit: Some(egui_winit),
|
||||
..
|
||||
} = viewport
|
||||
{
|
||||
egui_winit.handle_platform_output_with_event_loop(
|
||||
window,
|
||||
event_loop,
|
||||
platform_output,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handle_viewport_commands(&integration.egui_ctx, viewport_commands, viewports);
|
||||
}
|
||||
|
||||
if !show_ui && viewport_ui_cb.is_some() {
|
||||
// A hidden child viewport: it has no app logic to tick, and nothing to show,
|
||||
// so there is nothing to do. We run no egui pass at all, so all its ui state
|
||||
// is left untouched, and is still there when it becomes visible again.
|
||||
sleep_if_minimized(&shared.borrow(), viewport_id);
|
||||
|
||||
return Ok(if integration.should_close() {
|
||||
@@ -742,7 +712,8 @@ impl WgpuWinitRunning<'_> {
|
||||
|
||||
// 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 full_output =
|
||||
integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input, show_ui);
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@@ -758,15 +729,18 @@ impl WgpuWinitRunning<'_> {
|
||||
|
||||
let FullOutput {
|
||||
platform_output,
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
viewport_output,
|
||||
viewport_commands,
|
||||
pass_output,
|
||||
} = full_output;
|
||||
|
||||
pending_deltas.append(textures_delta);
|
||||
|
||||
remove_viewports_not_in(viewports, painter, viewport_from_window, &viewport_output);
|
||||
if let Some(pass_output) = &pass_output {
|
||||
remove_viewports_not_in(
|
||||
viewports,
|
||||
painter,
|
||||
viewport_from_window,
|
||||
&pass_output.viewport_output,
|
||||
);
|
||||
}
|
||||
|
||||
let Some(viewport) = viewports.get_mut(&viewport_id) else {
|
||||
return Ok(EventResult::Wait);
|
||||
@@ -785,74 +759,79 @@ impl WgpuWinitRunning<'_> {
|
||||
|
||||
egui_winit.handle_platform_output_with_event_loop(window, event_loop, platform_output);
|
||||
|
||||
let vsync_secs = if is_visible {
|
||||
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
|
||||
let mut vsync_secs = 0.0;
|
||||
|
||||
let mut screenshot_commands = vec![];
|
||||
viewport.actions_requested.retain(|cmd| {
|
||||
if let ActionRequested::Screenshot(info) = cmd {
|
||||
screenshot_commands.push(info.clone());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
let vsync_secs = painter.paint_and_update_textures(
|
||||
viewport_id,
|
||||
if let Some(pass_output) = pass_output {
|
||||
let egui::PassOutput {
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
app.clear_color(&egui_ctx.global_style().visuals),
|
||||
&clipped_primitives,
|
||||
pending_deltas,
|
||||
screenshot_commands,
|
||||
window,
|
||||
);
|
||||
viewport_output,
|
||||
} = pass_output;
|
||||
|
||||
for action in viewport.actions_requested.drain(..) {
|
||||
match action {
|
||||
ActionRequested::Screenshot { .. } => {
|
||||
// already handled above
|
||||
pending_deltas.append(textures_delta);
|
||||
|
||||
if is_visible {
|
||||
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
|
||||
|
||||
let mut screenshot_commands = vec![];
|
||||
viewport.actions_requested.retain(|cmd| {
|
||||
if let ActionRequested::Screenshot(info) = cmd {
|
||||
screenshot_commands.push(info.clone());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
ActionRequested::Cut => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
|
||||
}
|
||||
ActionRequested::Copy => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
|
||||
}
|
||||
ActionRequested::Paste => {
|
||||
if let Some(contents) = egui_winit.clipboard_text() {
|
||||
let contents = contents.replace("\r\n", "\n");
|
||||
if !contents.is_empty() {
|
||||
egui_winit
|
||||
.egui_input_mut()
|
||||
.events
|
||||
.push(egui::Event::Paste(contents));
|
||||
});
|
||||
vsync_secs = painter.paint_and_update_textures(
|
||||
viewport_id,
|
||||
pixels_per_point,
|
||||
app.clear_color(&egui_ctx.global_style().visuals),
|
||||
&clipped_primitives,
|
||||
pending_deltas,
|
||||
screenshot_commands,
|
||||
window,
|
||||
);
|
||||
|
||||
for action in viewport.actions_requested.drain(..) {
|
||||
match action {
|
||||
ActionRequested::Screenshot { .. } => {
|
||||
// already handled above
|
||||
}
|
||||
ActionRequested::Cut => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Cut);
|
||||
}
|
||||
ActionRequested::Copy => {
|
||||
egui_winit.egui_input_mut().events.push(egui::Event::Copy);
|
||||
}
|
||||
ActionRequested::Paste => {
|
||||
if let Some(contents) = egui_winit.clipboard_text() {
|
||||
let contents = contents.replace("\r\n", "\n");
|
||||
if !contents.is_empty() {
|
||||
egui_winit
|
||||
.egui_input_mut()
|
||||
.events
|
||||
.push(egui::Event::Paste(contents));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
integration.post_rendering(window);
|
||||
}
|
||||
|
||||
integration.post_rendering(window);
|
||||
let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect();
|
||||
|
||||
vsync_secs
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
handle_viewport_output(&viewport_output, viewports, painter, viewport_from_window);
|
||||
|
||||
let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect();
|
||||
// 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);
|
||||
}
|
||||
|
||||
handle_viewport_output(
|
||||
&integration.egui_ctx,
|
||||
&viewport_output,
|
||||
viewports,
|
||||
painter,
|
||||
viewport_from_window,
|
||||
);
|
||||
|
||||
// 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);
|
||||
handle_viewport_commands(&integration.egui_ctx, viewport_commands, viewports, painter);
|
||||
|
||||
let window = viewport_from_window
|
||||
.get(&window_id)
|
||||
@@ -863,15 +842,8 @@ impl WgpuWinitRunning<'_> {
|
||||
|
||||
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
|
||||
|
||||
if let Some(window) = window
|
||||
&& is_invisible_or_minimized(window)
|
||||
{
|
||||
// On Mac, a minimized Window uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/325
|
||||
// On Windows, an invisible window also uses up all CPU:
|
||||
// https://github.com/emilk/egui/issues/7776
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
if let Some(window) = window {
|
||||
sleep_if_invisible_or_minimized(window);
|
||||
}
|
||||
|
||||
if integration.should_close() {
|
||||
@@ -1173,13 +1145,17 @@ fn render_immediate_viewport(
|
||||
// Make sure no locks are held during this call.
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
viewport_commands,
|
||||
pass_output,
|
||||
} = egui_ctx.run_ui(input, |ui| {
|
||||
viewport_ui_cb(ui);
|
||||
});
|
||||
let egui::PassOutput {
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
viewport_output,
|
||||
} = egui_ctx.run_ui(input, |ui| {
|
||||
viewport_ui_cb(ui);
|
||||
});
|
||||
} = pass_output.expect("run_ui always runs a pass");
|
||||
|
||||
// ------------------------------------------
|
||||
|
||||
@@ -1226,13 +1202,8 @@ fn render_immediate_viewport(
|
||||
|
||||
egui_winit.handle_platform_output(window, platform_output);
|
||||
|
||||
handle_viewport_output(
|
||||
&egui_ctx,
|
||||
&viewport_output,
|
||||
viewports,
|
||||
painter,
|
||||
viewport_from_window,
|
||||
);
|
||||
handle_viewport_output(&viewport_output, viewports, painter, viewport_from_window);
|
||||
handle_viewport_commands(&egui_ctx, viewport_commands, viewports, painter);
|
||||
}
|
||||
|
||||
pub(crate) fn remove_viewports_not_in(
|
||||
@@ -1249,81 +1220,41 @@ pub(crate) fn remove_viewports_not_in(
|
||||
painter.gc_viewports(&active_viewports_ids);
|
||||
}
|
||||
|
||||
/// Add new viewports, and update existing ones:
|
||||
/// Apply commands to already existing viewports, without creating or removing any.
|
||||
///
|
||||
/// This is for commands that came out of [`egui::Context::run_logic`],
|
||||
/// which knows nothing about which viewports should exist.
|
||||
/// Which viewports should exist is decided by [`handle_viewport_output`];
|
||||
/// commands can also come from a frame that ran no pass
|
||||
/// (see [`egui::Context::run_frame`]), and such a frame knows nothing about
|
||||
/// which viewports should exist.
|
||||
///
|
||||
/// This also flushes any previously deferred commands,
|
||||
/// e.g. those produced by patching a [`ViewportBuilder`].
|
||||
fn handle_viewport_commands(
|
||||
egui_ctx: &egui::Context,
|
||||
viewport_commands: egui::OrderedViewportIdMap<Vec<egui::ViewportCommand>>,
|
||||
mut viewport_commands: egui::OrderedViewportIdMap<Vec<egui::ViewportCommand>>,
|
||||
viewports: &mut Viewports,
|
||||
painter: &mut egui_wgpu::winit::Painter,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
for (viewport_id, mut commands) in viewport_commands {
|
||||
let viewport_ids: Vec<ViewportId> = viewports.keys().copied().collect();
|
||||
|
||||
for viewport_id in viewport_ids {
|
||||
let Some(viewport) = viewports.get_mut(&viewport_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
viewport.deferred_commands.append(&mut commands);
|
||||
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
egui_winit::process_viewport_commands(
|
||||
egui_ctx,
|
||||
&mut viewport.info,
|
||||
std::mem::take(&mut viewport.deferred_commands),
|
||||
window,
|
||||
&mut viewport.actions_requested,
|
||||
);
|
||||
if let Some(mut commands) = viewport_commands.remove(&viewport_id) {
|
||||
viewport.deferred_commands.append(&mut commands);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// On Mac, a minimized Window uses up all CPU:
|
||||
/// <https://github.com/emilk/egui/issues/325>
|
||||
///
|
||||
/// On Windows, an invisible window also uses up all CPU:
|
||||
/// <https://github.com/emilk/egui/issues/7776>
|
||||
fn sleep_if_minimized(shared: &SharedState, viewport_id: ViewportId) {
|
||||
if let Some(viewport) = shared.viewports.get(&viewport_id)
|
||||
&& let Some(window) = viewport.window.as_ref()
|
||||
&& is_invisible_or_minimized(window)
|
||||
{
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_viewport_output(
|
||||
egui_ctx: &egui::Context,
|
||||
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
|
||||
viewports: &mut Viewports,
|
||||
painter: &mut egui_wgpu::winit::Painter,
|
||||
viewport_from_window: &mut HashMap<WindowId, ViewportId>,
|
||||
) {
|
||||
for (
|
||||
viewport_id,
|
||||
ViewportOutput {
|
||||
parent,
|
||||
class,
|
||||
builder,
|
||||
viewport_ui_cb,
|
||||
mut commands,
|
||||
repaint_delay: _, // ignored - we listened to the repaint callback instead
|
||||
},
|
||||
) in viewport_output.clone()
|
||||
{
|
||||
let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent);
|
||||
|
||||
let viewport =
|
||||
initialize_or_update_viewport(viewports, ids, class, builder, viewport_ui_cb, painter);
|
||||
if viewport.deferred_commands.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
let old_inner_size = window.inner_size();
|
||||
|
||||
viewport.deferred_commands.append(&mut commands);
|
||||
|
||||
egui_winit::process_viewport_commands(
|
||||
egui_ctx,
|
||||
&mut viewport.info,
|
||||
@@ -1346,6 +1277,38 @@ fn handle_viewport_output(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sleep_if_minimized(shared: &SharedState, viewport_id: ViewportId) {
|
||||
if let Some(viewport) = shared.viewports.get(&viewport_id)
|
||||
&& let Some(window) = viewport.window.as_ref()
|
||||
{
|
||||
sleep_if_invisible_or_minimized(window);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create, update and remove viewports (native windows), to match `viewport_output`.
|
||||
fn handle_viewport_output(
|
||||
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
|
||||
viewports: &mut Viewports,
|
||||
painter: &mut egui_wgpu::winit::Painter,
|
||||
viewport_from_window: &mut HashMap<WindowId, ViewportId>,
|
||||
) {
|
||||
for (
|
||||
viewport_id,
|
||||
ViewportOutput {
|
||||
parent,
|
||||
class,
|
||||
builder,
|
||||
viewport_ui_cb,
|
||||
repaint_delay: _, // ignored - we listened to the repaint callback instead
|
||||
},
|
||||
) in viewport_output.clone()
|
||||
{
|
||||
let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent);
|
||||
|
||||
initialize_or_update_viewport(viewports, ids, class, builder, viewport_ui_cb, painter);
|
||||
}
|
||||
|
||||
remove_viewports_not_in(viewports, painter, viewport_from_window, viewport_output);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,20 @@ pub fn is_invisible_or_minimized(window: &Window) -> bool {
|
||||
window.is_visible() == Some(false) || window.is_minimized() == Some(true)
|
||||
}
|
||||
|
||||
/// Sleep for a bit if the window is invisible or minimized.
|
||||
///
|
||||
/// On Mac, a minimized window uses up all CPU:
|
||||
/// <https://github.com/emilk/egui/issues/325>
|
||||
///
|
||||
/// On Windows, an invisible window also uses up all CPU:
|
||||
/// <https://github.com/emilk/egui/issues/7776>
|
||||
pub fn sleep_if_invisible_or_minimized(window: &Window) {
|
||||
if is_invisible_or_minimized(window) {
|
||||
profiling::scope!("minimized_sleep");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an egui context, restoring it from storage if possible.
|
||||
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
|
||||
profiling::function_scope!();
|
||||
|
||||
@@ -280,52 +280,41 @@ impl AppRunner {
|
||||
.and_then(|v| v.visible())
|
||||
.unwrap_or(true);
|
||||
|
||||
if !is_visible {
|
||||
// The tab is hidden, so we run no egui pass at all.
|
||||
// That way all ui state is left untouched, and is still there
|
||||
// when the tab is shown again.
|
||||
|
||||
let egui::LogicOutput {
|
||||
platform_output,
|
||||
viewport_commands,
|
||||
} = self.egui_ctx.run_logic(&raw_input, |ctx| {
|
||||
self.app.logic(ctx, &mut self.frame);
|
||||
});
|
||||
|
||||
// No pass consumed the input, so save it for the next one:
|
||||
self.input.raw.append(raw_input);
|
||||
|
||||
self.handle_viewport_commands(viewport_commands.into_values().flatten());
|
||||
self.handle_platform_output(platform_output);
|
||||
return;
|
||||
}
|
||||
|
||||
// `App::logic` may not show any ui, so it is called outside of the pass:
|
||||
self.app.logic(&self.egui_ctx, &mut self.frame);
|
||||
|
||||
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
|
||||
self.app.ui(ui, &mut self.frame);
|
||||
});
|
||||
// When the tab is hidden (`!is_visible`), we run no egui pass at all.
|
||||
// That way all ui state is left untouched, and is still there
|
||||
// when the tab is shown again.
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
viewport_commands,
|
||||
pass_output,
|
||||
} = self
|
||||
.egui_ctx
|
||||
.run_frame(raw_input, is_visible, |phase| match phase {
|
||||
egui::FramePhase::Logic(ctx) => {
|
||||
self.app.logic(ctx, &mut self.frame);
|
||||
}
|
||||
egui::FramePhase::Ui(ui) => {
|
||||
self.app.ui(ui, &mut self.frame);
|
||||
}
|
||||
});
|
||||
|
||||
self.handle_viewport_commands(viewport_commands.into_values().flatten());
|
||||
self.handle_platform_output(platform_output);
|
||||
|
||||
if let Some(egui::PassOutput {
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
viewport_output,
|
||||
} = full_output;
|
||||
}) = pass_output
|
||||
{
|
||||
if viewport_output.len() > 1 {
|
||||
log::warn!("Multiple viewports not yet supported on the web");
|
||||
}
|
||||
|
||||
if viewport_output.len() > 1 {
|
||||
log::warn!("Multiple viewports not yet supported on the web");
|
||||
self.textures_delta.append(textures_delta);
|
||||
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
|
||||
}
|
||||
self.handle_viewport_commands(
|
||||
viewport_output
|
||||
.into_values()
|
||||
.flat_map(|viewport_output| viewport_output.commands),
|
||||
);
|
||||
|
||||
self.handle_platform_output(platform_output);
|
||||
self.textures_delta.append(textures_delta);
|
||||
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
|
||||
}
|
||||
|
||||
fn handle_viewport_commands(&mut self, commands: impl Iterator<Item = ViewportCommand>) {
|
||||
|
||||
@@ -17,10 +17,10 @@ use epaint::{
|
||||
use crate::{
|
||||
Align2, CursorIcon, DeferredViewportUiCallback, FontDefinitions, Grid, Id, ImmediateViewport,
|
||||
ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory,
|
||||
ModifierNames, Modifiers, NumExt as _, Order, Painter, RawInput, Response, RichText,
|
||||
SafeAreaInsets, ScrollArea, Sense, Style, TextStyle, TextureHandle, TextureOptions, Ui,
|
||||
UiBuilder, ViewportBuilder, ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair,
|
||||
ViewportIdSet, ViewportOutput, Visuals, Widget as _, WidgetRect, WidgetText,
|
||||
ModifierNames, Modifiers, NumExt as _, Order, OrderedViewportIdMap, Painter, RawInput,
|
||||
Response, RichText, SafeAreaInsets, ScrollArea, Sense, Style, TextStyle, TextureHandle,
|
||||
TextureOptions, Ui, UiBuilder, ViewportBuilder, ViewportCommand, ViewportId, ViewportIdMap,
|
||||
ViewportIdPair, ViewportIdSet, ViewportOutput, Visuals, Widget as _, WidgetRect, WidgetText,
|
||||
animation_manager::AnimationManager,
|
||||
containers::{self, area::AreaState},
|
||||
data::output::PlatformOutput,
|
||||
@@ -32,7 +32,7 @@ use crate::{
|
||||
load::{self, Bytes, Loaders, SizedTexture},
|
||||
memory::{Options, Theme},
|
||||
os::OperatingSystem,
|
||||
output::{FullOutput, LogicOutput},
|
||||
output::{FullOutput, PassOutput},
|
||||
pass_state::PassState,
|
||||
plugin::{self, TypedPluginHandle},
|
||||
resize, response, scroll_area,
|
||||
@@ -240,6 +240,12 @@ pub struct ViewportState {
|
||||
pub output: PlatformOutput,
|
||||
pub commands: Vec<ViewportCommand>,
|
||||
|
||||
/// Input that was given to [`Context::run_frame`] while no pass ran.
|
||||
///
|
||||
/// No pass consumed it, so we keep it and prepend it to the input
|
||||
/// of the next pass.
|
||||
pending_raw_input: RawInput,
|
||||
|
||||
// ----------------------
|
||||
// Cross-frame statistics:
|
||||
pub num_multipass_in_row: usize,
|
||||
@@ -414,8 +420,35 @@ struct ContextImpl {
|
||||
}
|
||||
|
||||
impl ContextImpl {
|
||||
fn begin_pass(&mut self, mut new_raw_input: RawInput) {
|
||||
/// Apply the window-related parts of new input ([`RawInput::viewports`] and
|
||||
/// [`RawInput::focused`]) to the stored input, without disturbing the rest
|
||||
/// (events, time, …).
|
||||
///
|
||||
/// This lets app logic see the current state of the windows
|
||||
/// before (or without) a pass consuming the full input.
|
||||
fn ingest_window_state(&mut self, new_input: &RawInput) {
|
||||
let raw = &mut self.viewport_for(new_input.viewport_id).input.raw;
|
||||
raw.viewport_id = new_input.viewport_id;
|
||||
raw.viewports = new_input.viewports.clone();
|
||||
raw.focused = new_input.focused;
|
||||
}
|
||||
|
||||
fn begin_pass(&mut self, new_raw_input: RawInput) {
|
||||
let viewport_id = new_raw_input.viewport_id;
|
||||
|
||||
// Prepend any input that [`Context::run_frame`] received while no pass ran:
|
||||
let mut new_raw_input = {
|
||||
let mut pending = std::mem::take(
|
||||
&mut self
|
||||
.viewports
|
||||
.entry(viewport_id)
|
||||
.or_default()
|
||||
.pending_raw_input,
|
||||
);
|
||||
pending.append(new_raw_input); // The new input wins where they overlap
|
||||
pending
|
||||
};
|
||||
|
||||
let parent_id = new_raw_input
|
||||
.viewports
|
||||
.get(&viewport_id)
|
||||
@@ -714,8 +747,9 @@ impl ContextImpl {
|
||||
/// });
|
||||
/// });
|
||||
/// handle_platform_output(full_output.platform_output);
|
||||
/// let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point);
|
||||
/// paint(full_output.textures_delta, clipped_primitives);
|
||||
/// let pass_output = full_output.pass_output.expect("run_ui always runs a pass");
|
||||
/// let clipped_primitives = ctx.tessellate(pass_output.shapes, pass_output.pixels_per_point);
|
||||
/// paint(pass_output.textures_delta, clipped_primitives);
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
@@ -753,6 +787,23 @@ impl Default for Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// One phase of an app frame; see [`Context::run_frame`].
|
||||
pub enum FramePhase<'a> {
|
||||
/// Tick the app logic.
|
||||
///
|
||||
/// This happens exactly once per frame, before any pass,
|
||||
/// so it must not show any ui.
|
||||
Logic(&'a Context),
|
||||
|
||||
/// Show the ui.
|
||||
///
|
||||
/// This happens once per pass:
|
||||
/// not at all when nothing will be shown,
|
||||
/// and more than once per frame during multi-pass layout
|
||||
/// (see [`Context::request_discard`]).
|
||||
Ui(&'a mut Ui),
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Do read-only (shared access) transaction on Context
|
||||
fn read<R>(&self, reader: impl FnOnce(&ContextImpl) -> R) -> R {
|
||||
@@ -888,53 +939,101 @@ impl Context {
|
||||
output
|
||||
}
|
||||
|
||||
/// Run app logic without showing any ui.
|
||||
/// Run one frame of an app that separates its logic from its ui.
|
||||
///
|
||||
/// Use this instead of [`Self::run_ui`] when nothing will be shown,
|
||||
/// e.g. because the window is minimized or occluded,
|
||||
/// but you still want to let the app tick its logic
|
||||
/// (so that it can e.g. ask to be shown again with [`ViewportCommand::Focus`]).
|
||||
/// A frame consists of one [`FramePhase::Logic`],
|
||||
/// followed by one [`FramePhase::Ui`] per pass —
|
||||
/// or by no ui phase at all if `show_ui` is `false`,
|
||||
/// e.g. because the window is minimized or occluded.
|
||||
///
|
||||
/// No pass is run, so `f` must not show any ui.
|
||||
/// This means everything egui knows about the ui is left untouched:
|
||||
/// The logic phase runs outside of any pass. It always sees the current state of the
|
||||
/// windows ([`InputState::viewport`]), so it can e.g. tell that the window is hidden,
|
||||
/// and ask for it to be shown again with [`ViewportCommand::Focus`].
|
||||
/// The rest of the input (events, time, …) is not applied until the first pass,
|
||||
/// so during the logic phase it is still that of the previous pass.
|
||||
///
|
||||
/// When no pass is run, everything egui knows about the ui is left untouched:
|
||||
/// no widget state is garbage-collected, no animation advances,
|
||||
/// and nothing loses focus.
|
||||
///
|
||||
/// Of `new_input`, only [`RawInput::viewports`] is used: `f` can learn about the state of
|
||||
/// the windows with [`InputState::viewport`], but the ui input (events, time, …)
|
||||
/// is left as it was, and should be given to the next call to [`Self::run_ui`].
|
||||
///
|
||||
/// The returned [`LogicOutput`] is what [`FullOutput`] would have carried:
|
||||
/// anything `f` asked the integration to do.
|
||||
/// There is nothing to paint.
|
||||
/// egui keeps `new_input` and feeds it to the first pass of a later frame,
|
||||
/// so no input is lost while nothing is shown.
|
||||
/// The returned [`FullOutput`] then has no [`FullOutput::pass_output`]:
|
||||
/// there is nothing to paint, and the integration should leave its windows as they are.
|
||||
#[must_use]
|
||||
pub fn run_logic(&self, new_input: &RawInput, f: impl FnOnce(&Self)) -> LogicOutput {
|
||||
pub fn run_frame(
|
||||
&self,
|
||||
new_input: RawInput,
|
||||
show_ui: bool,
|
||||
mut frame: impl FnMut(FramePhase<'_>),
|
||||
) -> FullOutput {
|
||||
self.run_frame_dyn(new_input, show_ui, &mut frame)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn run_frame_dyn(
|
||||
&self,
|
||||
new_input: RawInput,
|
||||
show_ui: bool,
|
||||
frame: &mut dyn FnMut(FramePhase<'_>),
|
||||
) -> FullOutput {
|
||||
profiling::function_scope!();
|
||||
|
||||
let viewport_id = new_input.viewport_id;
|
||||
|
||||
self.write(|ctx| {
|
||||
// Consume any outstanding repaint request, so that a new request from `f`
|
||||
// reaches the integration instead of being considered already served:
|
||||
ctx.begin_pass_repaint_logic(viewport_id);
|
||||
if !show_ui {
|
||||
// No pass will consume the outstanding repaint request, so consume it here,
|
||||
// so that a new request from the app logic reaches the integration
|
||||
// instead of being considered already served:
|
||||
ctx.begin_pass_repaint_logic(viewport_id);
|
||||
}
|
||||
|
||||
// Tell the app about the windows, but leave the ui input alone:
|
||||
let raw = &mut ctx.viewport_for(viewport_id).input.raw;
|
||||
raw.viewport_id = viewport_id;
|
||||
raw.viewports = new_input.viewports.clone();
|
||||
raw.focused = new_input.focused;
|
||||
// Let the app logic see the current state of the windows:
|
||||
ctx.ingest_window_state(&new_input);
|
||||
});
|
||||
|
||||
f(self);
|
||||
frame(FramePhase::Logic(self));
|
||||
|
||||
self.write(|ctx| LogicOutput {
|
||||
platform_output: std::mem::take(&mut ctx.viewport_for(viewport_id).output),
|
||||
viewport_commands: ctx
|
||||
.viewports
|
||||
.iter_mut()
|
||||
.filter(|(_, viewport)| !viewport.commands.is_empty())
|
||||
.map(|(&id, viewport)| (id, std::mem::take(&mut viewport.commands)))
|
||||
.collect(),
|
||||
if show_ui {
|
||||
// Anything the logic phase asked for (viewport commands etc)
|
||||
// is still in the `Context`, and will come out of the pass we are about to run.
|
||||
self.run_ui_dyn(new_input, &mut |ui| frame(FramePhase::Ui(ui)))
|
||||
} else {
|
||||
// The plugins see the input of every frame exactly once,
|
||||
// so it must happen now, before we buffer it:
|
||||
let mut new_input = new_input;
|
||||
let plugins = self.read(|ctx| ctx.plugins.ordered_plugins());
|
||||
plugins.on_input(self, &mut new_input);
|
||||
|
||||
self.write(|ctx| {
|
||||
// No pass will consume the input, so keep it for the next one:
|
||||
ctx.viewport_for(viewport_id)
|
||||
.pending_raw_input
|
||||
.append(new_input);
|
||||
|
||||
FullOutput {
|
||||
platform_output: std::mem::take(&mut ctx.viewport_for(viewport_id).output),
|
||||
viewport_commands: ctx.take_viewport_commands(),
|
||||
pass_output: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Run app logic without showing any ui.
|
||||
///
|
||||
/// This is [`Self::run_frame`] without a ui phase.
|
||||
/// Use it when nothing will be shown,
|
||||
/// e.g. because the window is minimized or occluded,
|
||||
/// but you still want to let the app tick its logic
|
||||
/// (so that it can e.g. ask to be shown again with [`ViewportCommand::Focus`]).
|
||||
#[must_use]
|
||||
pub fn run_logic(&self, new_input: RawInput, f: impl FnOnce(&Self)) -> FullOutput {
|
||||
let mut f = Some(f);
|
||||
self.run_frame(new_input, false, |phase| {
|
||||
if let (FramePhase::Logic(ctx), Some(f)) = (phase, f.take()) {
|
||||
f(ctx);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2792,19 +2891,20 @@ impl ContextImpl {
|
||||
// just the top _immediate_ viewport.
|
||||
let is_last = self.viewport_stack.is_empty();
|
||||
|
||||
let viewport_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.
|
||||
self.take_viewport_commands()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
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,
|
||||
@@ -2813,7 +2913,6 @@ impl ContextImpl {
|
||||
class: viewport.class,
|
||||
builder: viewport.builder.clone(),
|
||||
viewport_ui_cb: viewport.viewport_ui_cb.clone(),
|
||||
commands,
|
||||
repaint_delay: viewport.repaint.repaint_delay,
|
||||
},
|
||||
)
|
||||
@@ -2838,12 +2937,24 @@ impl ContextImpl {
|
||||
|
||||
FullOutput {
|
||||
platform_output,
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
viewport_output,
|
||||
viewport_commands,
|
||||
pass_output: Some(PassOutput {
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
viewport_output,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Take all outstanding [`ViewportCommand`]s, keyed by the viewport they apply to.
|
||||
fn take_viewport_commands(&mut self) -> OrderedViewportIdMap<Vec<ViewportCommand>> {
|
||||
self.viewports
|
||||
.iter_mut()
|
||||
.filter(|(_, viewport)| !viewport.commands.is_empty())
|
||||
.map(|(&id, viewport)| (id, std::mem::take(&mut viewport.commands)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
|
||||
@@ -6,7 +6,7 @@ use epaint::text::CharIndex;
|
||||
|
||||
use crate::{OrderedViewportIdMap, RepaintCause, ViewportOutput, WidgetType};
|
||||
|
||||
/// What egui emits each frame from [`crate::Context::run_ui`].
|
||||
/// What egui emits each frame from [`crate::Context::run_frame`] and friends.
|
||||
///
|
||||
/// The backend should use this.
|
||||
#[derive(Clone, Default)]
|
||||
@@ -14,6 +14,72 @@ pub struct FullOutput {
|
||||
/// Non-rendering related output.
|
||||
pub platform_output: PlatformOutput,
|
||||
|
||||
/// The commands sent with [`crate::Context::send_viewport_cmd`] and friends.
|
||||
///
|
||||
/// These are one-shot commands (e.g. "focus this window"),
|
||||
/// and say nothing about which viewports should exist.
|
||||
pub viewport_commands: OrderedViewportIdMap<Vec<crate::ViewportCommand>>,
|
||||
|
||||
/// What the egui pass(es) produced: things to paint, and which viewports should exist.
|
||||
///
|
||||
/// This is `None` if no pass was run
|
||||
/// (i.e. the output came from [`crate::Context::run_logic`],
|
||||
/// because nothing was going to be shown).
|
||||
/// If so, all ui state was left untouched:
|
||||
/// the integration should paint nothing and leave its viewports as they are.
|
||||
pub pass_output: Option<PassOutput>,
|
||||
}
|
||||
|
||||
impl FullOutput {
|
||||
/// Add on new output.
|
||||
pub fn append(&mut self, newer: Self) {
|
||||
let Self {
|
||||
platform_output,
|
||||
viewport_commands,
|
||||
pass_output,
|
||||
} = newer;
|
||||
|
||||
self.platform_output.append(platform_output);
|
||||
|
||||
for (id, mut commands) in viewport_commands {
|
||||
self.viewport_commands
|
||||
.entry(id)
|
||||
.or_default()
|
||||
.append(&mut commands);
|
||||
}
|
||||
|
||||
match (&mut self.pass_output, pass_output) {
|
||||
(Some(old), Some(new)) => old.append(new),
|
||||
(old @ None, new) => *old = new,
|
||||
(Some(_), None) => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The output of the egui pass(es), if any was run.
|
||||
///
|
||||
/// Panics if no pass was run.
|
||||
/// This cannot happen for the output of [`crate::Context::run_ui`],
|
||||
/// which always runs at least one pass.
|
||||
#[track_caller]
|
||||
pub fn expect_pass(&self) -> &PassOutput {
|
||||
self.pass_output.as_ref().expect("No egui pass was run")
|
||||
}
|
||||
|
||||
/// [`epaint::textures::TexturesDelta`] will panic when dropped with still unapplied deltas,
|
||||
/// this is a helper to clear the deltas.
|
||||
pub fn drop_without_applying_deltas(mut self) {
|
||||
if let Some(pass_output) = &mut self.pass_output {
|
||||
pass_output.textures_delta.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What the egui pass(es) of one frame produced: things to paint, and which viewports should exist.
|
||||
///
|
||||
/// This is the part of [`FullOutput`] that only a real egui pass can produce,
|
||||
/// as opposed to the parts that app logic can also produce (see [`crate::Context::run_logic`]).
|
||||
#[derive(Clone, Default)]
|
||||
pub struct PassOutput {
|
||||
/// Texture changes since last frame (including the font texture).
|
||||
///
|
||||
/// The backend needs to apply [`crate::TexturesDelta::push`] _before_ painting,
|
||||
@@ -39,20 +105,18 @@ pub struct FullOutput {
|
||||
pub viewport_output: OrderedViewportIdMap<ViewportOutput>,
|
||||
}
|
||||
|
||||
impl FullOutput {
|
||||
impl PassOutput {
|
||||
/// Add on new output.
|
||||
pub fn append(&mut self, newer: Self) {
|
||||
use std::collections::btree_map::Entry;
|
||||
|
||||
let Self {
|
||||
platform_output,
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
viewport_output,
|
||||
} = newer;
|
||||
|
||||
self.platform_output.append(platform_output);
|
||||
self.textures_delta.append(textures_delta);
|
||||
self.shapes = shapes; // Only paint the latest
|
||||
self.pixels_per_point = pixels_per_point; // Use latest
|
||||
@@ -68,28 +132,6 @@ impl FullOutput {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`epaint::textures::TexturesDelta`] will panic when dropped with still unapplied deltas,
|
||||
/// this is a helper to clear the deltas.
|
||||
pub fn drop_without_applying_deltas(mut self) {
|
||||
self.textures_delta.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// What egui emits from [`crate::Context::run_logic`], i.e. from a tick where no ui was shown.
|
||||
///
|
||||
/// There is nothing to paint, but the app may still have asked the integration to do things,
|
||||
/// e.g. to show a hidden window again with [`crate::ViewportCommand::Focus`].
|
||||
#[derive(Clone, Default)]
|
||||
pub struct LogicOutput {
|
||||
/// Non-rendering related output.
|
||||
pub platform_output: PlatformOutput,
|
||||
|
||||
/// The commands sent with [`crate::Context::send_viewport_cmd`] and friends.
|
||||
///
|
||||
/// Note that this contains no information about which viewports exist:
|
||||
/// the integration should leave its viewports as they are.
|
||||
pub viewport_commands: OrderedViewportIdMap<Vec<crate::ViewportCommand>>,
|
||||
}
|
||||
|
||||
/// Information about text being edited.
|
||||
|
||||
@@ -121,8 +121,9 @@
|
||||
//! });
|
||||
//! });
|
||||
//! handle_platform_output(full_output.platform_output);
|
||||
//! let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point);
|
||||
//! paint(full_output.textures_delta, clipped_primitives);
|
||||
//! let pass_output = full_output.pass_output.expect("run_ui always runs a pass");
|
||||
//! let clipped_primitives = ctx.tessellate(pass_output.shapes, pass_output.pixels_per_point);
|
||||
//! paint(pass_output.textures_delta, clipped_primitives);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -462,12 +463,12 @@ pub mod text {
|
||||
pub use self::{
|
||||
atomics::*,
|
||||
containers::{menu::MenuBar, *},
|
||||
context::{Context, RepaintCause, RequestRepaintInfo},
|
||||
context::{Context, FramePhase, RepaintCause, RequestRepaintInfo},
|
||||
data::{
|
||||
Key, UserData,
|
||||
input::*,
|
||||
output::{
|
||||
self, CursorIcon, CustomCursorImage, FullOutput, LogicOutput, OpenUrl, OutputCommand,
|
||||
self, CursorIcon, CustomCursorImage, FullOutput, OpenUrl, OutputCommand, PassOutput,
|
||||
PlatformOutput, UserAttentionType, WidgetInfo,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1266,9 +1266,6 @@ pub struct ViewportOutput {
|
||||
/// `None` for immediate viewports and the ROOT viewport.
|
||||
pub viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
|
||||
|
||||
/// Commands to change the viewport, e.g. window title and size.
|
||||
pub commands: Vec<ViewportCommand>,
|
||||
|
||||
/// Schedule a repaint of this viewport after this delay.
|
||||
///
|
||||
/// It is preferable to instead install a [`Context::set_request_repaint_callback`],
|
||||
@@ -1286,7 +1283,6 @@ impl ViewportOutput {
|
||||
class,
|
||||
builder,
|
||||
viewport_ui_cb,
|
||||
mut commands,
|
||||
repaint_delay,
|
||||
} = newer;
|
||||
|
||||
@@ -1294,7 +1290,6 @@ impl ViewportOutput {
|
||||
self.class = class;
|
||||
let _ = self.builder.patch(builder); // we ignore the returned command, because `self.builder` will be the basis of a new patch
|
||||
self.viewport_ui_cb = viewport_ui_cb;
|
||||
self.commands.append(&mut commands);
|
||||
self.repaint_delay = self.repaint_delay.min(repaint_delay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,12 +28,13 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
// The most end-to-end benchmark.
|
||||
c.bench_function("demo_with_tessellate__realistic", |b| {
|
||||
b.iter(|| {
|
||||
let mut full_output = ctx.run_ui(RawInput::default(), |ui| {
|
||||
let full_output = ctx.run_ui(RawInput::default(), |ui| {
|
||||
demo_windows.ui(ui);
|
||||
});
|
||||
ctx.tessellate(full_output.shapes, full_output.pixels_per_point);
|
||||
let mut pass_output = full_output.pass_output.expect("run_ui always runs a pass");
|
||||
ctx.tessellate(pass_output.shapes, pass_output.pixels_per_point);
|
||||
|
||||
full_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
|
||||
pass_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,7 +51,10 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
demo_windows.ui(ui);
|
||||
});
|
||||
c.bench_function("demo_only_tessellate", |b| {
|
||||
b.iter(|| ctx.tessellate(full_output.shapes.clone(), full_output.pixels_per_point));
|
||||
b.iter(|| {
|
||||
let pass_output = full_output.expect_pass();
|
||||
ctx.tessellate(pass_output.shapes.clone(), pass_output.pixels_per_point)
|
||||
});
|
||||
});
|
||||
full_output.drop_without_applying_deltas();
|
||||
}
|
||||
|
||||
@@ -72,12 +72,13 @@ fn test_egui_e2e() {
|
||||
|
||||
const NUM_FRAMES: usize = 5;
|
||||
for _ in 0..NUM_FRAMES {
|
||||
let mut full_output = ctx.run_ui(raw_input.clone(), |ui| {
|
||||
let full_output = ctx.run_ui(raw_input.clone(), |ui| {
|
||||
demo_windows.ui(ui);
|
||||
});
|
||||
let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point);
|
||||
let mut pass_output = full_output.pass_output.expect("run_ui always runs a pass");
|
||||
let clipped_primitives = ctx.tessellate(pass_output.shapes, pass_output.pixels_per_point);
|
||||
assert!(!clipped_primitives.is_empty());
|
||||
full_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
|
||||
pass_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,16 +93,17 @@ fn test_egui_zero_window_size() {
|
||||
|
||||
const NUM_FRAMES: usize = 5;
|
||||
for _ in 0..NUM_FRAMES {
|
||||
let mut full_output = ctx.run_ui(raw_input.clone(), |ui| {
|
||||
let full_output = ctx.run_ui(raw_input.clone(), |ui| {
|
||||
demo_windows.ui(ui);
|
||||
});
|
||||
let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point);
|
||||
let mut pass_output = full_output.pass_output.expect("run_ui always runs a pass");
|
||||
let clipped_primitives = ctx.tessellate(pass_output.shapes, pass_output.pixels_per_point);
|
||||
assert!(
|
||||
clipped_primitives.is_empty(),
|
||||
"There should be nothing to show, has at least one primitive with clip_rect: {:?}",
|
||||
clipped_primitives[0].clip_rect
|
||||
);
|
||||
full_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
|
||||
pass_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,16 +71,20 @@ impl EguiGlow {
|
||||
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
viewport_commands,
|
||||
pass_output,
|
||||
} = self.egui_ctx.run_ui(raw_input, run_ui);
|
||||
let egui::PassOutput {
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
viewport_output,
|
||||
} = self.egui_ctx.run_ui(raw_input, run_ui);
|
||||
} = pass_output.expect("run_ui always runs a pass");
|
||||
|
||||
if viewport_output.len() > 1 {
|
||||
log::warn!("Multiple viewports not yet supported by EguiGlow");
|
||||
}
|
||||
for (_, ViewportOutput { commands, .. }) in viewport_output {
|
||||
for (_, commands) in viewport_commands {
|
||||
let mut actions_requested = Default::default();
|
||||
egui_winit::process_viewport_commands(
|
||||
&self.egui_ctx,
|
||||
|
||||
@@ -264,10 +264,13 @@ impl egui::Plugin for InspectionPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
let immediate_repaint = output
|
||||
let pass_output = output.expect_pass();
|
||||
|
||||
let immediate_repaint = pass_output
|
||||
.viewport_output
|
||||
.values()
|
||||
.any(|viewport| viewport.repaint_delay == Duration::ZERO);
|
||||
let pixels_per_point = pass_output.pixels_per_point;
|
||||
|
||||
let step = self.step;
|
||||
self.in_flight
|
||||
@@ -276,7 +279,7 @@ impl egui::Plugin for InspectionPlugin {
|
||||
if let Some(reply) = item.reply.take() {
|
||||
reply(Response::Tree {
|
||||
step,
|
||||
pixels_per_point: output.pixels_per_point,
|
||||
pixels_per_point,
|
||||
accesskit: output.platform_output.accesskit_update.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -147,7 +147,9 @@ impl<'a, State> Harness<'a, State> {
|
||||
response = app.run(ui, &mut state, false);
|
||||
});
|
||||
|
||||
renderer.handle_delta(&mut output.textures_delta);
|
||||
if let Some(pass_output) = &mut output.pass_output {
|
||||
renderer.handle_delta(&mut pass_output.textures_delta);
|
||||
}
|
||||
|
||||
let mut harness = Self {
|
||||
app,
|
||||
@@ -269,7 +271,9 @@ impl<'a, State> Harness<'a, State> {
|
||||
.take()
|
||||
.expect("AccessKit was disabled"),
|
||||
);
|
||||
self.renderer.handle_delta(&mut output.textures_delta);
|
||||
if let Some(pass_output) = &mut output.pass_output {
|
||||
self.renderer.handle_delta(&mut pass_output.textures_delta);
|
||||
}
|
||||
self.output = output;
|
||||
|
||||
self.handle_viewport_commands();
|
||||
@@ -630,7 +634,7 @@ impl<'a, State> Harness<'a, State> {
|
||||
/// This will add a [`RectShape`] to the output shapes, for the current frame.
|
||||
/// Will be overwritten on the next call to [`Self::run`].
|
||||
pub fn mask(&mut self, rect: Rect) {
|
||||
self.output.shapes.push(ClippedShape {
|
||||
self.pass_output_mut().shapes.push(ClippedShape {
|
||||
clip_rect: Rect::EVERYTHING,
|
||||
shape: Shape::Rect(RectShape::filled(rect, 0.0, Color32::MAGENTA)),
|
||||
});
|
||||
@@ -652,15 +656,20 @@ impl<'a, State> Harness<'a, State> {
|
||||
mouse_pos + egui::vec2(8.0, 16.0),
|
||||
];
|
||||
|
||||
output.shapes.push(ClippedShape {
|
||||
clip_rect: self.ctx.content_rect(),
|
||||
shape: egui::epaint::PathShape::convex_polygon(
|
||||
triangle,
|
||||
Color32::WHITE,
|
||||
egui::Stroke::new(1.0, Color32::BLACK),
|
||||
)
|
||||
.into(),
|
||||
});
|
||||
output
|
||||
.pass_output
|
||||
.as_mut()
|
||||
.expect("Harness always runs a pass")
|
||||
.shapes
|
||||
.push(ClippedShape {
|
||||
clip_rect: self.ctx.content_rect(),
|
||||
shape: egui::epaint::PathShape::convex_polygon(
|
||||
triangle,
|
||||
Color32::WHITE,
|
||||
egui::Stroke::new(1.0, Color32::BLACK),
|
||||
)
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
|
||||
self.renderer.render(&self.ctx, &output)
|
||||
@@ -677,18 +686,20 @@ impl<'a, State> Harness<'a, State> {
|
||||
/// Resize the harness to the last [`egui::ViewportCommand::InnerSize`] requested by the app
|
||||
/// during the last frame, if any.
|
||||
fn handle_inner_size(&mut self) {
|
||||
let new_inner_size =
|
||||
self.root_viewport_output()
|
||||
.commands
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|command| {
|
||||
if let egui::ViewportCommand::InnerSize(size) = command {
|
||||
Some(*size)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let new_inner_size = self
|
||||
.output
|
||||
.viewport_commands
|
||||
.get(&ViewportId::ROOT)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.rev()
|
||||
.find_map(|command| {
|
||||
if let egui::ViewportCommand::InnerSize(size) = command {
|
||||
Some(*size)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(size) = new_inner_size {
|
||||
self.set_size(size);
|
||||
@@ -702,13 +713,13 @@ impl<'a, State> Harness<'a, State> {
|
||||
/// If a screenshot was requested and no renderer is available, an error will be logged.
|
||||
#[cfg(any(feature = "wgpu", feature = "snapshot"))]
|
||||
fn handle_screenshots(&mut self) {
|
||||
// Collect all screenshot requests from this frame's viewport output.
|
||||
// Collect all screenshot requests from this frame's viewport commands.
|
||||
let requests: Vec<(ViewportId, egui::UserData)> = self
|
||||
.output
|
||||
.viewport_output
|
||||
.viewport_commands
|
||||
.iter()
|
||||
.flat_map(|(id, viewport)| {
|
||||
viewport.commands.iter().filter_map(move |command| {
|
||||
.flat_map(|(id, commands)| {
|
||||
commands.iter().filter_map(move |command| {
|
||||
if let egui::ViewportCommand::Screenshot(user_data) = command {
|
||||
Some((*id, user_data.clone()))
|
||||
} else {
|
||||
@@ -749,11 +760,22 @@ impl<'a, State> Harness<'a, State> {
|
||||
/// Get the root viewport output
|
||||
fn root_viewport_output(&self) -> &egui::ViewportOutput {
|
||||
self.output
|
||||
.expect_pass()
|
||||
.viewport_output
|
||||
.get(&ViewportId::ROOT)
|
||||
.expect("Missing root viewport")
|
||||
}
|
||||
|
||||
/// The output of the last pass.
|
||||
///
|
||||
/// The harness always runs a pass each step, so this always exists.
|
||||
fn pass_output_mut(&mut self) -> &mut egui::PassOutput {
|
||||
self.output
|
||||
.pass_output
|
||||
.as_mut()
|
||||
.expect("Harness always runs a pass")
|
||||
}
|
||||
|
||||
/// The root node of the test harness.
|
||||
pub fn root(&self) -> Node<'_> {
|
||||
Node::new(
|
||||
|
||||
@@ -178,7 +178,8 @@ impl crate::TestRenderer for WgpuTestRenderer {
|
||||
size_in_pixels: [size.x.round() as u32, size.y.round() as u32],
|
||||
};
|
||||
|
||||
let tessellated = ctx.tessellate(output.shapes.clone(), ctx.pixels_per_point());
|
||||
let tessellated =
|
||||
ctx.tessellate(output.expect_pass().shapes.clone(), ctx.pixels_per_point());
|
||||
|
||||
let user_buffers = renderer.update_buffers(
|
||||
&self.render_state.device,
|
||||
|
||||
@@ -145,7 +145,9 @@ fn accesskit_output_single_egui_frame(run_ui: impl FnMut(&mut Ui)) -> TreeUpdate
|
||||
ctx.enable_accesskit();
|
||||
|
||||
let mut output = ctx.run_ui(RawInput::default(), run_ui);
|
||||
output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
|
||||
if let Some(pass_output) = &mut output.pass_output {
|
||||
pass_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
|
||||
}
|
||||
|
||||
output
|
||||
.platform_output
|
||||
|
||||
@@ -402,7 +402,7 @@ pub fn horizontal_wrapped_text_should_not_overlap() {
|
||||
);
|
||||
}
|
||||
|
||||
for clipped in &harness.output().shapes {
|
||||
for clipped in &harness.output().expect_pass().shapes {
|
||||
if let egui::epaint::Shape::Text(text_shape) = &clipped.shape {
|
||||
let shape_rect = text_shape.visual_bounding_rect();
|
||||
assert!(
|
||||
@@ -638,7 +638,7 @@ fn window_fixed_size_is_outer_size() {
|
||||
}
|
||||
|
||||
let mut sizes = Vec::new();
|
||||
for clipped in &harness.output().shapes {
|
||||
for clipped in &harness.output().expect_pass().shapes {
|
||||
collect_filled_rect_sizes(&clipped.shape, &mut sizes);
|
||||
}
|
||||
|
||||
@@ -795,6 +795,7 @@ pub fn textedit_hint_text_should_follow_text_alignment() {
|
||||
// Find the hint text shape (the only text shape while the input is empty).
|
||||
let hint_shape = harness
|
||||
.output()
|
||||
.expect_pass()
|
||||
.shapes
|
||||
.iter()
|
||||
.find_map(|clipped| {
|
||||
|
||||
@@ -264,7 +264,7 @@ fn interact_on_ui_response_should_be_stable() {
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn has_red_warning_rect(output: &egui::FullOutput) -> bool {
|
||||
output.shapes.iter().any(|clipped| {
|
||||
output.expect_pass().shapes.iter().any(|clipped| {
|
||||
matches!(
|
||||
&clipped.shape,
|
||||
Shape::Rect(rect_shape)
|
||||
@@ -631,7 +631,7 @@ fn run_logic_should_not_disturb_ui_state() {
|
||||
.or_default()
|
||||
.occluded = Some(true);
|
||||
|
||||
let output = harness.ctx.run_logic(&raw_input, |ctx| {
|
||||
let output = harness.ctx.run_logic(raw_input, |ctx| {
|
||||
assert_eq!(
|
||||
ctx.input(|i| i.viewport().occluded),
|
||||
Some(true),
|
||||
@@ -642,6 +642,10 @@ fn run_logic_should_not_disturb_ui_state() {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
|
||||
});
|
||||
|
||||
assert!(
|
||||
output.pass_output.is_none(),
|
||||
"No pass was run, so there should be no pass output"
|
||||
);
|
||||
assert_eq!(
|
||||
output
|
||||
.viewport_commands
|
||||
@@ -660,3 +664,55 @@ fn run_logic_should_not_disturb_ui_state() {
|
||||
|
||||
assert_state(&harness);
|
||||
}
|
||||
|
||||
/// Input given to [`egui::Context::run_logic`] is not consumed by any pass,
|
||||
/// so egui must keep it and feed it to the next pass.
|
||||
#[test]
|
||||
fn run_logic_should_buffer_input_for_the_next_pass() {
|
||||
let ctx = egui::Context::default();
|
||||
|
||||
let mut raw_input = egui::RawInput::default();
|
||||
raw_input.events.push(egui::Event::Key {
|
||||
key: egui::Key::A,
|
||||
physical_key: None,
|
||||
pressed: true,
|
||||
repeat: false,
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
});
|
||||
|
||||
let logic_output = ctx.run_logic(raw_input, |_ctx| {});
|
||||
logic_output.drop_without_applying_deltas();
|
||||
|
||||
let ui_output = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
assert!(
|
||||
ui.input(|i| i.key_pressed(egui::Key::A)),
|
||||
"The key press from the pass-less frame should reach the next pass"
|
||||
);
|
||||
});
|
||||
ui_output.drop_without_applying_deltas();
|
||||
}
|
||||
|
||||
/// The logic phase of [`egui::Context::run_frame`] should see the current window state,
|
||||
/// also in frames where ui is shown (and not the state of the previous frame).
|
||||
#[test]
|
||||
fn logic_phase_should_see_fresh_window_state() {
|
||||
let ctx = egui::Context::default();
|
||||
|
||||
let mut raw_input = egui::RawInput::default();
|
||||
raw_input
|
||||
.viewports
|
||||
.entry(egui::ViewportId::ROOT)
|
||||
.or_default()
|
||||
.focused = Some(true);
|
||||
|
||||
let output = ctx.run_frame(raw_input, true, |phase| {
|
||||
if let egui::FramePhase::Logic(ctx) = phase {
|
||||
assert_eq!(
|
||||
ctx.input(|i| i.viewport().focused),
|
||||
Some(true),
|
||||
"App logic should see the current window state, not last frame's"
|
||||
);
|
||||
}
|
||||
});
|
||||
output.drop_without_applying_deltas();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user