From 80baae91cda7b39c82d3e32c93eba0889fa32787 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 15 Nov 2026 07:38:56 -0800 Subject: [PATCH] 3D (Classic): the panel's and a dialog's track cubes each keep their own focus and drag With the 3D and Materials panel and a classic 3D dialog both showing a track cube, the two cubes shared one widget id, so the keyboard focus of one moved the other, and a drag on one cube was read by the other: Escape there put the first cube's settings into it, and its button-up check ended the gesture before the cube being dragged saw the release. Each cube now has its own id, keyed by its dialog or panel, and only the cube a drag started on follows, restores or ends it. --- crates/ui-egui/src/dialogs/revolve/tests.rs | 13 ++++--- .../ui-egui/src/dialogs/tests_track_cube.rs | 34 ++++++++++++++++++- crates/ui-egui/src/dialogs/three_d_classic.rs | 28 ++++++++++++--- 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/crates/ui-egui/src/dialogs/revolve/tests.rs b/crates/ui-egui/src/dialogs/revolve/tests.rs index 1e22be9c76..f76a1dfaa1 100644 --- a/crates/ui-egui/src/dialogs/revolve/tests.rs +++ b/crates/ui-egui/src/dialogs/revolve/tests.rs @@ -61,6 +61,11 @@ impl Harness { self.ctx.read_response(Id::unique(id)).unwrap().rect } + /// The open dialog's track cube. + fn cube_id(&self) -> Id { + super::super::three_d_classic::cube_id(&self.app.ui.dialog.as_ref().unwrap().kind) + } + fn click(&mut self, p: Pos2) { self.frame(vec![Event::PointerMoved(p), button(p, true)]); self.frame(vec![button(p, false)]); @@ -484,7 +489,7 @@ fn the_track_cube_turns_the_classic_effects_live_in_one_undo_step() { h.settle(); h.rect("Off-Axis Front"); let start = ["rotationX", "rotationY", "rotationZ"].map(|k| h.value(k)); - let cube = h.response("three-d-track-cube"); + let cube = h.ctx.read_response(h.cube_id()).unwrap().rect; // A face, dragged right: the object turns, the document previews it, and the preset is Custom. h.drag(cube.center(), vec2(30.0, 0.0)); let turned = ["rotationX", "rotationY", "rotationZ"].map(|k| h.value(k)); @@ -513,7 +518,7 @@ fn the_track_cube_turns_the_classic_effects_live_in_one_undo_step() { assert!(h.value("rotationZ") > turned[2], "clockwise: {} -> {}", turned[2], h.value("rotationZ")); // With the keyboard focus (the drags gave it), Right turns about the screen's vertical axis. let z = h.value("rotationZ"); - assert!(h.ctx.memory(|m| m.has_focus(Id::unique("three-d-track-cube"))), "the drag gave the cube the keyboard focus"); + assert!(h.ctx.memory(|m| m.has_focus(h.cube_id())), "the drag gave the cube the keyboard focus"); h.frame(vec![Event::Key { key: egui::Key::PageUp, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }]); h.settle(); assert!((h.value("rotationZ") - (z - 1.0)).abs() < 0.011, "Page Up turns 1° counterclockwise"); @@ -535,11 +540,11 @@ fn the_track_cube_is_off_while_a_rotation_is_mixed() { let mut h = Harness::with("M100 100 L200 100 L200 200 L100 200 Z", CLASSIC_ROTATE, vec2(1400.0, 1000.0)); h.app.ui.dialog.as_mut().unwrap().fields.insert("rotationX".into(), Value::Null); h.settle(); - let cube = h.response("three-d-track-cube"); + let cube = h.ctx.read_response(h.cube_id()).unwrap().rect; h.drag(cube.center(), vec2(30.0, 0.0)); let d = h.app.ui.dialog.as_ref().unwrap(); assert_eq!(d.fields.get("rotationX"), Some(&Value::Null)); - assert!(!h.ctx.read_response(Id::unique("three-d-track-cube")).unwrap().sense.senses_drag()); + assert!(!h.ctx.read_response(h.cube_id()).unwrap().sense.senses_drag()); } #[test] diff --git a/crates/ui-egui/src/dialogs/tests_track_cube.rs b/crates/ui-egui/src/dialogs/tests_track_cube.rs index 39c0c3a934..9b99efb105 100644 --- a/crates/ui-egui/src/dialogs/tests_track_cube.rs +++ b/crates/ui-egui/src/dialogs/tests_track_cube.rs @@ -49,7 +49,8 @@ fn angles_stay_in_the_fields_range_to_a_hundredth() { } fn drag(grip: Grip, angles: [f64; 3], from: Pos2, to: Pos2, shift: bool) -> [f64; 3] { - let mut g = CubeDrag { grip, matrix: rotation(angles), origin: from, last: from, lock: None, fields: Default::default(), pass: 0 }; + let mut g = + CubeDrag { grip, matrix: rotation(angles), origin: from, last: from, lock: None, fields: Default::default(), pass: 0, owner: Id::NULL }; // In five steps, as a pointer moves. let mut near = angles; for k in 1..=5 { @@ -124,6 +125,7 @@ fn a_drag_ends_when_the_cube_is_no_longer_drawn() { lock: None, fields: Default::default(), pass, + owner: Id::NULL, }; ui.data_mut(|m| m.insert_temp(cube_drag_id(), drag)); assert!(cube_dragging(ui.ctx())); @@ -135,6 +137,36 @@ fn a_drag_ends_when_the_cube_is_no_longer_drawn() { assert!(!cube_dragging(&ctx), "two passes without the cube end the gesture"); } +/// With the 3D and Materials panel and a 3D dialog both showing a cube, a gesture on one cube is +/// left alone by the other: Escape there does not put the first cube's settings into it. +#[test] +fn a_cube_leaves_a_gesture_on_the_other_cube_alone() { + let ctx = egui::Context::default(); + let mut d = Dialog::new("effect", json!({ "rotationX": 10.0, "rotationY": 0.0, "rotationZ": 0.0 })); + let escape = egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE }; + let input = egui::RawInput { events: vec![escape], ..Default::default() }; + let mut out = ctx.run_ui(input, |ui| { + let pass = ui.ctx().cumulative_pass_nr(); + let other = CubeDrag { + grip: Grip::Face, + matrix: rotation([50.0, 0.0, 0.0]), + origin: Pos2::ZERO, + last: Pos2::ZERO, + lock: None, + fields: json!({ "rotationX": 50.0, "rotationY": 0.0, "rotationZ": 0.0 }).as_object().cloned().unwrap_or_default(), + pass, + owner: cube_id(crate::panels::three_d::ID), + }; + assert_ne!(cube_id(crate::panels::three_d::ID), cube_id(&d.kind), "each cube has its own focus"); + ui.data_mut(|m| m.insert_temp(cube_drag_id(), other)); + track_cube(ui, &mut d); + }); + out.textures_delta.clear(); + assert_eq!(d.f64("rotationX", 0.0), 10.0, "the dialog keeps its own rotation"); + // The button is up in that pass, and the other cube, drawn later, still has the release to handle. + assert!(ctx.data(|m| m.get_temp::(cube_drag_id())).is_some(), "the other cube's gesture is still there"); +} + /// The centre is on the front face, the ring's band on the ring, and an edge's middle on that edge. #[test] fn grips_follow_what_is_under_the_pointer() { diff --git a/crates/ui-egui/src/dialogs/three_d_classic.rs b/crates/ui-egui/src/dialogs/three_d_classic.rs index 6939fba6e1..ec3c3c766d 100644 --- a/crates/ui-egui/src/dialogs/three_d_classic.rs +++ b/crates/ui-egui/src/dialogs/three_d_classic.rs @@ -277,6 +277,9 @@ struct CubeDrag { fields: serde_json::Map, /// The last pass that drew the cube during the gesture. pass: u64, + /// The cube the gesture started on. The 3D and Materials panel and a 3D dialog can each show + /// a cube at the same time, and only this one follows the gesture. + owner: Id, } impl CubeDrag { @@ -287,6 +290,12 @@ impl CubeDrag { } } +/// The track cube of the dialog or panel of kind `kind`. The 3D and Materials panel and a 3D +/// dialog can show a cube at the same time, each with its own keyboard focus. +pub(super) fn cube_id(kind: &str) -> Id { + Id::unique(("three-d-track-cube", kind)) +} + fn cube_drag_id() -> Id { Id::unique("three-d-track-cube-drag") } @@ -359,7 +368,7 @@ pub(super) fn track_cube(ui: &mut egui::Ui, d: &mut Dialog) { label(ui, name); let (rect, _) = ui.allocate_exact_size(Vec2::splat(CUBE_BOX), Sense::hover()); let enabled = ui.is_enabled() && !mixed; - let id = Id::unique("three-d-track-cube"); + let id = cube_id(&d.kind); let resp = ui.interact(rect, id, if enabled { Sense::click_and_drag() } else { Sense::hover() }); let center = rect.center(); let value = format!("X {:.2}°, Y {:.2}°, Z {:.2}°", now[0], now[1], now[2]); @@ -375,14 +384,24 @@ pub(super) fn track_cube(ui: &mut egui::Ui, d: &mut Dialog) { ui.ctx().memory_mut(|m| m.stop_text_input()); let matrix = rotation(now); let pass = ui.ctx().cumulative_pass_nr(); - let drag = CubeDrag { grip: grip_at(matrix, center, origin), matrix, origin, last: origin, lock: None, fields: d.fields.clone(), pass }; + let drag = CubeDrag { + grip: grip_at(matrix, center, origin), + matrix, + origin, + last: origin, + lock: None, + fields: d.fields.clone(), + pass, + owner: id, + }; ui.data_mut(|m| m.insert_temp(cube_drag_id(), drag)); } if enabled && (resp.drag_started() || resp.clicked()) { resp.request_focus(); } let pass = ui.ctx().cumulative_pass_nr(); - let mut drag = ui.data(|m| m.get_temp::(cube_drag_id())); + // A gesture on the other cube (the panel's or the dialog's) is left to that cube. + let mut drag = ui.data(|m| m.get_temp::(cube_drag_id())).filter(|g| g.owner == id); if drag.as_ref().is_some_and(|g| !g.live(pass)) { ui.data_mut(|m| m.remove::(cube_drag_id())); drag = None; @@ -404,7 +423,8 @@ pub(super) fn track_cube(ui: &mut egui::Ui, d: &mut Dialog) { ui.data_mut(|m| m.insert_temp(cube_drag_id(), current.clone())); } } - if resp.drag_stopped() || !ui.input(|i| i.pointer.primary_down()) { + // Only this cube's gesture ends here; the other cube may still have the release to handle. + if drag.is_some() && (resp.drag_stopped() || !ui.input(|i| i.pointer.primary_down())) { ui.data_mut(|m| m.remove::(cube_drag_id())); } if enabled && resp.has_focus() {