diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index d0a19eb51..3d91b122b 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -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, - /// 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) { diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index f3c3a7cab..fa39cb5af 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -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: - /// - /// - /// On Windows, an invisible window also uses up all CPU: - /// 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>, + mut viewport_commands: egui::OrderedViewportIdMap>, ) { profiling::function_scope!(); - for (viewport_id, mut commands) in viewport_commands { + let viewport_ids: Vec = 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, - ) { - 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, + ) { + 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")] diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 5be541d17..55519f8fa 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -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>, + mut viewport_commands: egui::OrderedViewportIdMap>, viewports: &mut Viewports, + painter: &mut egui_wgpu::winit::Painter, ) { profiling::function_scope!(); - for (viewport_id, mut commands) in viewport_commands { + let viewport_ids: Vec = 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: -/// -/// -/// On Windows, an invisible window also uses up all CPU: -/// -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, - viewports: &mut Viewports, - painter: &mut egui_wgpu::winit::Painter, - viewport_from_window: &mut HashMap, -) { - 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, + viewports: &mut Viewports, + painter: &mut egui_wgpu::winit::Painter, + viewport_from_window: &mut HashMap, +) { + 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); } diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index b4ec62c09..36fb8a07a 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -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: +/// +/// +/// On Windows, an invisible window also uses up all CPU: +/// +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!(); diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 56e942a80..916b8077c 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -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) { diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 7736c7feb..84cfeb948 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -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, + /// 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(&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> { + self.viewports + .iter_mut() + .filter(|(_, viewport)| !viewport.commands.is_empty()) + .map(|(&id, viewport)| (id, std::mem::take(&mut viewport.commands))) + .collect() + } } impl Context { diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index ae1363641..16d25c3f7 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -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>, + + /// 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, +} + +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, } -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>, } /// Information about text being edited. diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 27ab7035c..d2308a794 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -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, }, }, diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index 962b065a3..920a57af3 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -1266,9 +1266,6 @@ pub struct ViewportOutput { /// `None` for immediate viewports and the ROOT viewport. pub viewport_ui_cb: Option>, - /// Commands to change the viewport, e.g. window title and size. - pub commands: Vec, - /// 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); } } diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index 36accead3..a5e55ab9b 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -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(); } diff --git a/crates/egui_demo_lib/src/lib.rs b/crates/egui_demo_lib/src/lib.rs index 45f4640bd..9072e2399 100644 --- a/crates/egui_demo_lib/src/lib.rs +++ b/crates/egui_demo_lib/src/lib.rs @@ -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 } } diff --git a/crates/egui_glow/src/winit.rs b/crates/egui_glow/src/winit.rs index 9d2b71310..e889bf70f 100644 --- a/crates/egui_glow/src/winit.rs +++ b/crates/egui_glow/src/winit.rs @@ -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, diff --git a/crates/egui_inspection/src/plugin.rs b/crates/egui_inspection/src/plugin.rs index 0ae857ab1..caed61d1b 100644 --- a/crates/egui_inspection/src/plugin.rs +++ b/crates/egui_inspection/src/plugin.rs @@ -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(), }); } diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 2a8575ef1..2e25b9686 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -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( diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index e5266aead..8d7010fab 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -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, diff --git a/crates/egui_kittest/tests/accesskit.rs b/crates/egui_kittest/tests/accesskit.rs index fea8f26da..06b27cbec 100644 --- a/crates/egui_kittest/tests/accesskit.rs +++ b/crates/egui_kittest/tests/accesskit.rs @@ -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 diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index 8aad4dea1..e5dffd221 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -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| { diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index 26772461e..d93e3e527 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -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(); +}