diff --git a/README.md b/README.md index 6fa53cbae11..ec20d703975 100644 --- a/README.md +++ b/README.md @@ -98,8 +98,7 @@ On Fedora Rawhide you need to run: * Portable: the same code works on the web and as a native app * Easy to integrate into any environment * A simple 2D graphics API for custom painting ([`epaint`](https://docs.rs/epaint)). -* No callbacks -* Pure immediate mode +* Pure immediate mode: no callbacks * Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs) * Modular: You should be able to use small parts of egui and combine them in new ways * Safe: there is no `unsafe` code in egui @@ -113,7 +112,6 @@ egui is *not* a framework. egui is a library you call into, not an environment y * Become the most powerful GUI library * Native looking interface -* Advanced and flexible layouts (that's fundamentally incompatible with immediate mode) ## State @@ -250,7 +248,8 @@ This is a fundamental shortcoming of immediate mode GUIs, and any attempt to res One workaround is to store the size and use it the next frame. This produces a frame-delay for the correct layout, producing occasional flickering the first frame something shows up. `egui` does this for some things such as windows and grid layouts. -You can also call the layout code twice (once to get the size, once to do the interaction), but that is not only more expensive, it's also complex to implement, and in some cases twice is not enough. `egui` never does this. +The "first-frame jitter" can be covered up with an extra _pass_, which egui supports via `Context::request_discard`. +The downside of this is the added CPU cost of a second pass, so egui only does this in very rare circumstances (the majority of frames are single-pass). For "atomic" widgets (e.g. a button) `egui` knows the size before showing it, so centering buttons, labels etc is possible in `egui` without any special workarounds. diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 1a666095709..29b832bab90 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -251,13 +251,13 @@ impl<'app> GlowWinitApp<'app> { .set_request_repaint_callback(move |info| { log::trace!("request_repaint_callback: {info:?}"); let when = Instant::now() + info.delay; - let frame_nr = info.current_frame_nr; + let cumulative_pass_nr = info.current_cumulative_pass_nr; event_loop_proxy .lock() .send_event(UserEvent::RequestRepaint { viewport_id: info.viewport_id, when, - frame_nr, + cumulative_pass_nr, }) .ok(); }); @@ -346,10 +346,8 @@ impl<'app> GlowWinitApp<'app> { } impl<'app> WinitApp for GlowWinitApp<'app> { - fn frame_nr(&self, viewport_id: ViewportId) -> u64 { - self.running - .as_ref() - .map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id)) + fn egui_ctx(&self) -> Option<&egui::Context> { + self.running.as_ref().map(|r| &r.integration.egui_ctx) } fn window(&self, window_id: WindowId) -> Option> { @@ -712,7 +710,7 @@ impl<'app> GlowWinitRunning<'app> { // give it time to settle: #[cfg(feature = "__screenshot")] - if integration.egui_ctx.frame_nr() == 2 { + if integration.egui_ctx.cumulative_pass_nr() == 2 { if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") { save_screenshot_and_exit(&path, &painter, screen_size_in_pixels); } @@ -1397,7 +1395,7 @@ fn render_immediate_viewport( let ImmediateViewport { ids, builder, - viewport_ui_cb, + mut viewport_ui_cb, } = immediate_viewport; let viewport_id = ids.this; diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index f38c1b8f27f..7964a1e9ad4 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -228,11 +228,16 @@ impl ApplicationHandler for WinitAppWrapper { let event_result = match event { UserEvent::RequestRepaint { when, - frame_nr, + cumulative_pass_nr, viewport_id, } => { - let current_frame_nr = self.winit_app.frame_nr(viewport_id); - if current_frame_nr == frame_nr || current_frame_nr == frame_nr + 1 { + let current_pass_nr = self + .winit_app + .egui_ctx() + .map_or(0, |ctx| ctx.cumulative_pass_nr_for(viewport_id)); + if current_pass_nr == cumulative_pass_nr + || current_pass_nr == cumulative_pass_nr + 1 + { log::trace!("UserEvent::RequestRepaint scheduling repaint at {when:?}"); if let Some(window_id) = self.winit_app.window_id_from_viewport_id(viewport_id) diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index b38b78c9b4e..997383f85c3 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -223,13 +223,13 @@ impl<'app> WgpuWinitApp<'app> { egui_ctx.set_request_repaint_callback(move |info| { log::trace!("request_repaint_callback: {info:?}"); let when = Instant::now() + info.delay; - let frame_nr = info.current_frame_nr; + let cumulative_pass_nr = info.current_cumulative_pass_nr; event_loop_proxy .lock() .send_event(UserEvent::RequestRepaint { when, - frame_nr, + cumulative_pass_nr, viewport_id: info.viewport_id, }) .ok(); @@ -324,10 +324,8 @@ impl<'app> WgpuWinitApp<'app> { } impl<'app> WinitApp for WgpuWinitApp<'app> { - fn frame_nr(&self, viewport_id: ViewportId) -> u64 { - self.running - .as_ref() - .map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id)) + fn egui_ctx(&self) -> Option<&egui::Context> { + self.running.as_ref().map(|r| &r.integration.egui_ctx) } fn window(&self, window_id: WindowId) -> Option> { @@ -916,7 +914,7 @@ fn render_immediate_viewport( let ImmediateViewport { ids, builder, - viewport_ui_cb, + mut viewport_ui_cb, } = immediate_viewport; let input = { diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index 049c90a63ca..e9d214103b8 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -25,6 +25,11 @@ pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Contex egui_ctx.set_embed_viewports(!IS_DESKTOP); + egui_ctx.options_mut(|o| { + // eframe supports multi-pass (Context::request_discard). + o.max_passes = 2.try_into().unwrap(); + }); + let memory = crate::native::epi_integration::load_egui_memory(storage).unwrap_or_default(); egui_ctx.memory_mut(|mem| *mem = memory); @@ -42,8 +47,8 @@ pub enum UserEvent { /// When to repaint. when: Instant, - /// What the frame number was when the repaint was _requested_. - frame_nr: u64, + /// What the cumulative pass number was when the repaint was _requested_. + cumulative_pass_nr: u64, }, /// A request related to [`accesskit`](https://accesskit.dev/). @@ -59,8 +64,7 @@ impl From for UserEvent { } pub trait WinitApp { - /// The current frame number, as reported by egui. - fn frame_nr(&self, viewport_id: ViewportId) -> u64; + fn egui_ctx(&self) -> Option<&egui::Context>; fn window(&self, window_id: WindowId) -> Option>; diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 88673118a91..3fc3f95caa8 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -269,6 +269,8 @@ impl AppRunner { ime, #[cfg(feature = "accesskit")] accesskit_update: _, // not currently implemented + num_completed_passes: _, // handled by `Context::run` + requested_discard: _, // handled by `Context::run` } = platform_output; super::set_cursor_icon(cursor_icon); diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 1a546794cb1..bbd68f8417a 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -823,6 +823,8 @@ impl State { ime, #[cfg(feature = "accesskit")] accesskit_update, + num_completed_passes: _, // `egui::Context::run` handles this + requested_discard: _, // `egui::Context::run` handles this } = platform_output; self.set_cursor_icon(window, cursor_icon); diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index 9883e2c755a..3d2bdd50b53 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -22,10 +22,10 @@ pub type IconPainter = Box ctx.frame_state_mut(|state| { + Side::Left => ctx.pass_state_mut(|state| { state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)); }), - Side::Right => ctx.frame_state_mut(|state| { + Side::Right => ctx.pass_state_mut(|state| { state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)); }), } @@ -885,12 +885,12 @@ impl TopBottomPanel { match side { TopBottomSide::Top => { - ctx.frame_state_mut(|state| { + ctx.pass_state_mut(|state| { state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max)); }); } TopBottomSide::Bottom => { - ctx.frame_state_mut(|state| { + ctx.pass_state_mut(|state| { state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max)); }); } @@ -1149,7 +1149,7 @@ impl CentralPanel { let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); // Only inform ctx about what we actually used, so we can shrink the native window to fit. - ctx.frame_state_mut(|state| state.allocate_central_panel(inner_response.response.rect)); + ctx.pass_state_mut(|state| state.allocate_central_panel(inner_response.response.rect)); inner_response } diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 959d653fffb..45304245ca4 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -1,9 +1,9 @@ //! Show popup windows, tooltips, context menus etc. -use frame_state::PerWidgetTooltipState; +use pass_state::PerWidgetTooltipState; use crate::{ - frame_state, vec2, AboveOrBelow, Align, Align2, Area, AreaState, Context, Frame, Id, + pass_state, vec2, AboveOrBelow, Align, Align2, Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Pos2, Rect, Response, Sense, Ui, UiKind, Vec2, Widget, WidgetText, }; @@ -162,7 +162,7 @@ fn show_tooltip_at_dyn<'c, R>( remember_that_tooltip_was_shown(ctx); - let mut state = ctx.frame_state_mut(|fs| { + let mut state = ctx.pass_state_mut(|fs| { // Remember that this is the widget showing the tooltip: fs.layers .entry(parent_layer) @@ -213,14 +213,14 @@ fn show_tooltip_at_dyn<'c, R>( state.tooltip_count += 1; state.bounding_rect = state.bounding_rect.union(response.rect); - ctx.frame_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state)); + ctx.pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state)); inner } /// What is the id of the next tooltip for this widget? pub fn next_tooltip_id(ctx: &Context, widget_id: Id) -> Id { - let tooltip_count = ctx.frame_state(|fs| { + let tooltip_count = ctx.pass_state(|fs| { fs.tooltips .widget_tooltips .get(&widget_id) @@ -409,7 +409,7 @@ pub fn popup_above_or_below_widget( let frame_margin = frame.total_margin(); let inner_width = widget_response.rect.width() - frame_margin.sum().x; - parent_ui.ctx().frame_state_mut(|fs| { + parent_ui.ctx().pass_state_mut(|fs| { fs.layers .entry(parent_ui.layer_id()) .or_default() diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 0921fa24615..ab7da8aff29 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1,7 +1,7 @@ #![allow(clippy::needless_range_loop)] use crate::{ - emath, epaint, frame_state, lerp, pos2, remap, remap_clamp, vec2, Context, Id, NumExt, Pos2, + emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, vec2, Context, Id, NumExt, Pos2, Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, }; @@ -819,10 +819,10 @@ impl Prepared { let scroll_delta = content_ui .ctx() - .frame_state_mut(|state| std::mem::take(&mut state.scroll_delta)); + .pass_state_mut(|state| std::mem::take(&mut state.scroll_delta)); for d in 0..2 { - // FrameState::scroll_delta is inverted from the way we apply the delta, so we need to negate it. + // PassState::scroll_delta is inverted from the way we apply the delta, so we need to negate it. let mut delta = -scroll_delta.0[d]; let mut animation = scroll_delta.1; @@ -830,11 +830,11 @@ impl Prepared { // is to avoid them leaking to other scroll areas. let scroll_target = content_ui .ctx() - .frame_state_mut(|state| state.scroll_target[d].take()); + .pass_state_mut(|state| state.scroll_target[d].take()); if scroll_enabled[d] { if let Some(target) = scroll_target { - let frame_state::ScrollTarget { + let pass_state::ScrollTarget { range, align, animation: animation_update, diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index dda34db920f..53ce7661531 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -13,9 +13,7 @@ use crate::{ animation_manager::AnimationManager, containers, data::output::PlatformOutput, - epaint, - frame_state::FrameState, - hit_test, + epaint, hit_test, input_state::{InputState, MultiTouchInfo, PointerEvent}, interaction, layers::GraphicLayers, @@ -25,6 +23,7 @@ use crate::{ menu, os::OperatingSystem, output::FullOutput, + pass_state::PassState, resize, scroll_area, util::IdTypeMap, viewport::ViewportClass, @@ -51,11 +50,11 @@ pub struct RequestRepaintInfo { /// Repaint after this duration. If zero, repaint as soon as possible. pub delay: Duration, - /// The current frame number. + /// The number of fully completed passes, of the entire lifetime of the [`Context`]. /// - /// This can be compared to [`Context::frame_nr`] to see if we've already - /// triggered the painting of the next frame. - pub current_frame_nr: u64, + /// This can be compared to [`Context::cumulative_pass_nr`] to see if we we still + /// need another repaint (ui pass / frame), or if one has already happened. + pub current_cumulative_pass_nr: u64, } // ---------------------------------------------------------------------------- @@ -98,8 +97,8 @@ struct NamedContextCallback { /// Callbacks that users can register #[derive(Clone, Default)] struct Plugins { - pub on_begin_frame: Vec, - pub on_end_frame: Vec, + pub on_begin_pass: Vec, + pub on_end_pass: Vec, } impl Plugins { @@ -115,12 +114,12 @@ impl Plugins { } } - fn on_begin_frame(&self, ctx: &Context) { - Self::call(ctx, "on_begin_frame", &self.on_begin_frame); + fn on_begin_pass(&self, ctx: &Context) { + Self::call(ctx, "on_begin_pass", &self.on_begin_pass); } - fn on_end_frame(&self, ctx: &Context) { - Self::call(ctx, "on_end_frame", &self.on_end_frame); + fn on_end_pass(&self, ctx: &Context) { + Self::call(ctx, "on_end_pass", &self.on_end_pass); } } @@ -129,7 +128,7 @@ impl Plugins { /// Repaint-logic impl ContextImpl { /// This is where we update the repaint logic. - fn begin_frame_repaint_logic(&mut self, viewport_id: ViewportId) { + fn begin_pass_repaint_logic(&mut self, viewport_id: ViewportId) { let viewport = self.viewports.entry(viewport_id).or_default(); std::mem::swap( @@ -138,7 +137,7 @@ impl ContextImpl { ); viewport.repaint.causes.clear(); - viewport.repaint.prev_frame_paint_delay = viewport.repaint.repaint_delay; + viewport.repaint.prev_pass_paint_delay = viewport.repaint.repaint_delay; if viewport.repaint.outstanding == 0 { // We are repainting now, so we can wait a while for the next repaint. @@ -150,7 +149,7 @@ impl ContextImpl { (callback)(RequestRepaintInfo { viewport_id, delay: Duration::ZERO, - current_frame_nr: viewport.repaint.frame_nr, + current_cumulative_pass_nr: viewport.repaint.cumulative_pass_nr, }); } } @@ -196,17 +195,17 @@ impl ContextImpl { (callback)(RequestRepaintInfo { viewport_id, delay, - current_frame_nr: viewport.repaint.frame_nr, + current_cumulative_pass_nr: viewport.repaint.cumulative_pass_nr, }); } } } #[must_use] - fn requested_immediate_repaint_prev_frame(&self, viewport_id: &ViewportId) -> bool { - self.viewports.get(viewport_id).map_or(false, |v| { - v.repaint.requested_immediate_repaint_prev_frame() - }) + fn requested_immediate_repaint_prev_pass(&self, viewport_id: &ViewportId) -> bool { + self.viewports + .get(viewport_id) + .map_or(false, |v| v.repaint.requested_immediate_repaint_prev_pass()) } #[must_use] @@ -241,38 +240,42 @@ pub struct ViewportState { pub input: InputState, - /// State that is collected during a frame and then cleared. - pub this_frame: FrameState, + /// State that is collected during a pass and then cleared. + pub this_pass: PassState, - /// The final [`FrameState`] from last frame. + /// The final [`PassState`] from last pass. /// /// Only read from. - pub prev_frame: FrameState, + pub prev_pass: PassState, - /// Has this viewport been updated this frame? + /// Has this viewport been updated this pass? pub used: bool, /// State related to repaint scheduling. repaint: ViewportRepaintInfo, // ---------------------- - // Updated at the start of the frame: + // Updated at the start of the pass: // /// Which widgets are under the pointer? pub hits: WidgetHits, - /// What widgets are being interacted with this frame? + /// What widgets are being interacted with this pass? /// - /// Based on the widgets from last frame, and input in this frame. + /// Based on the widgets from last pass, and input in this pass. pub interact_widgets: InteractionSnapshot, // ---------------------- - // The output of a frame: + // The output of a pass: // pub graphics: GraphicLayers, // Most of the things in `PlatformOutput` are not actually viewport dependent. pub output: PlatformOutput, pub commands: Vec, + + // ---------------------- + // Cross-frame statistics: + pub num_multipass_in_row: usize, } /// What called [`Context::request_repaint`]? @@ -313,37 +316,37 @@ impl std::fmt::Display for RepaintCause { /// Per-viewport state related to repaint scheduling. struct ViewportRepaintInfo { /// Monotonically increasing counter. - frame_nr: u64, + cumulative_pass_nr: u64, /// The duration which the backend will poll for new events /// before forcing another egui update, even if there's no new events. /// - /// Also used to suppress multiple calls to the repaint callback during the same frame. + /// Also used to suppress multiple calls to the repaint callback during the same pass. /// /// This is also returned in [`crate::ViewportOutput`]. repaint_delay: Duration, - /// While positive, keep requesting repaints. Decrement at the start of each frame. + /// While positive, keep requesting repaints. Decrement at the start of each pass. outstanding: u8, - /// What caused repaints during this frame? + /// What caused repaints during this pass? causes: Vec, - /// What triggered a repaint the previous frame? + /// What triggered a repaint the previous pass? /// (i.e: why are we updating now?) prev_causes: Vec, - /// What was the output of `repaint_delay` on the previous frame? + /// What was the output of `repaint_delay` on the previous pass? /// /// If this was zero, we are repainting as quickly as possible /// (as far as we know). - prev_frame_paint_delay: Duration, + prev_pass_paint_delay: Duration, } impl Default for ViewportRepaintInfo { fn default() -> Self { Self { - frame_nr: 0, + cumulative_pass_nr: 0, // We haven't scheduled a repaint yet. repaint_delay: Duration::MAX, @@ -354,14 +357,14 @@ impl Default for ViewportRepaintInfo { causes: Default::default(), prev_causes: Default::default(), - prev_frame_paint_delay: Duration::MAX, + prev_pass_paint_delay: Duration::MAX, } } } impl ViewportRepaintInfo { - pub fn requested_immediate_repaint_prev_frame(&self) -> bool { - self.prev_frame_paint_delay == Duration::ZERO + pub fn requested_immediate_repaint_prev_pass(&self) -> bool { + self.prev_pass_paint_delay == Duration::ZERO } } @@ -390,7 +393,7 @@ struct ContextImpl { /// See . tex_manager: WrappedTextureManager, - /// Set during the frame, becomes active at the start of the next frame. + /// Set during the pass, becomes active at the start of the next pass. new_zoom_factor: Option, os: OperatingSystem, @@ -417,7 +420,7 @@ struct ContextImpl { } impl ContextImpl { - fn begin_frame_mut(&mut self, mut new_raw_input: RawInput) { + fn begin_pass(&mut self, mut new_raw_input: RawInput) { let viewport_id = new_raw_input.viewport_id; let parent_id = new_raw_input .viewports @@ -429,7 +432,7 @@ impl ContextImpl { let is_outermost_viewport = self.viewport_stack.is_empty(); // not necessarily root, just outermost immediate viewport self.viewport_stack.push(ids); - self.begin_frame_repaint_logic(viewport_id); + self.begin_pass_repaint_logic(viewport_id); let viewport = self.viewports.entry(viewport_id).or_default(); @@ -458,23 +461,23 @@ impl ContextImpl { let viewport = self.viewports.entry(self.viewport_id()).or_default(); - self.memory.begin_frame(&new_raw_input, &all_viewport_ids); + self.memory.begin_pass(&new_raw_input, &all_viewport_ids); - viewport.input = std::mem::take(&mut viewport.input).begin_frame( + viewport.input = std::mem::take(&mut viewport.input).begin_pass( new_raw_input, - viewport.repaint.requested_immediate_repaint_prev_frame(), + viewport.repaint.requested_immediate_repaint_prev_pass(), pixels_per_point, &self.memory.options, ); let screen_rect = viewport.input.screen_rect; - viewport.this_frame.begin_frame(screen_rect); + viewport.this_pass.begin_pass(screen_rect); { let area_order = self.memory.areas().order_map(); - let mut layers: Vec = viewport.prev_frame.widgets.layer_ids().collect(); + let mut layers: Vec = viewport.prev_pass.widgets.layer_ids().collect(); layers.sort_by(|a, b| { if a.order == b.order { @@ -490,7 +493,7 @@ impl ContextImpl { let interact_radius = self.memory.options.style().interaction.interact_radius; crate::hit_test::hit_test( - &viewport.prev_frame.widgets, + &viewport.prev_pass.widgets, &layers, &self.memory.layer_transforms, pos, @@ -502,7 +505,7 @@ impl ContextImpl { viewport.interact_widgets = crate::interaction::interact( &viewport.interact_widgets, - &viewport.prev_frame.widgets, + &viewport.prev_pass.widgets, &viewport.hits, &viewport.input, self.memory.interaction_mut(), @@ -524,14 +527,14 @@ impl ContextImpl { #[cfg(feature = "accesskit")] if self.is_accesskit_enabled { crate::profile_scope!("accesskit"); - use crate::frame_state::AccessKitFrameState; + use crate::pass_state::AccessKitPassState; let id = crate::accesskit_root_id(); let mut builder = accesskit::NodeBuilder::new(accesskit::Role::Window); let pixels_per_point = viewport.input.pixels_per_point(); builder.set_transform(accesskit::Affine::scale(pixels_per_point.into())); let mut node_builders = IdMap::default(); node_builders.insert(id, builder); - viewport.this_frame.accesskit_state = Some(AccessKitFrameState { + viewport.this_pass.accesskit_state = Some(AccessKitPassState { node_builders, parent_stack: vec![id], }); @@ -575,8 +578,8 @@ impl ContextImpl { }); { - crate::profile_scope!("Fonts::begin_frame"); - fonts.begin_frame(pixels_per_point, max_texture_side); + crate::profile_scope!("Fonts::begin_pass"); + fonts.begin_pass(pixels_per_point, max_texture_side); } if is_new && self.memory.options.preload_font_glyphs { @@ -591,7 +594,7 @@ impl ContextImpl { #[cfg(feature = "accesskit")] fn accesskit_node_builder(&mut self, id: Id) -> &mut accesskit::NodeBuilder { - let state = self.viewport().this_frame.accesskit_state.as_mut().unwrap(); + let state = self.viewport().this_pass.accesskit_state.as_mut().unwrap(); let builders = &mut state.node_builders; if let std::collections::hash_map::Entry::Vacant(entry) = builders.entry(id) { entry.insert(Default::default()); @@ -737,14 +740,15 @@ impl Context { writer(&mut self.0.write()) } - /// Run the ui code for one frame. + /// Run the ui code for one 1. /// - /// Put your widgets into a [`crate::SidePanel`], [`crate::TopBottomPanel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`]. + /// At most [`Options::max_passes`] calls will be issued to `run_ui`, + /// and only on the rare occasion that [`Context::request_discard`] is called. + /// Usually, it `run_ui` will only be called once. /// - /// This will modify the internal reference to point to a new generation of [`Context`]. - /// Any old clones of this [`Context`] will refer to the old [`Context`], which will not get new input. + /// Put your widgets into a [`crate::SidePanel`], [`crate::TopBottomPanel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`]. /// - /// You can alternatively run [`Self::begin_frame`] and [`Context::end_frame`]. + /// Instead of calling `run`, you can alternatively use [`Self::begin_pass`] and [`Context::end_pass`]. /// /// ``` /// // One egui context that you keep reusing: @@ -760,38 +764,93 @@ impl Context { /// // handle full_output /// ``` #[must_use] - pub fn run(&self, new_input: RawInput, run_ui: impl FnOnce(&Self)) -> FullOutput { + pub fn run(&self, mut new_input: RawInput, mut run_ui: impl FnMut(&Self)) -> FullOutput { crate::profile_function!(); - self.begin_frame(new_input); - run_ui(self); - self.end_frame() + let viewport_id = new_input.viewport_id; + let max_passes = self.write(|ctx| ctx.memory.options.max_passes.get()); + + let mut output = FullOutput::default(); + debug_assert_eq!(output.platform_output.num_completed_passes, 0); + + loop { + crate::profile_scope!( + "pass", + output.platform_output.num_completed_passes.to_string() + ); + + // We must move the `num_passes` (back) to the viewport output so that [`Self::will_discard`] + // has access to the latest pass count. + self.write(|ctx| { + let viewport = ctx.viewport_for(viewport_id); + viewport.output.num_completed_passes = + std::mem::take(&mut output.platform_output.num_completed_passes); + output.platform_output.requested_discard = false; + }); + + self.begin_pass(new_input.take()); + run_ui(self); + output.append(self.end_pass()); + debug_assert!(0 < output.platform_output.num_completed_passes); + + if !output.platform_output.requested_discard { + break; // no need for another pass + } + + if max_passes <= output.platform_output.num_completed_passes { + #[cfg(feature = "log")] + log::debug!("Ignoring call request_discard, because max_passes={max_passes}"); + + break; + } + } + + self.write(|ctx| { + let did_multipass = 1 < output.platform_output.num_completed_passes; + let viewport = ctx.viewport_for(viewport_id); + if did_multipass { + viewport.num_multipass_in_row += 1; + } else { + viewport.num_multipass_in_row = 0; + } + }); + + output } /// An alternative to calling [`Self::run`]. /// + /// It is usually better to use [`Self::run`], because + /// `run` supports multi-pass layout using [`Self::request_discard`]. + /// /// ``` /// // One egui context that you keep reusing: /// let mut ctx = egui::Context::default(); /// /// // Each frame: /// let input = egui::RawInput::default(); - /// ctx.begin_frame(input); + /// ctx.begin_pass(input); /// /// egui::CentralPanel::default().show(&ctx, |ui| { /// ui.label("Hello egui!"); /// }); /// - /// let full_output = ctx.end_frame(); + /// let full_output = ctx.end_pass(); /// // handle full_output /// ``` - pub fn begin_frame(&self, new_input: RawInput) { + pub fn begin_pass(&self, new_input: RawInput) { crate::profile_function!(); - self.write(|ctx| ctx.begin_frame_mut(new_input)); + self.write(|ctx| ctx.begin_pass(new_input)); - // Plugins run just after the frame has started: - self.read(|ctx| ctx.plugins.clone()).on_begin_frame(self); + // Plugins run just after the pass starts: + self.read(|ctx| ctx.plugins.clone()).on_begin_pass(self); + } + + /// See [`Self::begin_pass`]. + #[deprecated = "Renamed begin_pass"] + pub fn begin_frame(&self, new_input: RawInput) { + self.begin_pass(new_input); } } @@ -874,7 +933,7 @@ impl Context { /// Read-only access to [`PlatformOutput`]. /// - /// This is what egui outputs each frame. + /// This is what egui outputs each pass and frame. /// /// ``` /// # let mut ctx = egui::Context::default(); @@ -891,28 +950,28 @@ impl Context { self.write(move |ctx| writer(&mut ctx.viewport().output)) } - /// Read-only access to [`FrameState`]. + /// Read-only access to [`PassState`]. /// - /// This is only valid between [`Context::begin_frame`] and [`Context::end_frame`]. + /// This is only valid during the call to [`Self::run`] (between [`Self::begin_pass`] and [`Self::end_pass`]). #[inline] - pub(crate) fn frame_state(&self, reader: impl FnOnce(&FrameState) -> R) -> R { - self.write(move |ctx| reader(&ctx.viewport().this_frame)) + pub(crate) fn pass_state(&self, reader: impl FnOnce(&PassState) -> R) -> R { + self.write(move |ctx| reader(&ctx.viewport().this_pass)) } - /// Read-write access to [`FrameState`]. + /// Read-write access to [`PassState`]. /// - /// This is only valid between [`Context::begin_frame`] and [`Context::end_frame`]. + /// This is only valid during the call to [`Self::run`] (between [`Self::begin_pass`] and [`Self::end_pass`]). #[inline] - pub(crate) fn frame_state_mut(&self, writer: impl FnOnce(&mut FrameState) -> R) -> R { - self.write(move |ctx| writer(&mut ctx.viewport().this_frame)) + pub(crate) fn pass_state_mut(&self, writer: impl FnOnce(&mut PassState) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.viewport().this_pass)) } - /// Read-only access to the [`FrameState`] from the previous frame. + /// Read-only access to the [`PassState`] from the previous pass. /// - /// This is swapped at the end of each frame. + /// This is swapped at the end of each pass. #[inline] - pub(crate) fn prev_frame_state(&self, reader: impl FnOnce(&FrameState) -> R) -> R { - self.write(move |ctx| reader(&ctx.viewport().prev_frame)) + pub(crate) fn prev_pass_state(&self, reader: impl FnOnce(&PassState) -> R) -> R { + self.write(move |ctx| reader(&ctx.viewport().prev_pass)) } /// Read-only access to [`Fonts`]. @@ -958,7 +1017,7 @@ impl Context { self.write(move |ctx| writer(&mut ctx.memory.options.tessellation_options)) } - /// If the given [`Id`] has been used previously the same frame at different position, + /// If the given [`Id`] has been used previously the same pass at different position, /// then an error will be printed on screen. /// /// This function is already called for all widgets that do any interaction, @@ -968,7 +1027,7 @@ impl Context { /// The most important thing is that [`Rect::min`] is approximately correct, /// because that's where the warning will be painted. If you don't know what size to pick, just pick [`Vec2::ZERO`]. pub fn check_for_id_clash(&self, id: Id, new_rect: Rect, what: &str) { - let prev_rect = self.frame_state_mut(move |state| state.used_ids.insert(id, new_rect)); + let prev_rect = self.pass_state_mut(move |state| state.used_ids.insert(id, new_rect)); if !self.options(|opt| opt.warn_on_id_clash) { return; @@ -976,7 +1035,7 @@ impl Context { let Some(prev_rect) = prev_rect else { return }; - // it is ok to reuse the same ID for e.g. a frame around a widget, + // It is ok to reuse the same ID for e.g. a frame around a widget, // or to check for interaction with the same widget twice: let is_same_rect = prev_rect.expand(0.1).contains_rect(new_rect) || new_rect.expand(0.1).contains_rect(prev_rect); @@ -1058,7 +1117,7 @@ impl Context { // We add all widgets here, even non-interactive ones, // because we need this list not only for checking for blocking widgets, // but also to know when we have reached the widget we are checking for cover. - viewport.this_frame.widgets.insert(w.layer_id, w); + viewport.this_pass.widgets.insert(w.layer_id, w); if w.sense.focusable { ctx.memory.interested_in_focus(w.id); @@ -1090,17 +1149,17 @@ impl Context { /// Read the response of some widget, which may be called _before_ creating the widget (!). /// - /// This is because widget interaction happens at the start of the frame, using the previous frame's widgets. + /// This is because widget interaction happens at the start of the pass, using the widget rects from the previous pass. /// - /// If the widget was not visible the previous frame (or this frame), this will return `None`. + /// If the widget was not visible the previous pass (or this pass), this will return `None`. pub fn read_response(&self, id: Id) -> Option { self.write(|ctx| { let viewport = ctx.viewport(); viewport - .this_frame + .this_pass .widgets .get(id) - .or_else(|| viewport.prev_frame.widgets.get(id)) + .or_else(|| viewport.prev_pass.widgets.get(id)) .copied() }) .map(|widget_rect| self.get_response(widget_rect)) @@ -1124,8 +1183,8 @@ impl Context { enabled, } = widget_rect; - // previous frame + "highlight next frame" == "highlight this frame" - let highlighted = self.prev_frame_state(|fs| fs.highlight_next_frame.contains(&id)); + // previous pass + "highlight next pass" == "highlight this pass" + let highlighted = self.prev_pass_state(|fs| fs.highlight_next_pass.contains(&id)); let mut res = Response { ctx: self.clone(), @@ -1246,7 +1305,7 @@ impl Context { #[cfg(debug_assertions)] self.write(|ctx| { if ctx.memory.options.style().debug.show_interactive_widgets { - ctx.viewport().this_frame.widgets.set_info(id, make_info()); + ctx.viewport().this_pass.widgets.set_info(id, make_info()); } }); @@ -1267,7 +1326,7 @@ impl Context { Self::layer_painter(self, LayerId::debug()) } - /// Print this text next to the cursor at the end of the frame. + /// Print this text next to the cursor at the end of the pass. /// /// If you call this multiple times, the text will be appended. /// @@ -1375,22 +1434,22 @@ impl Context { } } - /// The current frame number for the current viewport. - /// - /// Starts at zero, and is incremented at the end of [`Self::run`] or by [`Self::end_frame`]. + /// The total number of completed passes (usually there is one pass per rendered frame). /// - /// Between calls to [`Self::run`], this is the frame number of the coming frame. - pub fn frame_nr(&self) -> u64 { - self.frame_nr_for(self.viewport_id()) + /// Starts at zero, and is incremented for each completed pass inside of [`Self::run`] (usually once). + pub fn cumulative_pass_nr(&self) -> u64 { + self.cumulative_pass_nr_for(self.viewport_id()) } - /// The current frame number. - /// - /// Starts at zero, and is incremented at the end of [`Self::run`] or by [`Self::end_frame`]. + /// The total number of completed passes (usually there is one pass per rendered frame). /// - /// Between calls to [`Self::run`], this is the frame number of the coming frame. - pub fn frame_nr_for(&self, id: ViewportId) -> u64 { - self.read(|ctx| ctx.viewports.get(&id).map_or(0, |v| v.repaint.frame_nr)) + /// Starts at zero, and is incremented for each completed pass inside of [`Self::run`] (usually once). + pub fn cumulative_pass_nr_for(&self, id: ViewportId) -> u64 { + self.read(|ctx| { + ctx.viewports + .get(&id) + .map_or(0, |v| v.repaint.cumulative_pass_nr) + }) } /// Call this if there is need to repaint the UI, i.e. if you are showing an animation. @@ -1455,7 +1514,7 @@ impl Context { /// So, it's not that we are requesting repaint within X duration. We are rather timing out /// during app idle time where we are not receiving any new input events. /// - /// This repaints the current viewport + /// This repaints the current viewport. #[track_caller] pub fn request_repaint_after(&self, duration: Duration) { self.request_repaint_after_for(duration, self.viewport_id()); @@ -1498,23 +1557,23 @@ impl Context { /// So, it's not that we are requesting repaint within X duration. We are rather timing out /// during app idle time where we are not receiving any new input events. /// - /// This repaints the specified viewport + /// This repaints the specified viewport. #[track_caller] pub fn request_repaint_after_for(&self, duration: Duration, id: ViewportId) { let cause = RepaintCause::new(); self.write(|ctx| ctx.request_repaint_after(duration, id, cause)); } - /// Was a repaint requested last frame for the current viewport? + /// Was a repaint requested last pass for the current viewport? #[must_use] - pub fn requested_repaint_last_frame(&self) -> bool { - self.requested_repaint_last_frame_for(&self.viewport_id()) + pub fn requested_repaint_last_pass(&self) -> bool { + self.requested_repaint_last_pass_for(&self.viewport_id()) } - /// Was a repaint requested last frame for the given viewport? + /// Was a repaint requested last pass for the given viewport? #[must_use] - pub fn requested_repaint_last_frame_for(&self, viewport_id: &ViewportId) -> bool { - self.read(|ctx| ctx.requested_immediate_repaint_prev_frame(viewport_id)) + pub fn requested_repaint_last_pass_for(&self, viewport_id: &ViewportId) -> bool { + self.read(|ctx| ctx.requested_immediate_repaint_prev_pass(viewport_id)) } /// Has a repaint been requested for the current viewport? @@ -1553,34 +1612,78 @@ impl Context { let callback = Box::new(callback); self.write(|ctx| ctx.request_repaint_callback = Some(callback)); } + + /// Request to discard the visual output of this pass, + /// and to immediately do another one. + /// + /// This can be called to cover up visual glitches during a "sizing pass". + /// For instance, when a [`crate::Grid`] is first shown we don't yet know the + /// width and heights of its columns and rows. egui will do a best guess, + /// but it will likely be wrong. Next pass it can read the sizes from the previous + /// pass, and from there on the widths will be stable. + /// This means the first pass will look glitchy, and ideally should not be shown to the user. + /// So [`crate::Grid`] calls [`Self::request_discard`] to cover up this glitches. + /// + /// There is a limit to how many passes egui will perform, set by [`Options::max_passes`]. + /// Therefore, the request might be declined. + /// + /// You can check if the current pass will be discarded with [`Self::will_discard`]. + /// + /// You should be very conservative with when you call [`Self::request_discard`], + /// as it will cause an extra ui pass, potentially leading to extra CPU use and frame judder. + pub fn request_discard(&self) { + self.output_mut(|o| o.requested_discard = true); + + #[cfg(feature = "log")] + log::trace!( + "request_discard: {}", + if self.will_discard() { + "allowed" + } else { + "denied" + } + ); + } + + /// Will the visual output of this pass be discarded? + /// + /// If true, you can early-out from expensive graphics operations. + /// + /// See [`Self::request_discard`] for more. + pub fn will_discard(&self) -> bool { + self.write(|ctx| { + let vp = ctx.viewport(); + // NOTE: `num_passes` is incremented + vp.output.requested_discard + && vp.output.num_completed_passes + 1 < ctx.memory.options.max_passes.get() + }) + } } /// Callbacks impl Context { - /// Call the given callback at the start of each frame - /// of each viewport. + /// Call the given callback at the start of each pass of each viewport. /// /// This can be used for egui _plugins_. /// See [`crate::debug_text`] for an example. - pub fn on_begin_frame(&self, debug_name: &'static str, cb: ContextCallback) { + pub fn on_begin_pass(&self, debug_name: &'static str, cb: ContextCallback) { let named_cb = NamedContextCallback { debug_name, callback: cb, }; - self.write(|ctx| ctx.plugins.on_begin_frame.push(named_cb)); + self.write(|ctx| ctx.plugins.on_begin_pass.push(named_cb)); } - /// Call the given callback at the end of each frame - /// of each viewport. + /// Call the given callback at the end of each pass of each viewport. /// /// This can be used for egui _plugins_. /// See [`crate::debug_text`] for an example. - pub fn on_end_frame(&self, debug_name: &'static str, cb: ContextCallback) { + pub fn on_end_pass(&self, debug_name: &'static str, cb: ContextCallback) { let named_cb = NamedContextCallback { debug_name, callback: cb, }; - self.write(|ctx| ctx.plugins.on_end_frame.push(named_cb)); + self.write(|ctx| ctx.plugins.on_end_pass.push(named_cb)); } } @@ -1590,7 +1693,7 @@ impl Context { /// The default `egui` fonts only support latin and cyrillic alphabets, /// but you can call this to install additional fonts that support e.g. korean characters. /// - /// The new fonts will become active at the start of the next frame. + /// The new fonts will become active at the start of the next pass. pub fn set_fonts(&self, font_definitions: FontDefinitions) { crate::profile_function!(); @@ -1742,7 +1845,7 @@ impl Context { } /// Set the number of physical pixels for each logical point. - /// Will become active at the start of the next frame. + /// Will become active at the start of the next pass. /// /// This will actually translate to a call to [`Self::set_zoom_factor`]. pub fn set_pixels_per_point(&self, pixels_per_point: f32) { @@ -1773,9 +1876,9 @@ impl Context { } /// Sets zoom factor of the UI. - /// Will become active at the start of the next frame. + /// Will become active at the start of the next pass. /// - /// Note that calling this will not update [`Self::zoom_factor`] until the end of the frame. + /// Note that calling this will not update [`Self::zoom_factor`] until the end of the pass. /// /// This is used to calculate the `pixels_per_point` /// for the UI as `pixels_per_point = zoom_fator * native_pixels_per_point`. @@ -1935,25 +2038,32 @@ impl Context { } impl Context { - /// Call at the end of each frame. + /// Call at the end of each frame if you called [`Context::begin_pass`]. #[must_use] - pub fn end_frame(&self) -> FullOutput { + pub fn end_pass(&self) -> FullOutput { crate::profile_function!(); if self.options(|o| o.zoom_with_keyboard) { crate::gui_zoom::zoom_with_keyboard(self); } - // Plugins run just before the frame ends. - self.read(|ctx| ctx.plugins.clone()).on_end_frame(self); + // Plugins run just before the pass ends. + self.read(|ctx| ctx.plugins.clone()).on_end_pass(self); #[cfg(debug_assertions)] self.debug_painting(); - self.write(|ctx| ctx.end_frame()) + self.write(|ctx| ctx.end_pass()) } - /// Called at the end of the frame. + /// Call at the end of each frame if you called [`Context::begin_pass`]. + #[must_use] + #[deprecated = "Renamed end_pass"] + pub fn end_frame(&self) -> FullOutput { + self.end_pass() + } + + /// Called at the end of the pass. #[cfg(debug_assertions)] fn debug_painting(&self) { let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| { @@ -1966,7 +2076,7 @@ impl Context { let paint_widget_id = |id: Id, text: &str, color: Color32| { if let Some(widget) = - self.write(|ctx| ctx.viewport().this_frame.widgets.get(id).copied()) + self.write(|ctx| ctx.viewport().this_pass.widgets.get(id).copied()) { paint_widget(&widget, text, color); } @@ -1974,7 +2084,7 @@ impl Context { if self.style().debug.show_interactive_widgets { // Show all interactive widgets: - let rects = self.write(|ctx| ctx.viewport().this_frame.widgets.clone()); + let rects = self.write(|ctx| ctx.viewport().this_pass.widgets.clone()); for (layer_id, rects) in rects.layers() { let painter = Painter::new(self.clone(), *layer_id, Rect::EVERYTHING); for rect in rects { @@ -2012,7 +2122,7 @@ impl Context { paint_widget_id(id, "contains_pointer", Color32::BLUE); } - let widget_rects = self.write(|w| w.viewport().this_frame.widgets.clone()); + let widget_rects = self.write(|w| w.viewport().this_pass.widgets.clone()); let mut contains_pointer: Vec = contains_pointer.iter().copied().collect(); contains_pointer.sort_by_key(|&id| { @@ -2069,21 +2179,33 @@ impl Context { } } - if let Some(debug_rect) = self.frame_state_mut(|fs| fs.debug_rect.take()) { + if let Some(debug_rect) = self.pass_state_mut(|fs| fs.debug_rect.take()) { debug_rect.paint(&self.debug_painter()); } + + let num_multipass_in_row = self.viewport(|vp| vp.num_multipass_in_row); + if 3 <= num_multipass_in_row { + // If you see this message, it means we've been paying the cost of multi-pass for multiple frames in a row. + // This is likely a bug. `request_discard` should only be called in rare situations, when some layout changes. + self.debug_painter().debug_text( + Pos2::ZERO, + Align2::LEFT_TOP, + Color32::RED, + format!("egui PERF WARNING: request_discard has been called {num_multipass_in_row} frames in a row"), + ); + } } } impl ContextImpl { - fn end_frame(&mut self) -> FullOutput { + fn end_pass(&mut self) -> FullOutput { let ended_viewport_id = self.viewport_id(); let viewport = self.viewports.entry(ended_viewport_id).or_default(); let pixels_per_point = viewport.input.pixels_per_point; - viewport.repaint.frame_nr += 1; + viewport.repaint.cumulative_pass_nr += 1; - self.memory.end_frame(&viewport.this_frame.used_ids); + self.memory.end_pass(&viewport.this_pass.used_ids); if let Some(fonts) = self.fonts.get(&pixels_per_point.into()) { let tex_mngr = &mut self.tex_manager.0.write(); @@ -2120,7 +2242,7 @@ impl ContextImpl { #[cfg(feature = "accesskit")] { crate::profile_scope!("accesskit"); - let state = viewport.this_frame.accesskit_state.take(); + let state = viewport.this_pass.accesskit_state.take(); if let Some(state) = state { let root_id = crate::accesskit_root_id().accesskit_id(); let nodes = { @@ -2150,12 +2272,12 @@ impl ContextImpl { if self.memory.options.repaint_on_widget_change { crate::profile_function!("compare-widget-rects"); - if viewport.prev_frame.widgets != viewport.this_frame.widgets { + if viewport.prev_pass.widgets != viewport.this_pass.widgets { repaint_needed = true; // Some widget has moved } } - std::mem::swap(&mut viewport.prev_frame, &mut viewport.this_frame); + std::mem::swap(&mut viewport.prev_pass, &mut viewport.this_pass); if repaint_needed { self.request_repaint(ended_viewport_id, RepaintCause::new()); @@ -2188,15 +2310,15 @@ impl ContextImpl { if !viewport.used { #[cfg(feature = "log")] log::debug!( - "Removing viewport {:?} ({:?}): it was never used this frame", + "Removing viewport {:?} ({:?}): it was never used this pass", id, viewport.builder.title ); - return false; // Only keep children that have been updated this frame + return false; // Only keep children that have been updated this pass } - viewport.used = false; // reset so we can check again next frame + viewport.used = false; // reset so we can check again next pass } true @@ -2265,6 +2387,8 @@ impl ContextImpl { } }); + platform_output.num_completed_passes += 1; + FullOutput { platform_output, textures_delta, @@ -2333,13 +2457,13 @@ impl Context { /// This is the "background" area, what egui doesn't cover with panels (but may cover with windows). /// This is also the area to which windows are constrained. pub fn available_rect(&self) -> Rect { - self.frame_state(|s| s.available_rect()) + self.pass_state(|s| s.available_rect()) } /// How much space is used by panels and windows. pub fn used_rect(&self) -> Rect { self.write(|ctx| { - let mut used = ctx.viewport().this_frame.used_by_panels; + let mut used = ctx.viewport().this_pass.used_by_panels; for (_id, window) in ctx.memory.areas().visible_windows() { used = used.union(window.rect()); } @@ -2362,7 +2486,7 @@ impl Context { if let Some(pointer_pos) = pointer_pos { if let Some(layer) = self.layer_id_at(pointer_pos) { if layer.order == Order::Background { - !self.frame_state(|state| state.unused_rect.contains(pointer_pos)) + !self.pass_state(|state| state.unused_rect.contains(pointer_pos)) } else { true } @@ -2399,11 +2523,12 @@ impl Context { /// Highlight this widget, to make it look like it is hovered, even if it isn't. /// - /// The highlight takes on frame to take effect if you call this after the widget has been fully rendered. + /// If you call this after the widget has been fully rendered, + /// then it won't be highlighted until the next ui pass. /// /// See also [`Response::highlight`]. pub fn highlight_widget(&self, id: Id) { - self.frame_state_mut(|fs| fs.highlight_next_frame.insert(id)); + self.pass_state_mut(|fs| fs.highlight_next_pass.insert(id)); } /// Is an egui context menu open? @@ -2434,7 +2559,7 @@ impl Context { /// If you detect a click or drag and wants to know where it happened, use this. /// /// Latest position of the mouse, but ignoring any [`crate::Event::PointerGone`] - /// if there were interactions this frame. + /// if there were interactions this pass. /// When tapping a touch screen, this will be the location of the touch. #[inline(always)] pub fn pointer_interact_pos(&self) -> Option { @@ -2973,7 +3098,7 @@ impl Context { pub fn with_accessibility_parent(&self, _id: Id, f: impl FnOnce() -> R) -> R { // TODO(emilk): this isn't thread-safe - another thread can call this function between the push/pop calls #[cfg(feature = "accesskit")] - self.frame_state_mut(|fs| { + self.pass_state_mut(|fs| { if let Some(state) = fs.accesskit_state.as_mut() { state.parent_stack.push(_id); } @@ -2982,7 +3107,7 @@ impl Context { let result = f(); #[cfg(feature = "accesskit")] - self.frame_state_mut(|fs| { + self.pass_state_mut(|fs| { if let Some(state) = fs.accesskit_state.as_mut() { assert_eq!(state.parent_stack.pop(), Some(_id)); } @@ -3008,7 +3133,7 @@ impl Context { ) -> Option { self.write(|ctx| { ctx.viewport() - .this_frame + .this_pass .accesskit_state .is_some() .then(|| ctx.accesskit_node_builder(id)) @@ -3250,7 +3375,7 @@ impl Context { /// /// If this is the root viewport, this will return [`ViewportId::ROOT`]. /// - /// Don't use this outside of `Self::run`, or after `Self::end_frame`. + /// Don't use this outside of `Self::run`, or after `Self::end_pass`. pub fn viewport_id(&self) -> ViewportId { self.read(|ctx| ctx.viewport_id()) } @@ -3259,7 +3384,7 @@ impl Context { /// /// If this is the root viewport, this will return [`ViewportId::ROOT`]. /// - /// Don't use this outside of `Self::run`, or after `Self::end_frame`. + /// Don't use this outside of `Self::run`, or after `Self::end_pass`. pub fn parent_viewport_id(&self) -> ViewportId { self.read(|ctx| ctx.parent_viewport_id()) } @@ -3340,7 +3465,7 @@ impl Context { /// /// The given id must be unique for each viewport. /// - /// You need to call this each frame when the child viewport should exist. + /// You need to call this each pass when the child viewport should exist. /// /// You can check if the user wants to close the viewport by checking the /// [`crate::ViewportInfo::close_requested`] flags found in [`crate::InputState::viewport`]. @@ -3400,7 +3525,7 @@ impl Context { /// /// The given id must be unique for each viewport. /// - /// You need to call this each frame when the child viewport should exist. + /// You need to call this each pass when the child viewport should exist. /// /// You can check if the user wants to close the viewport by checking the /// [`crate::ViewportInfo::close_requested`] flags found in [`crate::InputState::viewport`]. @@ -3421,7 +3546,7 @@ impl Context { &self, new_viewport_id: ViewportId, builder: ViewportBuilder, - viewport_ui_cb: impl FnOnce(&Self, ViewportClass) -> T, + mut viewport_ui_cb: impl FnMut(&Self, ViewportClass) -> T, ) -> T { crate::profile_function!(); @@ -3484,7 +3609,7 @@ impl Context { /// For widgets that sense both clicks and drags, this will /// not be set until the mouse cursor has moved a certain distance. /// - /// NOTE: if the widget was released this frame, this will be `None`. + /// NOTE: if the widget was released this pass, this will be `None`. /// Use [`Self::drag_stopped_id`] instead. pub fn dragged_id(&self) -> Option { self.interaction_snapshot(|i| i.dragged) @@ -3500,14 +3625,14 @@ impl Context { self.dragged_id() == Some(id) } - /// This widget just started being dragged this frame. + /// This widget just started being dragged this pass. /// /// The same widget should also be found in [`Self::dragged_id`]. pub fn drag_started_id(&self) -> Option { self.interaction_snapshot(|i| i.drag_started) } - /// This widget was being dragged, but was released this frame + /// This widget was being dragged, but was released this pass pub fn drag_stopped_id(&self) -> Option { self.interaction_snapshot(|i| i.drag_stopped) } @@ -3556,3 +3681,138 @@ fn context_impl_send_sync() { fn assert_send_sync() {} assert_send_sync::(); } + +#[cfg(test)] +mod test { + use super::Context; + + #[test] + fn test_single_pass() { + let ctx = Context::default(); + ctx.options_mut(|o| o.max_passes = 1.try_into().unwrap()); + + // A single call, no request to discard: + { + let mut num_calls = 0; + let output = ctx.run(Default::default(), |ctx| { + num_calls += 1; + assert_eq!(ctx.output(|o| o.num_completed_passes), 0); + assert!(!ctx.output(|o| o.requested_discard)); + assert!(!ctx.will_discard()); + }); + assert_eq!(num_calls, 1); + assert_eq!(output.platform_output.num_completed_passes, 1); + assert!(!output.platform_output.requested_discard); + } + + // A single call, with a denied request to discard: + { + let mut num_calls = 0; + let output = ctx.run(Default::default(), |ctx| { + num_calls += 1; + ctx.request_discard(); + assert!(!ctx.will_discard(), "The request should have been denied"); + }); + assert_eq!(num_calls, 1); + assert_eq!(output.platform_output.num_completed_passes, 1); + assert!( + output.platform_output.requested_discard, + "The request should be reported" + ); + } + } + + #[test] + fn test_dual_pass() { + let ctx = Context::default(); + ctx.options_mut(|o| o.max_passes = 2.try_into().unwrap()); + + // Normal single pass: + { + let mut num_calls = 0; + let output = ctx.run(Default::default(), |ctx| { + assert_eq!(ctx.output(|o| o.num_completed_passes), 0); + assert!(!ctx.output(|o| o.requested_discard)); + assert!(!ctx.will_discard()); + num_calls += 1; + }); + assert_eq!(num_calls, 1); + assert_eq!(output.platform_output.num_completed_passes, 1); + assert!(!output.platform_output.requested_discard); + } + + // Request discard once: + { + let mut num_calls = 0; + let output = ctx.run(Default::default(), |ctx| { + assert_eq!(ctx.output(|o| o.num_completed_passes), num_calls); + + assert!(!ctx.will_discard()); + if num_calls == 0 { + ctx.request_discard(); + assert!(ctx.will_discard()); + } + + num_calls += 1; + }); + assert_eq!(num_calls, 2); + assert_eq!(output.platform_output.num_completed_passes, 2); + assert!( + !output.platform_output.requested_discard, + "The request should have been cleared when fulfilled" + ); + } + + // Request discard twice: + { + let mut num_calls = 0; + let output = ctx.run(Default::default(), |ctx| { + assert_eq!(ctx.output(|o| o.num_completed_passes), num_calls); + + assert!(!ctx.will_discard()); + ctx.request_discard(); + if num_calls == 0 { + assert!(ctx.will_discard(), "First request granted"); + } else { + assert!(!ctx.will_discard(), "Second request should be denied"); + } + + num_calls += 1; + }); + assert_eq!(num_calls, 2); + assert_eq!(output.platform_output.num_completed_passes, 2); + assert!( + output.platform_output.requested_discard, + "The unfulfilled request should be reported" + ); + } + } + + #[test] + fn test_multi_pass() { + let ctx = Context::default(); + ctx.options_mut(|o| o.max_passes = 10.try_into().unwrap()); + + // Request discard three times: + { + let mut num_calls = 0; + let output = ctx.run(Default::default(), |ctx| { + assert_eq!(ctx.output(|o| o.num_completed_passes), num_calls); + + assert!(!ctx.will_discard()); + if num_calls <= 2 { + ctx.request_discard(); + assert!(ctx.will_discard()); + } + + num_calls += 1; + }); + assert_eq!(num_calls, 4); + assert_eq!(output.platform_output.num_completed_passes, 4); + assert!( + !output.platform_output.requested_discard, + "The request should have been cleared when fulfilled" + ); + } + } +} diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index 5d50afec371..2d967c67be1 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -43,7 +43,7 @@ impl FullOutput { textures_delta, shapes, pixels_per_point, - viewport_output: viewports, + viewport_output, } = newer; self.platform_output.append(platform_output); @@ -51,7 +51,7 @@ impl FullOutput { self.shapes = shapes; // Only paint the latest self.pixels_per_point = pixels_per_point; // Use latest - for (id, new_viewport) in viewports { + for (id, new_viewport) in viewport_output { match self.viewport_output.entry(id) { std::collections::hash_map::Entry::Vacant(entry) => { entry.insert(new_viewport); @@ -123,6 +123,17 @@ pub struct PlatformOutput { /// NOTE: this needs to be per-viewport. #[cfg(feature = "accesskit")] pub accesskit_update: Option, + + /// How many ui passes is this the sum of? + /// + /// See [`crate::Context::request_discard`] for details. + /// + /// This is incremented at the END of each frame, + /// so this will be `0` for the first pass. + pub num_completed_passes: usize, + + /// Was [`crate::Context::request_discard`] called during the latest pass? + pub requested_discard: bool, } impl PlatformOutput { @@ -155,6 +166,8 @@ impl PlatformOutput { ime, #[cfg(feature = "accesskit")] accesskit_update, + num_completed_passes, + requested_discard, } = newer; self.cursor_icon = cursor_icon; @@ -167,6 +180,8 @@ impl PlatformOutput { self.events.append(&mut events); self.mutable_text_under_cursor = mutable_text_under_cursor; self.ime = ime.or(self.ime); + self.num_completed_passes += num_completed_passes; + self.requested_discard |= requested_discard; #[cfg(feature = "accesskit")] { diff --git a/crates/egui/src/debug_text.rs b/crates/egui/src/debug_text.rs index 23f41243cb8..bb9487bd32f 100644 --- a/crates/egui/src/debug_text.rs +++ b/crates/egui/src/debug_text.rs @@ -3,23 +3,23 @@ //! A plugin usually consist of a struct that holds some state, //! which is stored using [`Context::data_mut`]. //! The plugin registers itself onto a specific [`Context`] -//! to get callbacks on certain events ([`Context::on_begin_frame`], [`Context::on_end_frame`]). +//! to get callbacks on certain events ([`Context::on_begin_pass`], [`Context::on_end_pass`]). use crate::{ text, Align, Align2, Color32, Context, FontFamily, FontId, Id, Rect, Shape, Vec2, WidgetText, }; /// Register this plugin on the given egui context, -/// so that it will be called every frame. +/// so that it will be called every pass. /// /// This is a built-in plugin in egui, /// meaning [`Context`] calls this from its `Default` implementation, /// so this is marked as `pub(crate)`. pub(crate) fn register(ctx: &Context) { - ctx.on_end_frame("debug_text", std::sync::Arc::new(State::end_frame)); + ctx.on_end_pass("debug_text", std::sync::Arc::new(State::end_pass)); } -/// Print this text next to the cursor at the end of the frame. +/// Print this text next to the cursor at the end of the pass. /// /// If you call this multiple times, the text will be appended. /// @@ -61,12 +61,12 @@ struct Entry { /// This is a built-in plugin in egui. #[derive(Clone, Default)] struct State { - // This gets re-filled every frame. + // This gets re-filled every pass. entries: Vec, } impl State { - fn end_frame(ctx: &Context) { + fn end_pass(ctx: &Context) { let state = ctx.data_mut(|data| data.remove_temp::(Id::NULL)); if let Some(state) = state { state.paint(ctx); diff --git a/crates/egui/src/drag_and_drop.rs b/crates/egui/src/drag_and_drop.rs index 285fc403f32..13da9d314c1 100644 --- a/crates/egui/src/drag_and_drop.rs +++ b/crates/egui/src/drag_and_drop.rs @@ -23,10 +23,10 @@ pub struct DragAndDrop { impl DragAndDrop { pub(crate) fn register(ctx: &Context) { - ctx.on_end_frame("debug_text", std::sync::Arc::new(Self::end_frame)); + ctx.on_end_pass("debug_text", std::sync::Arc::new(Self::end_pass)); } - fn end_frame(ctx: &Context) { + fn end_pass(ctx: &Context) { let abort_dnd = ctx.input(|i| i.pointer.any_released() || i.key_pressed(crate::Key::Escape)); diff --git a/crates/egui/src/grid.rs b/crates/egui/src/grid.rs index d1eddadbba5..42296e36290 100644 --- a/crates/egui/src/grid.rs +++ b/crates/egui/src/grid.rs @@ -434,7 +434,14 @@ impl Grid { let mut ui_builder = UiBuilder::new().max_rect(max_rect); if prev_state.is_none() { - // Hide the ui this frame, and make things as narrow as possible. + // The initial frame will be glitchy, because we don't know the sizes of things to come. + + if ui.is_visible() { + // Try to cover up the glitchy initial frame: + ui.ctx().request_discard(); + } + + // Hide the ui this frame, and make things as narrow as possible: ui_builder = ui_builder.sizing_pass().invisible(); } diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 8c2c5e79254..7f743ee709a 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -262,7 +262,7 @@ impl Default for InputState { impl InputState { #[must_use] - pub fn begin_frame( + pub fn begin_pass( mut self, mut new: RawInput, requested_immediate_repaint_prev_frame: bool, @@ -285,9 +285,9 @@ impl InputState { let screen_rect = new.screen_rect.unwrap_or(self.screen_rect); self.create_touch_states_for_new_devices(&new.events); for touch_state in self.touch_states.values_mut() { - touch_state.begin_frame(time, &new, self.pointer.interact_pos); + touch_state.begin_pass(time, &new, self.pointer.interact_pos); } - let pointer = self.pointer.begin_frame(time, &new, options); + let pointer = self.pointer.begin_pass(time, &new, options); let mut keys_down = self.keys_down; let mut zoom_factor_delta = 1.0; // TODO(emilk): smoothing for zoom factor @@ -900,7 +900,7 @@ impl Default for PointerState { impl PointerState { #[must_use] - pub(crate) fn begin_frame( + pub(crate) fn begin_pass( mut self, time: f64, new: &RawInput, diff --git a/crates/egui/src/input_state/touch_state.rs b/crates/egui/src/input_state/touch_state.rs index e9425dba24c..df39d961925 100644 --- a/crates/egui/src/input_state/touch_state.rs +++ b/crates/egui/src/input_state/touch_state.rs @@ -134,7 +134,7 @@ impl TouchState { } } - pub fn begin_frame(&mut self, time: f64, new: &RawInput, pointer_pos: Option) { + pub fn begin_pass(&mut self, time: f64, new: &RawInput, pointer_pos: Option) { let mut added_or_removed_touches = false; for event in &new.events { match *event { diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 55b0f5d89b8..29cd9ddcac8 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -223,6 +223,25 @@ //! //! Read more about the pros and cons of immediate mode at . //! +//! ## Multi-pass immediate mode +//! By default, egui usually only does one pass for each rendered frame. +//! However, egui supports multi-pass immediate mode. +//! Another pass can be requested with [`Context::request_discard`]. +//! +//! This is used by some widgets to cover up "first-frame jitters". +//! For instance, the [`Grid`] needs to know the width of all columns before it can properly place the widgets. +//! But it cannot know the width of widgets to come. +//! So it stores the max widths of previous frames and uses that. +//! This means the first time a `Grid` is shown it will _guess_ the widths of the columns, and will usually guess wrong. +//! This means the contents of the grid will be wrong for one frame, before settling to the correct places. +//! Therefore `Grid` calls [`Context::request_discard`] when it is first shown, so the wrong placement is never +//! visible to the end user. +//! +//! This is an example of a form of multi-pass immediate mode, where earlier passes are used for sizing, +//! and later passes for layout. +//! +//! See [`Context::request_discard`] and [`Options::max_passes`] for more. +//! //! # Misc //! //! ## How widgets works @@ -379,7 +398,6 @@ mod context; mod data; pub mod debug_text; mod drag_and_drop; -mod frame_state; pub(crate) mod grid; pub mod gui_zoom; mod hit_test; @@ -394,6 +412,7 @@ mod memory; pub mod menu; pub mod os; mod painter; +mod pass_state; pub(crate) mod placer; mod response; mod sense; @@ -655,7 +674,7 @@ pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) { } /// For use in tests; especially doctests. -pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) { +pub fn __run_test_ui(add_contents: impl Fn(&mut Ui)) { let ctx = Context::default(); ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time) let _ = ctx.run(Default::default(), |ctx| { diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index ee12d8de845..b6711de3c52 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -302,7 +302,7 @@ pub trait BytesLoader { /// Implementations may use this to perform work at the end of a frame, /// such as evicting unused entries from a cache. - fn end_frame(&self, frame_index: usize) { + fn end_pass(&self, frame_index: usize) { let _ = frame_index; } @@ -367,9 +367,9 @@ pub trait ImageLoader { /// so that all of them may be fully reloaded. fn forget_all(&self); - /// Implementations may use this to perform work at the end of a frame, + /// Implementations may use this to perform work at the end of a pass, /// such as evicting unused entries from a cache. - fn end_frame(&self, frame_index: usize) { + fn end_pass(&self, frame_index: usize) { let _ = frame_index; } @@ -505,9 +505,9 @@ pub trait TextureLoader { /// so that all of them may be fully reloaded. fn forget_all(&self); - /// Implementations may use this to perform work at the end of a frame, + /// Implementations may use this to perform work at the end of a pass, /// such as evicting unused entries from a cache. - fn end_frame(&self, frame_index: usize) { + fn end_pass(&self, frame_index: usize) { let _ = frame_index; } diff --git a/crates/egui/src/load/texture_loader.rs b/crates/egui/src/load/texture_loader.rs index 88533e5d95d..6845e86ff82 100644 --- a/crates/egui/src/load/texture_loader.rs +++ b/crates/egui/src/load/texture_loader.rs @@ -62,7 +62,7 @@ impl TextureLoader for DefaultTextureLoader { self.cache.lock().clear(); } - fn end_frame(&self, _: usize) {} + fn end_pass(&self, _: usize) {} fn byte_size(&self) -> usize { self.cache diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index e2db6787136..cf0d2b872d1 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -1,5 +1,7 @@ #![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs +use std::num::NonZeroUsize; + use ahash::{HashMap, HashSet}; use epaint::emath::TSTransform; @@ -228,6 +230,23 @@ pub struct Options { /// (). pub repaint_on_widget_change: bool, + /// Maximum number of passes to run in one frame. + /// + /// Set to `1` for pure single-pass immediate mode. + /// Set to something larger than `1` to allow multi-pass when needed. + /// + /// Default is `2`. This means sometimes a frame will cost twice as much, + /// but usually only rarely (e.g. when showing a new panel for the first time). + /// + /// egui will usually only ever run one pass, even if `max_passes` is large. + /// + /// If this is `1`, [`crate::Context::request_discard`] will be ignored. + /// + /// Multi-pass is supported by [`crate::Context::run`]. + /// + /// See [`crate::Context::request_discard`] for more. + pub max_passes: NonZeroUsize, + /// This is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud. /// /// The only change to egui is that labels can be focused by pressing tab. @@ -297,6 +316,7 @@ impl Default for Options { zoom_with_keyboard: true, tessellation_options: Default::default(), repaint_on_widget_change: false, + max_passes: NonZeroUsize::new(2).unwrap(), screen_reader: false, preload_font_glyphs: true, warn_on_id_clash: cfg!(debug_assertions), @@ -311,7 +331,7 @@ impl Default for Options { } impl Options { - pub(crate) fn begin_frame(&mut self, new_raw_input: &RawInput) { + pub(crate) fn begin_pass(&mut self, new_raw_input: &RawInput) { self.system_theme = new_raw_input.system_theme; } @@ -352,6 +372,7 @@ impl Options { zoom_with_keyboard, tessellation_options, repaint_on_widget_change, + max_passes, screen_reader: _, // needs to come from the integration preload_font_glyphs: _, warn_on_id_clash, @@ -367,6 +388,11 @@ impl Options { CollapsingHeader::new("⚙ Options") .default_open(false) .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label("Max passes:"); + ui.add(crate::DragValue::new(max_passes).range(0..=10)); + }); + ui.checkbox( repaint_on_widget_change, "Repaint if any widget moves or changes id", @@ -515,7 +541,7 @@ impl Focus { self.focused_widget.as_ref().map(|w| w.id) } - fn begin_frame(&mut self, new_input: &crate::data::input::RawInput) { + fn begin_pass(&mut self, new_input: &crate::data::input::RawInput) { self.id_previous_frame = self.focused(); if let Some(id) = self.id_next_frame.take() { self.focused_widget = Some(FocusWidget::new(id)); @@ -576,7 +602,7 @@ impl Focus { } } - pub(crate) fn end_frame(&mut self, used_ids: &IdMap) { + pub(crate) fn end_pass(&mut self, used_ids: &IdMap) { if self.focus_direction.is_cardinal() { if let Some(found_widget) = self.find_widget_in_direction(used_ids) { self.focused_widget = Some(FocusWidget::new(found_widget)); @@ -726,7 +752,7 @@ impl Focus { } impl Memory { - pub(crate) fn begin_frame(&mut self, new_raw_input: &RawInput, viewports: &ViewportIdSet) { + pub(crate) fn begin_pass(&mut self, new_raw_input: &RawInput, viewports: &ViewportIdSet) { crate::profile_function!(); self.viewport_id = new_raw_input.viewport_id; @@ -739,18 +765,18 @@ impl Memory { // self.interactions is handled elsewhere - self.options.begin_frame(new_raw_input); + self.options.begin_pass(new_raw_input); self.focus .entry(self.viewport_id) .or_default() - .begin_frame(new_raw_input); + .begin_pass(new_raw_input); } - pub(crate) fn end_frame(&mut self, used_ids: &IdMap) { + pub(crate) fn end_pass(&mut self, used_ids: &IdMap) { self.caches.update(); - self.areas_mut().end_frame(); - self.focus_mut().end_frame(used_ids); + self.areas_mut().end_pass(); + self.focus_mut().end_pass(used_ids); } pub(crate) fn set_viewport_id(&mut self, viewport_id: ViewportId) { @@ -1149,7 +1175,7 @@ impl Areas { .any(|(_, children)| children.contains(layer)) } - pub(crate) fn end_frame(&mut self) { + pub(crate) fn end_pass(&mut self) { let Self { visible_last_frame, visible_current_frame, diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 1683125d4e9..0f6aa3e533f 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -171,7 +171,7 @@ fn menu_popup<'c, R>( let area_id = menu_id.with("__menu"); - ctx.frame_state_mut(|fs| { + ctx.pass_state_mut(|fs| { fs.layers .entry(parent_layer) .or_default() diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index ff7d9879c7f..feefe47db43 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -112,8 +112,11 @@ impl Painter { self.opacity_factor } + /// If `false`, nothing you paint will show up. + /// + /// Also checks [`Context::will_discard`]. pub(crate) fn is_visible(&self) -> bool { - self.fade_to_color != Some(Color32::TRANSPARENT) + self.fade_to_color != Some(Color32::TRANSPARENT) && !self.ctx.will_discard() } /// If `false`, nothing added to the painter will be visible diff --git a/crates/egui/src/frame_state.rs b/crates/egui/src/pass_state.rs similarity index 93% rename from crates/egui/src/frame_state.rs rename to crates/egui/src/pass_state.rs index ebfd0e47be1..bbeaca9b3c6 100644 --- a/crates/egui/src/frame_state.rs +++ b/crates/egui/src/pass_state.rs @@ -7,13 +7,13 @@ use crate::{pos2, Align2, Color32, FontId, NumExt, Painter}; /// Reset at the start of each frame. #[derive(Clone, Debug, Default)] -pub struct TooltipFrameState { +pub struct TooltipPassState { /// If a tooltip has been shown this frame, where was it? /// This is used to prevent multiple tooltips to cover each other. pub widget_tooltips: IdMap, } -impl TooltipFrameState { +impl TooltipPassState { pub fn clear(&mut self) { let Self { widget_tooltips } = self; widget_tooltips.clear(); @@ -69,7 +69,7 @@ impl ScrollTarget { #[cfg(feature = "accesskit")] #[derive(Clone)] -pub struct AccessKitFrameState { +pub struct AccessKitPassState { pub node_builders: IdMap, pub parent_stack: Vec, } @@ -167,16 +167,18 @@ impl DebugRect { } } -/// State that is collected during a frame, then saved for the next frame, +/// State that is collected during a pass, then saved for the next pass, /// and then cleared. /// +/// (NOTE: we usually run only one pass per frame). +/// /// One per viewport. #[derive(Clone)] -pub struct FrameState { - /// All [`Id`]s that were used this frame. +pub struct PassState { + /// All [`Id`]s that were used this pass. pub used_ids: IdMap, - /// All widgets produced this frame. + /// All widgets produced this pass. pub widgets: WidgetRects, /// Per-layer state. @@ -184,7 +186,7 @@ pub struct FrameState { /// Not all layers registers themselves there though. pub layers: HashMap, - pub tooltips: TooltipFrameState, + pub tooltips: TooltipPassState, /// Starts off as the `screen_rect`, shrinks as panels are added. /// The [`crate::CentralPanel`] does not change this. @@ -213,16 +215,16 @@ pub struct FrameState { pub scroll_delta: (Vec2, style::ScrollAnimation), #[cfg(feature = "accesskit")] - pub accesskit_state: Option, + pub accesskit_state: Option, - /// Highlight these widgets the next frame. - pub highlight_next_frame: IdSet, + /// Highlight these widgets the next pass. + pub highlight_next_pass: IdSet, #[cfg(debug_assertions)] pub debug_rect: Option, } -impl Default for FrameState { +impl Default for PassState { fn default() -> Self { Self { used_ids: Default::default(), @@ -236,7 +238,7 @@ impl Default for FrameState { scroll_delta: (Vec2::default(), style::ScrollAnimation::none()), #[cfg(feature = "accesskit")] accesskit_state: None, - highlight_next_frame: Default::default(), + highlight_next_pass: Default::default(), #[cfg(debug_assertions)] debug_rect: None, @@ -244,8 +246,8 @@ impl Default for FrameState { } } -impl FrameState { - pub(crate) fn begin_frame(&mut self, screen_rect: Rect) { +impl PassState { + pub(crate) fn begin_pass(&mut self, screen_rect: Rect) { crate::profile_function!(); let Self { used_ids, @@ -259,7 +261,7 @@ impl FrameState { scroll_delta, #[cfg(feature = "accesskit")] accesskit_state, - highlight_next_frame, + highlight_next_pass, #[cfg(debug_assertions)] debug_rect, @@ -285,7 +287,7 @@ impl FrameState { *accesskit_state = None; } - highlight_next_frame.clear(); + highlight_next_pass.clear(); } /// How much space is still available after panels has been added. diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 3a415e36961..b2fdd5e283e 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -2,8 +2,8 @@ use std::{any::Any, sync::Arc}; use crate::{ emath::{Align, Pos2, Rect, Vec2}, - frame_state, menu, AreaState, Context, CursorIcon, Id, LayerId, Order, PointerButton, Sense, - Ui, WidgetRect, WidgetText, + menu, pass_state, AreaState, Context, CursorIcon, Id, LayerId, Order, PointerButton, Sense, Ui, + WidgetRect, WidgetText, }; // ---------------------------------------------------------------------------- @@ -600,7 +600,7 @@ impl Response { return true; } - let any_open_popups = self.ctx.prev_frame_state(|fs| { + let any_open_popups = self.ctx.prev_pass_state(|fs| { fs.layers .get(&self.layer_id) .map_or(false, |layer| !layer.open_popups.is_empty()) @@ -648,7 +648,7 @@ impl Response { let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id); let tooltip_has_interactive_widget = self.ctx.viewport(|vp| { - vp.prev_frame + vp.prev_pass .widgets .get_layer(tooltip_layer_id) .any(|w| w.enabled && w.sense.interactive()) @@ -696,7 +696,7 @@ impl Response { } } - let is_other_tooltip_open = self.ctx.prev_frame_state(|fs| { + let is_other_tooltip_open = self.ctx.prev_pass_state(|fs| { if let Some(already_open_tooltip) = fs .layers .get(&self.layer_id) @@ -896,13 +896,13 @@ impl Response { align: Option, animation: crate::style::ScrollAnimation, ) { - self.ctx.frame_state_mut(|state| { - state.scroll_target[0] = Some(frame_state::ScrollTarget::new( + self.ctx.pass_state_mut(|state| { + state.scroll_target[0] = Some(pass_state::ScrollTarget::new( self.rect.x_range(), align, animation, )); - state.scroll_target[1] = Some(frame_state::ScrollTarget::new( + state.scroll_target[1] = Some(pass_state::ScrollTarget::new( self.rect.y_range(), align, animation, diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index a2d78da5dea..fe5eac00e78 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -118,11 +118,8 @@ impl Default for LabelSelectionState { impl LabelSelectionState { pub(crate) fn register(ctx: &Context) { - ctx.on_begin_frame( - "LabelSelectionState", - std::sync::Arc::new(Self::begin_frame), - ); - ctx.on_end_frame("LabelSelectionState", std::sync::Arc::new(Self::end_frame)); + ctx.on_begin_pass("LabelSelectionState", std::sync::Arc::new(Self::begin_pass)); + ctx.on_end_pass("LabelSelectionState", std::sync::Arc::new(Self::end_pass)); } pub fn load(ctx: &Context) -> Self { @@ -138,7 +135,7 @@ impl LabelSelectionState { }); } - fn begin_frame(ctx: &Context) { + fn begin_pass(ctx: &Context) { let mut state = Self::load(ctx); if ctx.input(|i| i.pointer.any_pressed() && !i.modifiers.shift) { @@ -159,7 +156,7 @@ impl LabelSelectionState { state.store(ctx); } - fn end_frame(ctx: &Context) { + fn end_pass(ctx: &Context) { let mut state = Self::load(ctx); if state.is_dragging { diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 1fdc001e53a..9638d7a69c9 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -10,10 +10,11 @@ use crate::{ ecolor::Hsva, emath, epaint, epaint::text::Fonts, - frame_state, grid, + grid, layout::{Direction, Layout}, menu, menu::MenuState, + pass_state, placer::Placer, pos2, style, util::IdTypeMap, @@ -469,6 +470,9 @@ impl Ui { } /// If `false`, any widgets added to the [`Ui`] will be invisible and non-interactive. + /// + /// This is `false` if any parent had [`UiBuilder::invisible`] + /// or if [`Context::will_discard`]. #[inline] pub fn is_visible(&self) -> bool { self.painter.is_visible() @@ -659,6 +663,9 @@ impl Ui { } /// Can be used for culling: if `false`, then no part of `rect` will be visible on screen. + /// + /// This is false if the whole `Ui` is invisible (see [`UiBuilder::invisible`]) + /// or if [`Context::will_discard`] is true. pub fn is_rect_visible(&self, rect: Rect) -> bool { self.is_visible() && rect.intersects(self.clip_rect()) } @@ -1336,9 +1343,9 @@ impl Ui { ) { for d in 0..2 { let range = Rangef::new(rect.min[d], rect.max[d]); - self.ctx().frame_state_mut(|state| { + self.ctx().pass_state_mut(|state| { state.scroll_target[d] = - Some(frame_state::ScrollTarget::new(range, align, animation)); + Some(pass_state::ScrollTarget::new(range, align, animation)); }); } } @@ -1378,9 +1385,9 @@ impl Ui { let target = self.next_widget_position(); for d in 0..2 { let target = Rangef::point(target[d]); - self.ctx().frame_state_mut(|state| { + self.ctx().pass_state_mut(|state| { state.scroll_target[d] = - Some(frame_state::ScrollTarget::new(target, align, animation)); + Some(pass_state::ScrollTarget::new(target, align, animation)); }); } } @@ -1420,7 +1427,7 @@ impl Ui { /// Same as [`Self::scroll_with_delta`], but allows you to specify the [`style::ScrollAnimation`]. pub fn scroll_with_delta_animation(&self, delta: Vec2, animation: style::ScrollAnimation) { - self.ctx().frame_state_mut(|state| { + self.ctx().pass_state_mut(|state| { state.scroll_delta.0 += delta; state.scroll_delta.1 = animation; }); @@ -1456,8 +1463,8 @@ impl Ui { /// See also [`Self::add`] and [`Self::put`]. /// /// ``` - /// # let mut my_value = 42; /// # egui::__run_test_ui(|ui| { + /// # let mut my_value = 42; /// ui.add_sized([40.0, 20.0], egui::DragValue::new(&mut my_value)); /// # }); /// ``` @@ -2876,14 +2883,14 @@ fn register_rect(ui: &Ui, rect: Rect) { let callstack = String::default(); // We only show one debug rectangle, or things get confusing: - let debug_rect = frame_state::DebugRect { + let debug_rect = pass_state::DebugRect { rect, callstack, is_clicking, }; let mut kept = false; - ui.ctx().frame_state_mut(|fs| { + ui.ctx().pass_state_mut(|fs| { if let Some(final_debug_rect) = &mut fs.debug_rect { // or maybe pick the one with deepest callstack? if final_debug_rect.rect.contains_rect(rect) { diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index 9dd71b4747b..e685d3ee6b7 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -1168,5 +1168,5 @@ pub struct ImmediateViewport<'a> { pub builder: ViewportBuilder, /// The user-code that shows the GUI. - pub viewport_ui_cb: Box, + pub viewport_ui_cb: Box, } diff --git a/crates/egui/tests/accesskit.rs b/crates/egui/tests/accesskit.rs index e5dc3d97a93..bcc26d024b9 100644 --- a/crates/egui/tests/accesskit.rs +++ b/crates/egui/tests/accesskit.rs @@ -130,7 +130,7 @@ fn multiple_disabled_widgets() { ); } -fn accesskit_output_single_egui_frame(run_ui: impl FnOnce(&Context)) -> TreeUpdate { +fn accesskit_output_single_egui_frame(run_ui: impl FnMut(&Context)) -> TreeUpdate { let ctx = Context::default(); ctx.enable_accesskit(); diff --git a/crates/egui_demo_app/src/backend_panel.rs b/crates/egui_demo_app/src/backend_panel.rs index e6cc0903058..b1df6f417ea 100644 --- a/crates/egui_demo_app/src/backend_panel.rs +++ b/crates/egui_demo_app/src/backend_panel.rs @@ -147,8 +147,8 @@ impl BackendPanel { if cfg!(debug_assertions) { ui.collapsing("More…", |ui| { ui.horizontal(|ui| { - ui.label("Frame number:"); - ui.monospace(ui.ctx().frame_nr().to_string()); + ui.label("Total ui passes:"); + ui.monospace(ui.ctx().cumulative_pass_nr().to_string()); }); if ui .button("Wait 2s, then request repaint after another 3s") @@ -161,6 +161,16 @@ impl BackendPanel { ctx.request_repaint_after(std::time::Duration::from_secs(3)); }); } + + ui.horizontal(|ui| { + if ui.button("Request discard").clicked() { + ui.ctx().request_discard(); + + if !ui.ctx().will_discard() { + ui.label("Discard denied!"); + } + } + }); }); } } diff --git a/crates/egui_demo_app/src/main.rs b/crates/egui_demo_app/src/main.rs index 61c0e94bb65..f7b36a5aa08 100644 --- a/crates/egui_demo_app/src/main.rs +++ b/crates/egui_demo_app/src/main.rs @@ -24,7 +24,13 @@ fn main() -> eframe::Result { { // Silence wgpu log spam (https://github.com/gfx-rs/wgpu/issues/3206) - let mut rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_owned()); + let mut rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| { + if cfg!(debug_assertions) { + "debug".to_owned() + } else { + "info".to_owned() + } + }); for loud_crate in ["naga", "wgpu_core", "wgpu_hal"] { if !rust_log.contains(&format!("{loud_crate}=")) { rust_log += &format!(",{loud_crate}=warn"); diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index 75c68a0de32..d3820603d5c 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -69,7 +69,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { { let ctx = egui::Context::default(); - ctx.begin_frame(RawInput::default()); + ctx.begin_pass(RawInput::default()); egui::CentralPanel::default().show(&ctx, |ui| { c.bench_function("Painter::rect", |b| { @@ -81,7 +81,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { }); }); - // Don't call `end_frame` to not have to drain the huge paint list + // Don't call `end_pass` to not have to drain the huge paint list } { diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 7f8cea7f27b..0f8f87c870a 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -360,7 +360,7 @@ impl FontDefinitions { /// /// If you are using `egui`, use `egui::Context::set_fonts` and `egui::Context::fonts`. /// -/// You need to call [`Self::begin_frame`] and [`Self::font_image_delta`] once every frame. +/// You need to call [`Self::begin_pass`] and [`Self::font_image_delta`] once every frame. #[derive(Clone)] pub struct Fonts(Arc>); @@ -389,7 +389,7 @@ impl Fonts { /// /// This function will react to changes in `pixels_per_point` and `max_texture_side`, /// as well as notice when the font atlas is getting full, and handle that. - pub fn begin_frame(&self, pixels_per_point: f32, max_texture_side: usize) { + pub fn begin_pass(&self, pixels_per_point: f32, max_texture_side: usize) { let mut fonts_and_cache = self.0.lock(); let pixels_per_point_changed = fonts_and_cache.fonts.pixels_per_point != pixels_per_point; @@ -503,7 +503,7 @@ impl Fonts { /// How full is the font atlas? /// /// This increases as new fonts and/or glyphs are used, - /// but can also decrease in a call to [`Self::begin_frame`]. + /// but can also decrease in a call to [`Self::begin_pass`]. pub fn font_atlas_fill_ratio(&self) -> f32 { self.lock().fonts.atlas.lock().fill_ratio() } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index df20c63c31d..2099e3cf6f1 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -479,7 +479,7 @@ impl TextWrapping { /// Needs to be recreated if the underlying font atlas texture changes, which /// happens under the following conditions: /// - `pixels_per_point` or `max_texture_size` change. These parameters are set -/// in [`crate::text::Fonts::begin_frame`]. When using `egui` they are set +/// in [`crate::text::Fonts::begin_pass`]. When using `egui` they are set /// from `egui::InputState` and can change at any time. /// - The atlas has become full. This can happen any time a new glyph is added /// to the atlas, which in turn can happen any time new text is laid out. diff --git a/tests/test_viewports/src/main.rs b/tests/test_viewports/src/main.rs index 52cba800abd..24046e041ac 100644 --- a/tests/test_viewports/src/main.rs +++ b/tests/test_viewports/src/main.rs @@ -215,7 +215,7 @@ fn generic_ui(ui: &mut egui::Ui, children: &[Arc>], close_ let ctx = ui.ctx().clone(); ui.label(format!( "Frame nr: {} (this increases when this viewport is being rendered)", - ctx.frame_nr() + ctx.cumulative_pass_nr() )); ui.horizontal(|ui| { let mut show_spinner =