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.