Merge pull request #5027 from dotCooCoo/feat/type-orientation-follows-thread-r

Type: Type Orientation is a setting of the whole thread
This commit is contained in:
BFlat
2026-11-09 01:15:39 +08:00
committed by GitHub
7 changed files with 258 additions and 9 deletions
+5 -1
View File
@@ -475,11 +475,15 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
}
// Threaded type read from a file flows through its frames (the threaded type of a restored
// document keeps its text as saved), and every frame of a thread takes its first frame's
// Optical Margin Alignment setting.
// Optical Margin Alignment setting and writing direction. When that changes the direction of a
// frame of a restored document, the story flows through that frame's thread again. Threaded
// frames keep their size: Auto Size is off for them.
if !doc.text_threads.is_empty() && !restored {
crate::cmd::threads::reflow(&Document::new(1.0, 1.0), &mut doc);
} else if !doc.text_threads.is_empty() {
crate::cmd::threads::keep_frame_sizes(&mut doc);
crate::cmd::threads::share_optical_margin(&mut doc);
crate::cmd::threads::share_orientation(&mut doc);
// A frame whose first paragraph continues from the frame before draws it without the
// paragraph's first-line indent and list marker.
crate::cmd::threads::mark_continued(&mut doc);
+61 -3
View File
@@ -168,7 +168,17 @@ pub(crate) fn reflow(before: &Document, doc: &mut Document) {
}
}
let optical = if live.len() >= 2 { optical_margin(before, doc, &live) } else { None };
let frames: Vec<TextObject> = live.iter().filter_map(|id| text_of(doc, *id).cloned()).collect();
let vertical = if live.len() >= 2 { thread_setting(before, doc, &live, |t| t.vertical) } else { None };
// The story is measured in each frame with the settings the frame takes.
let frames: Vec<TextObject> = live
.iter()
.filter_map(|id| text_of(doc, *id).cloned())
.map(|mut t| {
t.optical_margin = optical.unwrap_or(t.optical_margin);
t.vertical = vertical.unwrap_or(t.vertical);
t
})
.collect();
let refs: Vec<&TextObject> = frames.iter().collect();
let parts = vectorcraft_text::thread::distribute(vectorcraft_text::FontDb::global(), &refs, &story, &paras);
for (id, part) in live.iter().zip(parts) {
@@ -177,11 +187,13 @@ pub(crate) fn reflow(before: &Document, doc: &mut Document) {
next.runs = part.runs;
next.set_paragraph_styles(part.paras);
let optical = optical.unwrap_or(t.optical_margin);
let vertical = vertical.unwrap_or(t.vertical);
let continues = part.continues;
if next.runs != t.runs
|| next.para != t.para
|| next.paras != t.paras
|| optical != t.optical_margin
|| vertical != t.vertical
|| part.list_count != t.list_count
|| continues != t.first_para_continues
{
@@ -189,6 +201,7 @@ pub(crate) fn reflow(before: &Document, doc: &mut Document) {
t.para = next.para;
t.paras = next.paras;
t.optical_margin = optical;
t.vertical = vertical;
t.list_count = part.list_count;
t.first_para_continues = continues;
refresh_bounds(t);
@@ -235,11 +248,37 @@ pub(crate) fn restart_unthreaded_counts(doc: &mut std::sync::Arc<Document>) {
/// The Optical Margin Alignment setting that every frame of a thread with the frames `live` takes:
/// that of the first frame whose setting changed between `before` and `doc`, else the first frame's.
fn optical_margin(before: &Document, doc: &Document, live: &[NodeId]) -> Option<bool> {
let now = |id: &NodeId| text_of(doc, *id).map(|t| t.optical_margin);
let changed = live.iter().find(|id| text_of(before, **id).is_some_and(|t| Some(t.optical_margin) != now(id)));
thread_setting(before, doc, live, |t| t.optical_margin)
}
/// The value of the frame setting `get` that every frame of a thread with the frames `live` takes:
/// that of the first frame whose value changed between `before` and `doc`, or the first frame's value
/// when none changed.
fn thread_setting<T: PartialEq>(before: &Document, doc: &Document, live: &[NodeId], get: impl Fn(&TextObject) -> T) -> Option<T> {
let now = |id: &NodeId| text_of(doc, *id).map(&get);
let changed = live.iter().find(|id| text_of(before, **id).is_some_and(|t| Some(get(t)) != now(id)));
changed.or(live.first()).and_then(now)
}
/// Turn Auto Size off for the frames of each thread of two or more frames, as [`reflow`] does,
/// without re-flowing the story: for a document restored from a file as it was saved, so a later
/// change to a frame's settings keeps the frame's size.
pub(crate) fn keep_frame_sizes(doc: &mut Document) {
for thread in doc.text_threads.clone() {
let live: Vec<NodeId> = thread.into_iter().filter(|id| text_of(doc, *id).is_some()).collect();
if live.len() < 2 {
continue;
}
for id in live {
if text_of(doc, id).is_some_and(|t| t.area.fit == vectorcraft_doc::AreaFit::AutoHeight)
&& let Some(NodeKind::Text(t)) = doc.node_mut(id).map(|n| &mut n.kind)
{
t.area.fit = vectorcraft_doc::AreaFit::None;
}
}
}
}
/// Give every frame of each thread the first frame's Optical Margin Alignment setting, without
/// re-flowing the story: for a document restored from a file as it was saved.
pub(crate) fn share_optical_margin(doc: &mut Document) {
@@ -257,6 +296,25 @@ pub(crate) fn share_optical_margin(doc: &mut Document) {
}
}
/// Give every frame of each thread the first frame's writing direction, and re-flow each thread in
/// which that changes a frame: for a document restored from a file as it was saved.
pub(crate) fn share_orientation(doc: &mut Document) {
let before = doc.clone();
for thread in doc.text_threads.clone() {
let live: Vec<NodeId> = thread.into_iter().filter(|id| text_of(doc, *id).is_some()).collect();
let Some(vertical) = thread_setting(doc, doc, &live, |t| t.vertical) else { continue };
// Only the frames whose direction changes are copied, so only their threads re-flow.
let turn: Vec<NodeId> = live.into_iter().filter(|id| text_of(doc, *id).is_some_and(|t| t.vertical != vertical)).collect();
for id in turn {
if let Some(NodeKind::Text(t)) = doc.node_mut(id).map(|n| &mut n.kind) {
t.vertical = vertical;
refresh_bounds(t);
}
}
}
reflow(&before, doc);
}
/// Mark the frames of each thread whose first paragraph continues from the frame before
/// ([`TextObject::first_para_continues`]), without re-flowing the story: for a document restored
/// from a file as it was saved.
+171
View File
@@ -1090,6 +1090,177 @@ fn a_thread_saved_with_differing_frames_opens_with_the_first_frames_setting() {
}
}
/// The writing direction of each of the frames `ids` (true: vertical).
fn vertical<const N: usize>(s: &Session, ids: [u64; N]) -> [bool; N] {
ids.map(|id| {
let Some(t) = text_of(&s.doc().unwrap().doc, NodeId(id)) else { panic!("not area type") };
t.vertical
})
}
/// Whether each frame of the thread `ids` holds the part of the story that its layout, in its
/// writing direction, has room for: the parts that [`vectorcraft_text::thread::distribute`]
/// gives the frames as they are now.
fn flows_as_laid_out(s: &Session, ids: &[u64]) -> bool {
let d = &s.doc().unwrap().doc;
let frames: Vec<&TextObject> = ids.iter().map(|id| text_of(d, NodeId(*id)).unwrap()).collect();
let story = vectorcraft_text::thread::story(&frames);
let paras = vectorcraft_text::thread::story_paras(&frames, &[]);
let parts = vectorcraft_text::thread::distribute(vectorcraft_text::FontDb::global(), &frames, &story, &paras);
let texts = |runs: &[TextRun]| runs.iter().map(|r| r.text.as_str()).collect::<String>();
parts.iter().zip(&frames).all(|(part, f)| texts(&part.runs) == f.plain_text())
}
/// Type Orientation is a setting of the whole thread: Type › Type Orientation in any frame sets
/// it in every frame of the thread, in one undo step, and the story flows through the frames in
/// that direction. Typing keeps it, and frames taken out of the thread keep it.
#[test]
fn type_orientation_is_set_for_every_frame_of_a_thread() {
let (mut s, a, b) = setup();
s.execute("text.thread.create", &json!({})).unwrap();
s.execute("select.set", &json!({"ids": [a]})).unwrap();
let r = s.execute("type.orientation.vertical", &json!({})).unwrap();
assert_eq!(r, json!({"vertical": true, "ids": [a, b]}));
assert_eq!(vertical(&s, [a, b]), [true, true], "the second frame is vertical too");
assert!(flows_as_laid_out(&s, &[a, b]));
assert_eq!(format!("{}{}", text(&s, a), text(&s, b)), TEXT.repeat(4));
// Set back from the second frame, both are horizontal, and one undo makes both vertical.
s.execute("select.set", &json!({"ids": [b]})).unwrap();
assert_eq!(s.execute("type.orientation.horizontal", &json!({})).unwrap()["ids"], json!([b, a]));
assert_eq!(vertical(&s, [a, b]), [false, false]);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(vertical(&s, [a, b]), [true, true]);
// Typing in the first frame re-flows the story and keeps the direction.
s.execute("text.editRange", &json!({"id": a, "start": 0, "end": 0, "insert": "NEW "})).unwrap();
assert_eq!(vertical(&s, [a, b]), [true, true]);
assert!(flows_as_laid_out(&s, &[a, b]));
// Any edit that sets one frame sets the thread.
s.edit("Set", |d, _| {
let Some(NodeKind::Text(t)) = d.node_mut(NodeId(b)).map(|n| &mut n.kind) else { panic!("not text") };
t.vertical = false;
refresh_bounds(t);
Ok(())
})
.unwrap();
assert_eq!(vertical(&s, [a, b]), [false, false]);
assert!(flows_as_laid_out(&s, &[a, b]));
s.execute("edit.undo", &json!({})).unwrap();
// Released or unthreaded, each frame keeps it.
s.execute("select.set", &json!({"ids": [b]})).unwrap();
s.execute("text.thread.releaseSelection", &json!({})).unwrap();
assert_eq!(vertical(&s, [a, b]), [true, true]);
s.execute("edit.undo", &json!({})).unwrap();
s.execute("text.thread.remove", &json!({})).unwrap();
assert!(threads(&s).is_empty());
assert_eq!(vertical(&s, [a, b]), [true, true]);
}
/// Threading type gives the joined thread the writing direction of its first frame, and the
/// story flows through every frame in that direction.
#[test]
fn a_thread_takes_its_first_frames_type_orientation() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 600, "height": 300})).unwrap();
let area = json!({"width": 150, "height": 60});
let a = new_id(s.execute("text.create", &json!({"x": 10, "y": 10, "text": TEXT.repeat(4), "area": area, "vertical": true})).unwrap());
let b = new_id(s.execute("shape.rectangle", &json!({"x": 200, "y": 10, "width": 60, "height": 200})).unwrap());
let c = new_id(s.execute("shape.rectangle", &json!({"x": 300, "y": 10, "width": 150, "height": 200})).unwrap());
s.execute("select.set", &json!({"ids": [a, b, c]})).unwrap();
s.execute("text.thread.create", &json!({})).unwrap();
assert_eq!(vertical(&s, [a, b, c]), [true; 3]);
assert!(!text(&s, b).is_empty() && !text(&s, c).is_empty());
assert!(flows_as_laid_out(&s, &[a, b, c]), "the middle frame holds what fits it as vertical type");
// From `c`'s out port into horizontal area type `d`: `d` joins the thread and becomes vertical.
let d = new_id(s.execute("text.create", &json!({"x": 10, "y": 100, "text": "Own text.", "area": {"width": 150, "height": 190}})).unwrap());
s.execute("text.thread.add", &json!({"from": c, "to": d})).unwrap();
assert_eq!(threads(&s), [[a, b, c, d]]);
assert_eq!(vertical(&s, [a, b, c, d]), [true; 4]);
assert!(flows_as_laid_out(&s, &[a, b, c, d]));
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(vertical(&s, [d]), [false], "undo gives `d` its own direction back");
}
/// A file saved with the frames of one thread in different writing directions opens with the
/// first frame's direction in every frame, and the story flows through them in it: a native
/// file, and an EPS file restored from the document it carries.
#[test]
fn a_thread_saved_with_differing_orientations_opens_with_the_first_frames() {
let (mut s, a, b) = setup();
s.execute("text.thread.create", &json!({})).unwrap();
for (first, name) in [(true, "old.vectorcraft"), (false, "old.vectorcraft"), (true, "old.eps"), (false, "old.eps")] {
for (id, on) in [(a, first), (b, !first)] {
let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
let Some(NodeKind::Text(t)) = doc.node_mut(NodeId(id)).map(|n| &mut n.kind) else { panic!("not text") };
t.vertical = on;
refresh_bounds(t);
}
let bytes = if name.ends_with(".eps") {
let r = s.execute("document.exportEps", &json!({})).unwrap();
vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap()
} else {
vectorcraft_format::save(&s.doc().unwrap().doc, false)
};
let r = s.execute("document.open", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(&bytes)})).unwrap();
assert_eq!(r["restored"], name.ends_with(".eps"), "{r}");
assert_eq!(threads(&s), [[a, b]], "{name}");
assert_eq!(vertical(&s, [a, b]), [first, first], "{name}, first frame vertical: {first}");
assert!(flows_as_laid_out(&s, &[a, b]), "{name}, first frame vertical: {first}");
assert_eq!(format!("{}{}", text(&s, a), text(&s, b)), TEXT.repeat(4), "{name}");
}
}
/// A thread restored from an EPS file whose frames share one writing direction keeps its text
/// in its frames as saved.
#[test]
fn a_restored_thread_in_one_direction_keeps_its_text_as_saved() {
let (mut s, a, b) = setup();
s.execute("text.thread.create", &json!({})).unwrap();
let story = TEXT.repeat(4);
for (id, part) in [(a, &story[..10]), (b, &story[10..])] {
let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
let Some(NodeKind::Text(t)) = doc.node_mut(NodeId(id)).map(|n| &mut n.kind) else { panic!("not text") };
t.runs = vec![TextRun::new(part.to_string(), t.runs[0].style.clone())];
refresh_bounds(t);
}
let r = s.execute("document.exportEps", &json!({})).unwrap();
let bytes = vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap();
let r = s.execute("document.open", &json!({"name": "saved.eps", "dataBase64": vectorcraft_format::base64_encode(&bytes)})).unwrap();
assert_eq!(r["restored"], true, "{r}");
assert_eq!((text(&s, a), text(&s, b)), (story[..10].to_string(), story[10..].to_string()));
}
/// A threaded frame restored from a file with Auto Size on keeps its size when it turns from
/// vertical to horizontal: as the file opens, when the first frame is horizontal, and with Type
/// Orientation, when the whole thread is vertical.
#[test]
fn a_restored_threaded_frame_with_auto_size_keeps_its_size_when_it_turns_horizontal() {
for first_vertical in [false, true] {
let (mut s, a, b) = setup();
s.execute("text.thread.create", &json!({})).unwrap();
for (id, on) in [(a, first_vertical), (b, true)] {
let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
let Some(NodeKind::Text(t)) = doc.node_mut(NodeId(id)).map(|n| &mut n.kind) else { panic!("not text") };
t.vertical = on;
if id == b {
t.area.fit = vectorcraft_doc::AreaFit::AutoHeight;
}
refresh_bounds(t);
}
let saved = frame_box(&s, b);
let r = s.execute("document.exportEps", &json!({})).unwrap();
let bytes = vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap();
let r = s.execute("document.open", &json!({"name": "saved.eps", "dataBase64": vectorcraft_format::base64_encode(&bytes)})).unwrap();
assert_eq!(r["restored"], true, "{r}");
if first_vertical {
s.execute("select.set", &json!({"ids": [a]})).unwrap();
s.execute("type.orientation.horizontal", &json!({})).unwrap();
}
assert_eq!(vertical(&s, [a, b]), [false, false], "first frame vertical: {first_vertical}");
assert_eq!(frame_box(&s, b), saved, "first frame vertical: {first_vertical}");
assert!(flows_as_laid_out(&s, &[a, b]), "first frame vertical: {first_vertical}");
}
}
/// One count runs through a thread: the second frame's numbered paragraphs go on from the
/// first frame's, also after an edit, a save and a reopen. A pasted or duplicated copy of a
/// frame, a frame converted to point type, the frames of a thread that Remove Threading breaks
+13 -5
View File
@@ -16,15 +16,21 @@ use super::{
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!("type.orientation.vertical", "Vertical", ["Type", "Orientation"], None, "{ids?|id?} set vertical writing", has_selection, |s, p| {
orientation(s, p, true)
}),
cmd!(
"type.orientation.vertical",
"Vertical",
["Type", "Orientation"],
None,
"{ids?|id?} set vertical writing for the selected type and every frame of its threads → {vertical, ids}",
has_selection,
|s, p| orientation(s, p, true)
),
cmd!(
"type.orientation.horizontal",
"Horizontal",
["Type", "Orientation"],
None,
"{ids?|id?} set horizontal writing",
"{ids?|id?} set horizontal writing for the selected type and every frame of its threads → {vertical, ids}",
has_selection,
|s, p| orientation(s, p, false)
),
@@ -139,8 +145,10 @@ pub fn specs() -> Vec<CommandSpec> {
]
}
/// Type › Type Orientation: set vertical or horizontal writing on every selected text object and
/// every frame of their threads, in one undo step.
fn orientation(s: &mut Session, p: &Value, vertical: bool) -> Result<Value> {
let ids = text_targets(s, p, "Text Orientation")?;
let ids = super::threads::with_threads(&s.doc()?.doc, &text_targets(s, p, "Text Orientation")?);
s.edit("Text Orientation", |d, _| {
for id in &ids {
let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else {
+6
View File
@@ -481,6 +481,12 @@ for horizontal writing. This is the same option as `text.create` / `text.createI
on the OpenType `vert` feature, so a font's vertical glyph alternates are used; it does not change the engine's
CJK composition.
`type.orientation.vertical` and `type.orientation.horizontal` (Type › Type Orientation) switch existing type objects
(the selection, or `ids`) and reply `{vertical, ids}`. The writing direction is a setting of the whole thread: the
commands set every frame of a selected frame's thread, and `ids` lists them all. Threading frames gives the thread its
first frame's direction, frames taken out of a thread keep theirs, and a file whose frames of one thread differ opens
with the first frame's direction in all of them.
For SVG/SVGZ, `export`'s top-level `outlineText: true` writes text as paths; DXF takes it too (its type is outlined anyway unless `preserve: editability`).
PDF instead uses `options: {"advanced": {"outlineText": true}}`; its default is false (live text).
+1
View File
@@ -1035,6 +1035,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
New Symbol and Redefine Symbol on a frame of threaded text take it out of the thread with the text it shows, which the symbol's art holds, and the other frames keep the text they hold. When the last frame goes into the symbol, the text it has no room for goes after the text of the last frame that stays. When every frame of a thread goes into the symbol, the copy of the last frame keeps all its text.
With Optical Margin Alignment on, or Roman Hanging Punctuation on in the first paragraph, the glyphs at the ends of left-to-right type on a path hang outside its brackets, measured along the path. The type hangs at the start bracket when its line is placed there, hangs at the end bracket when the line is placed there, and hangs at both when it is centered. Glyphs that hang past the end bracket do not make the type overflow, and a hang stops at an open path's end. SVG export writes such type as outlines.
The frames of threaded text share one Optical Margin Alignment setting: turning it on or off in one frame sets it in every frame, threading frames gives the thread its first frame's setting, and frames taken out of a thread keep it.
The frames of threaded text share one writing direction: Type › Type Orientation (`type.orientation.vertical`, `type.orientation.horizontal`) in one frame sets every frame, threading frames gives the thread its first frame's direction, frames taken out of a thread keep theirs, and the story flows through every frame in the thread's direction.
Type › Optical Margin Alignment (`type.opticalMarginAlignment`) hangs the punctuation and letter edges at the ends of a type object's lines outside its margins: at the left of left-aligned lines, at the right of right-aligned lines, and at both ends of centered and justified lines, in point and area type of left-to-right paragraphs. Quotation marks, hyphens, full stops and commas hang wholly; asterisks, tildes, ellipses, en and em dashes, colons and semicolons hang by half; and A, V, W, Y, v, w and y hang by a tenth of their width. It is off by default and saved with the type object. Justification's Glyph Scaling leaves the glyphs that hang at their width, so they hang exactly their share, and Standard burasagari counts what hangs at a line's start as room before hanging a comma; `crates/text/src/tests/hang.rs` checks whole paragraphs with both composers, ragged, centred and justified, with Line-end Punctuation Half Width and bidirectional lines.
Optical Margin Alignment and Roman Hanging Punctuation hang right-to-left paragraphs of point and area type, and type on a path whose first paragraph runs right to left, too. Auto aligns a right-to-left line at its right, or at the end bracket of a path, so it hangs there. A right-to-left line is read from right to left: where it hangs at its left margin, `."` ending it hangs both glyphs.
The Paragraph panel menu's Reset Panel (`text.setStyle`, `text.setFormat` and `text.tabs.clear`) gives the selected type's paragraphs, or the paragraphs the Type tool's selection touches, Auto alignment, no indents or paragraph spacing, Hyphenate off with the default Hyphenation and Justification options, Auto direction, Bottom-to-Bottom Leading, the Line-end Punctuation Half Width Mojikumi Set, Regular burasagari, the Hard kinsoku set, the Every-line Composer, Roman Hanging Punctuation off, no kashidas and no tab stops, as one undo step.
+1
View File
@@ -21,6 +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; Type › Type Orientation sets every frame of a thread, and a thread's frames share one writing direction |
| 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)) |