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.
This commit is contained in:
dotCooCoo
2026-11-15 07:38:56 -08:00
parent d1213a923e
commit 80baae91cd
3 changed files with 66 additions and 9 deletions
+9 -4
View File
@@ -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]
+33 -1
View File
@@ -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::<CubeDrag>(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() {
+24 -4
View File
@@ -277,6 +277,9 @@ struct CubeDrag {
fields: serde_json::Map<String, Value>,
/// 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::<CubeDrag>(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::<CubeDrag>(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::<CubeDrag>(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::<CubeDrag>(cube_drag_id()));
}
if enabled && resp.has_focus() {