Merge #280: Fix text edits rolling back prior document changes (@justinjohn0306)

This commit is contained in:
bflatastic
2026-10-15 19:32:25 +07:00
3 changed files with 103 additions and 9 deletions
+20
View File
@@ -77,6 +77,26 @@ fn double_clicks_reach_and_edit_text_in_a_group_wherever_they_land() {
}
}
#[test]
fn editing_text_keeps_other_document_changes_since_open() {
let mut s = Session::new();
let first = id_of(&ex(&mut s, "shape.rectangle", json!({"x": 20, "y": 20, "width": 80, "height": 40}))["id"]).unwrap();
let deleted = id_of(&ex(&mut s, "shape.rectangle", json!({"x": 120, "y": 20, "width": 80, "height": 40}))["id"]).unwrap();
let text = id_of(&ex(&mut s, "text.create", json!({"text": "A", "x": 20, "y": 100}))["id"]).unwrap();
ex(&mut s, "node.set", json!({"ids": [first], "props": {"x": 650}}));
ex(&mut s, "edit.delete", json!({"ids": [deleted]}));
let bounds = s.doc().unwrap().world_bounds(text).unwrap();
double_click(&mut s, bounds.center().x, bounds.center().y, 0.0, 1.0);
assert_eq!(editing(&s), Some(text));
crate::textedit::insert(&mut s, "B").unwrap();
crate::textedit::end(&mut s).unwrap();
assert_eq!(s.doc().unwrap().world_bounds(first).unwrap().x0, 650.0);
assert!(!s.doc().unwrap().contains(deleted));
assert_eq!(s.doc().unwrap().node(text).unwrap().kind.text().unwrap().characters, "B");
}
#[test]
fn a_press_drags_only_past_the_threshold_in_screen_pixels() {
for (zoom, by, moves) in [(1.0, 2.0, false), (1.0, 4.0, true), (4.0, 0.5, false), (4.0, 1.0, true), (0.25, 8.0, false), (0.25, 16.0, true)] {
+21 -9
View File
@@ -428,6 +428,13 @@ 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 {
// 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;
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);
}
}
// Leaving the canvas: nothing is hovered there any more.
if !app.canvas.dragging && std::mem::take(&mut app.canvas.hovering) {
app.session.vector_pointer_left();
@@ -539,19 +546,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) {
+62
View File
@@ -59,3 +59,65 @@ fn a_drag_shows_on_the_canvas_while_it_moves() {
assert!(u.app.worker.as_ref().is_some_and(|w| !w.in_flight()), "nothing left to render");
u.button(p + vec2(30.0, 10.0), false);
}
#[test]
fn releasing_a_canvas_drag_outside_finishes_the_interaction() {
let mut s = Session::new();
let r =
figcraft_engine::params::id_of(&s.execute("shape.rectangle", &json!({"x": 40, "y": 40, "width": 80, "height": 60})).unwrap()["id"]).unwrap();
s.execute("select.set", &json!({"ids": [r]})).unwrap();
let mut u = Ui::new(s);
let start = u.app.to_screen(80.0, 70.0);
let outside = egui::pos2(u.app.canvas.rect.max.x + 20.0, start.y);
u.button(start, true);
u.frame(vec![Event::PointerMoved(outside)]);
u.button(outside, false);
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");
}