From d5936f80350579ddececc766ecde52208070eb0e Mon Sep 17 00:00:00 2001 From: Tanmay Singh Date: Fri, 30 Oct 2026 22:22:53 +0530 Subject: [PATCH] fix: stop a click beside an object from deadlocking the window A click on the row of a formula, diagram or picture, beside the object itself, lands on the row's own response. `show_block_zoomed` read that response's `double_clicked()` inside `ui.data_mut`, and in egui 0.36 `double_clicked()` calls `ctx.input`, which takes the context lock that `data_mut` already holds. The lock is not re-entrant, so the window froze; a debug build panics after 10s with "Failed to acquire RwLock write after 10s. Deadlock?". Read `double_clicked` before `data_mut`, as `clicked` already was. The new UI test clicks beside a display formula; without the fix it fails with the same epaint panic as the report. Fixes AlienTechSoftware/notecraft#15 Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/preview.rs | 5 ++++- crates/ui-egui/tests/ui.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/preview.rs b/crates/ui-egui/src/preview.rs index e29be20..a4d5c2b 100644 --- a/crates/ui-egui/src/preview.rs +++ b/crates/ui-egui/src/preview.rs @@ -661,7 +661,10 @@ pub(crate) fn show_block_zoomed( let rect = egui::Rect::from_min_max(start, egui::pos2(start.x + ui.available_width().max(1.0), end.y.max(start.y + 1.0))); let response = ui.interact(rect, id, egui::Sense::click()); if let Some(kind) = object_kind(block) { + // Read every response flag before `data_mut`: `clicked` and + // `double_clicked` take the context lock that `data_mut` holds. let clicked = response.clicked(); + let double_clicked = response.double_clicked(); let pointer = response.interact_pointer_pos(); ui.data_mut(|data| { let mut hits = data.get_temp::>(object_hits_id()).unwrap_or_default(); @@ -670,7 +673,7 @@ pub(crate) fn show_block_zoomed( if clicked { data.insert_temp(object_clicked_id(), (i, kind.to_string())); } - if response.double_clicked() { + if double_clicked { data.insert_temp(object_double_clicked_id(), (i, kind.to_string())); } }); diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index 2f61179..14b4bbe 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -3705,6 +3705,32 @@ fn double_clicking_an_attachment_card_opens_it() { assert!(h.query_all_by_label_contains("Open attachment").count() >= 1, "double click opened the attachment prompt: {}", look_at(&mut h)); } +/// Issue #15: a click beside a formula, diagram or picture lands on its row's own +/// response. Reading that response's click state inside `data_mut` took the context lock twice +/// and froze the window (a debug build panics after 10s). +#[test] +fn clicking_beside_a_formula_does_not_deadlock() { + let (_t, mut app, s) = sample_app("ui-object-row-click"); + let md = s.md_page.to_string_lossy().to_string(); + app.open_path(&md); + let mut h = harness(app, LIGHT); + settle(&mut h); + ask(&mut h, "engine.execute", json!({"command": "page.save", "params": {"path": md.clone(), "body": "Some words.\n\n$$\nx^2\n$$\n"}})) + .expect("write a page with a formula"); + ask(&mut h, "engine.execute", json!({"command": "page.containers", "params": {"path": md.clone(), "boxes": "0,0,700,2"}})) + .expect("arrange the page in a wide box"); + ask(&mut h, "ui.open", json!({"path": md})).expect("open the formula page"); + settle(&mut h); + let origin = look_at(&mut h).pointer("/page/origin").and_then(Value::as_array).cloned().unwrap_or_default(); + let ox = origin.first().and_then(Value::as_f64).unwrap_or(0.0); + let oy = origin.get(1).and_then(Value::as_f64).unwrap_or(0.0); + for count in [1, 2] { + ask(&mut h, "ui.click", json!({"x": ox + 680.0, "y": oy + 60.0, "count": count})).expect("click beside the formula"); + settle(&mut h); + } + assert_eq!(look_at(&mut h).pointer("/page/blocks").and_then(Value::as_u64), Some(2), "the page still draws its formula"); +} + #[test] fn delete_and_backspace_remove_a_selected_picture() { for key in ["Delete", "Backspace"] {