Merge main into fix/options-dialog-menus

This commit is contained in:
dotCooCoo
2026-11-11 19:59:50 -08:00
46 changed files with 611 additions and 56 deletions
+6 -2
View File
@@ -673,10 +673,14 @@ fn undo_group_end(s: &mut Session, p: &Value) -> Result<Value> {
/// the step before. As any new edit it ends the steps to redo, so Redo never swaps the document
/// under an interaction that would then preview on, or cancel to, the document from before (#1719).
/// A typing session also ends in the Type tool, whose caret and marked text (IME) must follow the
/// document.
/// document. A preview that Real-time Drawing off left unapplied is applied first, as the tool's own
/// commit applies it; when it fails, the interaction is canceled.
pub(crate) fn settle_interaction(s: &mut Session) -> Result<()> {
let typing = typing_in_progress(s);
s.commit_interaction()?;
match s.unapplied_interaction_preview().filter(|_| !s.previewing) {
Some((cmd, p)) if s.preview(&cmd, &p).is_err() => s.cancel_interaction()?,
_ => s.commit_interaction()?,
}
if typing {
s.set_tool_option("endTyping", &Value::Bool(true));
}
+57
View File
@@ -3784,3 +3784,60 @@ fn a_filter_that_calls_commands_is_given_the_edit_in_progress() {
assert_eq!(x, json!(17.0), "undoing the filter brings back the moved rectangle");
run(&mut s, "plugin.remove", json!({"id": id}));
}
/// Installs a filter `id` that calls commands and hands back what it is given, with `commands` (a
/// JSON list of command specs) in its manifest.
fn install_echo_calling(s: &mut Session, id: &str, commands: &str) {
let grant = json!(["doc.read", "doc.write", "commands"]);
let manifest = format!(
r#"{{"id":"{id}","name":"Test {id}","kind":"filter","api":2,"permissions":{grant},"commandAllowlist":["select.all"],"commands":{commands}}}"#
);
let wasm = wat::parse_str(tpl::module(&manifest, tpl::ECHO, "", 1)).unwrap();
run(s, "plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&wasm), "grant": grant, "replace": true}));
}
/// With Real-time Drawing off, a 50 × 50 rectangle at (10, 10) whose move by 7 pt is still being
/// dragged: the tool has stored the move, which the document does not hold yet.
fn deferred_move(s: &mut Session) -> NodeId {
let r = NodeId(run(s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50}))["id"].as_u64().unwrap());
run(s, "prefs.set", json!({"key": "realTimeDrawing", "value": false}));
let move_by = vectorcraft_tools::Action::Preview("object.move".into(), json!({"dx": 7, "dy": 0}));
s.apply_actions(vec![vectorcraft_tools::Action::Begin("Move".into()), move_by]).unwrap();
assert_eq!(node(s, r).geometric_bounds().unwrap().x0, 10.0, "the move is not applied while it is dragged");
r
}
/// The rectangle a run named `label` during [`deferred_move`] left: moved, with the interaction
/// ended and the move an undo step of its own before the run's.
fn assert_moved_once(s: &Session, r: NodeId, label: &str) {
assert!(!s.in_interaction());
assert_eq!(node(s, r).geometric_bounds().unwrap().x0, 17.0, "the filter was given the moved rectangle");
let labels: Vec<&str> = s.doc().unwrap().history.undo.iter().rev().take(2).map(|e| e.label.as_str()).collect();
assert_eq!(labels, [label, "Move"]);
}
/// With Real-time Drawing off, `plugin.run` of a filter that calls commands during a drag applies
/// the dragged move first and gives the filter the moved rectangle, which it hands back as it was.
#[test]
fn a_filter_that_calls_commands_is_given_a_deferred_move() {
let id = "test.engine.callsdeferred";
let mut s = session();
install_echo_calling(&mut s, id, "[]");
let r = deferred_move(&mut s);
run(&mut s, "plugin.run", json!({"id": id}));
assert_moved_once(&s, r, &format!("Test {id}"));
run(&mut s, "plugin.remove", json!({"id": id}));
}
/// A plug-in command that calls commands, run with Real-time Drawing off during a drag, is given
/// the moved rectangle as `plugin.run` is.
#[test]
fn a_plugin_command_that_calls_commands_is_given_a_deferred_move() {
let id = "test.engine.callsdeferredcmd";
let mut s = session();
install_echo_calling(&mut s, id, r#"[{"id":"same","label":"Same"}]"#);
let r = deferred_move(&mut s);
run(&mut s, &format!("plugin.{id}.same"), json!({}));
assert_moved_once(&s, r, "Same");
run(&mut s, "plugin.remove", json!({"id": id}));
}
+36
View File
@@ -2047,6 +2047,42 @@ fn real_time_drawing_turned_on_mid_drag_still_commits_deferred_preview() {
assert_eq!(paths(&s), 1, "the deferred rectangle is committed");
}
/// With Real-time Drawing off, Undo during a drag keeps the dragged move as its own step and takes
/// back only that: the rectangle drawn before stays, and Redo brings the move back.
#[test]
fn undo_during_a_deferred_drag_takes_back_only_the_drag() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
let r = NodeId(s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap());
set_pref(&mut s, "realTimeDrawing", json!(false));
let x = |s: &Session| s.doc().unwrap().doc.node(r).and_then(vectorcraft_doc::Node::geometric_bounds).map(|b| b.x0);
let move_by = vectorcraft_tools::Action::Preview("object.move".into(), json!({"dx": 7, "dy": 0}));
s.apply_actions(vec![vectorcraft_tools::Action::Begin("Move".into()), move_by]).unwrap();
assert_eq!(x(&s), Some(10.0));
s.execute("edit.undo", &json!({})).unwrap();
assert!(!s.in_interaction());
assert_eq!(x(&s), Some(10.0), "the rectangle stays");
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(x(&s), Some(17.0), "Redo brings the move back");
}
/// Undo during a drag whose stored preview fails cancels the drag, as its release would, and then
/// takes back the step before it.
#[test]
fn undo_during_a_deferred_drag_that_fails_cancels_it() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
set_pref(&mut s, "realTimeDrawing", json!(false));
let bad = vectorcraft_tools::Action::Preview("no.such.command".into(), json!({}));
s.apply_actions(vec![vectorcraft_tools::Action::Begin("Bad".into()), bad]).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
assert!(!s.in_interaction());
let d = s.doc().unwrap();
assert_eq!(d.doc.layers.iter().map(|l| l.children().map_or(0, |c| c.len())).sum::<usize>(), 0, "the rectangle is undone");
assert!(d.history.undo.iter().all(|e| e.label != "Bad"));
}
/// Repeated overlay redraws with Real-time Drawing off reuse the cached deferred feedback.
#[test]
fn deferred_preview_overlays_are_cached_across_redraws() {