From 643d8f502fbe0b625f410cd1bb034df075dc0cf0 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 1 Feb 2024 11:33:23 +0100 Subject: [PATCH] Auto-repaint when widgets move or changes id. (#3930) With this PR, if a widget moves or repaints, egui will automatically repaint. Usually this is what you want: if something is moving we should repaint until it stops moving. However, this could potentially create false positives in some rare circumstances, so there is an option to turn it off with `Options::repaint_on_widget_change`. --- crates/egui/src/context.rs | 130 +++++++++++++++++++++++++------------ crates/egui/src/lib.rs | 2 +- crates/egui/src/memory.rs | 58 ++++++++++++++++- 3 files changed, 145 insertions(+), 45 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index fa00bddff3c..8cbf00a63a0 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -176,12 +176,50 @@ impl ContextImpl { /// Used to store each widgets [Id], [Rect] and [Sense] each frame. /// Used to check for overlaps between widgets when handling events. -struct WidgetRect { - id: Id, - rect: Rect, - sense: Sense, +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WidgetRect { + /// Where the widget is. + pub rect: Rect, + + /// The globally unique widget id. + /// + /// For interactive widgets, this better be globally unique. + /// If not there will get weird bugs, + /// and also big red warning test on the screen in debug builds + /// (see [`Options::warn_on_id_clash`]). + /// + /// You can ensure globally unique ids using [`Ui::push_id`]. + pub id: Id, + + /// How the widget responds to interaction. + pub sense: Sense, +} + +/// Stores the positions of all widgets generated during a single egui update/frame. +/// +/// Acgtually, only those that are on screen. +#[derive(Default, Clone, PartialEq, Eq)] +pub struct WidgetRects { + /// All widgets, in painting order. + pub by_layer: HashMap>, +} + +impl WidgetRects { + /// Clear the contents while retaining allocated memory. + pub fn clear(&mut self) { + for rects in self.by_layer.values_mut() { + rects.clear(); + } + } + + /// Insert the given widget rect in the given layer. + pub fn insert(&mut self, layer_id: LayerId, widget_rect: WidgetRect) { + self.by_layer.entry(layer_id).or_default().push(widget_rect); + } } +// ---------------------------------------------------------------------------- + /// State stored per viewport #[derive(Default)] struct ViewportState { @@ -208,10 +246,10 @@ struct ViewportState { used: bool, /// Written to during the frame. - layer_rects_this_frame: HashMap>, + layer_rects_this_frame: WidgetRects, /// Read - layer_rects_prev_frame: HashMap>, + layer_rects_prev_frame: WidgetRects, /// State related to repaint scheduling. repaint: ViewportRepaintInfo, @@ -360,14 +398,6 @@ impl ContextImpl { .native_pixels_per_point .unwrap_or(1.0); - { - std::mem::swap( - &mut viewport.layer_rects_prev_frame, - &mut viewport.layer_rects_this_frame, - ); - viewport.layer_rects_this_frame.clear(); - } - let all_viewport_ids: ViewportIdSet = self.all_viewport_ids(); let viewport = self.viewports.entry(self.viewport_id()).or_default(); @@ -607,12 +637,12 @@ impl Default for Context { } impl Context { - // Do read-only (shared access) transaction on Context + /// Do read-only (shared access) transaction on Context fn read(&self, reader: impl FnOnce(&ContextImpl) -> R) -> R { reader(&self.0.read()) } - // Do read-write (exclusive access) transaction on Context + /// Do read-write (exclusive access) transaction on Context fn write(&self, writer: impl FnOnce(&mut ContextImpl) -> R) -> R { writer(&mut self.0.write()) } @@ -843,19 +873,21 @@ impl Context { // 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: - if prev_rect.expand(0.1).contains_rect(new_rect) - || new_rect.expand(0.1).contains_rect(prev_rect) - { + let is_same_rect = prev_rect.expand(0.1).contains_rect(new_rect) + || new_rect.expand(0.1).contains_rect(prev_rect); + if is_same_rect { return; } let show_error = |widget_rect: Rect, text: String| { + let screen_rect = self.screen_rect(); + let text = format!("🔥 {text}"); let color = self.style().visuals.error_fg_color; let painter = self.debug_painter(); painter.rect_stroke(widget_rect, 0.0, (1.0, color)); - let below = widget_rect.bottom() + 32.0 < self.input(|i| i.screen_rect.bottom()); + let below = widget_rect.bottom() + 32.0 < screen_rect.bottom(); let text_rect = if below { painter.debug_text( @@ -1780,7 +1812,24 @@ impl ContextImpl { let shapes = viewport.graphics.drain(self.memory.areas().order()); - if viewport.input.wants_repaint() { + let mut repaint_needed = false; + + { + if self.memory.options.repaint_on_widget_change { + crate::profile_function!("compare-widget-rects"); + if viewport.layer_rects_prev_frame != viewport.layer_rects_this_frame { + repaint_needed = true; // Some widget has moved + } + } + + std::mem::swap( + &mut viewport.layer_rects_prev_frame, + &mut viewport.layer_rects_this_frame, + ); + viewport.layer_rects_this_frame.clear(); + } + + if repaint_needed || viewport.input.wants_repaint() { self.request_repaint(ended_viewport_id); } @@ -2100,6 +2149,8 @@ impl Context { /// /// Will return false if some other area is covering the given layer. /// + /// The given rectangle is assumed to have been clipped by its parent clip rect. + /// /// See also [`Response::contains_pointer`]. pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool { if !rect.is_positive() { @@ -2129,6 +2180,8 @@ impl Context { /// If another widget is covering us and is listening for the same input (click and/or drag), /// this will return false. /// + /// The given rectangle is assumed to have been clipped by its parent clip rect. + /// /// See also [`Response::contains_pointer`]. pub fn widget_contains_pointer( &self, @@ -2137,6 +2190,10 @@ impl Context { sense: Sense, rect: Rect, ) -> bool { + if !rect.is_positive() { + return false; // don't even remember this widget + } + let contains_pointer = self.rect_contains_pointer(layer_id, rect); let mut blocking_widget = None; @@ -2146,19 +2203,17 @@ 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 reach the widget we are checking for cover. + // but also to know when we have reached the widget we are checking for cover. viewport .layer_rects_this_frame - .entry(layer_id) - .or_default() - .push(WidgetRect { id, rect, sense }); + .insert(layer_id, WidgetRect { id, rect, sense }); // Check if any other widget is covering us. // Whichever widget is added LAST (=on top) gets the input. if contains_pointer { let pointer_pos = viewport.input.pointer.interact_pos(); if let Some(pointer_pos) = pointer_pos { - if let Some(rects) = viewport.layer_rects_prev_frame.get(&layer_id) { + if let Some(rects) = viewport.layer_rects_prev_frame.by_layer.get(&layer_id) { for blocking in rects.iter().rev() { if blocking.id == id { // There are no earlier widgets before this one, @@ -2293,25 +2348,14 @@ impl Context { impl Context { /// Show a ui for settings (style and tessellation options). pub fn settings_ui(&self, ui: &mut Ui) { - use crate::containers::*; + let prev_options = self.options(|o| o.clone()); + let mut options = prev_options.clone(); - CollapsingHeader::new("🎑 Style") - .default_open(true) - .show(ui, |ui| { - self.style_ui(ui); - }); + options.ui(ui); - CollapsingHeader::new("✒ Painting") - .default_open(true) - .show(ui, |ui| { - let prev_tessellation_options = self.tessellation_options(|o| *o); - let mut tessellation_options = prev_tessellation_options; - tessellation_options.ui(ui); - ui.vertical_centered(|ui| reset_button(ui, &mut tessellation_options)); - if tessellation_options != prev_tessellation_options { - self.tessellation_options_mut(move |o| *o = tessellation_options); - } - }); + if options != prev_options { + self.options_mut(move |o| *o = options); + } } /// Show the state of egui, including its input and output. diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 67cfacc2c31..cdf198751ce 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -410,7 +410,7 @@ pub mod text { pub use { containers::*, - context::{Context, RequestRepaintInfo}, + context::{Context, RequestRepaintInfo, WidgetRect, WidgetRects}, data::{ input::*, output::{ diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index d1881c93450..8f4c8b169b8 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -154,7 +154,7 @@ impl FocusDirection { // ---------------------------------------------------------------------------- /// Some global options that you can read and write. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct Options { @@ -184,6 +184,11 @@ pub struct Options { /// Controls the tessellator. pub tessellation_options: epaint::TessellationOptions, + /// If any widget moves or changes id, repaint everything. + /// + /// This is `true` by default. + pub repaint_on_widget_change: bool, + /// 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. @@ -216,6 +221,7 @@ impl Default for Options { zoom_factor: 1.0, zoom_with_keyboard: true, tessellation_options: Default::default(), + repaint_on_widget_change: true, screen_reader: false, preload_font_glyphs: true, warn_on_id_clash: cfg!(debug_assertions), @@ -223,6 +229,56 @@ impl Default for Options { } } +impl Options { + /// Show the options in the ui. + pub fn ui(&mut self, ui: &mut crate::Ui) { + let Self { + style, // covered above + zoom_factor: _, // TODO + zoom_with_keyboard, + tessellation_options, + repaint_on_widget_change, + screen_reader: _, // needs to come from the integration + preload_font_glyphs: _, + warn_on_id_clash, + } = self; + + use crate::Widget as _; + + CollapsingHeader::new("⚙ Options") + .default_open(false) + .show(ui, |ui| { + ui.checkbox( + repaint_on_widget_change, + "Repaint if any widget moves or changes id", + ); + + ui.checkbox( + zoom_with_keyboard, + "Zoom with keyboard (Cmd +, Cmd -, Cmd 0)", + ); + + ui.checkbox(warn_on_id_clash, "Warn if two widgets have the same Id"); + }); + + use crate::containers::*; + CollapsingHeader::new("🎑 Style") + .default_open(true) + .show(ui, |ui| { + std::sync::Arc::make_mut(style).ui(ui); + }); + + CollapsingHeader::new("✒ Painting") + .default_open(true) + .show(ui, |ui| { + tessellation_options.ui(ui); + ui.vertical_centered(|ui| crate::reset_button(ui, tessellation_options)); + }); + + ui.vertical_centered(|ui| crate::reset_button(ui, self)); + } +} + // ---------------------------------------------------------------------------- /// Say there is a button in a scroll area.