mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #3729 from storytold/land/3360
G11 #834: bezier drag previews follow the pointer to the release
This commit is contained in:
@@ -124,20 +124,27 @@ fn drag_layer_id() -> egui::Id {
|
||||
fn on_canvas(ui: &Ui, resp: &egui::Response, p: Pos2) -> bool {
|
||||
resp.rect.contains(p) && ui.clip_rect().contains(p) && ui.ctx().layer_id_at(p).is_none_or(|l| l == resp.layer_id)
|
||||
}
|
||||
/// The document point last sent to the tool as a drag during [`Drag::Tool`].
|
||||
/// The document point last sent to the tool as a drag during [`Drag::Tool`]; none until the press
|
||||
/// has dragged.
|
||||
fn tool_drag_doc_id() -> egui::Id {
|
||||
egui::Id::new("canvas-tool-drag-doc")
|
||||
}
|
||||
/// Whether the tool should get another drag event: the pointer moved this frame, or its document
|
||||
/// position changed without a delta (some Wayland compositors coalesce motion until release, #834).
|
||||
/// The document point of the press that began [`Drag::Tool`], sent to the tool as its Down.
|
||||
fn tool_press_doc_id() -> egui::Id {
|
||||
egui::Id::new("canvas-tool-press-doc")
|
||||
}
|
||||
/// Whether the tool should get another drag event, and if so records `p` as the last position
|
||||
/// sent: the pointer moved this frame, or its document position changed without a delta (#834).
|
||||
/// egui's frame delta is zero when some Wayland compositors coalesce motion until release, and in
|
||||
/// the frame the pointer comes back into the window after leaving it (`PointerGone` forgot where
|
||||
/// it was), though it is somewhere new; judged on the delta alone, the preview stayed behind.
|
||||
fn tool_drag_moved(ui: &Ui, xf: &Xf, p: Pos2, pointer: &egui::PointerState) -> bool {
|
||||
if pointer.delta() != egui::Vec2::ZERO {
|
||||
return true;
|
||||
}
|
||||
let at = xf.to_doc(p);
|
||||
let id = tool_drag_doc_id();
|
||||
let prev = ui.data(|d| d.get_temp::<Point>(id));
|
||||
if prev == Some(at) {
|
||||
// Before the press has dragged, the last position the tool got is the press itself: a press
|
||||
// held still is not a drag.
|
||||
let prev = ui.data(|d| d.get_temp::<Point>(id).or_else(|| d.get_temp::<Point>(tool_press_doc_id())));
|
||||
if pointer.delta() == egui::Vec2::ZERO && prev == Some(at) {
|
||||
return false;
|
||||
}
|
||||
ui.data_mut(|d| d.insert_temp(id, at));
|
||||
@@ -998,6 +1005,9 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
|
||||
dd.insert_temp(drag_id(), d);
|
||||
dd.insert_temp(drag_pos_id(), p);
|
||||
dd.insert_temp(drag_layer_id(), resp.layer_id);
|
||||
// A new press has not dragged yet, whatever an interrupted one left behind.
|
||||
dd.remove::<Point>(tool_drag_doc_id());
|
||||
dd.insert_temp(tool_press_doc_id(), xf.to_doc(p));
|
||||
});
|
||||
}
|
||||
} else if drag.is_none()
|
||||
@@ -1085,10 +1095,13 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The last position the tool's drag got: none if the press never dragged.
|
||||
let last_drag: Option<Point> = ui.data(|dd| dd.get_temp(tool_drag_doc_id()));
|
||||
ui.data_mut(|dd| {
|
||||
dd.remove::<Drag>(drag_id());
|
||||
dd.remove::<Pos2>(drag_pos_id());
|
||||
dd.remove::<Point>(tool_drag_doc_id());
|
||||
dd.remove::<Point>(tool_press_doc_id());
|
||||
});
|
||||
match d {
|
||||
Drag::ZoomBox { start } => {
|
||||
@@ -1118,6 +1131,16 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
|
||||
if resp.double_clicked() && app.session.tool_id() == "artboard" {
|
||||
app.session.set_tool_option("suppressFitClick", &json!(true));
|
||||
}
|
||||
// A move that came in the release's own frame shapes the preview first: the
|
||||
// tools commit the last preview, which should be where the pointer let go
|
||||
// (#834). Only once the press has dragged, so a click's jitter stays a click
|
||||
// (the Pen's corner anchor, no nudge with Direct Selection).
|
||||
if let Some(last) = last_drag
|
||||
&& last != xf.to_doc(p)
|
||||
{
|
||||
let ev = PointerEvent { kind: PointerKind::Drag, pos: xf.to_doc(p), mods: mods(m, space), pressure: pen_pressure(ui, false) };
|
||||
dispatch(app, &ev, view);
|
||||
}
|
||||
let ev = PointerEvent { kind: PointerKind::Up, pos: xf.to_doc(p), mods: mods(m, space), pressure: pen_pressure(ui, false) };
|
||||
dispatch(app, &ev, view);
|
||||
}
|
||||
@@ -8236,6 +8259,108 @@ mod tests {
|
||||
assert!(moved, "a new document position runs even when egui reports no delta");
|
||||
}
|
||||
|
||||
/// #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.
|
||||
#[test]
|
||||
fn stationary_press_is_not_a_drag() {
|
||||
let ctx = egui::Context::default();
|
||||
let rect = egui::Rect::from_min_size(Pos2::ZERO, vec2(400.0, 300.0));
|
||||
let v = View::default();
|
||||
let xf = Xf::new(rect, &v);
|
||||
let a = pos2(100.0, 100.0);
|
||||
let b = pos2(150.0, 120.0);
|
||||
let mut out =
|
||||
ctx.run_ui(egui::RawInput { screen_rect: Some(rect), events: vec![egui::Event::PointerMoved(a)], ..Default::default() }, |ui| {
|
||||
ui.data_mut(|d| {
|
||||
d.remove::<Point>(tool_drag_doc_id());
|
||||
d.insert_temp(tool_press_doc_id(), xf.to_doc(a));
|
||||
});
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
let mut held = true;
|
||||
out = ctx.run_ui(egui::RawInput { screen_rect: Some(rect), ..Default::default() }, |ui| {
|
||||
let pointer = ui.input(|i| i.pointer.clone());
|
||||
assert_eq!(pointer.delta(), egui::Vec2::ZERO);
|
||||
held = tool_drag_moved(ui, &xf, a, &pointer);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
assert!(!held, "a press held still at its own position is not sent as a drag");
|
||||
let mut moved = false;
|
||||
out = ctx.run_ui(egui::RawInput { screen_rect: Some(rect), events: vec![egui::Event::PointerMoved(b)], ..Default::default() }, |ui| {
|
||||
let pointer = ui.input(|i| i.pointer.clone());
|
||||
moved = tool_drag_moved(ui, &xf, b, &pointer);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
assert!(moved, "the first move after the press is a drag");
|
||||
}
|
||||
|
||||
/// #834: a bezier drag's preview follows every position the pointer is seen at. egui's frame
|
||||
/// delta is zero in the frame the pointer comes back into the window after leaving it
|
||||
/// (`PointerGone` forgets where it was), so a drag gated on it kept the preview where the
|
||||
/// pointer left until the next move; a move in the release's own frame was lost the same way,
|
||||
/// and the release committed the stale shape. A click's jitter in its release frame stays a
|
||||
/// click.
|
||||
#[test]
|
||||
fn a_bezier_drag_preview_follows_the_pointer_back_into_the_window_and_to_the_release() {
|
||||
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 ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
|
||||
let button = |pos, pressed| Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
|
||||
let near = |p: Point, q: Pos2| p.distance(xf.to_doc(q)) < 1e-3;
|
||||
let first_anchor = |app: &VectorcraftApp| {
|
||||
let st = app.session.active().unwrap();
|
||||
st.doc.node(st.selection.objects[0]).unwrap().path_data().unwrap().subpaths[0].anchors[0]
|
||||
};
|
||||
// The Pen pulls a handle out of a new anchor: the pointer leaves the window mid-drag, comes
|
||||
// back further on and stops there, then moves once more in the frame it lets go.
|
||||
app.select_tool("pen");
|
||||
let a = app.canvas_rect.unwrap().center() - vec2(100.0, 60.0);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(a)]);
|
||||
frame(&mut app, &ctx, vec![button(a, true)]);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(a + vec2(40.0, 0.0))]);
|
||||
assert!(near(first_anchor(&app).h_out, a + vec2(40.0, 0.0)), "{:?}", first_anchor(&app));
|
||||
frame(&mut app, &ctx, vec![Event::PointerGone]);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(a + vec2(80.0, 0.0))]);
|
||||
assert!(near(first_anchor(&app).h_out, a + vec2(80.0, 0.0)), "back in the window: {:?}", first_anchor(&app));
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
assert!(near(first_anchor(&app).h_out, a + vec2(80.0, 0.0)), "still: {:?}", first_anchor(&app));
|
||||
let end = a + vec2(100.0, 20.0);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(end), button(end, false)]);
|
||||
assert!(near(first_anchor(&app).h_out, end), "released: {:?}", first_anchor(&app));
|
||||
assert!(app.session.active().unwrap().interaction.is_none(), "committed");
|
||||
// Direct Selection moving that anchor: the same path through the engine's previews.
|
||||
app.select_tool("directSelection");
|
||||
let anchor_at = xf.to_screen(first_anchor(&app).p);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(anchor_at)]);
|
||||
frame(&mut app, &ctx, vec![button(anchor_at, true)]);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(anchor_at + vec2(30.0, 30.0))]);
|
||||
assert!(near(first_anchor(&app).p, anchor_at + vec2(30.0, 30.0)), "{:?}", first_anchor(&app));
|
||||
frame(&mut app, &ctx, vec![Event::PointerGone]);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(anchor_at + vec2(60.0, 30.0))]);
|
||||
assert!(near(first_anchor(&app).p, anchor_at + vec2(60.0, 30.0)), "back in the window: {:?}", first_anchor(&app));
|
||||
let end = anchor_at + vec2(60.0, 50.0);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(end), button(end, false)]);
|
||||
assert!(near(first_anchor(&app).p, end), "released: {:?}", first_anchor(&app));
|
||||
// A click whose release frame brings a little jitter is still a click: the Pen places a
|
||||
// corner anchor (no handle pulled out by the jitter) and the anchor stays where pressed.
|
||||
app.session.execute("select.none", &json!({})).unwrap();
|
||||
app.select_tool("pen");
|
||||
let c = app.canvas_rect.unwrap().center() + vec2(120.0, 80.0);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(c)]);
|
||||
frame(&mut app, &ctx, vec![button(c, true), Event::PointerMoved(c + vec2(0.6, 0.2)), button(c + vec2(0.6, 0.2), false)]);
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
// (egui hands the press the frame's last pointer position, so it lands within the jitter.)
|
||||
let a = first_anchor(&app);
|
||||
assert!(a.p.distance(xf.to_doc(c)) < 1.0 && a.h_out == a.p && a.h_in == a.p, "corner anchor where pressed: {a:?}");
|
||||
assert!(app.session.active().unwrap().interaction.is_none(), "committed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_moves_keeps_touch_samples_and_pressure_but_leaves_mouse_unchanged() {
|
||||
let ctx = egui::Context::default();
|
||||
|
||||
+7
-2
File diff suppressed because one or more lines are too long
+3
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4
-3
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user