Canvas: Shift, Alt and Cmd/Ctrl pressed or released mid-drag apply with the pointer held still (#4711)

This commit is contained in:
bflatastic
2026-10-31 00:26:15 +07:00
parent a2d3841579
commit 79fe17934a
3 changed files with 50 additions and 2 deletions
+10
View File
@@ -78,6 +78,16 @@ pub(super) fn tool_drag_moved(ui: &Ui, xf: &Xf, p: Pos2, pointer: &egui::Pointer
true
}
/// 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 {
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)
}
/// 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`]
+3 -2
View File
@@ -1,7 +1,7 @@
use super::artboards::{artboard_name_at, begin_artboard_name_edit, name_press_id, select_named_artboard};
use super::drag::{
HoverContext, apply_requests, dispatch, drag_art_out, drag_id, drag_layer_id, drag_pos_id, drop_press, mods, on_canvas, pen_pressure,
tool_drag_doc_id, tool_drag_moved, tool_frame_moves, tool_holds, tool_press_doc_id,
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,
@@ -361,7 +361,8 @@ 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.
if tool_drag_moved(ui, &xf, p, pointer) || !moves.is_empty() {
let mods_changed = tool_drag_mods_changed(ui, mods(m, space));
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), pressure }, view);
+37
View File
@@ -6054,6 +6054,43 @@ fn tool_drag_moved_without_pointer_delta() {
assert!(moved, "a new document position runs even when egui reports no delta");
}
/// #4711: Shift pressed and released while a corner handle is held still scales proportionally and
/// then freely again at once, without waiting for the pointer to move.
#[test]
fn a_modifier_changed_mid_scale_applies_with_the_pointer_held_still() {
use egui::{Event, Modifiers};
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
app.ui.view.smart_guides = false;
app.ui.view.snap_to_point = false;
let id = app.session.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 100, "height": 50})).unwrap()["id"].as_u64().unwrap();
app.select_tool("selection");
let ctx = egui::Context::default();
frame(&mut app, &ctx, vec![]);
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
let with = |app: &mut VectorcraftApp, mut events: Vec<Event>, modifiers: Modifiers| {
events.insert(0, Event::ModifiersChanged(modifiers));
let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), events, ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| show(app, ui));
out.textures_delta.clear();
};
let button = |pos, pressed| Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
let height =
|app: &VectorcraftApp| app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().geometric_bounds().unwrap().height();
let corner = xf.to_screen(Point::new(200.0, 150.0));
let to = xf.to_screen(Point::new(300.0, 150.0));
with(&mut app, vec![Event::PointerMoved(corner)], Modifiers::NONE);
with(&mut app, vec![button(corner, true)], Modifiers::NONE);
with(&mut app, vec![Event::PointerMoved(to)], Modifiers::NONE);
assert!((height(&app) - 50.0).abs() < 1e-6, "a free scale keeps the height: {}", height(&app));
with(&mut app, vec![], Modifiers::SHIFT);
assert!((height(&app) - 100.0).abs() < 1e-6, "Shift scales proportionally at once: {}", height(&app));
with(&mut app, vec![], Modifiers::NONE);
assert!((height(&app) - 50.0).abs() < 1e-6, "releasing Shift frees it at once: {}", height(&app));
with(&mut app, vec![button(to, false)], Modifiers::NONE);
assert!(app.session.active().unwrap().interaction.is_none(), "committed");
}
/// #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.