From 0382ba5ee8b0a13e5af15035552dce1ea4a6000a Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 01:52:19 -0800 Subject: [PATCH] Type: Cmd+Enter (Ctrl+Enter) ends text editing and keeps the text While the Type tool edits text, Cmd+Enter on macOS and Ctrl+Enter on Windows and Linux typed a line break. It now ends editing as Escape does: the typed text stays, the type stays selected, and the tool used before editing is selected again. A held chord ends editing once; its repeats type nothing and do not open the Move dialog. Enter, Shift+Enter and Ctrl+Enter on macOS still type line breaks, and so does the chord with Alt or Shift. Cmd+Ctrl+Enter on macOS ends editing too. A command bound to the chord still runs instead, and editing continues. While an IME composes, the chord stays the IME's. --- crates/tools/src/text.rs | 19 +++++++ crates/tools/src/text/tests.rs | 23 ++++++++ crates/tools/src/text/tool.rs | 9 +-- crates/ui-egui/src/canvas/tests.rs | 77 ++++++++++++++++++++++++-- crates/ui-egui/src/shortcuts.rs | 4 +- crates/ui-egui/src/shortcuts/tests.rs | 25 +++++++++ crates/ui-egui/src/shortcuts/typing.rs | 7 ++- docs/target-app-parity.md | 1 + docs/type-parity.md | 2 +- 9 files changed, 148 insertions(+), 19 deletions(-) diff --git a/crates/tools/src/text.rs b/crates/tools/src/text.rs index 0bc29a558..c7a5a1ad4 100644 --- a/crates/tools/src/text.rs +++ b/crates/tools/src/text.rs @@ -396,6 +396,25 @@ impl TypeTool { out } + /// Stop editing and go back to the tool used before double-clicking into type (or to the + /// Selection tool), and leave the type selected. + fn end_editing(&mut self, cx: &ToolContext) -> Vec { + let target = self.prev_tool.take().unwrap_or_else(|| "selection".into()); + let mut out = self.finish(cx); + out.push(Action::SwitchTool(target)); + out + } + + /// Enter types a line break, and Shift+Enter types a forced line break. Cmd+Enter (Ctrl+Enter on + /// Windows and Linux) ends editing as Escape does and keeps the typed text. `Mods` does not tell + /// Ctrl apart from Cmd+Ctrl, so Cmd+Ctrl+Enter on macOS ends editing too. + fn enter(&mut self, cx: &ToolContext, mods: Mods) -> Vec { + if mods.cmd && !mods.shift && !mods.alt { + return self.end_editing(cx); + } + self.replace(cx, if mods.shift { "\u{2028}" } else { "\n" }, None) + } + /// Replace the selection with `insert` (styled `runs` when given) inside the typing session. fn replace(&mut self, cx: &ToolContext, insert: &str, styled: Option>) -> Vec { let Some(id) = self.editing else { return vec![] }; diff --git a/crates/tools/src/text/tests.rs b/crates/tools/src/text/tests.rs index b0494a3e1..4ae1424f6 100644 --- a/crates/tools/src/text/tests.rs +++ b/crates/tools/src/text/tests.rs @@ -1267,6 +1267,29 @@ fn escape_returns_to_previous_tool_when_set() { assert_eq!(acts2.last(), Some(&Action::SwitchTool("selection".into()))); } +/// Cmd+Enter (Ctrl+Enter on Windows and Linux) ends editing as Escape does and types no line +/// break. Ctrl+Enter on macOS, Shift and Alt with it still type one. +#[test] +fn cmd_enter_ends_editing_like_escape() { + let (d, _id, mut tool) = editing("Hello"); + tool.set_option("prevTool", &json!("directSelection")); + let acts = key(&mut tool, &d, ToolKey::Enter, CMD); + assert!(acts.iter().all(|a| !matches!(a, Action::Preview(..))), "no line break: {acts:?}"); + assert_eq!(acts.last(), Some(&Action::SwitchTool("directSelection".into()))); + assert!(!tool.wants_text(), "editing ended"); + // Ctrl+Enter on Windows and Linux, and Cmd+Ctrl+Enter on macOS. + let cmd_ctrl = Mods { ctrl: true, ..CMD }; + let (d, _id, mut tool) = editing("Hello"); + assert_eq!(key(&mut tool, &d, ToolKey::Enter, cmd_ctrl).last(), Some(&Action::SwitchTool("selection".into()))); + let mac_ctrl = Mods { ctrl: true, ..Mods::default() }; + for mods in [mac_ctrl, Mods { shift: true, ..CMD }, Mods { alt: true, ..CMD }] { + let (d, _id, mut tool) = editing("Hello"); + let acts = key(&mut tool, &d, ToolKey::Enter, mods); + assert!(acts.iter().any(|a| matches!(a, Action::Preview(..))), "{mods:?} types a line break: {acts:?}"); + assert!(tool.wants_text(), "{mods:?} keeps editing"); + } +} + /// 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 { let mut acts = tool.pointer(c, &PointerEvent::new(PointerKind::Down, x, y)); diff --git a/crates/tools/src/text/tool.rs b/crates/tools/src/text/tool.rs index 1e9b1be88..9f97f2372 100644 --- a/crates/tools/src/text/tool.rs +++ b/crates/tools/src/text/tool.rs @@ -228,13 +228,8 @@ impl Tool for TypeTool { // Escape ends editing and goes back to the tool used before double-clicking into type // (or to the Selection tool by default), the type selected: the keyboard is free for // tool shortcuts again (#1686). - ToolKey::Escape => { - let target = self.prev_tool.take().unwrap_or_else(|| "selection".into()); - let mut out = self.finish(cx); - out.push(Action::SwitchTool(target)); - out - } - ToolKey::Enter => self.replace(cx, if mods.shift { "\u{2028}" } else { "\n" }, None), + ToolKey::Escape => self.end_editing(cx), + ToolKey::Enter => self.enter(cx, mods), ToolKey::Tab => self.replace(cx, "\t", None), ToolKey::Backspace | ToolKey::Delete if a != b || story => self.replace(cx, "", None), ToolKey::Backspace if self.caret > 0 => { diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index f00836a0a..f630d801d 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -6371,7 +6371,7 @@ fn editing_keys_after_an_ime_commit_apply_in_the_same_frame() { } /// Cmd/Ctrl+Enter bound to a command runs the command and types no line break, also when a plain -/// Enter comes in the same frame. Unbound, it types a line break as Enter does. +/// Enter comes in the same frame. Unbound, it ends editing and keeps the text. #[test] fn a_command_on_cmd_enter_runs_without_typing_a_line_break() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); @@ -6386,18 +6386,83 @@ fn a_command_on_cmd_enter_runs_without_typing_a_line_break() { modifiers: egui::Modifiers::COMMAND, }); typing_frame(&mut app, &ctx, vec![egui::Event::Text("a".into())]); - typing_frame(&mut app, &ctx, cmd_enter.to_vec()); - assert_eq!(plain(&app, id), "a\n", "nothing is bound to Cmd+Enter"); crate::keymap::set_override(&mut app.ui.shortcut_overrides, "view.grid", "Cmd+Enter"); crate::shortcut_editor::sync(&app.ui); let grid = app.ui.view.grid; typing_frame(&mut app, &ctx, cmd_enter.to_vec()); - let ran = (plain(&app, id), app.ui.view.grid); + let ran = (plain(&app, id), app.ui.view.grid, app.session.tool_wants_text()); typing_frame(&mut app, &ctx, [tap(egui::Key::Enter).to_vec(), cmd_enter.to_vec()].concat()); app.ui.shortcut_overrides.clear(); crate::shortcut_editor::sync(&app.ui); - assert_eq!((ran.0.as_str(), ran.1), ("a\n", !grid), "Cmd+Enter runs Show Grid only"); - assert_eq!((plain(&app, id).as_str(), app.ui.view.grid), ("a\n\n", grid), "Enter types a line break and Cmd+Enter runs Show Grid"); + assert_eq!((ran.0.as_str(), ran.1, ran.2), ("a", !grid, true), "Cmd+Enter runs Show Grid only"); + assert_eq!((plain(&app, id).as_str(), app.ui.view.grid), ("a\n", grid), "Enter types a line break and Cmd+Enter runs Show Grid"); + assert!(app.session.tool_wants_text(), "a bound Cmd+Enter does not end editing"); + typing_frame(&mut app, &ctx, cmd_enter.to_vec()); + assert_eq!(plain(&app, id), "a\n", "nothing is bound to Cmd+Enter: no line break"); + assert!(!app.session.tool_wants_text(), "unbound, Cmd+Enter ends editing"); + assert_eq!(app.session.tool_id(), "selection"); +} + +/// Cmd+Enter (Ctrl+Enter on Windows and Linux) ends editing like Escape: a repeat of it, and text, +/// keys and a paste after it in the same frame, leave the type as it was. Enter, Shift+Enter and +/// Ctrl+Enter on macOS still type line breaks. +#[test] +fn cmd_enter_ends_text_editing_and_keeps_the_text() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.prefs.placeholder_text = false; + let ctx = egui::Context::default(); + let id = start_typing(&mut app, &ctx); + let key = |pressed, repeat, modifiers| egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed, repeat, modifiers }; + let mac_ctrl = egui::Modifiers { ctrl: true, ..Default::default() }; + typing_frame(&mut app, &ctx, vec![egui::Event::Text("a".into())]); + typing_frame(&mut app, &ctx, vec![key(true, false, egui::Modifiers::NONE), key(false, false, egui::Modifiers::NONE)]); + typing_frame(&mut app, &ctx, vec![key(true, false, egui::Modifiers::SHIFT), key(false, false, egui::Modifiers::SHIFT)]); + typing_frame(&mut app, &ctx, vec![key(true, false, mac_ctrl), key(false, false, mac_ctrl)]); + assert_eq!(plain(&app, id), "a\n\u{2028}\n"); + assert!(app.session.tool_wants_text()); + let held = vec![ + key(true, false, egui::Modifiers::COMMAND), + key(true, true, egui::Modifiers::COMMAND), + egui::Event::Text("b".into()), + egui::Event::Key { key: egui::Key::Backspace, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }, + egui::Event::Paste("c".into()), + egui::Event::Key { key: egui::Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }, + ]; + typing_frame(&mut app, &ctx, held); + assert!(!app.session.tool_wants_text(), "editing ended"); + assert_eq!(app.session.tool_id(), "selection"); + assert_eq!(plain(&app, id), "a\n\u{2028}\n", "the text is kept, and nothing after the chord changes it"); + assert_eq!(app.session.active().unwrap().selection.objects, vec![vectorcraft_doc::NodeId(id)], "the type stays selected"); + typing_frame(&mut app, &ctx, vec![key(true, true, egui::Modifiers::COMMAND)]); + typing_frame(&mut app, &ctx, vec![key(false, false, egui::Modifiers::COMMAND)]); + assert!(app.ui.dialog.is_none(), "the repeats of the chord open no dialog"); + assert_eq!(plain(&app, id), "a\n\u{2028}\n"); +} + +/// While the IME composes, Cmd+Enter is the IME's: it does not end editing, drop the marked text or +/// type a line break, in the frame that starts the composition or in a later one. +#[test] +fn cmd_enter_while_composing_keeps_the_composition_and_editing() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.prefs.placeholder_text = false; + let ctx = egui::Context::default(); + let id = start_typing(&mut app, &ctx); + let cmd_enter = [true, false].map(|pressed| egui::Event::Key { + key: egui::Key::Enter, + physical_key: None, + pressed, + repeat: false, + modifiers: egui::Modifiers::COMMAND, + }); + typing_frame(&mut app, &ctx, vec![egui::Event::Text("a".into())]); + typing_frame(&mut app, &ctx, [vec![preedit("か", 1)], cmd_enter.to_vec()].concat()); + assert!(app.session.tool_composing(), "the same frame keeps the composition"); + assert!(app.session.tool_wants_text(), "the same frame keeps editing"); + assert_eq!(plain(&app, id), "aか", "the same frame keeps the marked text and types no line break"); + typing_frame(&mut app, &ctx, cmd_enter.to_vec()); + assert!(app.session.tool_composing(), "a later frame keeps the composition"); + assert!(app.session.tool_wants_text(), "a later frame keeps editing"); + assert_eq!(plain(&app, id), "aか", "a later frame keeps the marked text and types no line break"); } /// Reset to a blank document, select the Type tool and click document point (100, 100), returning diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index f6ceaec9d..f35944a6e 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -236,8 +236,8 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) { let composing = app.session.tool_composing(); // Tool keys first (Enter/Escape end paths; arrows change polygon sides while dragging). let busy = app.session.tool_busy(); - // Enter in the Type tool types a line break, so it goes to the tool with the frame's text, - // below: the loop then skips Enter, the first key. + // Enter in the Type tool types a line break (Cmd+Enter ends editing), so it goes to the tool + // with the frame's text, below: the loop then skips Enter, the first key. let enter_types = app.session.tool_wants_text(); for (k, tk) in [(Key::Enter, ToolKey::Enter), (Key::Escape, ToolKey::Escape)].into_iter().skip(usize::from(enter_types)) { if !typing && !composing && !take_claimed_repeat(app, ctx, k, tk, view) && pressed_anew(ctx, k) { diff --git a/crates/ui-egui/src/shortcuts/tests.rs b/crates/ui-egui/src/shortcuts/tests.rs index 5fbffb533..b64dd4f7a 100644 --- a/crates/ui-egui/src/shortcuts/tests.rs +++ b/crates/ui-egui/src/shortcuts/tests.rs @@ -1288,6 +1288,31 @@ fn holding_the_enter_that_confirms_a_dialog_types_no_line_break() { assert_ne!(text_and_layers(&mut app, text).0, before, "a new press types one"); } +/// A held Cmd+Enter ends text editing once. Its repeats add no line break and do not open the Move +/// dialog for the type it leaves selected; a new press does, as after Escape. +#[test] +fn holding_cmd_enter_ends_text_editing_once() { + let mut app = app_with_a_selected_rectangle(); + let text = app.session.execute("text.create", &json!({"x": 20, "y": 250, "text": "Hi"})).unwrap()["id"].as_u64().unwrap(); + app.select_tool("type"); + app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap(); + assert!(app.session.tool_wants_text()); + let held = HeldKey::new(Key::Enter, Modifiers::COMMAND); + held.frame(&mut app, vec![]); + let before = text_and_layers(&mut app, text); + held.press(&mut app); + assert!(!app.session.tool_wants_text(), "the press ends editing"); + assert_eq!(app.session.tool_id(), "selection"); + for repeat in 0..4 { + held.press(&mut app); + assert_eq!(text_and_layers(&mut app, text), before, "repeat {repeat}: no line break"); + assert_eq!(open_dialog(&app), None, "repeat {repeat}: no dialog"); + } + held.release(&mut app); + held.press(&mut app); + assert_eq!(open_dialog(&app).as_deref(), Some("move"), "a new press opens Move"); +} + /// A release that never reaches the main window costs the next press: egui takes that press for a /// repeat, and its release clears the key, so the press after it acts again. #[test] diff --git a/crates/ui-egui/src/shortcuts/typing.rs b/crates/ui-egui/src/shortcuts/typing.rs index cdd8c16f8..a3127adde 100644 --- a/crates/ui-egui/src/shortcuts/typing.rs +++ b/crates/ui-egui/src/shortcuts/typing.rs @@ -52,7 +52,7 @@ enum Seq { InterruptNavigation, InterruptCommand, ResetCompleted(egui::ImeResetId), - /// Enter, which types a line break. + /// Enter, which types a line break. Cmd+Enter (Ctrl+Enter on Windows and Linux) ends editing. Enter(Mods), /// An editing key with modifiers, an arrow, Home, End, Tab or a clipboard event. Edit(TextInput), @@ -74,7 +74,8 @@ pub(super) fn type_text(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool { // characters in one frame interleaves correctly. Taken as two passes (all text, then the keys) // the deletes land after the inserts and the diacritics are lost. Enter, the other editing keys // and the clipboard events join them: ← or a paste lands between the texts around it, and each - // Enter types its own line break. Each handled event is removed from the input. + // Enter types its own line break (or, as Cmd+Enter, ends editing there). Each handled event is + // removed from the input. let seq = type_sequence(ctx); let viewport = ctx.viewport_id(); let mut old_composition_retired = false; @@ -177,7 +178,7 @@ fn type_sequence(ctx: &egui::Context) -> Vec { let tk = if key == Key::Backspace { ToolKey::Backspace } else { ToolKey::Delete }; seq.push(Seq::Erase(tk, Mods { shift: m.shift, ..Mods::default() })); } - // A command bound to the chord (Cmd+Enter) runs instead of the line break, in the + // A command bound to the chord (Cmd+Enter) runs instead of ending editing, in the // shortcuts after this. egui::Event::Key { key: Key::Enter, pressed, modifiers: m, .. } if !chord_runs_a_command(Key::Enter, m, true) => { if pressed { diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index a368c794c..2b4a39f25 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -861,6 +861,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur While a drag rotates the view, a dial at the center of the canvas shows the angle. - **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). + As VectorCraft's own rule, Cmd+Enter (Ctrl+Enter on Windows and Linux) finishes editing as Escape does and keeps the typed text; a command bound to that chord runs instead. Inside a ligature (one glyph for several characters, such as fi), the Type tool's caret, clicks, arrow keys and selection stop between its characters, at the font's ligature carets ([OpenType GDEF ligature caret list](https://learn.microsoft.com/en-us/typography/opentype/spec/gdef)) or, when the font does not list one valid caret for each stop, at the glyph's width divided equally. Contour-point carets use the font's unhinted outline at its default variation location; pixel-size device adjustments are not applied. 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)). diff --git a/docs/type-parity.md b/docs/type-parity.md index b36cb4892..010c74aa5 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -21,7 +21,7 @@ against Illustrator's public documentation; **estimated** unless noted. | Mouse wheel over the closed font-family field | done | #5253: Character, Properties and Control step selected text through the family list using saved font filters (excluding search text), one undo step per notch; the style is matched and the list stops at either end. Escape and leaving the field keep the change. Order, bounds and Undo grouping are VectorCraft's choices; scores unchanged. | | 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; 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)) | +| 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), and Cmd+Enter (Ctrl+Enter on Windows and Linux) does the same unless a command is bound to it; `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 |