Skip to content

Commit 8ce0e1c

Browse files
authored
Avoid deadlocks by using lambdas for context lock (#2625)
ctx.input().key_pressed(Key::A) -> ctx.input(|i| i.key_pressed(Key::A))
1 parent eee4cf6 commit 8ce0e1c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+1440
-1118
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
.DS_Store
12
**/target
23
**/target_ra
4+
**/target_wasm
35
/.*.json
46
/.vscode
57
/media/*
6-
.DS_Store

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
55

66

77
## Unreleased
8+
* ⚠️ BREAKING: `egui::Context` now use closures for locking ([#2625](https://github.com/emilk/egui/pull/2625)):
9+
* `ctx.input().key_pressed(Key::A)` -> `ctx.input(|i| i.key_pressed(Key::A))`
10+
* `ui.memory().toggle_popup(popup_id)` -> `ui.memory_mut(|mem| mem.toggle_popup(popup_id))`
11+
812
### Added ⭐
913
* Add `Response::drag_started_by` and `Response::drag_released_by` for convenience, similar to `dragged` and `dragged_by` ([#2507](https://github.com/emilk/egui/pull/2507)).
1014
* Add `PointerState::*_pressed` to check if the given button was pressed in this frame ([#2507](https://github.com/emilk/egui/pull/2507)).
@@ -18,6 +22,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
1822
* Add `ProgressBar::fill` if you want to set the fill color manually. ([#2618](https://github.com/emilk/egui/pull/2618)).
1923
* Add `Button::rounding` to enable round buttons ([#2616](https://github.com/emilk/egui/pull/2616)).
2024
* Add `WidgetVisuals::optional_bg_color` - set it to `Color32::TRANSPARENT` to hide button backgrounds ([#2621](https://github.com/emilk/egui/pull/2621)).
25+
* Add `Context::screen_rect` and `Context::set_cursor_icon` ([#2625](https://github.com/emilk/egui/pull/2625)).
2126

2227
### Changed 🔧
2328
* Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)).

Cargo.lock

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ egui uses the builder pattern for construction widgets. For instance: `ui.add(La
371371

372372
Instead of using matching `begin/end` style function calls (which can be error prone) egui prefers to use `FnOnce` closures passed to a wrapping function. Lambdas are a bit ugly though, so I'd like to find a nicer solution to this. More discussion of this at <https://github.com/emilk/egui/issues/1004#issuecomment-1001650754>.
373373

374+
egui uses a single `RwLock` for short-time locks on each access of `Context` data. This is to leave implementation simple and transactional and allow users to run their UI logic in parallel. Instead of creating mutex guards, egui uses closures passed to a wrapping function, e.g. `ctx.input(|i| i.key_down(Key::A))`. This is to make it less likely that a user would accidentally double-lock the `Context`, which would lead to a deadlock.
375+
374376
### Inspiration
375377

376378
The one and only [Dear ImGui](https://github.com/ocornut/imgui) is a great Immediate Mode GUI for C++ which works with many backends. That library revolutionized how I think about GUI code and turned GUI programming from something I hated to do to something I now enjoy.
@@ -396,6 +398,7 @@ Notable contributions by:
396398
* [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543).
397399
* [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868).
398400
* [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050).
401+
* [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625).
399402
* And [many more](https://github.com/emilk/egui/graphs/contributors?type=a).
400403

401404
egui is licensed under [MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE).

crates/eframe/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ persistence = [
5555
## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you
5656
puffin = ["dep:puffin", "egui_glow?/puffin", "egui-wgpu?/puffin"]
5757

58-
## Enable screen reader support (requires `ctx.options().screen_reader = true;`)
58+
## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`)
5959
screen_reader = ["egui-winit/screen_reader", "tts"]
6060

6161
## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit.

crates/eframe/src/epi.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ pub trait App {
173173
}
174174

175175
/// If `true` a warm-up call to [`Self::update`] will be issued where
176-
/// `ctx.memory().everything_is_visible()` will be set to `true`.
176+
/// `ctx.memory(|mem| mem.everything_is_visible())` will be set to `true`.
177177
///
178178
/// This can help pre-caching resources loaded by different parts of the UI, preventing stutter later on.
179179
///

crates/eframe/src/native/epi_integration.rs

+8-5
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,8 @@ impl EpiIntegration {
257257
) -> Self {
258258
let egui_ctx = egui::Context::default();
259259

260-
*egui_ctx.memory() = load_egui_memory(storage.as_deref()).unwrap_or_default();
260+
let memory = load_egui_memory(storage.as_deref()).unwrap_or_default();
261+
egui_ctx.memory_mut(|mem| *mem = memory);
261262

262263
let native_pixels_per_point = window.scale_factor() as f32;
263264

@@ -315,11 +316,12 @@ impl EpiIntegration {
315316

316317
pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) {
317318
crate::profile_function!();
318-
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
319-
self.egui_ctx.memory().set_everything_is_visible(true);
319+
let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone());
320+
self.egui_ctx
321+
.memory_mut(|mem| mem.set_everything_is_visible(true));
320322
let full_output = self.update(app, window);
321323
self.pending_full_output.append(full_output); // Handle it next frame
322-
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
324+
self.egui_ctx.memory_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge.
323325
self.egui_ctx.clear_animations();
324326
}
325327

@@ -446,7 +448,8 @@ impl EpiIntegration {
446448
}
447449
if _app.persist_egui_memory() {
448450
crate::profile_scope!("egui_memory");
449-
epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, &*self.egui_ctx.memory());
451+
self.egui_ctx
452+
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
450453
}
451454
{
452455
crate::profile_scope!("App::save");

crates/eframe/src/web/backend.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -313,10 +313,11 @@ impl AppRunner {
313313

314314
pub fn warm_up(&mut self) -> Result<(), JsValue> {
315315
if self.app.warm_up_enabled() {
316-
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
317-
self.egui_ctx.memory().set_everything_is_visible(true);
316+
let saved_memory: egui::Memory = self.egui_ctx.memory(|m| m.clone());
317+
self.egui_ctx
318+
.memory_mut(|m| m.set_everything_is_visible(true));
318319
self.logic()?;
319-
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
320+
self.egui_ctx.memory_mut(|m| *m = saved_memory); // We don't want to remember that windows were huge.
320321
self.egui_ctx.clear_animations();
321322
}
322323
Ok(())
@@ -388,7 +389,7 @@ impl AppRunner {
388389
}
389390

390391
fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
391-
if self.egui_ctx.options().screen_reader {
392+
if self.egui_ctx.options(|o| o.screen_reader) {
392393
self.screen_reader
393394
.speak(&platform_output.events_description());
394395
}

crates/eframe/src/web/storage.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub fn load_memory(ctx: &egui::Context) {
1515
if let Some(memory_string) = local_storage_get("egui_memory_ron") {
1616
match ron::from_str(&memory_string) {
1717
Ok(memory) => {
18-
*ctx.memory() = memory;
18+
ctx.memory_mut(|m| *m = memory);
1919
}
2020
Err(err) => {
2121
tracing::error!("Failed to parse memory RON: {}", err);
@@ -29,7 +29,7 @@ pub fn load_memory(_: &egui::Context) {}
2929

3030
#[cfg(feature = "persistence")]
3131
pub fn save_memory(ctx: &egui::Context) {
32-
match ron::to_string(&*ctx.memory()) {
32+
match ctx.memory(|mem| ron::to_string(mem)) {
3333
Ok(ron) => {
3434
local_storage_set("egui_memory_ron", &ron);
3535
}

crates/egui-winit/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ impl State {
615615
egui_ctx: &egui::Context,
616616
platform_output: egui::PlatformOutput,
617617
) {
618-
if egui_ctx.options().screen_reader {
618+
if egui_ctx.options(|o| o.screen_reader) {
619619
self.screen_reader
620620
.speak(&platform_output.events_description());
621621
}

crates/egui/src/containers/area.rs

+16-16
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use crate::*;
66

77
/// State that is persisted between frames.
8-
// TODO(emilk): this is not currently stored in `memory().data`, but maybe it should be?
8+
// TODO(emilk): this is not currently stored in `Memory::data`, but maybe it should be?
99
#[derive(Clone, Copy, Debug)]
1010
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
1111
pub(crate) struct State {
@@ -231,7 +231,7 @@ impl Area {
231231

232232
let layer_id = LayerId::new(order, id);
233233

234-
let state = ctx.memory().areas.get(id).copied();
234+
let state = ctx.memory(|mem| mem.areas.get(id).copied());
235235
let is_new = state.is_none();
236236
if is_new {
237237
ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place
@@ -278,7 +278,7 @@ impl Area {
278278
// Important check - don't try to move e.g. a combobox popup!
279279
if movable {
280280
if move_response.dragged() {
281-
state.pos += ctx.input().pointer.delta();
281+
state.pos += ctx.input(|i| i.pointer.delta());
282282
}
283283

284284
state.pos = ctx
@@ -288,9 +288,9 @@ impl Area {
288288

289289
if (move_response.dragged() || move_response.clicked())
290290
|| pointer_pressed_on_area(ctx, layer_id)
291-
|| !ctx.memory().areas.visible_last_frame(&layer_id)
291+
|| !ctx.memory(|m| m.areas.visible_last_frame(&layer_id))
292292
{
293-
ctx.memory().areas.move_to_top(layer_id);
293+
ctx.memory_mut(|m| m.areas.move_to_top(layer_id));
294294
ctx.request_repaint();
295295
}
296296

@@ -329,7 +329,7 @@ impl Area {
329329
}
330330

331331
let layer_id = LayerId::new(self.order, self.id);
332-
let area_rect = ctx.memory().areas.get(self.id).map(|area| area.rect());
332+
let area_rect = ctx.memory(|mem| mem.areas.get(self.id).map(|area| area.rect()));
333333
if let Some(area_rect) = area_rect {
334334
let clip_rect = ctx.available_rect();
335335
let painter = Painter::new(ctx.clone(), layer_id, clip_rect);
@@ -358,7 +358,7 @@ impl Prepared {
358358
}
359359

360360
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
361-
let screen_rect = ctx.input().screen_rect();
361+
let screen_rect = ctx.screen_rect();
362362

363363
let bounds = if let Some(bounds) = self.drag_bounds {
364364
bounds.intersect(screen_rect) // protect against infinite bounds
@@ -410,29 +410,29 @@ impl Prepared {
410410

411411
state.size = content_ui.min_rect().size();
412412

413-
ctx.memory().areas.set_state(layer_id, state);
413+
ctx.memory_mut(|m| m.areas.set_state(layer_id, state));
414414

415415
move_response
416416
}
417417
}
418418

419419
fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
420420
if let Some(pointer_pos) = ctx.pointer_interact_pos() {
421-
let any_pressed = ctx.input().pointer.any_pressed();
421+
let any_pressed = ctx.input(|i| i.pointer.any_pressed());
422422
any_pressed && ctx.layer_id_at(pointer_pos) == Some(layer_id)
423423
} else {
424424
false
425425
}
426426
}
427427

428428
fn automatic_area_position(ctx: &Context) -> Pos2 {
429-
let mut existing: Vec<Rect> = ctx
430-
.memory()
431-
.areas
432-
.visible_windows()
433-
.into_iter()
434-
.map(State::rect)
435-
.collect();
429+
let mut existing: Vec<Rect> = ctx.memory(|mem| {
430+
mem.areas
431+
.visible_windows()
432+
.into_iter()
433+
.map(State::rect)
434+
.collect()
435+
});
436436
existing.sort_by_key(|r| r.left().round() as i32);
437437

438438
let available_rect = ctx.available_rect();

crates/egui/src/containers/collapsing_header.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ pub struct CollapsingState {
2626

2727
impl CollapsingState {
2828
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
29-
ctx.data()
30-
.get_persisted::<InnerState>(id)
31-
.map(|state| Self { id, state })
29+
ctx.data_mut(|d| {
30+
d.get_persisted::<InnerState>(id)
31+
.map(|state| Self { id, state })
32+
})
3233
}
3334

3435
pub fn store(&self, ctx: &Context) {
35-
ctx.data().insert_persisted(self.id, self.state);
36+
ctx.data_mut(|d| d.insert_persisted(self.id, self.state));
3637
}
3738

3839
pub fn id(&self) -> Id {
@@ -64,7 +65,7 @@ impl CollapsingState {
6465

6566
/// 0 for closed, 1 for open, with tweening
6667
pub fn openness(&self, ctx: &Context) -> f32 {
67-
if ctx.memory().everything_is_visible() {
68+
if ctx.memory(|mem| mem.everything_is_visible()) {
6869
1.0
6970
} else {
7071
ctx.animate_bool(self.id, self.state.open)

crates/egui/src/containers/combo_box.rs

+4-9
Original file line numberDiff line numberDiff line change
@@ -242,18 +242,13 @@ fn combo_box_dyn<'c, R>(
242242
) -> InnerResponse<Option<R>> {
243243
let popup_id = button_id.with("popup");
244244

245-
let is_popup_open = ui.memory().is_popup_open(popup_id);
245+
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
246246

247-
let popup_height = ui
248-
.ctx()
249-
.memory()
250-
.areas
251-
.get(popup_id)
252-
.map_or(100.0, |state| state.size.y);
247+
let popup_height = ui.memory(|m| m.areas.get(popup_id).map_or(100.0, |state| state.size.y));
253248

254249
let above_or_below =
255250
if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height
256-
< ui.ctx().input().screen_rect().bottom()
251+
< ui.ctx().screen_rect().bottom()
257252
{
258253
AboveOrBelow::Below
259254
} else {
@@ -334,7 +329,7 @@ fn combo_box_dyn<'c, R>(
334329
});
335330

336331
if button_response.clicked() {
337-
ui.memory().toggle_popup(popup_id);
332+
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
338333
}
339334
let inner = crate::popup::popup_above_or_below_widget(
340335
ui,

0 commit comments

Comments
 (0)