Merge main into fix/fitted-gradient-turns-with-transform

This commit is contained in:
dotCooCoo
2026-11-08 12:09:50 -08:00
111 changed files with 3772 additions and 307 deletions
+62 -3
View File
@@ -28,16 +28,28 @@ fn create_text(s: &mut Session, t: &str) -> NodeId {
NodeId(s.execute("text.create", &json!({"x": 10, "y": 50, "text": t, "size": 20})).unwrap()["id"].as_u64().unwrap())
}
/// Click with the Type tool at (x, y) and type `keys` one at a time; returns the new text object.
/// Click with the Type tool at (x, y) and type `keys` one at a time on the keyboard; returns the
/// new text object.
fn type_at(s: &mut Session, x: f64, y: f64, keys: &[&str]) -> NodeId {
enter_at(s, x, y, keys, true)
}
/// Click with the Type tool at (x, y) and enter `texts` one at a time, typed on the keyboard
/// ([`Session::tool_typed_text`]) or inserted as the Glyphs panel and pastes do
/// ([`Session::tool_text`]); returns the new text object.
fn enter_at(s: &mut Session, x: f64, y: f64, texts: &[&str], keyboard: bool) -> NodeId {
let v = ViewInfo::default();
s.select_tool("type", v).unwrap();
for k in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::new(k, x, y), v).unwrap();
}
let id = s.doc().unwrap().selection.objects[0];
for k in keys {
s.tool_text(k, v).unwrap();
for t in texts {
if keyboard {
s.tool_typed_text(t, v).unwrap();
} else {
s.tool_text(t, v).unwrap();
}
}
s.tool_key(ToolKey::Escape, Default::default(), v).unwrap();
id
@@ -177,6 +189,53 @@ fn typographers_quotes_setting_changes_typed_quotes() {
assert_eq!(text(&s, id).plain_text(), "\"b'");
}
/// A lone typed `”` is set like a straight quote where a quote opens: the macOS Serbian Cyrillic
/// layout has no straight quote and types `”` on Shift+2 (#5247), so Serbian text still gets
/// „…”. Where a quote closes it stays `”`. `“ „` and the single quotes stay as typed, so `’90s`
/// keeps its apostrophe. Pasted text and the Glyphs panel keep their quotes, and with the option
/// off `”` stays as typed.
#[test]
fn a_typed_closing_double_quote_opens_where_a_quote_opens() {
let mut s = session();
s.execute("document.setup", &json!({"language": "Serbian: Cyrillic"})).unwrap();
let id = type_at(&mut s, 50.0, 50.0, &["”", "П", "р", "о", "б", "а", "”"]);
assert_eq!(text(&s, id).plain_text(), "„Проба”");
// The other curly quotes stay as typed.
let id = type_at(&mut s, 50.0, 100.0, &["„", "a", " ", "’", "b", "’", "“"]);
assert_eq!(text(&s, id).plain_text(), "„a ’b’“");
// In English a `”` that opens is `“`; the apostrophes stay.
s.execute("document.setup", &json!({"language": "English: USA"})).unwrap();
let id = type_at(&mut s, 50.0, 150.0, &["”", "i", "t", "’", "s", "”"]);
assert_eq!(text(&s, id).plain_text(), "“it’s”");
// An apostrophe after a space stays an apostrophe, and a typed `‘` stays `‘`.
let id = type_at(&mut s, 50.0, 200.0, &["t", "h", "e", " ", "’", "9", "0", "s", " ", "‘", "a"]);
assert_eq!(text(&s, id).plain_text(), "the ’90s ‘a");
// Where a quote closes, `”` stays `”` even if the document's closing quote is another.
s.execute("document.setup", &json!({"language": "German"})).unwrap();
let id = type_at(&mut s, 50.0, 250.0, &["”", "a", "”"]);
assert_eq!(text(&s, id).plain_text(), "„a”");
// More than one character is pasted text, not a typed quote.
let id = type_at(&mut s, 50.0, 300.0, &["”a”"]);
assert_eq!(text(&s, id).plain_text(), "”a”");
// An IME commit of a lone `”` that will open is not verbatim (it is rewritten); from the Glyphs
// panel it would be inserted as it is.
let v = ViewInfo::default();
s.select_tool("type", v).unwrap();
for k in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::new(k, 50.0, 312.0), v).unwrap();
}
assert!(!s.tool_typed_text_is_verbatim("”", v));
assert!(s.tool_text_is_verbatim("”", v));
s.tool_key(ToolKey::Escape, Default::default(), v).unwrap();
// A `”` inserted from the Glyphs panel or pasted alone stays as inserted.
let id = enter_at(&mut s, 50.0, 325.0, &["”", "a"], false);
assert_eq!(text(&s, id).plain_text(), "”a");
// With the option off a typed `”` stays as typed.
s.execute("document.setup", &json!({"typographersQuotes": false})).unwrap();
let id = type_at(&mut s, 50.0, 350.0, &["”", "b", "”"]);
assert_eq!(text(&s, id).plain_text(), "”b”");
}
#[test]
fn smart_punctuation_uses_the_document_quotes() {
let mut s = session();
+81
View File
@@ -2673,6 +2673,87 @@ fn pen_with_cmd_borrows_direct_selection_and_goes_on_drawing() {
assert_eq!(s.tool_id(), "rectangle");
}
/// #6123: anchors that Cmd (Ctrl) held with the Pen selects with the Direct Selection it lends are
/// what Copy and Cut take, as with Direct Selection chosen; the last anchor the Pen leaves selected
/// while drawing is not, Copy taking the whole path (#2122), nor after a Cmd gesture that picks
/// nothing.
#[test]
fn pen_cmd_selected_anchors_are_what_copy_and_cut_take() {
let (none, cmd) = (Mods::default(), Mods { cmd: true, ..Mods::default() });
let v = view();
let mut s = session();
s.select_tool("pen", v).unwrap();
for x in [100.0, 200.0, 300.0, 400.0] {
gesture(&mut s, &[(x, 300.0)], none);
}
let id = s.doc().unwrap().selection.objects[0];
let pasted = |s: &mut Session| {
let ids = s.execute("edit.paste", &json!({})).unwrap()["ids"].clone();
let ids: Vec<NodeId> = ids.as_array().unwrap().iter().map(|i| NodeId(i.as_u64().unwrap())).collect();
ids.iter().map(|i| path(s, *i).subpaths[0].anchors.len()).collect::<Vec<_>>()
};
// A Cmd marquee around the middle two anchors: Copy takes them and the segment between.
gesture(&mut s, &[(150.0, 250.0), (350.0, 350.0)], cmd);
assert_eq!(s.tool_id(), "pen");
assert_eq!(s.doc().unwrap().selection.anchors[&id], [(0, 1), (0, 2)].into());
s.execute("edit.copy", &json!({})).unwrap();
assert_eq!(pasted(&mut s), [2]);
// Cut takes the segment between them out of the path, as with Direct Selection chosen.
s.execute("edit.undo", &json!({})).unwrap();
s.execute("edit.cut", &json!({})).unwrap();
let left = path(&s, id).subpaths.iter().map(|sp| sp.anchors.len()).collect::<Vec<_>>();
assert_eq!((s.clipboard.nodes.len(), left), (1, vec![2, 2]));
// Drawing on: the last anchor the Pen leaves selected doesn't make Copy take that anchor.
s.execute("edit.undo", &json!({})).unwrap();
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
gesture(&mut s, &[(500.0, 300.0)], none);
assert_eq!(s.doc().unwrap().selection.anchors[&id], [(0, 4)].into());
s.execute("edit.copy", &json!({})).unwrap();
assert_eq!(pasted(&mut s), [5]);
// A Cmd+Shift marquee over nothing picks nothing: the Pen's last anchor stays its own.
s.execute("edit.undo", &json!({})).unwrap();
let shift_cmd = Mods { shift: true, ..cmd };
gesture(&mut s, &[(600.0, 500.0), (700.0, 600.0)], shift_cmd);
assert_eq!(s.doc().unwrap().selection.anchors[&id], [(0, 4)].into());
s.execute("edit.copy", &json!({})).unwrap();
assert_eq!(pasted(&mut s), [5]);
// The tool started afresh in the same document (another view of it) keeps a pick picked.
s.execute("edit.undo", &json!({})).unwrap();
gesture(&mut s, &[(150.0, 250.0), (350.0, 350.0)], cmd);
s.reset_tool();
s.execute("edit.copy", &json!({})).unwrap();
assert_eq!(pasted(&mut s), [2]);
}
/// #6123: with the other tools, Cmd (Ctrl) lends the selection tool chosen last. Direct Selection
/// lent picks anchors, which Copy takes, as with the Curvature tool, which Cmd lends Direct
/// Selection until a selection tool is chosen; the Selection tool lent selects the path whole.
#[test]
fn cmd_selected_anchors_with_other_tools_are_what_copy_takes() {
let cmd = Mods { cmd: true, ..Mods::default() };
let v = view();
let copied = |tool: &str, last_selection: Option<&str>| {
let mut s = session();
let anchors = json!([{"x": 100, "y": 300}, {"x": 200, "y": 300}, {"x": 300, "y": 300}, {"x": 400, "y": 300}]);
s.execute("path.create", &json!({"anchors": anchors})).unwrap();
s.execute("select.none", &json!({})).unwrap();
if let Some(t) = last_selection {
s.select_tool(t, v).unwrap();
}
s.select_tool(tool, v).unwrap();
gesture(&mut s, &[(150.0, 250.0), (350.0, 350.0)], cmd);
assert_eq!(s.tool_id(), tool);
s.execute("edit.copy", &json!({})).unwrap();
let ids = s.execute("edit.paste", &json!({})).unwrap()["ids"].clone();
let id = NodeId(ids[0].as_u64().unwrap());
path(&s, id).subpaths[0].anchors.len()
};
assert_eq!(copied("rectangle", Some("directSelection")), 2);
assert_eq!(copied("ellipse", Some("directSelection")), 2);
assert_eq!(copied("curvature", None), 2);
assert_eq!(copied("rectangle", None), 4, "the Selection tool lent selects the path whole");
}
/// #4980: Cmd/Ctrl held with the Pen drags as Direct Selection: an anchor of the path being drawn
/// moves on its own and the Pen goes on drawing that path from it; another path's straight segment
/// moves both its ends and a curved one bends; each drag is one undo step and the pointer is the