Merge pull request #4644 from ham9zah/feat/type-tool-group
Type tools: a Type tool that uses the clicked path, pointers that say what a click makes, and an icon per tool
@@ -224,13 +224,16 @@ Generated-in-code art is original and has no file to list. This covers the defau
|
||||
| `assets/icons/dc-swap.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-swatch-kinds.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-symbol-sprayer.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-touch-type.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-touch-type.svg` | VectorCraft contributors (designed by ham9zah) | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-transform-panel.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-transparency.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-twirl.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-type-area.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-type-path.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-type-vertical.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-type.svg` | VectorCraft contributors (designed by ham9zah) | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-type-area.svg` | VectorCraft contributors (designed by ham9zah) | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-type-path.svg` | VectorCraft contributors (designed by ham9zah) | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-type-vertical.svg` | VectorCraft contributors (designed by ham9zah) | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-type-vertical-area.svg` | VectorCraft contributors (designed by ham9zah) | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-type-vertical-path.svg` | VectorCraft contributors (designed by ham9zah) | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-width-profile.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-width.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
| `assets/icons/dc-wrinkle.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | |
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 5h9M10.5 5v11"/><rect x="14" y="12" width="7" height="8" stroke-dasharray="1.5 1.5"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4h10M20 7v10M17 20H7M4 17V7" stroke-width="1"/><path d="M8 10V8h8v2M12 8v8"/><rect x="2.5" y="2.5" width="3" height="3" rx=".75" stroke-width="1"/><rect x="18.5" y="2.5" width="3" height="3" rx=".75" stroke-width="1"/><rect x="18.5" y="18.5" width="3" height="3" rx=".75" stroke-width="1"/><rect x="2.5" y="18.5" width="3" height="3" rx=".75" stroke-width="1"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 287 B After Width: | Height: | Size: 563 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" stroke-dasharray="2 2"/><path d="M8 8h8M12 8v9"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="2.5" width="17" height="19" rx="2.5" stroke-width="1.25"/><path d="M8 8V6h8v2M12 6v7"/><path d="M7.5 16.5h9M7.5 18.5H13" stroke-width="1"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 280 B After Width: | Height: | Size: 346 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 19 C8 9 16 9 21 19"/><path d="M8 5h8M12 5v8"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 5.5v-2h8v2M12 3.5v10"/><path d="M3 18.5c3-3.5 6-3.5 9 0s6 3.5 9 0" stroke-width="1.25"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 248 B After Width: | Height: | Size: 290 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="8.5" y="2.5" width="13" height="19" rx="2.5" stroke-width="1.25"/><path d="M11.5 8V6h7v2M15 6v8"/><path d="M12 17h6" stroke-width="1"/><path d="M4 4v15.5M1.75 17.25 4 19.5l2.25-2.25" stroke-width="1.25"/></svg>
|
||||
|
After Width: | Height: | Size: 403 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 5.5v-2h10v2M15 3.5V13"/><path d="M4 3v10.5M1.75 11.25 4 13.5l2.25-2.25" stroke-width="1.25"/><path d="M3 18.5c3-3.5 6-3.5 9 0s6 3.5 9 0" stroke-width="1.25"/></svg>
|
||||
|
After Width: | Height: | Size: 361 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4h10M12 4v8"/><path d="M9 15h6M12 15v6"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 6.5v-2h10v2M15 4.5V19"/><path d="M4 4v15.5M1.75 17.25 4 19.5l2.25-2.25" stroke-width="1.25"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 243 B After Width: | Height: | Size: 296 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 6.5v-2h14v2M12 4.5v15"/></svg>
|
||||
|
After Width: | Height: | Size: 226 B |
@@ -277,3 +277,41 @@ fn direct_selection_drags_an_anchor_of_the_path_and_the_type_reflows() {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(path(&s).subpaths[0].anchors[1].p, middle);
|
||||
}
|
||||
|
||||
/// Type on a path made by clicking a path with the plain Type tool (an open path) or Type on a
|
||||
/// Path (a closed one) shows its brackets as soon as the Selection or Direct Selection tool is
|
||||
/// chosen, and its centre bracket, dragged across the path, flips it.
|
||||
#[test]
|
||||
fn type_on_a_path_made_with_the_type_tool_has_brackets_at_once() {
|
||||
let v = ViewInfo::default();
|
||||
for (tool, d, mods) in [
|
||||
("selection", "M100 300 L500 300", Mods::default()),
|
||||
("directSelection", "M100 300 L500 300", Mods::default()),
|
||||
("selection", "M100 300 L500 300 L500 500 L100 500 Z", Mods::default()),
|
||||
] {
|
||||
let mut s = session();
|
||||
s.execute("path.create", &json!({ "d": d })).unwrap();
|
||||
s.select_tool(if d.ends_with('Z') { "typeOnPath" } else { "type" }, v).unwrap();
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(kind, 300.0, 300.0).with_mods(mods), v).unwrap();
|
||||
}
|
||||
let id = *s.doc().unwrap().selection.whole().first().expect("the new type is selected");
|
||||
assert!(matches!(text(&s, id).kind, TextKind::OnPath { .. }), "{tool} {d}");
|
||||
s.execute("text.setText", &json!({"ids": [id.0], "text": "Slide"})).unwrap();
|
||||
s.select_tool(tool, v).unwrap();
|
||||
let lines = s.overlays(v).iter().filter(|o| matches!(o, vectorcraft_tools::Overlay::Line { .. })).count();
|
||||
assert!(lines >= 5, "{tool} {d}: the brackets are drawn at once ({lines} lines)");
|
||||
if d.ends_with('Z') {
|
||||
continue;
|
||||
}
|
||||
// The centre bracket stands midway between the start (300) and the end of the path (500).
|
||||
let (start, end) = brackets(&s, id);
|
||||
assert!(near(start, 0.5) && end.is_none(), "{start} {end:?}");
|
||||
let centre = Point::new(400.0, 295.0);
|
||||
assert_eq!(s.cursor(centre, Mods::default(), v), vectorcraft_tools::Cursor::PathBracket, "{tool}");
|
||||
drag(&mut s, tool, centre, Point::new(400.0, 330.0), Mods::default());
|
||||
let (start, end) = brackets(&s, id);
|
||||
assert!(near(start, 0.0) && end.is_some_and(|e| near(e, 0.5)), "{tool}: flipped, {start} {end:?}");
|
||||
assert!(ink(&s, id).y0 > 299.0, "{tool}: below the path");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ pub const TOOL_GROUPS: &[&[ToolInfo]] = &[
|
||||
t("areaType", "Area Type Tool", None, "tool-type-area"),
|
||||
t("typeOnPath", "Type on a Path Tool", None, "tool-type-path"),
|
||||
t("verticalType", "Vertical Type Tool", None, "tool-type-vertical"),
|
||||
t("verticalAreaType", "Vertical Area Type Tool", None, "tool-type-vertical"),
|
||||
t("verticalTypeOnPath", "Vertical Type on a Path Tool", None, "tool-type-path"),
|
||||
t("verticalAreaType", "Vertical Area Type Tool", None, "tool-type-vertical-area"),
|
||||
t("verticalTypeOnPath", "Vertical Type on a Path Tool", None, "tool-type-vertical-path"),
|
||||
t("touchType", "Touch Type Tool", Some("Shift+T"), "tool-touch-type"),
|
||||
],
|
||||
&[
|
||||
|
||||
@@ -165,6 +165,14 @@ pub enum Cursor {
|
||||
SymbolStyler,
|
||||
/// The vertical type tools: a turned I-beam.
|
||||
TypeVertical,
|
||||
/// A type tool over a closed path's outline: a click sets area type inside it.
|
||||
TypeArea,
|
||||
/// ...the vertical type tools.
|
||||
TypeAreaVertical,
|
||||
/// A type tool over a path's outline: a click sets type along it.
|
||||
TypeOnPath,
|
||||
/// ...the vertical type tools.
|
||||
TypeOnPathVertical,
|
||||
/// The Scissors over a path segment (a click splits the path there)...
|
||||
Scissors,
|
||||
/// ...or away from every segment (a click does nothing).
|
||||
|
||||
@@ -241,7 +241,7 @@ look = "The Pen nib with a curve badge."
|
||||
id = "Text"
|
||||
group = "Type"
|
||||
shown_when = [
|
||||
"Every horizontal type tool: Type, Area Type and Type on a Path (the Area and Path ones show the crosshair over a suitable path).",
|
||||
"Every horizontal type tool: Type, Area Type and Type on a Path, away from a path a click would set type in or along (TypeArea, TypeOnPath).",
|
||||
"Stands in for TypeVertical until it's drawn.",
|
||||
]
|
||||
hotspot = "The I-beam's centre."
|
||||
@@ -250,10 +250,38 @@ look = "An I-beam with a short crossbar marking the baseline."
|
||||
[[cursor]]
|
||||
id = "TypeVertical"
|
||||
group = "Type"
|
||||
shown_when = ["The vertical type tools: Vertical Type, Vertical Area Type and Vertical Type on a Path (the Area and Path ones show the crosshair over a suitable path)."]
|
||||
shown_when = ["The vertical type tools: Vertical Type, Vertical Area Type and Vertical Type on a Path, away from a path a click would set type in or along (TypeAreaVertical, TypeOnPathVertical)."]
|
||||
hotspot = "The I-beam's centre."
|
||||
look = "The Text I-beam turned a quarter turn."
|
||||
|
||||
[[cursor]]
|
||||
id = "TypeArea"
|
||||
group = "Type"
|
||||
shown_when = ["Type over a closed path's outline, and Area Type over one with Alt up: a click sets area type inside the path."]
|
||||
hotspot = "The I-beam's centre."
|
||||
look = "The Text I-beam with a small badge below right: a square frame with three short lines of type in it."
|
||||
|
||||
[[cursor]]
|
||||
id = "TypeAreaVertical"
|
||||
group = "Type"
|
||||
shown_when = ["Vertical Type over a closed path's outline, and Vertical Area Type over one with Alt up: a click sets vertical area type inside the path."]
|
||||
hotspot = "The I-beam's centre."
|
||||
look = "TypeArea turned a quarter turn."
|
||||
|
||||
[[cursor]]
|
||||
id = "TypeOnPath"
|
||||
group = "Type"
|
||||
shown_when = ["Type over an open path's outline; Type on a Path over any path's outline (Alt up); Area Type over an open path with Alt held: a click sets type along the path."]
|
||||
hotspot = "The I-beam's centre."
|
||||
look = "The Text I-beam with a small badge below right: a short smooth curve with a dot on it."
|
||||
|
||||
[[cursor]]
|
||||
id = "TypeOnPathVertical"
|
||||
group = "Type"
|
||||
shown_when = ["The vertical type tools where TypeOnPath shows: a click sets vertical type along the path."]
|
||||
hotspot = "The I-beam's centre."
|
||||
look = "TypeOnPath turned a quarter turn."
|
||||
|
||||
# ── Transform and resize ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
[[cursor]]
|
||||
@@ -262,7 +290,6 @@ group = "Transform and resize"
|
||||
shown_when = [
|
||||
"The generic drawing cursor: all shape, line, grid, flare, cut, graph and liquify tools, and Place.",
|
||||
"The Rotate, Reflect, Scale and Shear tools with a selection (Reflect, Scale and Shear even mid-drag).",
|
||||
"The Area Type and Type on a Path tools over a suitable path.",
|
||||
"The Artboard tool outside every artboard; the Gradient tool away from the annotator; Free Transform corners in Distort or Perspective mode.",
|
||||
"With Use Precise Cursors on, it replaces the Pen, Eyedropper, Slice, Blend and Shape Builder cursors.",
|
||||
"Stands in for every drawing tool cursor not drawn yet.",
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
//! The Type tools: Type (T), Area Type and Type on a Path.
|
||||
//!
|
||||
//! Type: click places point type, drag draws an area-type frame, clicking into existing text
|
||||
//! places the caret. Area Type / Type on a Path: click a path to turn it into a text frame or a
|
||||
//! baseline (`text.createInPath`). While type is being edited, a click that lands on no type only
|
||||
//! places the caret, and a click on a path's outline makes type in or along it: area type inside a
|
||||
//! closed path and type on a path along an open one, as the reference app's Type tool does ("Select
|
||||
//! the Type tool […] and click anywhere on the object's path", [Create text](https://web.archive.org/web/20220127090642/https://helpx.adobe.com/illustrator/using/creating-text.html);
|
||||
//! "To create horizontal text along a path, select the Type tool or the Type On A Path tool […] If
|
||||
//! the path is closed, rather than open, you must use the Type On A Path tool", [Create type on a
|
||||
//! path](https://web.archive.org/web/20220127090635/https://helpx.adobe.com/illustrator/using/creating-type-path.html)),
|
||||
//! Alt or not: type along a closed path stays with Type on a Path.
|
||||
//! Area Type / Type on a Path: click a path to turn it into a text frame or a baseline
|
||||
//! (`text.createInPath`). While type is being edited, a click that lands on no type only
|
||||
//! ends the editing, and the next click makes new type. Point type placed by a click and left
|
||||
//! empty is discarded when editing ends (`text.discardEmpty`); frames keep their shape. Where new
|
||||
//! type goes (the click, the frame's corners) snaps to Smart Guides ([`DrawSnap`]), hovering too.
|
||||
@@ -609,15 +616,29 @@ impl TypeTool {
|
||||
self.caret = r.end;
|
||||
}
|
||||
|
||||
/// Path under `p` for the Area Type / Type on a Path tools.
|
||||
fn path_at(cx: &ToolContext, p: Point, closed: bool) -> Option<NodeId> {
|
||||
let h = hit_test(cx.doc, p, vectorcraft_doc::hit::HitOptions { tol: cx.tol(4.0), outline: true, ..Default::default() })?;
|
||||
/// The path whose outline is under `p`, and whether it is closed: within the isolated group
|
||||
/// being edited, and no guide while guides are hidden or locked ([`ToolContext::hit_options`]).
|
||||
fn path_at(cx: &ToolContext, p: Point) -> Option<(NodeId, bool)> {
|
||||
let h = hit_test(cx.doc, p, vectorcraft_doc::hit::HitOptions { tol: cx.tol(4.0), outline: true, ..cx.hit_options() })?;
|
||||
match &cx.doc.node(h.leaf)?.kind {
|
||||
NodeKind::Path { path, .. } if !closed || path.is_closed() => Some(h.leaf),
|
||||
NodeKind::Path { path, .. } => Some((h.leaf, path.is_closed())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// What a click at `p` with the tool acting as `mode` makes of the path there: the path, and
|
||||
/// whether the type goes along it (else inside it); None when it makes no type in a path. The
|
||||
/// Type tools set type inside a closed path and along an open one; Area Type needs a closed
|
||||
/// path and Type on a Path takes either.
|
||||
fn in_path(cx: &ToolContext, p: Point, mode: Mode) -> Option<(NodeId, bool)> {
|
||||
let (id, closed) = Self::path_at(cx, p)?;
|
||||
match mode {
|
||||
Mode::Type => Some((id, !closed)),
|
||||
Mode::Area => closed.then_some((id, false)),
|
||||
Mode::OnPath => Some((id, true)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start editing the type under `p` with the caret there → the actions (ending the previous
|
||||
/// edit, selecting the type); None when no type is under `p`. A click among the characters
|
||||
/// edits them, whatever Type Object Selection by Path Only says.
|
||||
@@ -660,18 +681,17 @@ impl TypeTool {
|
||||
if was_editing && area.is_none() {
|
||||
return out;
|
||||
}
|
||||
// Area Type / Type on a Path: click a path.
|
||||
if mode != Mode::Type && drag.is_none() {
|
||||
let on_path = mode == Mode::OnPath;
|
||||
if let Some(pid) = Self::path_at(cx, start, !on_path) {
|
||||
let mode = if on_path { "onPath" } else { "area" };
|
||||
out.push(Action::Exec(
|
||||
"text.createInPath".into(),
|
||||
json!({"path": pid.0, "mode": mode, "text": "", "at": [start.x, start.y], "vertical": vertical, "placeholder": cx.placeholder_text}),
|
||||
));
|
||||
out.push(Action::Notify("text.editNew".into()));
|
||||
return out;
|
||||
}
|
||||
// A click on a path: type in it or along it.
|
||||
if drag.is_none()
|
||||
&& let Some((pid, on_path)) = Self::in_path(cx, start, mode)
|
||||
{
|
||||
let mode = if on_path { "onPath" } else { "area" };
|
||||
out.push(Action::Exec(
|
||||
"text.createInPath".into(),
|
||||
json!({"path": pid.0, "mode": mode, "text": "", "at": [start.x, start.y], "vertical": vertical, "placeholder": cx.placeholder_text}),
|
||||
));
|
||||
out.push(Action::Notify("text.editNew".into()));
|
||||
return out;
|
||||
}
|
||||
let mut params = match area {
|
||||
Some(r) => {
|
||||
|
||||
@@ -807,7 +807,7 @@ fn the_pointer_shows_the_type_tool_shift_or_alt_switches_to() {
|
||||
assert_eq!(cursor("verticalType", away, SHIFT), Cursor::Text);
|
||||
assert_eq!(cursor("verticalAreaType", away, SHIFT), Cursor::Text);
|
||||
assert_eq!(cursor("areaType", on_line, Mods::default()), Cursor::Text, "no frame for area type");
|
||||
assert_eq!(cursor("areaType", on_line, ALT), Cursor::Crosshair, "a baseline for type on a path");
|
||||
assert_eq!(cursor("areaType", on_line, ALT), Cursor::TypeOnPath, "a baseline for type on a path");
|
||||
assert_eq!(cursor("typeOnPath", on_line, ALT), Cursor::Text);
|
||||
let mut tool = TypeTool::new("type");
|
||||
tool.pointer(&c, &PointerEvent::new(PointerKind::Down, away.x, away.y).with_mods(SHIFT));
|
||||
@@ -816,6 +816,83 @@ fn the_pointer_shows_the_type_tool_shift_or_alt_switches_to() {
|
||||
assert_eq!(tool.cursor(&c, away, Mods::default()), Cursor::Text);
|
||||
}
|
||||
|
||||
/// The plain Type tools make type in or along a path clicked on its outline: area type inside a
|
||||
/// closed path (Alt held or not: type along a closed path is Type on a Path's) and type on a path
|
||||
/// along an open one. A click away from paths still makes point type, a drag still draws a frame
|
||||
/// (even from a path's outline), and a click on existing type edits it. The pointer says which.
|
||||
#[test]
|
||||
fn the_type_tool_makes_type_in_or_along_a_clicked_path() {
|
||||
let (mut d, rect, line) = doc_with_rect_and_line();
|
||||
let (sel, p) = (Selection::default(), paint());
|
||||
let (on_line, on_rect, away) = ((250.0, 400.0), (100.0, 150.0), (450.0, 450.0));
|
||||
for id in ["type", "verticalType"] {
|
||||
let vertical = id == "verticalType";
|
||||
let c = cx(&d, &sel, &p);
|
||||
for (at, mods, path, mode, cursor) in [
|
||||
(on_rect, Mods::default(), rect, "area", if vertical { Cursor::TypeAreaVertical } else { Cursor::TypeArea }),
|
||||
(on_line, Mods::default(), line, "onPath", if vertical { Cursor::TypeOnPathVertical } else { Cursor::TypeOnPath }),
|
||||
(on_rect, ALT, rect, "area", if vertical { Cursor::TypeAreaVertical } else { Cursor::TypeArea }),
|
||||
(on_line, ALT, line, "onPath", if vertical { Cursor::TypeOnPathVertical } else { Cursor::TypeOnPath }),
|
||||
] {
|
||||
assert_eq!(TypeTool::new(id).cursor(&c, Point::new(at.0, at.1), mods), cursor, "{id} {at:?} {mods:?}");
|
||||
let mut tool = TypeTool::new(id);
|
||||
let acts = gesture(&mut tool, &c, at, None, mods, Mods::default());
|
||||
let (cmd, params) = made(&acts).unwrap_or_else(|| panic!("{id} {at:?} {mods:?}: {acts:?}"));
|
||||
assert_eq!(cmd, "text.createInPath", "{id} {at:?} {mods:?}");
|
||||
assert_eq!(
|
||||
(¶ms["path"], ¶ms["mode"], ¶ms["vertical"]),
|
||||
(&json!(path.0), &json!(mode), &json!(vertical)),
|
||||
"{id} {at:?} {mods:?}"
|
||||
);
|
||||
assert_eq!(tool.id(), id, "the tool chosen stays");
|
||||
}
|
||||
// Shift switches the direction over a path too.
|
||||
let acts = gesture(&mut TypeTool::new(id), &c, on_rect, None, SHIFT, Mods::default());
|
||||
assert_eq!(made(&acts).map(|(_, p)| (p["mode"].clone(), p["vertical"].clone())), Some((json!("area"), json!(!vertical))));
|
||||
// Away from paths: point type.
|
||||
assert_eq!(
|
||||
TypeTool::new(id).cursor(&c, Point::new(away.0, away.1), Mods::default()),
|
||||
if vertical { Cursor::TypeVertical } else { Cursor::Text }
|
||||
);
|
||||
let acts = gesture(&mut TypeTool::new(id), &c, away, None, Mods::default(), Mods::default());
|
||||
let (cmd, params) = made(&acts).unwrap_or_else(|| panic!("{acts:?}"));
|
||||
assert_eq!((cmd, params.get("area")), ("text.create", None), "{id}: point type");
|
||||
// A drag from the rectangle's outline draws a frame.
|
||||
let acts = gesture(&mut TypeTool::new(id), &c, on_rect, Some((200.0, 250.0)), Mods::default(), Mods::default());
|
||||
let (cmd, params) = made(&acts).unwrap_or_else(|| panic!("{acts:?}"));
|
||||
assert_eq!((cmd, ¶ms["area"]), ("text.create", &json!({"width": 100.0, "height": 100.0})), "{id}: drag");
|
||||
}
|
||||
// A click on existing type over the rectangle's outline edits the type.
|
||||
let l = d.layers[0].id;
|
||||
let tid = d.alloc_id();
|
||||
let t = TextObject::point(Point::new(95.0, 155.0), "Hello", Default::default());
|
||||
d.insert(Some(l), 2, Node::new(tid, NodeKind::Text(Box::new(t)))).unwrap();
|
||||
let c = cx(&d, &sel, &p);
|
||||
let mut tool = TypeTool::new("type");
|
||||
let acts = gesture(&mut tool, &c, (100.0, 150.0), None, Mods::default(), Mods::default());
|
||||
assert!(made(&acts).is_none(), "{acts:?}");
|
||||
assert_eq!(tool.editing, Some(tid));
|
||||
}
|
||||
|
||||
/// A guide is no path to set type in or along while guides are hidden or locked: the click makes
|
||||
/// point type, as it does on the selection tools' terms.
|
||||
#[test]
|
||||
fn the_type_tool_leaves_hidden_or_locked_guides_alone() {
|
||||
let (mut d, rect, _) = doc_with_rect_and_line();
|
||||
if let Some(NodeKind::Path { guide, .. }) = d.node_mut(rect).map(|n| &mut n.kind) {
|
||||
*guide = true;
|
||||
}
|
||||
let (sel, p) = (Selection::default(), paint());
|
||||
let mut c = cx(&d, &sel, &p);
|
||||
c.guides = false;
|
||||
assert_eq!(TypeTool::new("type").cursor(&c, Point::new(100.0, 150.0), Mods::default()), Cursor::Text);
|
||||
let acts = gesture(&mut TypeTool::new("type"), &c, (100.0, 150.0), None, Mods::default(), Mods::default());
|
||||
assert_eq!(made(&acts).map(|(cmd, _)| cmd), Some("text.create"), "{acts:?}");
|
||||
c.guides = true;
|
||||
let acts = gesture(&mut TypeTool::new("type"), &c, (100.0, 150.0), None, Mods::default(), Mods::default());
|
||||
assert_eq!(made(&acts).map(|(cmd, _)| cmd), Some("text.createInPath"), "{acts:?}");
|
||||
}
|
||||
|
||||
/// While type is being edited, Shift and Alt switch no tool: a frame dragged out is the tool
|
||||
/// chosen's, and the pointer stays its own.
|
||||
#[test]
|
||||
|
||||
@@ -636,9 +636,15 @@ impl Tool for TypeTool {
|
||||
(Some(_), Some(held)) => held,
|
||||
_ => self.acting(m),
|
||||
};
|
||||
if mode != Mode::Type && Self::path_at(cx, p, mode == Mode::Area).is_some() {
|
||||
return Cursor::Crosshair;
|
||||
// While type is being edited a click on a path only ends the editing.
|
||||
let in_path = if self.editing.is_some() { None } else { Self::in_path(cx, p, mode) };
|
||||
match (in_path, vertical) {
|
||||
(Some((_, false)), false) => Cursor::TypeArea,
|
||||
(Some((_, false)), true) => Cursor::TypeAreaVertical,
|
||||
(Some((_, true)), false) => Cursor::TypeOnPath,
|
||||
(Some((_, true)), true) => Cursor::TypeOnPathVertical,
|
||||
(None, false) => Cursor::Text,
|
||||
(None, true) => Cursor::TypeVertical,
|
||||
}
|
||||
if vertical { Cursor::TypeVertical } else { Cursor::Text }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,8 @@ pub(super) fn cursor_icon(c: Cursor) -> egui::CursorIcon {
|
||||
Cursor::PencilNew => C::Crosshair,
|
||||
Cursor::Blend | Cursor::BlendObject | Cursor::BlendAnchor | Cursor::ShapeBuilder | Cursor::ShapeBuilderErase => C::Crosshair,
|
||||
Cursor::PencilClose | Cursor::PencilStraight | Cursor::PencilContinue | Cursor::PaintbrushClose => C::Crosshair,
|
||||
Cursor::TypeVertical => C::VerticalText,
|
||||
Cursor::TypeVertical | Cursor::TypeAreaVertical | Cursor::TypeOnPathVertical => C::VerticalText,
|
||||
Cursor::TypeArea | Cursor::TypeOnPath => C::Text,
|
||||
// The system has no tip outline to go round: its crosshair aims as well.
|
||||
Cursor::BlobBrush | Cursor::Eraser => C::Crosshair,
|
||||
// Not drawn yet: the cursor each borrows.
|
||||
|
||||
@@ -366,6 +366,42 @@ fn ibeam(p: &mut Ink, o: Pos2) {
|
||||
line(p, o + vec2(-2.0, 3.0), o + vec2(2.0, 3.0));
|
||||
}
|
||||
|
||||
/// A type tool over a path: the Text I-beam (turned a quarter turn for vertical type) with a badge
|
||||
/// below right of it, as the Pen and the Blend tool mark what a click does: a small frame with
|
||||
/// lines of type in it for area type inside a closed path, a smooth curve with a point on it for
|
||||
/// type along a path.
|
||||
fn type_in_path(p: &mut Ink, o: Pos2, vertical: bool, on_path: bool) {
|
||||
let at = |x: f32, y: f32| if vertical { o + vec2(-y, x) } else { o + vec2(x, y) };
|
||||
line(p, at(0.0, -8.0), at(0.0, 8.0));
|
||||
line(p, at(-3.0, -8.0), at(3.0, -8.0));
|
||||
line(p, at(-3.0, 8.0), at(3.0, 8.0));
|
||||
line(p, at(-2.0, 3.0), at(2.0, 3.0));
|
||||
let b = o + if vertical { vec2(2.0, 6.0) } else { vec2(6.0, 2.0) };
|
||||
if on_path {
|
||||
// A quarter-wave rising to the right, smooth at this size.
|
||||
let curve: Vec<Pos2> = (0..=16)
|
||||
.map(|i| {
|
||||
let t = i as f32 / 16.0;
|
||||
b + vec2(10.0 * t, 8.0 - 8.0 * (t * std::f32::consts::FRAC_PI_2).sin().powf(1.6))
|
||||
})
|
||||
.collect();
|
||||
strokes(p, &[curve]);
|
||||
let dot = b + vec2(4.0, 8.0 - 8.0 * (0.4 * std::f32::consts::FRAC_PI_2).sin().powf(1.6));
|
||||
p.circle_filled(dot, 2.4, HALO);
|
||||
p.circle_filled(dot, 1.5, INK);
|
||||
} else {
|
||||
poly(p, vec![b, b + vec2(9.0, 0.0), b + vec2(9.0, 9.0), b + vec2(0.0, 9.0)], HALO, INK);
|
||||
let rows: Vec<Vec<Pos2>> = if vertical {
|
||||
[6.5, 4.5, 2.5].iter().map(|&x| vec![b + vec2(x, 2.5), b + vec2(x, 6.5)]).collect()
|
||||
} else {
|
||||
[2.5, 4.5, 6.5].iter().map(|&y| vec![b + vec2(2.5, y), b + vec2(6.5, y)]).collect()
|
||||
};
|
||||
for r in rows {
|
||||
p.add(Shape::line(r, Stroke::new(1.0, INK)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Blend tool: a crosshair with a square below right of the hotspot, hollow away from art,
|
||||
/// filled over an object; over an anchor point a ringed dot (the blend starts there).
|
||||
fn blend(p: &mut Ink, o: Pos2, badge: Cursor) {
|
||||
@@ -440,7 +476,7 @@ fn magnifier(p: &mut Ink, o: Pos2, out: bool) {
|
||||
|
||||
/// Every cursor with a glyph: its own, drawn here, or the one it borrows.
|
||||
#[cfg(test)]
|
||||
const GLYPHS: [Cursor; 89] = [
|
||||
const GLYPHS: [Cursor; 93] = [
|
||||
Cursor::Hand,
|
||||
Cursor::HandGrab,
|
||||
Cursor::ZoomIn,
|
||||
@@ -519,6 +555,10 @@ const GLYPHS: [Cursor; 89] = [
|
||||
Cursor::SymbolScreener,
|
||||
Cursor::SymbolStyler,
|
||||
Cursor::TypeVertical,
|
||||
Cursor::TypeArea,
|
||||
Cursor::TypeAreaVertical,
|
||||
Cursor::TypeOnPath,
|
||||
Cursor::TypeOnPathVertical,
|
||||
Cursor::Scissors,
|
||||
Cursor::MeshAdd,
|
||||
Cursor::PuppetPin,
|
||||
@@ -605,6 +645,10 @@ fn glyph(c: Cursor, p: Pos2) -> Option<Vec<Shape>> {
|
||||
Cursor::PenJoin => pen(ink, p, "j"),
|
||||
Cursor::PenConvert => pen(ink, p, "^"),
|
||||
Cursor::Text => ibeam(ink, p),
|
||||
Cursor::TypeArea => type_in_path(ink, p, false, false),
|
||||
Cursor::TypeAreaVertical => type_in_path(ink, p, true, false),
|
||||
Cursor::TypeOnPath => type_in_path(ink, p, false, true),
|
||||
Cursor::TypeOnPathVertical => type_in_path(ink, p, true, true),
|
||||
Cursor::AddStop => stop_badge(ink, p, true),
|
||||
Cursor::RemoveStop => stop_badge(ink, p, false),
|
||||
Cursor::Slice => slice(ink, p),
|
||||
|
||||
@@ -174,9 +174,12 @@ pub static ICONS: &[(&str, &[u8])] = &[
|
||||
("dc-transform-panel", include_bytes!("../../../assets/icons/dc-transform-panel.svg")),
|
||||
("dc-transparency", include_bytes!("../../../assets/icons/dc-transparency.svg")),
|
||||
("dc-twirl", include_bytes!("../../../assets/icons/dc-twirl.svg")),
|
||||
("dc-type", include_bytes!("../../../assets/icons/dc-type.svg")),
|
||||
("dc-type-area", include_bytes!("../../../assets/icons/dc-type-area.svg")),
|
||||
("dc-type-path", include_bytes!("../../../assets/icons/dc-type-path.svg")),
|
||||
("dc-type-vertical", include_bytes!("../../../assets/icons/dc-type-vertical.svg")),
|
||||
("dc-type-vertical-area", include_bytes!("../../../assets/icons/dc-type-vertical-area.svg")),
|
||||
("dc-type-vertical-path", include_bytes!("../../../assets/icons/dc-type-vertical-path.svg")),
|
||||
("dc-width", include_bytes!("../../../assets/icons/dc-width.svg")),
|
||||
("dc-width-profile", include_bytes!("../../../assets/icons/dc-width-profile.svg")),
|
||||
("dc-wrinkle", include_bytes!("../../../assets/icons/dc-wrinkle.svg")),
|
||||
|
||||
@@ -29,10 +29,12 @@ pub fn tool_icon(name: &str) -> &'static str {
|
||||
"tool-pen-delete" => "pen-tool-delete",
|
||||
"tool-anchor" => "dc-anchor",
|
||||
"tool-curvature" => "spline",
|
||||
"tool-type" => "type",
|
||||
"tool-type" => "dc-type",
|
||||
"tool-type-area" => "dc-type-area",
|
||||
"tool-type-path" => "dc-type-path",
|
||||
"tool-type-vertical" => "dc-type-vertical",
|
||||
"tool-type-vertical-area" => "dc-type-vertical-area",
|
||||
"tool-type-vertical-path" => "dc-type-vertical-path",
|
||||
"tool-touch-type" => "dc-touch-type",
|
||||
"tool-line" => "dc-line",
|
||||
"tool-arc" => "dc-arc",
|
||||
@@ -212,6 +214,20 @@ mod tests {
|
||||
assert_ne!(add, delete, "plus and minus differ");
|
||||
}
|
||||
|
||||
/// The seven type tools each have an icon of their own, drawn for VectorCraft (the vertical
|
||||
/// Area and Path tools used to borrow the horizontal ones' icons).
|
||||
#[test]
|
||||
fn every_type_tool_has_its_own_icon_with_the_same_t() {
|
||||
let tools = ["type", "areaType", "typeOnPath", "verticalType", "verticalAreaType", "verticalTypeOnPath", "touchType"];
|
||||
let names: Vec<&str> = tools.iter().map(|id| tool_icon(vectorcraft_tools::catalog::tool_info(id).unwrap().icon)).collect();
|
||||
let unique: std::collections::BTreeSet<_> = names.iter().collect();
|
||||
assert_eq!(unique.len(), tools.len(), "{names:?}");
|
||||
for name in &names {
|
||||
assert!(name.starts_with("dc-"), "{name} is drawn for VectorCraft");
|
||||
assert!(exists(name), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_catalog_preserves_fallback_and_tool_aliases() {
|
||||
let ImageSource::Bytes { uri, bytes } = source("missing-icon") else { panic!("expected SVG bytes") };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Type parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Glyphs panel: the character selected with the Type tool is highlighted and scrolled into view, #4908; previously minor: Serbian in Cyrillic and Latin and Croatian among the languages, with their quote marks; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units (#394); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (the Type tools make area type or type on a path from a clicked path; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in
|
||||
[target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the
|
||||
@@ -20,7 +20,7 @@ against Illustrator's public documentation; **estimated** unless noted.
|
||||
|---|---|---|
|
||||
| Soft return (forced line break, Shift+Enter) | done | Shift+Enter inserts a soft return (U+2028), breaking the line without starting a new paragraph; paragraph spacing (space before/after) and first-line indents apply only at paragraph ends; continuation lines in lists do not repeat the marker; soft return lines in justified text do not spread (#2754) |
|
||||
| Native font-family labels | partial | With Show Font Names in English off, localized legacy names are used when typographic names contain no native label (#2208); loaded-font and catalog-only regressions. Choosing among multiple native languages is unchanged. |
|
||||
| Point, area and type-on-a-path type; vertical variants | done / partial | vertical point, area and path type have initial support; fullwidth `!` / `?` keep the nominal glyph upright when no vertical alternate exists (Unicode Tu); in a non-rectangular area a word too wide for the top lines moves down to the first line that holds it (#1615); Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802); `text.create` and `text.createInPath` take `justify` (#1752); MCP `add_text` forwards `vertical` to both commands |
|
||||
| Point, area and type-on-a-path type; vertical variants | done / partial | vertical point, area and path type have initial support; fullwidth `!` / `?` keep the nominal glyph upright when no vertical alternate exists (Unicode Tu); in a non-rectangular area a word too wide for the top lines moves down to the first line that holds it (#1615); Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802); `text.create` and `text.createInPath` take `justify` (#1752); MCP `add_text` forwards `vertical` to both commands; the Type and Vertical Type tools make area type when a closed path's outline is clicked and type on a path when an open one is, with pointers that say which, as the public help describes ([Create text](https://web.archive.org/web/20220127090642/https://helpx.adobe.com/illustrator/using/creating-text.html), [Create type on a path](https://web.archive.org/web/20220127090635/https://helpx.adobe.com/illustrator/using/creating-type-path.html)) |
|
||||
| Area Type Options (rows, columns, inset, first baseline, Text Flow by rows or by columns, Auto Size, Shrink Text to Fit) | done | Text Flow fills the cells down each column first (By Columns, the default, as files from before it open) or across each row first (By Rows), in the dialog and `text.areaOptions {flow}` (#4978); the commands that make or restyle type say when it does not fit its frame; MCP `add_text` takes Auto Size and Shrink Text to Fit (#1471) |
|
||||
| Threaded text, Fit Headline, Text Wrap | done | Fit Headline sees wrapping in a frame one line tall (#1444); a thread holds area type and type on a path in any mix, as Illustrator links type in an area or along a path ([help](https://helpx.adobe.com/illustrator/using/manage-text-area.html)) |
|
||||
| Threading from the ports: selected area type and type on a path show an in port and an out port, empty, with an arrow on a side where the frame is threaded, or with a red "+" in the out port when the text doesn't fit; a click on either port loads the text; a click on a closed path, area type or type on a path threads into it, a click on empty canvas makes a frame of the same kind, shape and size there, a drag draws a rectangular frame; the frame placed comes after the clicked one from its out port and before it from its in port (the story then starts there); Escape or another tool cancels; a double-click on a threaded port breaks the thread there | partial | #2572: the Selection tool, `text.thread.add {from, to?, rect?, at?, before?}`, as the reference app's help describes ports and threading ([archived help, Threading text between objects](https://web.archive.org/web/20210119051048/https://helpx.adobe.com/illustrator/using/creating-text.html), [create text threads](https://helpx.adobe.com/illustrator/desktop/design-with-text/add-manage-text/create-text-threads-between-objects.html)); area type's in port sits on the left side below the top-left corner, type on a path's just before its start bracket; Show Text Threads draws its lines between the same ports; with more than 1,000 such objects selected no ports are drawn. a double-click on a port where the frame is threaded breaks the thread there, the story flowing into the frames before the break and the frames after it left empty, still threaded with each other when two or more (`text.thread.break`, [archived help](https://web.archive.org/web/20220127090642/https://helpx.adobe.com/illustrator/using/creating-text.html)). Remaining: the ports only take clicks with the Selection tool, and the loaded cursor borrows the I-beam until it's drawn |
|
||||
@@ -75,6 +75,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-01 | minor | The Type and Vertical Type tools make area type in a clicked closed path and type on a path along a clicked open one, with their own pointers; each type tool has its own icon; scores unchanged (estimated) |
|
||||
| 2026-10-31 | minor | Units › East Asian Type acts (#394): Q and Ha (a quarter of a millimetre each) are units that preference offers; scores unchanged |
|
||||
| 2026-11-01 | minor | Glyphs panel (#4908): the character selected with the Type tool (a range's first, else the one before the caret) is highlighted in the grid and scrolled into view when the selection moves to another character; Show › Alternates for Current Selection not yet; scores unchanged |
|
||||
| 2026-11-01 | minor | Serbian: Cyrillic (`sr-Cyrl`), Serbian: Latin (`sr-Latn`) and Croatian (`hr`) in the language list, with their quote marks (Serbian „…” and ’…’ per the Pravopis srpskoga jezika, Matica srpska 2010, ISBN 978-86-7946-079-0, т. 168, and [CLDR `sr`](https://github.com/unicode-org/cldr/blob/main/common/main/sr.xml); Croatian „…“ and ‚…‘ per [CLDR `hr`](https://github.com/unicode-org/cldr/blob/main/common/main/hr.xml)); Serbian text gets the fonts' Serbian forms (italic б, г, д, п, т); scores unchanged |
|
||||
|
||||