#280: a drag released off the window ends where the pointer was last seen, not mid-canvas; the test follows #278

This commit is contained in:
bflatastic
2026-10-15 18:34:23 +07:00
parent 29954e6067
commit 477f019737
2 changed files with 64 additions and 17 deletions
+18 -17
View File
@@ -371,16 +371,12 @@ fn input(app: &mut FigApp, ui: &mut Ui, resp: &egui::Response) -> bool {
}
}
let Some(pos) = hover_pos.filter(|p| app.canvas.rect.contains(*p)) else {
if app.canvas.dragging && ctx.input(|i| i.pointer.button_released(egui::PointerButton::Primary)) {
// A drag released off the canvas, or off the window, ends where the pointer was last seen.
if app.canvas.dragging && !ctx.input(|i| i.pointer.primary_down()) {
app.canvas.dragging = false;
let pos = hover_pos.unwrap_or(app.canvas.rect.center());
let ev = pointer_event(app, PointerKind::Up, pos, &mods);
app.canvas.pressure = None;
app.canvas.last_pointer = Some(Point::new(ev.x, ev.y));
if let Err(e) = app.session.pointer(&ev) {
app.toast(e.to_string());
if let Some(at) = hover_pos.or_else(|| app.canvas.last_pointer.map(|p| app.to_screen(p.x, p.y))) {
send(app, PointerKind::Up, at, &mods);
}
app.handle_ui_requests();
}
// Leaving the canvas: nothing is hovered there any more.
if !app.canvas.dragging && std::mem::take(&mut app.canvas.hovering) {
@@ -493,19 +489,24 @@ fn input(app: &mut FigApp, ui: &mut Ui, resp: &egui::Response) -> bool {
app.canvas.pressure = force;
}
if let Some(k) = kind {
let ev = pointer_event(app, k, pos, &mods);
if k == PointerKind::Up {
app.canvas.pressure = None;
}
app.canvas.last_pointer = Some(Point::new(ev.x, ev.y));
if let Err(e) = app.session.pointer(&ev) {
app.toast(e.to_string());
}
app.handle_ui_requests();
send(app, k, pos, &mods);
}
true
}
/// Pointer event `kind` at screen point `pos`, to the tools.
fn send(app: &mut FigApp, kind: PointerKind, pos: Pos2, m: &egui::Modifiers) {
let ev = pointer_event(app, kind, pos, m);
if kind == PointerKind::Up {
app.canvas.pressure = None;
}
app.canvas.last_pointer = Some(Point::new(ev.x, ev.y));
if let Err(e) = app.session.pointer(&ev) {
app.toast(e.to_string());
}
app.handle_ui_requests();
}
/// Right-click at screen `pos`: select what's under the pointer (`deep`: the nested layer), then
/// open the context menu there.
pub fn open_context_menu(app: &mut FigApp, pos: Pos2, deep: bool) {
+46
View File
@@ -75,3 +75,49 @@ fn releasing_a_canvas_drag_outside_finishes_the_interaction() {
assert!(u.app.session.ds().unwrap().interaction.is_none(), "the document interaction is committed");
}
fn key(u: &mut Ui, key: egui::Key) {
let modifiers = egui::Modifiers::NONE;
u.frame(vec![
Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers },
Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers },
]);
}
#[test]
fn a_drag_released_off_the_window_ends_where_the_pointer_was_last_seen_and_later_edits_stay() {
// #278: a drag left open made a later text edit roll back the changes made since.
let mut s = Session::new();
let id = |v: serde_json::Value| figcraft_engine::params::id_of(&v["id"]).unwrap();
let kept = id(s.execute("shape.rectangle", &json!({"x": 40, "y": 400, "width": 80, "height": 40})).unwrap());
let gone = id(s.execute("shape.rectangle", &json!({"x": 160, "y": 400, "width": 80, "height": 40})).unwrap());
let text = id(s.execute("text.create", &json!({"text": "A", "x": 40, "y": 500})).unwrap());
s.execute("tool.rectangle", &json!({})).unwrap();
let mut u = Ui::new(s);
// A rectangle drawn from the middle of the canvas, released off the window, the pointer gone.
let start = u.app.canvas.rect.center();
u.button(start, true);
for i in 1..=6 {
u.frame(vec![Event::PointerMoved(start + vec2(10.0, 6.0) * i as f32)]);
}
let release =
Event::PointerButton { pos: egui::pos2(-50.0, -50.0), button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() };
u.frame(vec![release, Event::PointerGone]);
let ds = u.app.session.ds().unwrap();
assert!(ds.interaction.is_none(), "the drawing is committed");
let (drawn, zoom) = (ds.selection[0], ds.view.zoom);
let b = ds.doc.world_bounds(drawn).unwrap();
// Not the 100 x 100 shape of a click, as an end in the middle of the canvas would make.
assert!((b.width() * zoom - 60.0).abs() < 1.5 && (b.height() * zoom - 36.0).abs() < 1.5, "{b:?} at zoom {zoom}");
// #278's steps: a move in the panel, a delete, then a text edit.
u.app.run("node.set", json!({"ids": [kept], "props": {"x": 650}}));
u.app.run("edit.delete", json!({"ids": [gone]}));
u.app.run("select.set", json!({"ids": [text]}));
key(&mut u, egui::Key::Enter);
u.frame(vec![Event::Text("B".into())]);
key(&mut u, egui::Key::Escape);
let doc = u.app.session.doc().unwrap();
assert_eq!(doc.world_bounds(kept).unwrap().x0, 650.0, "the move stays");
assert!(!doc.contains(gone), "the delete stays");
assert_eq!(doc.node(text).unwrap().kind.text().unwrap().characters, "B");
}