mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Type: a click away from the text being edited ends editing without making new text
While a Type tool edited text, a click on blank canvas ended the editing and also placed new point type at the click. Area Type and Type on a Path did the same with a click on a path. Such a click now only ends the editing. The typing session is committed, empty point type that a click placed is discarded as before, and the Type tool in use stays active; the next click makes new type. The same applies when the editing began with a double-click from a selection tool. A click on other type still edits it at once, and a drag that draws an area-type frame still makes the frame.
This commit is contained in:
@@ -62,10 +62,14 @@ fn clicking_without_typing_leaves_no_text_and_no_history() {
|
||||
click(&mut s, 150.0, 100.0);
|
||||
s.select_tool("rectangle", v).unwrap();
|
||||
assert!(texts(&s).is_empty());
|
||||
// Click in two places: the first is gone when the second is placed.
|
||||
// Click in two places: the second click discards the first type and places none (#2871); a
|
||||
// third places new type.
|
||||
s.select_tool("type", v).unwrap();
|
||||
click(&mut s, 100.0, 300.0);
|
||||
click(&mut s, 400.0, 300.0);
|
||||
assert!(texts(&s).is_empty());
|
||||
assert!(!s.tool_wants_text());
|
||||
click(&mut s, 400.0, 300.0);
|
||||
assert_eq!(texts(&s).len(), 1);
|
||||
s.select_tool("selection", v).unwrap();
|
||||
assert!(texts(&s).is_empty());
|
||||
@@ -95,6 +99,25 @@ fn typed_text_stays_and_typing_it_away_leaves_nothing() {
|
||||
assert_eq!(undo_labels(&s), kept);
|
||||
}
|
||||
|
||||
/// A click on blank canvas while type is being edited ends the editing and makes no new type; the
|
||||
/// Type tool stays, and the next click places point type (#2871, #2854).
|
||||
#[test]
|
||||
fn a_click_away_from_typed_text_ends_editing_without_new_type() {
|
||||
let mut s = session();
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("type", v).unwrap();
|
||||
click(&mut s, 100.0, 100.0);
|
||||
s.tool_text("Hi", v).unwrap();
|
||||
click(&mut s, 400.0, 300.0);
|
||||
assert_eq!(texts(&s).iter().map(|t| t.2.as_str()).collect::<Vec<_>>(), ["Hi"]);
|
||||
assert_eq!(s.tool_id(), "type");
|
||||
assert!(!s.tool_wants_text());
|
||||
assert_eq!(undo_labels(&s), ["Type", "Typing"]);
|
||||
click(&mut s, 400.0, 300.0);
|
||||
assert_eq!(texts(&s).len(), 2);
|
||||
assert!(s.tool_wants_text());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frames_stay_when_left_empty() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -1181,7 +1181,10 @@ fn new_type_starts_with_placeholder_text() {
|
||||
gesture(&mut s, v, &[(Down, 100.0, 150.0), (Drag, 300.0, 300.0), (Up, 300.0, 300.0)]);
|
||||
assert!(text(&s).len() > placeholder.len());
|
||||
set_pref(&mut s, "placeholderText", json!(false));
|
||||
gesture(&mut s, v, &[(Down, 50.0, 350.0), (Up, 50.0, 350.0)]);
|
||||
// The first click ends editing the area type (#2871); the second places point type.
|
||||
for _ in 0..2 {
|
||||
gesture(&mut s, v, &[(Down, 50.0, 350.0), (Up, 50.0, 350.0)]);
|
||||
}
|
||||
assert_eq!(text(&s), "");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
//!
|
||||
//! 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`). 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.
|
||||
//! 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.
|
||||
//!
|
||||
//! While editing: caret movement by character / word (Cmd or Alt) / line (Up/Down) / line ends
|
||||
//! (Home/End; Cmd = whole text), Shift extends the selection, drag selects, double-click selects a
|
||||
@@ -271,14 +272,21 @@ impl TypeTool {
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// The button released after a press at `start` (new type goes at `at`).
|
||||
/// The button released after a press at `start` (new type goes at `at`). While type is being
|
||||
/// edited, a click that lands on no type only ends the editing; the next click makes new type
|
||||
/// (#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> {
|
||||
// Click into existing text: place the caret.
|
||||
if let Some(out) = self.edit_at(cx, start) {
|
||||
return out;
|
||||
}
|
||||
let was_editing = self.editing.is_some_and(|id| Self::text(cx, id).is_some());
|
||||
let mut out = self.finish(cx);
|
||||
let drag = self.drag.take();
|
||||
let area = drag.map(|d| Rect::from_points(at, d)).filter(|r| r.width() > cx.tol(6.0) && r.height() > cx.tol(6.0));
|
||||
if was_editing && area.is_none() {
|
||||
return out;
|
||||
}
|
||||
// Area Type / Type on a Path: click a path.
|
||||
if self.mode != Mode::Type && drag.is_none() {
|
||||
let on_path = self.mode == Mode::OnPath;
|
||||
@@ -292,7 +300,6 @@ impl TypeTool {
|
||||
return out;
|
||||
}
|
||||
}
|
||||
let area = drag.map(|d| Rect::from_points(at, d)).filter(|r| r.width() > cx.tol(6.0) && r.height() > cx.tol(6.0));
|
||||
let mut params = match area {
|
||||
Some(r) => json!({"x": r.x0, "y": r.y0, "text": "", "area": {"width": r.width(), "height": r.height()}}),
|
||||
None => json!({"x": at.x, "y": at.y, "text": ""}),
|
||||
|
||||
@@ -401,6 +401,108 @@ fn escape_returns_to_previous_tool_when_set() {
|
||||
assert_eq!(acts2.last(), Some(&Action::SwitchTool("selection".into())));
|
||||
}
|
||||
|
||||
/// A click from `tool` at (x, y) → the actions of the press and the release.
|
||||
fn click(tool: &mut TypeTool, c: &ToolContext, x: f64, y: f64) -> Vec<Action> {
|
||||
let mut acts = tool.pointer(c, &PointerEvent::new(PointerKind::Down, x, y));
|
||||
acts.extend(tool.pointer(c, &PointerEvent::new(PointerKind::Up, x, y)));
|
||||
acts
|
||||
}
|
||||
|
||||
fn creates(acts: &[Action]) -> bool {
|
||||
acts.iter().any(|a| matches!(a, Action::Exec(cmd, _) if cmd == "text.create" || cmd == "text.createInPath"))
|
||||
}
|
||||
|
||||
/// While type is being edited, a click on blank canvas ends the editing and makes no new type;
|
||||
/// the Type tool stays, and the next click makes point type (#2871, #2854). The same holds when
|
||||
/// the editing began with a double-click from a selection tool.
|
||||
#[test]
|
||||
fn a_click_away_from_the_type_being_edited_only_ends_editing() {
|
||||
for prev in [None, Some("selection")] {
|
||||
let (d, _, mut tool) = editing("Hello");
|
||||
if let Some(prev) = prev {
|
||||
tool.set_option("prevTool", &json!(prev));
|
||||
}
|
||||
let (sel, p) = (Selection::default(), paint());
|
||||
let c = cx(&d, &sel, &p);
|
||||
let acts = click(&mut tool, &c, 400.0, 400.0);
|
||||
assert!(!creates(&acts), "{prev:?}: {acts:?}");
|
||||
assert!(!acts.iter().any(|a| matches!(a, Action::SwitchTool(_))), "{prev:?}: {acts:?}");
|
||||
assert_eq!((tool.id(), tool.editing), ("type", None));
|
||||
assert_eq!(tool.prev_tool.as_deref(), prev);
|
||||
let acts = click(&mut tool, &c, 400.0, 400.0);
|
||||
assert!(
|
||||
acts.iter().any(|a| matches!(a, Action::Exec(cmd, v) if cmd == "text.create" && v["x"] == 400.0 && v.get("area").is_none())),
|
||||
"{acts:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Point type a click placed and left empty is still discarded by the click away, and that click
|
||||
/// places no other type.
|
||||
#[test]
|
||||
fn a_click_away_from_empty_new_type_discards_it_and_makes_none() {
|
||||
let (d, id, mut tool) = editing("");
|
||||
tool.fresh = Some(id);
|
||||
let (sel, p) = (Selection::default(), paint());
|
||||
let acts = click(&mut tool, &cx(&d, &sel, &p), 400.0, 400.0);
|
||||
assert_eq!(acts, vec![Action::Exec("text.discardEmpty".into(), json!({"id": id.0}))]);
|
||||
}
|
||||
|
||||
/// Area Type and Type on a Path: a click on a path while other type is being edited ends the
|
||||
/// editing only; the next click turns the path into a frame or a baseline.
|
||||
#[test]
|
||||
fn a_click_on_a_path_while_editing_only_ends_editing() {
|
||||
let (mut d, rect) = crate::testutil::doc_with_rect();
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let t = TextObject::point(Point::new(300.0, 400.0), "Hello", CharStyle { size: 20.0, ..Default::default() });
|
||||
d.insert(Some(l), 1, Node::new(id, NodeKind::Text(Box::new(t)))).unwrap();
|
||||
let (sel, p) = (Selection::default(), paint());
|
||||
let c = cx(&d, &sel, &p);
|
||||
for (tool_id, at) in [("areaType", (100.0, 150.0)), ("typeOnPath", (150.0, 100.0))] {
|
||||
let mut tool = TypeTool::new(tool_id);
|
||||
tool.start_editing(id, 0);
|
||||
let acts = click(&mut tool, &c, at.0, at.1);
|
||||
assert!(!creates(&acts), "{tool_id}: {acts:?}");
|
||||
assert_eq!(tool.editing, None);
|
||||
let acts = click(&mut tool, &c, at.0, at.1);
|
||||
assert!(
|
||||
acts.iter().any(|a| matches!(a, Action::Exec(cmd, v) if cmd == "text.createInPath" && v["path"] == json!(rect.0))),
|
||||
"{tool_id}: {acts:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// While type is being edited, a drag that draws an area-type frame still makes it, and a click
|
||||
/// on other type edits that type at once.
|
||||
#[test]
|
||||
fn a_drag_or_a_click_on_other_type_while_editing_acts_at_once() {
|
||||
let (mut d, id, mut tool) = editing("Hello");
|
||||
let (sel, p) = (Selection::default(), paint());
|
||||
let acts = {
|
||||
let c = cx(&d, &sel, &p);
|
||||
tool.pointer(&c, &PointerEvent::new(PointerKind::Down, 300.0, 300.0));
|
||||
tool.pointer(&c, &PointerEvent::new(PointerKind::Drag, 400.0, 400.0));
|
||||
tool.pointer(&c, &PointerEvent::new(PointerKind::Up, 400.0, 400.0))
|
||||
};
|
||||
let area = json!({"width": 100.0, "height": 100.0});
|
||||
assert!(acts.iter().any(|a| matches!(a, Action::Exec(cmd, v) if cmd == "text.create" && v["area"] == area)), "{acts:?}");
|
||||
let l = d.layers[0].id;
|
||||
let other = d.alloc_id();
|
||||
let t = TextObject::point(Point::new(100.0, 300.0), "Other", CharStyle { size: 20.0, ..Default::default() });
|
||||
d.insert(Some(l), 1, Node::new(other, NodeKind::Text(Box::new(t)))).unwrap();
|
||||
let c = cx(&d, &sel, &p);
|
||||
let t = TypeTool::text(&c, other).unwrap().clone();
|
||||
let lay = vectorcraft_text::layout(FontDb::global(), &t);
|
||||
let (top, bot) = vectorcraft_text::caret_position(&lay, 2);
|
||||
let at = t.xf * top.midpoint(bot);
|
||||
tool.start_editing(id, 0);
|
||||
let acts = click(&mut tool, &c, at.x, at.y);
|
||||
assert!(!creates(&acts), "{acts:?}");
|
||||
assert!(acts.contains(&Action::Exec("select.set".into(), json!({"ids": [other.0]}))), "{acts:?}");
|
||||
assert_eq!(tool.editing, Some(other));
|
||||
}
|
||||
|
||||
/// Selected area type whose text doesn't fit its frame shows the red out port with any tool, not
|
||||
/// only the Type tools (#2323); text that fits shows none.
|
||||
#[test]
|
||||
|
||||
@@ -6711,11 +6711,12 @@ mod tests {
|
||||
fn clicking_away_mid_composition_keeps_the_text_and_interrupts_the_ime() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
let other = app.session.execute("text.create", &json!({"x": 300, "y": 250, "text": "Other", "size": 24})).unwrap()["id"].as_u64().unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
typing_frame(&mut app, &ctx, vec![]);
|
||||
app.select_tool("type");
|
||||
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
|
||||
let (a, b) = (xf.to_screen(Point::new(100.0, 100.0)), xf.to_screen(Point::new(300.0, 250.0)));
|
||||
let (a, b) = (xf.to_screen(Point::new(100.0, 100.0)), xf.to_screen(Point::new(310.0, 245.0)));
|
||||
let click =
|
||||
|p: Pos2, pressed| egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
typing_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(a), click(a, true)]);
|
||||
@@ -6723,17 +6724,29 @@ mod tests {
|
||||
let id = app.session.active().unwrap().selection.objects[0].0;
|
||||
typing_frame(&mut app, &ctx, vec![preedit("しょうこ", 4)]);
|
||||
// macOS sends nothing for a click away; the tool keeps the marked text and the IME is
|
||||
// told to drop its composition.
|
||||
typing_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(b), click(b, true)]);
|
||||
let ime = typing_frame(&mut app, &ctx, vec![click(b, false)]);
|
||||
// told to drop its composition. A press on other type edits that type at once.
|
||||
let ime = typing_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(b), click(b, true)]);
|
||||
assert_eq!(plain(&app, id), "しょうこ");
|
||||
assert!(!app.session.tool_composing());
|
||||
assert!(ime.expect("editing the new text").should_interrupt_composition);
|
||||
assert_eq!(app.session.active().unwrap().selection.objects, vec![vectorcraft_doc::NodeId(other)]);
|
||||
assert!(ime.expect("editing the other text").should_interrupt_composition);
|
||||
assert!(app.ime_marked.is_none());
|
||||
assert!(app.take_ime_discard(), "the host tells the system IME to drop its marked text");
|
||||
typing_frame(&mut app, &ctx, vec![click(b, false)]);
|
||||
// Interrupted once, not every frame.
|
||||
assert!(!typing_frame(&mut app, &ctx, vec![]).unwrap().should_interrupt_composition);
|
||||
assert!(!app.take_ime_discard());
|
||||
// A click on blank canvas ends the editing (#2871): the marked text stays, the frame asks
|
||||
// for no IME, and the host tells the system IME to drop its composition.
|
||||
let blank = xf.to_screen(Point::new(150.0, 200.0));
|
||||
typing_frame(&mut app, &ctx, vec![preedit("が", 1)]);
|
||||
typing_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(blank), click(blank, true)]);
|
||||
let ime = typing_frame(&mut app, &ctx, vec![click(blank, false)]);
|
||||
assert!(plain(&app, other).contains('が'), "{}", plain(&app, other));
|
||||
assert!(!app.session.tool_composing() && !app.session.tool_wants_text());
|
||||
assert!(ime.is_none());
|
||||
assert!(app.ime_marked.is_none());
|
||||
assert!(app.take_ime_discard());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -549,6 +549,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Graph column designs:** Object › Graph › Column (`graph.column`) draws the columns, bars or stacked segments and the legend swatch of the series picked with Group Selection with a graph design: Vertically Scaled stretches it to each column's length, Uniformly Scaled scales it both ways and centers it, Repeating stacks a copy for each Each Design Represents value and chops or scales the last one (For Fractions), and Sliding keeps the part above the design's guide line in proportion and stretches the part below. The dialog lists the designs with a preview; the choice is saved with the graph and follows the design's rename and delete.
|
||||
- **Other:** Eyedropper, Gradient annotator, Artboard (Alt-drag duplicates an artboard, and its art with Move/Copy Artwork with Artboard on; Copy, Cut and Paste take the artboard with its art, Paste right of the last artboard, Paste in Place where it was, into this or another document, #446; Object › Artboards › Rearrange All Artboards lays them out in a grid by row or by column, in one row or one column, left to right or right to left, from the menu, the Artboards panel or Properties' Rearrange All, #681; Scale Artwork with Artboard, in the Control bar and Properties beside Move Artwork with Artboard, resizes the artboard proportionally with the art fully inside it and its guides, strokes and corners per the Scale preferences, #602), Measure, Type, Hand, Zoom, Rotate View.
|
||||
- **Editing type:** double-clicking type with a selection tool (Selection or Direct Selection) switches to the Type tool with the caret where it was clicked; pressing Escape finishes editing and switches back to the selection tool used before double-clicking it (when in isolation mode, the first Escape exits text editing while preserving isolation mode, and a second Escape exits isolation).
|
||||
While a Type tool edits type, a click that lands on no type ends the editing without making new type, and that tool stays active; the next click makes new type (#2871, #2854).
|
||||
Duplicate Artboards, the Artboard tool's Alt-drag and Paste give the new artboard an id that no other artboard has, also when an artboard's id is the largest possible value (#1718). Deleting the copy or the original then leaves the other's guides.
|
||||
- **Touch Type:** a click with the Touch Type tool (Shift+T) selects one character and shows a box around it. Dragging the box or its lower-left handle moves the character. The upper-left handle scales it vertically, the lower-right handle scales it horizontally, the upper-right handle scales it proportionally, and the upper-middle handle rotates it. Each drag is one undo step that sets the character's kerning, baseline shift, scales or rotation, and the type stays editable (`text.touchType`; behavior reference: [Touch Type tool](https://web.archive.org/web/20261007185617/https://helpx.adobe.com/illustrator/using/tool-techniques/touch-type-tool.html), [Add text with the Type, Area Type, and Touch Type tools](https://web.archive.org/web/20260926095922/https://helpx.adobe.com/illustrator/desktop/design-with-text/add-manage-text/add-text-illustrator-text-tools.html)).
|
||||
- **Temporary tools:** Cmd with any other tool drags with the selection tool used last (Selection, Direct Selection or Group Selection). Held Space pans and Cmd+Space zooms (Cmd+Alt+Space out) whatever the tool.
|
||||
|
||||
Reference in New Issue
Block a user