mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 23:00:35 +00:00
Type tool: a click on a path's outline makes area type or type on a path, with cursors that say which
This commit is contained in:
@@ -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 and Area Type over a closed path's outline, Alt up for Type: a click sets area type inside the path."]
|
||||
hotspot = "The I-beam's centre."
|
||||
look = "The I-beam between two arcs, like a pair of parentheses around it: the type goes inside the shape."
|
||||
|
||||
[[cursor]]
|
||||
id = "TypeAreaVertical"
|
||||
group = "Type"
|
||||
shown_when = ["Vertical Type and Vertical Area Type over a closed path's outline, Alt up for Vertical Type: 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, or a closed one with Alt held; Type on a Path over any path's outline; Area Type over an open path with Alt held: a click sets type along the path."]
|
||||
hotspot = "The I-beam's centre."
|
||||
look = "The I-beam crossing a wavy line below its centre: the type runs along the line."
|
||||
|
||||
[[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.",
|
||||
|
||||
+40
-17
@@ -1,8 +1,16 @@
|
||||
//! 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/20220127000000/https://helpx.adobe.com/illustrator/using/creating-type-path.html)).
|
||||
//! Alt (Option) held over a closed path sets type along it instead: our own shortcut, so the Type
|
||||
//! tool reaches both without switching tools.
|
||||
//! 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.
|
||||
@@ -178,6 +186,8 @@ pub struct TypeTool {
|
||||
drag: Option<Point>,
|
||||
/// The Type tool the press acts as, with the keys held at the press ([`Self::acting`]).
|
||||
held: Option<(Mode, bool)>,
|
||||
/// Alt (Option) was held at the press: the Type tool sets type along a closed path, not in it.
|
||||
held_alt: bool,
|
||||
/// Smart Guides for where new type goes.
|
||||
snap: DrawSnap,
|
||||
/// The press landed in the edited text: dragging selects.
|
||||
@@ -609,15 +619,28 @@ 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> {
|
||||
/// The path whose outline is under `p`, and whether it is closed.
|
||||
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, ..Default::default() })?;
|
||||
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, or along a closed one with
|
||||
/// `alt`; Area Type needs a closed path and Type on a Path takes either.
|
||||
fn in_path(cx: &ToolContext, p: Point, mode: Mode, alt: bool) -> Option<(NodeId, bool)> {
|
||||
let (id, closed) = Self::path_at(cx, p)?;
|
||||
match mode {
|
||||
Mode::Type => Some((id, alt || !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.
|
||||
@@ -639,6 +662,7 @@ impl TypeTool {
|
||||
/// (#2871). A drag that draws an area-type frame makes it either way.
|
||||
fn on_up(&mut self, cx: &ToolContext, start: Point, at: Point) -> Vec<Action> {
|
||||
let (mode, vertical) = self.held.take().unwrap_or((self.mode, self.vertical));
|
||||
let alt = std::mem::take(&mut self.held_alt);
|
||||
// Click into existing text: place the caret.
|
||||
if let Some(out) = self.edit_at(cx, start) {
|
||||
return out;
|
||||
@@ -660,18 +684,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, alt)
|
||||
{
|
||||
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,64 @@ 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, type on a path along an open one or, with Alt held, along a closed 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, "onPath", if vertical { Cursor::TypeOnPathVertical } else { Cursor::TypeOnPath }),
|
||||
(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));
|
||||
}
|
||||
|
||||
/// 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]
|
||||
|
||||
@@ -55,6 +55,7 @@ impl Tool for TypeTool {
|
||||
}
|
||||
self.press = Some((ev.pos, self.snap.press(cx, ev.pos, self.editing.as_slice(), None)));
|
||||
self.held = Some(self.acting(ev.mods));
|
||||
self.held_alt = ev.mods.alt && self.editing.is_none();
|
||||
self.drag = None;
|
||||
vec![]
|
||||
}
|
||||
@@ -632,13 +633,19 @@ impl Tool for TypeTool {
|
||||
/// The pointer of the Type tool that a press would act as with the keys held, or, while a
|
||||
/// press is held, of the one it acts as ([`TypeTool::acting`]).
|
||||
fn cursor(&self, cx: &ToolContext, p: Point, m: Mods) -> Cursor {
|
||||
let (mode, vertical) = match (self.press, self.held) {
|
||||
(Some(_), Some(held)) => held,
|
||||
_ => self.acting(m),
|
||||
let ((mode, vertical), alt) = match (self.press, self.held) {
|
||||
(Some(_), Some(held)) => (held, self.held_alt),
|
||||
_ => (self.acting(m), m.alt && self.editing.is_none()),
|
||||
};
|
||||
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, alt) };
|
||||
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,28 @@ 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 I-beam (turned a quarter turn for vertical type, without its
|
||||
/// baseline mark) between two arcs for area type inside a closed path, or crossing a wavy line 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));
|
||||
if on_path {
|
||||
let wave = (0..=12).map(|i| {
|
||||
let x = -9.0 + 1.5 * i as f32;
|
||||
at(x, 4.0 + 2.0 * (x * 0.55).sin())
|
||||
});
|
||||
strokes(p, &[wave.collect()]);
|
||||
} else {
|
||||
strokes(
|
||||
p,
|
||||
&[vec![at(-6.0, -8.0), at(-8.5, -4.0), at(-8.5, 4.0), at(-6.0, 8.0)], vec![at(6.0, -8.0), at(8.5, -4.0), at(8.5, 4.0), at(6.0, 8.0)]],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
@@ -519,6 +541,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 +631,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),
|
||||
|
||||
Reference in New Issue
Block a user