Skip to content

Commit 3a1244f

Browse files
authored
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`.
1 parent d72f92c commit 3a1244f

File tree

3 files changed

+145
-45
lines changed

3 files changed

+145
-45
lines changed

crates/egui/src/context.rs

+87-43
Original file line numberDiff line numberDiff line change
@@ -176,12 +176,50 @@ impl ContextImpl {
176176

177177
/// Used to store each widgets [Id], [Rect] and [Sense] each frame.
178178
/// Used to check for overlaps between widgets when handling events.
179-
struct WidgetRect {
180-
id: Id,
181-
rect: Rect,
182-
sense: Sense,
179+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
180+
pub struct WidgetRect {
181+
/// Where the widget is.
182+
pub rect: Rect,
183+
184+
/// The globally unique widget id.
185+
///
186+
/// For interactive widgets, this better be globally unique.
187+
/// If not there will get weird bugs,
188+
/// and also big red warning test on the screen in debug builds
189+
/// (see [`Options::warn_on_id_clash`]).
190+
///
191+
/// You can ensure globally unique ids using [`Ui::push_id`].
192+
pub id: Id,
193+
194+
/// How the widget responds to interaction.
195+
pub sense: Sense,
196+
}
197+
198+
/// Stores the positions of all widgets generated during a single egui update/frame.
199+
///
200+
/// Acgtually, only those that are on screen.
201+
#[derive(Default, Clone, PartialEq, Eq)]
202+
pub struct WidgetRects {
203+
/// All widgets, in painting order.
204+
pub by_layer: HashMap<LayerId, Vec<WidgetRect>>,
205+
}
206+
207+
impl WidgetRects {
208+
/// Clear the contents while retaining allocated memory.
209+
pub fn clear(&mut self) {
210+
for rects in self.by_layer.values_mut() {
211+
rects.clear();
212+
}
213+
}
214+
215+
/// Insert the given widget rect in the given layer.
216+
pub fn insert(&mut self, layer_id: LayerId, widget_rect: WidgetRect) {
217+
self.by_layer.entry(layer_id).or_default().push(widget_rect);
218+
}
183219
}
184220

221+
// ----------------------------------------------------------------------------
222+
185223
/// State stored per viewport
186224
#[derive(Default)]
187225
struct ViewportState {
@@ -208,10 +246,10 @@ struct ViewportState {
208246
used: bool,
209247

210248
/// Written to during the frame.
211-
layer_rects_this_frame: HashMap<LayerId, Vec<WidgetRect>>,
249+
layer_rects_this_frame: WidgetRects,
212250

213251
/// Read
214-
layer_rects_prev_frame: HashMap<LayerId, Vec<WidgetRect>>,
252+
layer_rects_prev_frame: WidgetRects,
215253

216254
/// State related to repaint scheduling.
217255
repaint: ViewportRepaintInfo,
@@ -360,14 +398,6 @@ impl ContextImpl {
360398
.native_pixels_per_point
361399
.unwrap_or(1.0);
362400

363-
{
364-
std::mem::swap(
365-
&mut viewport.layer_rects_prev_frame,
366-
&mut viewport.layer_rects_this_frame,
367-
);
368-
viewport.layer_rects_this_frame.clear();
369-
}
370-
371401
let all_viewport_ids: ViewportIdSet = self.all_viewport_ids();
372402

373403
let viewport = self.viewports.entry(self.viewport_id()).or_default();
@@ -607,12 +637,12 @@ impl Default for Context {
607637
}
608638

609639
impl Context {
610-
// Do read-only (shared access) transaction on Context
640+
/// Do read-only (shared access) transaction on Context
611641
fn read<R>(&self, reader: impl FnOnce(&ContextImpl) -> R) -> R {
612642
reader(&self.0.read())
613643
}
614644

615-
// Do read-write (exclusive access) transaction on Context
645+
/// Do read-write (exclusive access) transaction on Context
616646
fn write<R>(&self, writer: impl FnOnce(&mut ContextImpl) -> R) -> R {
617647
writer(&mut self.0.write())
618648
}
@@ -843,19 +873,21 @@ impl Context {
843873

844874
// it is ok to reuse the same ID for e.g. a frame around a widget,
845875
// or to check for interaction with the same widget twice:
846-
if prev_rect.expand(0.1).contains_rect(new_rect)
847-
|| new_rect.expand(0.1).contains_rect(prev_rect)
848-
{
876+
let is_same_rect = prev_rect.expand(0.1).contains_rect(new_rect)
877+
|| new_rect.expand(0.1).contains_rect(prev_rect);
878+
if is_same_rect {
849879
return;
850880
}
851881

852882
let show_error = |widget_rect: Rect, text: String| {
883+
let screen_rect = self.screen_rect();
884+
853885
let text = format!("🔥 {text}");
854886
let color = self.style().visuals.error_fg_color;
855887
let painter = self.debug_painter();
856888
painter.rect_stroke(widget_rect, 0.0, (1.0, color));
857889

858-
let below = widget_rect.bottom() + 32.0 < self.input(|i| i.screen_rect.bottom());
890+
let below = widget_rect.bottom() + 32.0 < screen_rect.bottom();
859891

860892
let text_rect = if below {
861893
painter.debug_text(
@@ -1780,7 +1812,24 @@ impl ContextImpl {
17801812

17811813
let shapes = viewport.graphics.drain(self.memory.areas().order());
17821814

1783-
if viewport.input.wants_repaint() {
1815+
let mut repaint_needed = false;
1816+
1817+
{
1818+
if self.memory.options.repaint_on_widget_change {
1819+
crate::profile_function!("compare-widget-rects");
1820+
if viewport.layer_rects_prev_frame != viewport.layer_rects_this_frame {
1821+
repaint_needed = true; // Some widget has moved
1822+
}
1823+
}
1824+
1825+
std::mem::swap(
1826+
&mut viewport.layer_rects_prev_frame,
1827+
&mut viewport.layer_rects_this_frame,
1828+
);
1829+
viewport.layer_rects_this_frame.clear();
1830+
}
1831+
1832+
if repaint_needed || viewport.input.wants_repaint() {
17841833
self.request_repaint(ended_viewport_id);
17851834
}
17861835

@@ -2100,6 +2149,8 @@ impl Context {
21002149
///
21012150
/// Will return false if some other area is covering the given layer.
21022151
///
2152+
/// The given rectangle is assumed to have been clipped by its parent clip rect.
2153+
///
21032154
/// See also [`Response::contains_pointer`].
21042155
pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool {
21052156
if !rect.is_positive() {
@@ -2129,6 +2180,8 @@ impl Context {
21292180
/// If another widget is covering us and is listening for the same input (click and/or drag),
21302181
/// this will return false.
21312182
///
2183+
/// The given rectangle is assumed to have been clipped by its parent clip rect.
2184+
///
21322185
/// See also [`Response::contains_pointer`].
21332186
pub fn widget_contains_pointer(
21342187
&self,
@@ -2137,6 +2190,10 @@ impl Context {
21372190
sense: Sense,
21382191
rect: Rect,
21392192
) -> bool {
2193+
if !rect.is_positive() {
2194+
return false; // don't even remember this widget
2195+
}
2196+
21402197
let contains_pointer = self.rect_contains_pointer(layer_id, rect);
21412198

21422199
let mut blocking_widget = None;
@@ -2146,19 +2203,17 @@ impl Context {
21462203

21472204
// We add all widgets here, even non-interactive ones,
21482205
// because we need this list not only for checking for blocking widgets,
2149-
// but also to know when we have reach the widget we are checking for cover.
2206+
// but also to know when we have reached the widget we are checking for cover.
21502207
viewport
21512208
.layer_rects_this_frame
2152-
.entry(layer_id)
2153-
.or_default()
2154-
.push(WidgetRect { id, rect, sense });
2209+
.insert(layer_id, WidgetRect { id, rect, sense });
21552210

21562211
// Check if any other widget is covering us.
21572212
// Whichever widget is added LAST (=on top) gets the input.
21582213
if contains_pointer {
21592214
let pointer_pos = viewport.input.pointer.interact_pos();
21602215
if let Some(pointer_pos) = pointer_pos {
2161-
if let Some(rects) = viewport.layer_rects_prev_frame.get(&layer_id) {
2216+
if let Some(rects) = viewport.layer_rects_prev_frame.by_layer.get(&layer_id) {
21622217
for blocking in rects.iter().rev() {
21632218
if blocking.id == id {
21642219
// There are no earlier widgets before this one,
@@ -2293,25 +2348,14 @@ impl Context {
22932348
impl Context {
22942349
/// Show a ui for settings (style and tessellation options).
22952350
pub fn settings_ui(&self, ui: &mut Ui) {
2296-
use crate::containers::*;
2351+
let prev_options = self.options(|o| o.clone());
2352+
let mut options = prev_options.clone();
22972353

2298-
CollapsingHeader::new("🎑 Style")
2299-
.default_open(true)
2300-
.show(ui, |ui| {
2301-
self.style_ui(ui);
2302-
});
2354+
options.ui(ui);
23032355

2304-
CollapsingHeader::new("✒ Painting")
2305-
.default_open(true)
2306-
.show(ui, |ui| {
2307-
let prev_tessellation_options = self.tessellation_options(|o| *o);
2308-
let mut tessellation_options = prev_tessellation_options;
2309-
tessellation_options.ui(ui);
2310-
ui.vertical_centered(|ui| reset_button(ui, &mut tessellation_options));
2311-
if tessellation_options != prev_tessellation_options {
2312-
self.tessellation_options_mut(move |o| *o = tessellation_options);
2313-
}
2314-
});
2356+
if options != prev_options {
2357+
self.options_mut(move |o| *o = options);
2358+
}
23152359
}
23162360

23172361
/// Show the state of egui, including its input and output.

crates/egui/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ pub mod text {
410410

411411
pub use {
412412
containers::*,
413-
context::{Context, RequestRepaintInfo},
413+
context::{Context, RequestRepaintInfo, WidgetRect, WidgetRects},
414414
data::{
415415
input::*,
416416
output::{

crates/egui/src/memory.rs

+57-1
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ impl FocusDirection {
154154
// ----------------------------------------------------------------------------
155155

156156
/// Some global options that you can read and write.
157-
#[derive(Clone, Debug)]
157+
#[derive(Clone, Debug, PartialEq)]
158158
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
159159
#[cfg_attr(feature = "serde", serde(default))]
160160
pub struct Options {
@@ -184,6 +184,11 @@ pub struct Options {
184184
/// Controls the tessellator.
185185
pub tessellation_options: epaint::TessellationOptions,
186186

187+
/// If any widget moves or changes id, repaint everything.
188+
///
189+
/// This is `true` by default.
190+
pub repaint_on_widget_change: bool,
191+
187192
/// This is a signal to any backend that we want the [`crate::PlatformOutput::events`] read out loud.
188193
///
189194
/// The only change to egui is that labels can be focused by pressing tab.
@@ -216,13 +221,64 @@ impl Default for Options {
216221
zoom_factor: 1.0,
217222
zoom_with_keyboard: true,
218223
tessellation_options: Default::default(),
224+
repaint_on_widget_change: true,
219225
screen_reader: false,
220226
preload_font_glyphs: true,
221227
warn_on_id_clash: cfg!(debug_assertions),
222228
}
223229
}
224230
}
225231

232+
impl Options {
233+
/// Show the options in the ui.
234+
pub fn ui(&mut self, ui: &mut crate::Ui) {
235+
let Self {
236+
style, // covered above
237+
zoom_factor: _, // TODO
238+
zoom_with_keyboard,
239+
tessellation_options,
240+
repaint_on_widget_change,
241+
screen_reader: _, // needs to come from the integration
242+
preload_font_glyphs: _,
243+
warn_on_id_clash,
244+
} = self;
245+
246+
use crate::Widget as _;
247+
248+
CollapsingHeader::new("⚙ Options")
249+
.default_open(false)
250+
.show(ui, |ui| {
251+
ui.checkbox(
252+
repaint_on_widget_change,
253+
"Repaint if any widget moves or changes id",
254+
);
255+
256+
ui.checkbox(
257+
zoom_with_keyboard,
258+
"Zoom with keyboard (Cmd +, Cmd -, Cmd 0)",
259+
);
260+
261+
ui.checkbox(warn_on_id_clash, "Warn if two widgets have the same Id");
262+
});
263+
264+
use crate::containers::*;
265+
CollapsingHeader::new("🎑 Style")
266+
.default_open(true)
267+
.show(ui, |ui| {
268+
std::sync::Arc::make_mut(style).ui(ui);
269+
});
270+
271+
CollapsingHeader::new("✒ Painting")
272+
.default_open(true)
273+
.show(ui, |ui| {
274+
tessellation_options.ui(ui);
275+
ui.vertical_centered(|ui| crate::reset_button(ui, tessellation_options));
276+
});
277+
278+
ui.vertical_centered(|ui| crate::reset_button(ui, self));
279+
}
280+
}
281+
226282
// ----------------------------------------------------------------------------
227283

228284
/// Say there is a button in a scroll area.

0 commit comments

Comments
 (0)