diff --git a/crates/text/src/layout/flow.rs b/crates/text/src/layout/flow.rs index e7753c8c04..0d4ec2834e 100644 --- a/crates/text/src/layout/flow.rs +++ b/crates/text/src/layout/flow.rs @@ -24,6 +24,10 @@ fn pushes_in(para: &ParaStyle) -> bool { /// Paragraphs longer than this (glyphs) are always broken line by line. const MAX_COMPOSE_GLYPHS: usize = 200_000; +/// The widths tried, halving the difference each time, for the longest break of a line placed again +/// at its own metrics in more room than it was broken in ([`place_flow_line`]). +const REFIT_STEPS: usize = 24; + /// Whether every line of area type in `regions` is as wide however tall it is and wherever it is set: /// rectangular cells without text wrap. fn constant_width(regions: Option<&[Region]>) -> bool { @@ -415,25 +419,31 @@ fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementC let word = word_width(sg, *i, line_end, para.kinsoku); // A composed line in a frame whose lines are not all as wide is placed at its own tallest // metrics, as the composer placed it (`shaped::Shaped`): its room is the room across its height. - let est = match composed.as_ref().and_then(|lines| lines.get(*para_line)) { + let composed_line = composed.as_ref().and_then(|lines| lines.get(*para_line)).is_some_and(|&(end, _)| end > *i); + let mut est = match composed.as_ref().and_then(|lines| lines.get(*para_line)) { Some(&(end, _)) if end > *i && !constant_width(pen.regions) => sg.get(*i..end).and_then(Metrics::max).unwrap_or(est), _ => est, }; + // A line broken here in such a frame is placed again when it turns out taller than it was + // placed: its room is the room across its own height. + let refit = !composed_line && pen.regions.is_some() && !constant_width(pen.regions); // Place, break, then settle the baseline on the line's real metrics (moving on to the // next row/column if it no longer fits). loop { + let before = refit.then(|| pen.clone()); let first_in_region = pen.prev.is_none(); - let Some((mut baseline, x0, x1, same_baseline)) = pen.place(est, ind_l, ind_r, word) else { + let Some((mut baseline, mut x0, mut x1, same_baseline)) = pen.place(est, ind_l, ind_r, word) else { cx.out.overflow = cx.text.len() > if *i < n { sg[*i].byte } else { pr.start }; return None; }; - let width = x1 - x0 - ind_l - ind_r; let hyphenate = (para.hyphenate && (*hyphen_limit == 0 || *hyphen_run < *hyphen_limit)).then_some((¶.hyphenation, *zone)); - let (end, hyph, pushed_in) = match composed.as_ref().and_then(|lines| lines.get(*para_line)) { + let glyphs: &[SGlyph] = sg; + // The line broken in room `width` wide: its end, whether it ends with a hyphen, and whether + // it pushed characters in. + let break_in = |width: f64| match composed.as_ref().and_then(|lines| lines.get(*para_line)) { Some(&(end, hyph)) if end > *i => (end, hyph, false), _ if *i < n => { let start = *i; - let glyphs: &[SGlyph] = sg; // A horizontal line with right-to-left text is drawn in another order than its // text's, and its tabs take their stops in the order they are drawn: whether the line // ending before glyph `end` fits is measured as it is drawn ([`drawn_width`]). @@ -478,8 +488,56 @@ fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementC } _ => (n, false, false), }; - let m = Metrics::max(&sg[*i..end]).unwrap_or(*pm); - baseline += if same_baseline || (pen.regions.is_none() && first_in_region) { + let (mut end, mut hyph, mut pushed_in) = break_in(x1 - x0 - ind_l - ind_r); + let metrics = |end: usize| glyphs.get(*i..end).and_then(Metrics::max).unwrap_or(*pm); + let m = metrics(end); + // The baseline moves from where `est` placed it to where the line's metrics set it, unless + // the line was placed at metrics of its own (below). + let mut settle = true; + if let Some(before) = before + && m != est + { + if m.union(est) != est { + // Taller than it was placed: place it again at metrics that hold both, and break it + // again in the room there. + *pen = before; + est = est.union(m); + continue; + } + // Broken again in the room of a taller line, it no longer holds what made that line + // taller. It goes where its own metrics place it when the room there holds the room it + // was broken in, and otherwise stays where it was placed, inside the room measured. + settle = false; + let mut own = before; + if let Some((b, a, z, _)) = own.place(m, ind_l, ind_r, word) + && a <= x0 + EPS + && z >= x1 - EPS + { + // The room there may hold more of the line, up to what would make it taller than + // `m`: the longest break that does not is between the two widths (a line breaks no + // earlier in more room). + let (mut lo, mut hi) = (x1 - x0 - ind_l - ind_r, z - a - ind_l - ind_r); + let wider = break_in(hi); + if metrics(wider.0).union(m) == m { + (end, hyph, pushed_in) = wider; + } else { + for _ in 0..REFIT_STEPS { + let mid = (lo + hi) / 2.0; + let line = break_in(mid); + if metrics(line.0).union(m) == m { + (end, hyph, pushed_in) = line; + lo = mid; + } else { + hi = mid; + } + } + } + *pen = own; + (baseline, x0, x1) = (b, a, z); + } + } + let m = metrics(end); + baseline += if !settle || same_baseline || (pen.regions.is_none() && first_in_region) { 0.0 } else if pen.model == LeadingModel::EmBoxTop { // The em box top is fixed (the frame's top, or the line above's): the baseline diff --git a/crates/text/src/tests/combos.rs b/crates/text/src/tests/combos.rs index 4f57aa6972..cdb5a31fee 100644 --- a/crates/text/src/tests/combos.rs +++ b/crates/text/src/tests/combos.rs @@ -378,8 +378,8 @@ fn x_width() -> f64 { fn composer_x_mixed_sizes_in_a_stepped_frame() { // A frame as wide as the first line down to 40 pt and half as wide below. A 36 pt word makes // the line it is on reach below the step, where the frame is narrow: the every-line composer - // breaks the paragraph with each line's measure where its height sets it, not as the - // single-line composer does, and every line keeps within its room. + // breaks the paragraph with each line's measure where its height sets it, and every line keeps + // within its room. let w = x_width(); let set = |x: CharStyle, composer: Composer| { let mut t = stepped(w, w * 0.5, 40.0, 300.0); @@ -394,8 +394,10 @@ fn composer_x_mixed_sizes_in_a_stepped_frame() { // The 36 pt line reaches below the step and is set at the narrow measure. let tall = &l.lines[1]; assert!(tall.ascent > 30.0 && tall.baseline + tall.descent > 40.0 && tall.avail.1 < w * 0.5 + 0.01, "{:?}", l.lines); + // The single-line composer measures each line across its height too, and breaks this + // paragraph the same way. let (greedy, _) = set(sans(36.0), Composer::SingleLine); - assert_ne!(every, greedy); + assert_eq!(every, greedy); // Uniform sizes keep the breaks they had. let (uniform, _) = set(sans(12.0), Composer::EveryLine); assert_eq!(uniform, ["Destroy all creatures with toughness X or", "less. Then draw a card for each creature", "destroyed this way."]); @@ -435,8 +437,10 @@ fn composer_x_mixed_sizes_around_text_wrap() { across && !text[g.byte..].starts_with(char::is_whitespace) && g.origin.x < object.x1 && g.origin.x + g.advance > object.x0 }); assert_eq!(hits.count(), 0, "{:?}", l.lines); + // The single-line composer measures each line across its height too, and breaks this + // paragraph the same way. let (greedy, _, _) = set(Composer::SingleLine); - assert_ne!(every, greedy); + assert_eq!(every, greedy); } #[test] @@ -463,13 +467,42 @@ fn composer_x_mixed_sizes_in_a_frame_that_widens() { assert_eq!(greedy, ["Destroy all", "creatures", "with toughness X or less. Then draw", "a card for each creature destroyed this", "way."]); } +#[test] +fn single_line_composer_measures_a_line_across_its_tallest_glyph() { + // A frame wide enough for "Destroy all creatures X" down to 20 pt and as wide as "Destroy all" + // below, and a rectangle as wide with an object (6 pt wrap offset) beside that part. The first + // line starts with 12 pt words and takes in a 36 pt word, which sets it lower and reaches below + // 20 pt: the single-line composer measures its room across that height, every line that + // reaches the narrow part is set in it, and the first line keeps the 12 pt words that fit + // above it. + let w = natural_width("Destroy all creatures X", 12.0) + 30.0; + let narrow = natural_width("Destroy all", 12.0) + 2.0; + let runs = vec![run("Destroy all creatures ", sans(12.0)), run("X", sans(36.0)), run(" or less. Then draw a card.", sans(12.0))]; + let object = Rect::new(narrow + 6.0, 26.0, w + 10.0, 300.0); + let mut beside = area(runs.clone(), Rect::new(0.0, 0.0, w, 300.0)); + beside.wrap = vec![vectorcraft_doc::WrapShape { path: PathData::from_bezpath(&object.to_path(0.1)), wrap: Default::default() }]; + let mut shaped = stepped(w, narrow, 20.0, 300.0); + shaped.runs = runs; + for (name, mut t) in [("stepped frame", shaped), ("text wrap", beside)] { + t.para.composer = Composer::SingleLine; + let l = layout(db(), &t); + assert!(past_their_room(&t, &l).is_empty() && !l.overflow, "{name}: {:?}", l.lines); + let low = l.lines.iter().filter(|li| li.baseline + li.descent > 20.01); + assert!(low.clone().all(|li| li.avail.1 <= narrow + 0.01), "{name}: {:?}", l.lines); + // The 36 pt word is set, on a line in the narrow part. + assert!(low.clone().any(|li| li.ascent > 30.0), "{name}: {:?}", l.lines); + assert_eq!(line_texts(&t, &l)[0], "Destroy all creatures", "{name}: {:?}", l.lines); + assert!(l.lines[0].baseline < 20.0 && l.lines[0].avail.1 > w - 0.01, "{name}: {:?}", l.lines); + } +} + #[test] fn composer_x_mixed_sizes_overflow_a_shaped_frame() { // A stepped frame too short for the paragraph: the every-line composer still breaks the lines // that fit as one paragraph, with the lines past the frame's end measured as wide as the last // line the frame holds. Here that is the narrow part, so the lines that fit break as in a frame - // tall enough for the whole paragraph, and not as the single-line composer breaks them. They - // are set in order, each once and within its room, and the frame overflows. + // tall enough for the whole paragraph. They are set in order, each once and within its room, + // and the frame overflows. let w = x_width(); let set = |bottom: f64, composer: Composer| { let mut t = stepped(w, w * 0.5, 40.0, bottom); @@ -487,8 +520,10 @@ fn composer_x_mixed_sizes_overflow_a_shaped_frame() { assert_eq!(every, ["Destroy all creatures with toughness", "X or less. Then", "draw a card for each"]); let (whole, ..) = set(300.0, Composer::EveryLine); assert_eq!(every, whole[..every.len()]); + // The single-line composer measures each line across its height too, and breaks the lines + // that fit the same way. let (greedy, ..) = set(75.0, Composer::SingleLine); - assert_ne!(every, greedy); + assert_eq!(every, greedy); // A frame with no room for a line sets none. for composer in [Composer::EveryLine, Composer::SingleLine] { let mut t = stepped(w, w * 0.5, 4.0, 8.0); @@ -529,8 +564,10 @@ fn composer_x_mixed_sizes_overflow_around_text_wrap() { assert_eq!(hits.count(), 0, "{:?}", l.lines); // The lines that fit break as in `composer_x_mixed_sizes_around_text_wrap`'s taller frame. assert_eq!(every, ["Destroy all creatures with", "toughness\u{a0}X", "or less. Then", "draw a card for", "each creature"]); + // The single-line composer measures each line across its height too, and breaks the lines + // that fit the same way. let (greedy, ..) = set(Composer::SingleLine); - assert_ne!(every, greedy); + assert_eq!(every, greedy); } #[test] diff --git a/crates/text/src/thread/tests.rs b/crates/text/src/thread/tests.rs index adbf3b2c70..4ea7696aae 100644 --- a/crates/text/src/thread/tests.rs +++ b/crates/text/src/thread/tests.rs @@ -415,8 +415,7 @@ fn stepped_frame(w: f64, narrow: f64, y: f64, bottom: f64) -> TextObject { /// A paragraph of mixed type sizes that a stepped frame cannot hold goes on in the next frame. The /// every-line composer breaks the lines the first frame holds as one paragraph, as in a frame tall -/// enough for all of it and not as the single-line composer breaks them, and the first frame -/// takes the text of those lines. The next frame sets the rest at its own width, without the +/// enough for all of it, and the first frame takes the text of those lines. The next frame sets the rest at its own width, without the /// first-line indent, and no character is lost or repeated. The frames take the same shares as /// when each is measured with the whole rest of the story. #[test] @@ -460,8 +459,10 @@ fn a_mixed_size_paragraph_continued_from_a_shaped_frame_is_composed_in_both() { assert_eq!(every, whole[..every.len()], "indent {indent}"); if indent == 0.0 { assert_eq!(every, ["Destroy all creatures with toughness", "X or less. Then", "draw a card for each"]); + // The single-line composer measures each line across its height too, and breaks the + // first frame's lines the same way. let (greedy, ..) = shares(Composer::SingleLine, indent); - assert_eq!(greedy, ["Destroy all creatures with toughness X", "or less. Then draw a", "card for each", "creature destroyed"]); + assert_eq!(greedy, every); } // The next frame starts at its left edge, and its lines keep within it. assert!((line_start(&lays[1], 0) - 200.0).abs() < 1e-6, "{}", line_start(&lays[1], 0)); diff --git a/docs/mcp.md b/docs/mcp.md index f2e3ef7414..4ff3afc93c 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -10670,6 +10670,10 @@ threaded frame, which composes them again at its own widths. Such a paragraph fa frame has no room for its first line, or when the search for its breaks passes VectorCraft's own limits (1,000,000 lines tried and 2,000,000 baseline steps for one paragraph, 2,000,000 lines and 4,000,000 steps for all the paragraphs of a frame). +Single-line measures each line's room the same way: a line that takes in a taller character is placed again across its +new height and broken again in the room there. When that leaves the taller character for the next line, the line is +set at its own height where the room there is no narrower, with the words before that character that fit there, and +otherwise stays where it was placed. The UI command `type.lineComposer` (Cmd+Alt+Shift+C, in remote mode `press_key {key: "C", mods: {cmd: true, alt: true, shift: true}}` or `invoke_menu`) sets `singleLine` when every paragraph of the selected type (or every paragraph the Type tool's selection touches) uses Every-line, else `everyLine`, and returns the composer it set. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 161f356368..702bd52716 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1345,7 +1345,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Mouse wheel over the font family field (#5253):** In the Character and Properties panels and the Control bar, turning the mouse wheel over the closed font family field applies the next family to the selected type (the previous one when turned up), keeping the style when the family has it, else its closest. Each notch is one undo step; leaving the field or Escape keeps the family, and Undo takes it back. The steps use the font menu's order at the first turn and keep it until the pointer leaves the field or the text selection changes, so recent-font updates do not reshuffle later turns. That order follows the font filters every font menu shares (one chosen in the Glyphs panel's menu or a dialog's applies too) but ignores its search text, puts recent families first, and stops at the first and last family. A turn with a modifier key is left to the panel and the canvas. Behavior reference: the [Illustrator CS6 reference](https://help.adobe.com/archive/en/illustrator/cs6/illustrator_reference.pdf#page=10) describes editing hovered dropdown controls with the mouse wheel; [Find and apply fonts](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/find-and-apply-fonts.html) names the family field's places. Applying it to the font field, the order, the ends and the undo steps are VectorCraft's own rules. Tests: `crates/ui-egui/src/tests/font_menu.rs`, `crates/ui-egui/src/font_menu/tests.rs`, `crates/ui-egui/tests/font_wheel.rs`. - **Type:** Character size, leading, baseline shift and horizontal-scale controls account for object/group transforms (#1034): exact document-point sizes, mixed-value display and keyboard increments, range edits, resettable horizontal stretch, undo/redo and native save/reopen (`tests_typescale`, Character panel tests). Point size is the transformed em-height axis, independent of glyph ink bounds; affine rotation, reflection and shear are retained. Group scaling still scales text; preserving font sizes is a separate enhancement. Behaviour reference: [Transform text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/transform-text.html) (public documentation). Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), paragraph attributes per paragraph (alignment, indents, spacing, hyphenation, hanging punctuation, tab stops, paragraph style, direction, mojikumi and leading model; the Paragraph, Tabs and Paragraph Styles panels act on the paragraphs the Type tool's selection or caret touches, Return continues the paragraph's style, threads and SVG keep them), Area Type Options (rows/columns/inset/first baseline, Text Flow by rows or by columns (#4978), vertical Align Top/Center/Bottom/Justify per row/column, reflowing non-rectangular frames and text wrap), threaded text across any closed shapes, Fit Headline, the Every-line Composer for justified and ragged text (paragraph-wide breaks for an even rag, Single-line per object from the Paragraph panel menu or `text.setFormat {composer}`). Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`), and an anchor of the path of type on a path outside perspective, the type reflowing along it (`text.reshapePath`, #3980). Point type still scales. Auto Size: the frame height follows the text after every edit, in the same undo step (rectangular frames of horizontal type in one row; columns get the least height that holds them; a height set by hand or threading turns it off), and the "Auto Size New Area Type" preference gives it to new area type. Shrink Text to Fit (beyond Illustrator): overflowing area type scales its sizes, leading and baseline shifts down by the largest factor (in 0.1 pt steps of the first run, down to a minimum percentage) that fits, at layout time so every export sees it. `text.areaOptions` and `document.inspect` report overflow and the shrink factor. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. The Character panel's Variable Font sliders set each axis (see Fonts). Beyond Illustrator: inline graphics (`text.insertInline`, Type ▸ Insert Inline Symbol) set a document symbol in a run of type as one character that flows, wraps and justifies like a glyph (InDesign-style inline objects, e.g. mana symbols in card rules text): sized to a multiple of the font size, centred on the cap height, opening up tall lines, following Redefine Symbol; a character of its run's size for Character Alignment and Top-to-Top leading, placed by the bidi reordering in right-to-left text (a neutral U+FFFC); drawn on the canvas, kept as live text plus a `` in SVG and as vector art in PDF; Backspace removes it whole. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Arabic typefaces: builds made with a craft-fonts checkout that holds its Arabic families (Noto Sans Arabic, the Arabic fallback, plus Cairo, Tajawal, Almarai, IBM Plex Sans Arabic, Noto Kufi Arabic, Amiri, Alexandria, Fustat, Reem Kufi, Aref Ruqaa and Gulzar once craft-fonts adds them, storytold/craft-fonts#11; releases get them when their craft-fonts pin moves) list them in the font menus on desktop; the web build fetches them from beside the wasm (Noto Sans Arabic before start, so Arabic never shows as boxes, the rest in the background). Font Size and Leading are combo boxes in the Character and Properties panels (Font Size in the Control bar too, beside the Character link, font family and style): type, step with ↑/↓ or the spinner, scrub the label, or pick a preset (6–72 pt, shown in the type unit; Leading also Auto), each pick one undo step; Kerning and Tracking list -100…200; a size, leading, kerning or tracking the selected text doesn't share shows blank (#458). Type › Bold and Italic (beyond Illustrator, as page layout apps have them; Cmd+Shift+B / Cmd+Shift+I while the Type tool edits text) switch the selected type to its family's own Bold or Italic face and back, keeping its width (a Narrow face stays Narrow); a family without that face says so (#724). The commands that make or restyle type (and the MCP `add_text` tool) say in their result when area type does not fit its frame, and `add_text` takes Auto Size and Shrink Text to Fit (#1500, fixes #1471). Cmd+Shift+C (Align center) works on Windows and Linux too (#1940). A double-click on area type's bottom middle handle turns on Auto Size (#1940). Print and Print Preview warn about the area type and type on a path that prints without the text that doesn't fit, and give their ids. A tab stop in the Tabs panel drags from where the press landed, a stop just added included (#1487, fixes #1458). Area type in a non-rectangular shape moves a word too wide for the top lines down to the first line that holds it instead of breaking it (#1617, fixes #1615). Stylistic sets (ss01–ss20), stylistic and titling alternates, proportional and lining figures, and superscript and subscript glyphs can be turned on as OpenType features of a character style (`text.setStyle {features}`), and the OpenType panel keeps them (#1611, fixes #1607). Escape while typing ends the edit, keeps the type selected and switches back to the tool that was active before type editing began (kept while clicks move editing to other text), or to the Selection tool when there was none (#1802, #1686, #1656). Select › Same matches type on its characters' fill and stroke (#1746, #1737). `text.create`, `text.createInPath` and the MCP `add_text` tool take `justify`, so point type can be centred on or end at its point (#1752, #1748). The Character panel menu's Show Font Height Options shows a Set Font Height Reference dropdown under Font Size (`text.setFormat` or `text.setRangeStyle` `{fontHeightReference}`). Each reference shows an icon of the height it measures before its name, in the open list and in the closed field (#6304): a frame around a letter for Em Box, a bar from the baseline to the top of a capital for Cap Height and of a lowercase x for x-Height, and a frame inset in the em square for ICF Box. With Em Box, the default, the font size is the size of the em square; with Cap Height, x-Height or ICF Box, the characters are set at the em size at which the font's OS/2 cap height, its x height or the height of its ideographic character face equals the font size. With a font that has no such value, or one outside 0.1 to 2 em, the size measures the em box. SVG, PDF and legacy `.ai` export and the CSS Properties rules write the em size the characters are drawn at, and DXF export writes 0.7 times that em size as each TEXT entity's height (group 40, the height of its capitals). Behavior reference: [Vary font height](https://web.archive.org/web/20251118174722/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/vary-font-height.html). - The Every-line Composer composes a paragraph whose characters differ in line metrics (type size, font, leading or vertical scale, or an inline graphic taller than the text) in area type of any shape, rows, columns and text wrap included. In a frame that is not a rectangle, or with text wrap, each line it tries is measured where the lines above it put it, across the line's own height, so a taller line gets the room of the narrower part of the frame, or of the span beside a wrap object, that it reaches. When the frame cannot hold all of such a paragraph, the lines it holds are still composed with the whole paragraph, the lines past the frame's end measured as wide as the last line the frame holds (VectorCraft's own rule); they overflow or go on in the next threaded frame, which composes them again. Such a paragraph is broken with the Single-line Composer when the frame has no room for its first line or when the search passes VectorCraft's own limits. Behavior reference: [the help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/set-hyphenation-and-line-breaks.html), and for text that flows on between threaded objects, [the help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/add-manage-text/create-text-threads-between-objects.html). + The Every-line Composer composes a paragraph whose characters differ in line metrics (type size, font, leading or vertical scale, or an inline graphic taller than the text) in area type of any shape, rows, columns and text wrap included. In a frame that is not a rectangle, or with text wrap, each line it tries is measured where the lines above it put it, across the line's own height, so a taller line gets the room of the narrower part of the frame, or of the span beside a wrap object, that it reaches. When the frame cannot hold all of such a paragraph, the lines it holds are still composed with the whole paragraph, the lines past the frame's end measured as wide as the last line the frame holds (VectorCraft's own rule); they overflow or go on in the next threaded frame, which composes them again. Such a paragraph is broken with the Single-line Composer when the frame has no room for its first line or when the search passes VectorCraft's own limits. The Single-line Composer measures a line's room across the height of its tallest character too: a line that takes in a taller character is placed again and broken again in the room there. Behavior reference: [the help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/set-hyphenation-and-line-breaks.html), and for text that flows on between threaded objects, [the help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/add-manage-text/create-text-threads-between-objects.html). A text wrap object's Offset may be negative as well as positive ([the help page](https://help.adobe.com/archive/en_US/illustrator/cs4/illustrator_cs4_help.pdf#page=307)). A negative Offset (to -1000 pt) shrinks the wrap object by that distance, measured from each edge itself and from the edges of its holes, so lines beside it, above it and below it run that far into it, and an object narrower than twice the distance no longer moves text aside. With Invert Wrap, a negative Offset keeps the text that far inside the object. These are VectorCraft's own rules for the shape of the shrunk object. Text Wrap Options, `object.textWrap.make` and `object.textWrap.options` and the MCP `text_wrap` tool take the negative value, and layout uses it (`crates/text/src/layout/region.rs` tests, `cmd::textwrap::tests::text_wrap_options_negative_offset_reflows_and_round_trips`). In area type of a shape other than a rectangle, each line's whole band, ascent to descent, stays inside the frame: where an edge pinches in within the band, the line ends at the narrowest point, and a line runs beside a hole that starts and ends within its band, not across it. In area type of any shape, a line also stays clear of a text wrap object that reaches into any part of its band, and inside an Invert Wrap object over its whole band (`crates/text/src/layout/region.rs` tests). In area type whose frame has several contours, a line runs across an edge two contours share only when their edges lie exactly on one line, so a gap between contours keeps lines from crossing it, even a gap one unit in the last place wide near the edge of the canvas; an edge divided by an anchor exactly on it still lets lines cross (`layout::region::separate_slanted_contours_keep_a_four_ulp_gap_near_canvas_limit`, `shared_slanted_edge_still_joins_near_canvas_limit`, `area_type_sets_no_line_across_a_four_ulp_gap_between_slanted_contours`).