Merge pull request #4770 from dotCooCoo/fix/backtick-mid-drag-preview

Canvas: pressing or releasing backquote mid-drag updates the live preview
This commit is contained in:
BFlat
2026-10-31 06:27:14 +07:00
committed by GitHub
3 changed files with 56 additions and 19 deletions
+7 -6
View File
@@ -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::<Point>(tool_drag_doc_id()).is_some(), d.get_temp::<Mods>(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::<Point>(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
+4 -13
View File
@@ -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);
+45
View File
@@ -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.