Skip to content

Commit

Permalink
Auto-repaint when widgets move or changes id. (emilk#3930)
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
emilk authored and hacknus committed Oct 30, 2024
1 parent c73bbfb commit 643d8f5
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 45 deletions.
130 changes: 87 additions & 43 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<LayerId, Vec<WidgetRect>>,
}

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 {
Expand All @@ -208,10 +246,10 @@ struct ViewportState {
used: bool,

/// Written to during the frame.
layer_rects_this_frame: HashMap<LayerId, Vec<WidgetRect>>,
layer_rects_this_frame: WidgetRects,

/// Read
layer_rects_prev_frame: HashMap<LayerId, Vec<WidgetRect>>,
layer_rects_prev_frame: WidgetRects,

/// State related to repaint scheduling.
repaint: ViewportRepaintInfo,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<R>(&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<R>(&self, writer: impl FnOnce(&mut ContextImpl) -> R) -> R {
writer(&mut self.0.write())
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion crates/egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ pub mod text {

pub use {
containers::*,
context::{Context, RequestRepaintInfo},
context::{Context, RequestRepaintInfo, WidgetRect, WidgetRects},
data::{
input::*,
output::{
Expand Down
58 changes: 57 additions & 1 deletion crates/egui/src/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -216,13 +221,64 @@ 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),
}
}
}

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.
Expand Down

0 comments on commit 643d8f5

Please sign in to comment.