Merge pull request #5575 from dotCooCoo/fix/backquote-at-release

Canvas: a drag's release reads Backtick as it was at the button-up
This commit is contained in:
BFlat
2026-11-04 18:52:05 +07:00
committed by GitHub
3 changed files with 114 additions and 37 deletions
+40
View File
@@ -89,6 +89,46 @@ pub(super) fn tool_drag_mods_changed(ui: &Ui, m: Mods, backquote: bool) -> bool
dragged && last.is_some_and(|l| l != held)
}
/// Whether Backtick is held at the end of this frame, and whether it was at the end of the frame
/// before (`None` before the first). The canvas records it once a frame.
pub(super) fn backquote_seen(ui: &Ui) -> (bool, Option<bool>) {
let id = egui::Id::unique("canvas-backquote-seen");
let now = ui.input(|i| i.key_down(egui::Key::Backtick));
let before = ui.data_mut(|d| {
let before = d.get_temp::<bool>(id);
d.insert_temp(id, now);
before
});
(now, before)
}
/// Whether Backtick was held when `button` went up in this frame, with `before` its state at the end
/// of the frame before ([`backquote_seen`]). Its state as the frame began is the opposite of its
/// first press or release in the frame; when the window lost the focus first (which lets go of every
/// key without a release), it is `before`; with neither, it is its state now. The frame's presses,
/// releases and focus losses before the button-up then apply in the order they came. Without a
/// button-up in this frame, its state now.
pub(super) fn backquote_at_release(ui: &Ui, button: egui::PointerButton, before: Option<bool>) -> bool {
ui.input(|i| {
let now = i.key_down(egui::Key::Backtick);
// A press or release of Backtick (`Some(pressed)`), or a focus loss (`None`).
let step = |e: &egui::Event| match e {
egui::Event::Key { key: egui::Key::Backtick, pressed, repeat: false, .. } => Some(Some(*pressed)),
egui::Event::WindowFocused(false) => Some(None),
_ => None,
};
let Some(up) = i.events.iter().position(|e| matches!(e, egui::Event::PointerButton { button: b, pressed: false, .. } if *b == button)) else {
return now;
};
let start = match i.events.iter().find_map(step) {
Some(Some(down)) => !down,
Some(None) => before.unwrap_or(now),
None => now,
};
i.events[..up].iter().filter_map(step).fold(start, |_, s| s.unwrap_or(false))
})
}
/// The pen pressure (0..1) of the press in progress: the force of this frame's pen or touch
/// input, else the pressure in the host's feed while a pen is the pointer that moved last
/// ([`crate::Services::stylus`]: [`StylusFeed::press`] for a new press, [`StylusFeed::held`]
+13 -37
View File
@@ -1,7 +1,8 @@
use super::artboards::{artboard_name_at, begin_artboard_name_edit, name_press_id, select_named_artboard};
use super::drag::{
FrameMove, HoverContext, apply_requests, dispatch, drag_art_out, drag_id, drag_layer_id, drag_pos_id, drop_press, mods, move_pose, on_canvas,
pen_pose, pen_pressure, tool_drag_doc_id, tool_drag_mods_changed, tool_drag_moved, tool_frame_moves, tool_holds, tool_press_doc_id,
FrameMove, HoverContext, apply_requests, backquote_at_release, backquote_seen, dispatch, drag_art_out, drag_id, drag_layer_id, drag_pos_id,
drop_press, mods, move_pose, on_canvas, pen_pose, pen_pressure, tool_drag_doc_id, tool_drag_mods_changed, tool_drag_moved, tool_frame_moves,
tool_holds, tool_press_doc_id,
};
use super::view::{
HOLD_DELAY, HOLD_ZOOM, SCRUB_SLOP, SCRUB_ZOOM, WheelTurn, animated_zoom, pan_pixels, rotate_about, scroll_view, touch_gestures, trackpad_turn,
@@ -19,7 +20,8 @@ pub(super) fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Respo
// double-taps) where it was seen last (#491).
let at = hover.or_else(|| pointer.interact_pos().filter(|p| rect.contains(*p)));
let view = app.view_info();
let f = Pointing { resp, rect, pointer: &pointer, m, space, xf, v, view };
let (_, backquote_before) = backquote_seen(ui);
let f = Pointing { resp, rect, pointer: &pointer, m, space, xf, v, view, backquote_before };
touch_gestures(app, ui, rect, view);
// Pressed and held still, a pen or a finger opens the context menu (egui's long touch): the
// tool's press is dropped, as a right-click presses nothing.
@@ -186,6 +188,8 @@ struct Pointing<'a> {
xf: Xf,
v: View,
view: vectorcraft_engine::ViewInfo,
/// Whether Backtick was held at the end of the frame before ([`backquote_seen`]).
backquote_before: Option<bool>,
}
/// Zoom around the pointer, and scroll ([`wheel`]); a trackpad rotation turns the view. Returns the pointer, the
@@ -433,6 +437,8 @@ fn drag_released(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, d: Drag, p: Po
dd.remove::<Point>(tool_drag_doc_id());
dd.remove::<Point>(tool_press_doc_id());
});
// Backtick as it was at the button-up: a key pressed or released later in the frame does not count.
let backquote = backquote_at_release(ui, egui::PointerButton::Primary, f.backquote_before);
match d {
Drag::ZoomBox { start } => {
let start = zoom_marquee_start(ui, start, before, p, space);
@@ -472,7 +478,6 @@ fn drag_released(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, d: Drag, p: Po
let (moves, last_pose) = tool_frame_moves(ui, app, true);
let pen = move_pose(app, &xf, last_pose);
if let Some(last) = last_drag {
let backquote = ui.input(|i| i.key_down(egui::Key::Backtick));
dispatch_frame_moves(app, ui, f, backquote, moves);
if last != xf.to_doc(p) {
let ev = PointerEvent {
@@ -490,7 +495,7 @@ fn drag_released(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, d: Drag, p: Po
kind: PointerKind::Up,
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),
pen,
};
@@ -501,30 +506,8 @@ fn drag_released(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, d: Drag, p: Po
egui::DragAndDrop::clear_payload(ui.ctx());
let (pressure, pen) = (pen_pressure(ui, app, false), pen_pose(app, &xf));
let pos = xf.to_doc(p);
dispatch(
app,
&PointerEvent {
kind: PointerKind::Drag,
pos,
mods: mods(m, space),
backquote: ui.input(|i| i.key_down(egui::Key::Backtick)),
pressure,
pen,
},
view,
);
dispatch(
app,
&PointerEvent {
kind: PointerKind::Up,
pos,
mods: mods(m, space),
backquote: ui.input(|i| i.key_down(egui::Key::Backtick)),
pressure,
pen,
},
view,
);
dispatch(app, &PointerEvent { kind: PointerKind::Drag, pos, mods: mods(m, space), backquote, pressure, pen }, view);
dispatch(app, &PointerEvent { kind: PointerKind::Up, pos, mods: mods(m, space), backquote, pressure, pen }, view);
}
Drag::Art => {
// Released over a panel: the move goes back. (The app ends it before the panels
@@ -533,14 +516,7 @@ fn drag_released(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, d: Drag, p: Po
let _ = app.session.cancel_interaction();
dispatch(
app,
&PointerEvent {
kind: PointerKind::Up,
pos: Point::ZERO,
mods: Mods::default(),
backquote: ui.input(|i| i.key_down(egui::Key::Backtick)),
pressure: 1.0,
pen: None,
},
&PointerEvent { kind: PointerKind::Up, pos: Point::ZERO, mods: Mods::default(), backquote, pressure: 1.0, pen: None },
view,
);
}
+61
View File
@@ -3051,6 +3051,67 @@ fn egui_backtick_drag_uses_key_state_at_release() {
);
}
/// The release takes Backtick as it was at the button-up, when the key goes up or down later in the
/// same frame.
#[test]
fn backtick_changed_after_the_button_up_in_its_frame_does_not_change_the_release() {
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![key(true), Event::PointerMoved(to)]);
frame(&mut app, &ctx, vec![button(to, false), key(false)]);
let pattern_only = node(&app);
assert!(app.session.active().unwrap().interaction.is_none(), "committed");
assert_eq!(pattern_only.geometric_bounds(), before.geometric_bounds(), "Backtick held at the button-up keeps geometry still");
assert!((pattern_x(&pattern_only) - pattern_x(&before) - 20.0).abs() < 0.001, "Backtick held at the button-up moves the pattern");
app.run("edit.undo", json!({})).unwrap();
assert_eq!(node(&app), before);
frame(&mut app, &ctx, vec![Event::PointerMoved(from), button(from, true)]);
frame(&mut app, &ctx, vec![Event::PointerMoved(to)]);
frame(&mut app, &ctx, vec![button(to, false), key(true)]);
let ordinary = node(&app);
frame(&mut app, &ctx, vec![key(false)]);
assert!(
(ordinary.geometric_bounds().unwrap().x0 - before.geometric_bounds().unwrap().x0 - 20.0).abs() < 0.001,
"Backtick pressed after the button-up commits an ordinary move"
);
assert!((pattern_x(&ordinary) - pattern_x(&before)).abs() < 0.001, "the ordinary move keeps the pattern matrix");
// A focus loss after the button-up lets go of Backtick with no release event.
app.run("edit.undo", json!({})).unwrap();
assert_eq!(node(&app), before);
frame(&mut app, &ctx, vec![Event::PointerMoved(from), button(from, true)]);
frame(&mut app, &ctx, vec![key(true), Event::PointerMoved(to)]);
frame(&mut app, &ctx, vec![button(to, false), Event::WindowFocused(false)]);
let unfocused = node(&app);
assert_eq!(unfocused.geometric_bounds(), before.geometric_bounds(), "a focus loss after the button-up keeps geometry still");
assert!((pattern_x(&unfocused) - pattern_x(&before) - 20.0).abs() < 0.001, "a focus loss after the button-up moves the pattern");
}
#[test]
fn mirror_axis_drag_uses_canvas_coordinates_in_a_zoomed_rotated_view() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());