diff --git a/crates/ui-egui/src/canvas/drag.rs b/crates/ui-egui/src/canvas/drag.rs index 5d1bbea68..f15fdd608 100644 --- a/crates/ui-egui/src/canvas/drag.rs +++ b/crates/ui-egui/src/canvas/drag.rs @@ -79,13 +79,14 @@ pub(super) fn tool_drag_moved(ui: &Ui, xf: &Xf, p: Pos2, pointer: &egui::Pointer } /// Whether the tool should get another drag event at a pointer held still because Shift, Alt, -/// Cmd/Ctrl or Space went down or up (#4711): a scale switches to proportional or from the centre -/// at once. Records `m` as the modifiers last seen; only once the press has dragged. -pub(super) fn tool_drag_mods_changed(ui: &Ui, m: Mods) -> bool { +/// Cmd/Ctrl, Space or Backtick went down or up (#4711): a scale switches to proportional or from the centre +/// at once. Records `m` and the Backtick state as last seen; only once the press has dragged. +pub(super) fn tool_drag_mods_changed(ui: &Ui, m: Mods, backquote: bool) -> bool { let id = egui::Id::unique("canvas-tool-drag-mods"); - let (dragged, last) = ui.data(|d| (d.get_temp::(tool_drag_doc_id()).is_some(), d.get_temp::(id))); - ui.data_mut(|d| d.insert_temp(id, m)); - dragged && last.is_some_and(|l| l != m) + let held = (m, backquote); + let (dragged, last) = ui.data(|d| (d.get_temp::(tool_drag_doc_id()).is_some(), d.get_temp::<(Mods, bool)>(id))); + ui.data_mut(|d| d.insert_temp(id, held)); + dragged && last.is_some_and(|l| l != held) } /// The pen pressure (0..1) of the press in progress: the force of this frame's pen or touch diff --git a/crates/ui-egui/src/canvas/input.rs b/crates/ui-egui/src/canvas/input.rs index c38697260..728a38b09 100644 --- a/crates/ui-egui/src/canvas/input.rs +++ b/crates/ui-egui/src/canvas/input.rs @@ -379,27 +379,18 @@ fn drag_held(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, d: Drag, p: Pos2) // the last position only, so a costly drag preview runs once a frame. let moves = tool_frame_moves(ui, app); // A frame whose pointer ends where the last one did still moved in between. - let mods_changed = tool_drag_mods_changed(ui, mods(m, space)); + let backquote = ui.input(|i| i.key_down(egui::Key::Backtick)); + let mods_changed = tool_drag_mods_changed(ui, mods(m, space), backquote); if tool_drag_moved(ui, &xf, p, pointer) || mods_changed || !moves.is_empty() { for (at, force) in moves { let pressure = force.unwrap_or_else(|| pen_pressure(ui, app, false)); - dispatch( - app, - &PointerEvent { - kind: PointerKind::Drag, - pos: xf.to_doc(at), - mods: mods(m, space), - backquote: ui.input(|i| i.key_down(egui::Key::Backtick)), - pressure, - }, - view, - ); + dispatch(app, &PointerEvent { kind: PointerKind::Drag, pos: xf.to_doc(at), mods: mods(m, space), backquote, pressure }, view); } let ev = PointerEvent { kind: PointerKind::Drag, pos: xf.to_doc(p), mods: mods(m, space), - backquote: ui.input(|i| i.key_down(egui::Key::Backtick)), + backquote, pressure: pen_pressure(ui, app, false), }; dispatch(app, &ev, view); diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index 311affa54..b1de29383 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -6135,6 +6135,51 @@ fn a_modifier_changed_mid_scale_applies_with_the_pointer_held_still() { assert!(app.session.active().unwrap().interaction.is_none(), "committed"); } +/// Backtick switches a moved Selection drag's live preview while the pointer stays still. +#[test] +fn backtick_changed_mid_selection_drag_applies_with_the_pointer_held_still() { + use egui::Event; + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + let tile = app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap(); + app.run("object.pattern.make", json!({"ids": [tile], "name": "Dots", "width": 20, "height": 20})).unwrap(); + app.run("object.pattern.done", json!({})).unwrap(); + let id = app.run("shape.rectangle", json!({"x": 100, "y": 100, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap(); + app.run("paint.setFill", json!({"ids": [id], "swatch": "Dots"})).unwrap(); + app.run("select.set", json!({"ids": [id]})).unwrap(); + app.select_tool("selection"); + app.ui.task_bar = false; + app.ui.view.rulers = false; + app.ui.view.smart_guides = false; + app.ui.view.snap_to_point = false; + let ctx = egui::Context::default(); + frame(&mut app, &ctx, vec![]); + let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()); + let from = xf.to_screen(Point::new(125.0, 125.0)); + let to = xf.to_screen(Point::new(145.0, 125.0)); + let button = |pos, pressed| Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + let key = |pressed| Event::Key { key: egui::Key::Backtick, physical_key: None, pressed, repeat: false, modifiers: Default::default() }; + let node = |app: &VectorcraftApp| app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().clone(); + let pattern_x = |node: &vectorcraft_doc::Node| { + let vectorcraft_color::Paint::Pattern { xf, .. } = node.appearance.fill_paint() else { panic!("pattern fill expected") }; + xf.as_coeffs()[4] + }; + let before = node(&app); + frame(&mut app, &ctx, vec![Event::PointerMoved(from), button(from, true)]); + frame(&mut app, &ctx, vec![Event::PointerMoved(to)]); + let moved = node(&app); + assert!((moved.geometric_bounds().unwrap().x0 - before.geometric_bounds().unwrap().x0 - 20.0).abs() < 0.001); + frame(&mut app, &ctx, vec![key(true)]); + let pattern_only = node(&app); + assert_eq!(pattern_only.geometric_bounds(), before.geometric_bounds(), "Backtick restores geometry in the live preview"); + assert!((pattern_x(&pattern_only) - pattern_x(&before) - 20.0).abs() < 0.001, "Backtick moves the pattern in the live preview"); + frame(&mut app, &ctx, vec![key(false)]); + let ordinary = node(&app); + assert_eq!(ordinary.geometric_bounds(), moved.geometric_bounds(), "releasing Backtick restores the ordinary move in the live preview"); + assert!((pattern_x(&ordinary) - pattern_x(&before)).abs() < 0.001, "the ordinary move restores the pattern matrix"); + frame(&mut app, &ctx, vec![button(to, false)]); +} + /// #834 review: a press held still is not a drag. Before the press has dragged, the first held /// frame without a delta at the press's own position sends nothing, so a click stays a click; /// the first move after it does.