diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 333890c53..26e2bfebf 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -109,7 +109,8 @@ pub use style_libs::StyleLibrary; pub use text::{ AreaFit, AreaOptions, Burasagari, CharAlign, CharPosition, CharStyle, Composer, Digits, FirstBaseline, HyphenationOptions, InlineArt, JustificationOptions, Justify, Kinsoku, LeadingModel, Mojikumi, ParaDirection, ParaStyle, PathAlign, PathEffect, RubyAlign, RubyPosition, - RubySpan, ScriptMetrics, SingleWordJustify, TabAlign, TabStop, TextKind, TextObject, TextRun, TextStyleDef, TextWrap, VerticalAlign, WrapShape, + RubySpan, ScriptMetrics, SingleWordJustify, TabAlign, TabStop, TextFlow, TextKind, TextObject, TextRun, TextStyleDef, TextWrap, VerticalAlign, + WrapShape, }; pub use trace::TraceView; pub use variables::{DataSet, DataValue, Variable, VariableKind, Variables}; diff --git a/crates/doc/src/text.rs b/crates/doc/src/text.rs index a3ab2ccc5..c1a36e058 100644 --- a/crates/doc/src/text.rs +++ b/crates/doc/src/text.rs @@ -13,7 +13,7 @@ mod para_style; mod runs; mod tabs_lists; -pub use area::{AreaFit, AreaOptions, PathAlign, PathEffect, TextWrap, VerticalAlign, WrapShape}; +pub use area::{AreaFit, AreaOptions, PathAlign, PathEffect, TextFlow, TextWrap, VerticalAlign, WrapShape}; use char_style::yes; pub use char_style::{CharAlign, CharPosition, CharStyle, Digits, ScriptMetrics}; pub use para_style::{ diff --git a/crates/doc/src/text/area.rs b/crates/doc/src/text/area.rs index b30afd412..f53a48857 100644 --- a/crates/doc/src/text/area.rs +++ b/crates/doc/src/text/area.rs @@ -29,6 +29,19 @@ impl VerticalAlign { } } +/// Area Type Options › Text Flow: the order text fills the cells of area type with several rows +/// and columns. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum TextFlow { + /// Down each column's rows, then on to the next column (the default, and what a document + /// from before the option reads as). + #[default] + Columns, + /// Across each row's columns, then on to the next row. + Rows, +} + /// Text Wrap Options of a wrap object (Object → Text Wrap). #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] #[serde(default)] @@ -135,6 +148,9 @@ pub struct AreaOptions { pub first_baseline_min: f64, /// Vertical alignment of the lines in each row/column. pub vertical_align: VerticalAlign, + /// Text Flow: by columns (the default, left out when saved) or by rows. + #[serde(skip_serializing_if = "crate::skip::is_default")] + pub flow: TextFlow, /// How the frame and its text fit each other (Auto Size, Shrink Text to Fit). #[serde(skip_serializing_if = "crate::skip::is_default")] pub fit: AreaFit, @@ -150,6 +166,7 @@ impl Default for AreaOptions { first_baseline: FirstBaseline::Ascent, first_baseline_min: 0.0, vertical_align: VerticalAlign::Top, + flow: TextFlow::Columns, fit: AreaFit::None, } } diff --git a/crates/doc/src/text/tests.rs b/crates/doc/src/text/tests.rs index 89a2591a9..bd53024b1 100644 --- a/crates/doc/src/text/tests.rs +++ b/crates/doc/src/text/tests.rs @@ -293,6 +293,19 @@ fn paragraph_styles_split_merge_and_normalize() { assert_eq!(back.para_at(3).space_before, 4.0); } +/// Text Flow is saved only when by rows: area type by columns (the default) saves as it did before +/// the option, and files from before it read by columns; other values don't read. +#[test] +fn text_flow_is_saved_only_when_by_rows() { + let by_columns = serde_json::to_value(AreaOptions::default()).unwrap(); + assert!(by_columns.get("flow").is_none()); + assert_eq!(serde_json::from_value::(by_columns).unwrap().flow, TextFlow::Columns); + let by_rows = serde_json::to_value(AreaOptions { flow: TextFlow::Rows, ..AreaOptions::default() }).unwrap(); + assert_eq!(by_rows["flow"], "rows"); + assert_eq!(serde_json::from_value::(by_rows).unwrap().flow, TextFlow::Rows); + assert!(serde_json::from_value::(serde_json::json!({"flow": "diagonal"})).is_err()); +} + /// 120 × 40 area type at (40, 40), its text drawn at twice its size. fn area_type() -> TextObject { let mut t = TextObject::point(Point::ZERO, "Some text", CharStyle::default()); diff --git a/crates/engine/src/cmd/typecmd.rs b/crates/engine/src/cmd/typecmd.rs index 97ffea356..002851b70 100644 --- a/crates/engine/src/cmd/typecmd.rs +++ b/crates/engine/src/cmd/typecmd.rs @@ -87,7 +87,7 @@ pub fn specs() -> Vec { "Area Type Options…", ["Type"], None, - "{ids?|id?, width?: pt, height?: pt, rows?, columns?, gutter?: pt, inset?: pt, firstBaseline?: ascent|capHeight|xHeight|leading|fixed, firstBaselineMin?: pt, verticalAlign?: top|center|bottom|justify, fit?: none|autoHeight|shrinkText, fitMinPercent?: 10..100, …the keys of its own reply (it can be passed back; the values it only reports are ignored)} set the selected area type's options; width and height size the type area from its top-left corner, along the type's own axes, and the text reflows at its size (with fit autoHeight, Auto Size, the frame's height follows the text after every edit and height is ignored; setting the height by hand turns it off); fit shrinkText scales overflowing text down (size, leading, baseline shift) by the largest factor down to fitMinPercent % (default 50) that makes it fit (none given: query) → the first object's options, plus overflow (the text doesn't fit its frame) and fitScale (Shrink Text's factor, 1 unshrunk)", + "{ids?|id?, width?: pt, height?: pt, rows?, columns?, gutter?: pt, inset?: pt, firstBaseline?: ascent|capHeight|xHeight|leading|fixed, firstBaselineMin?: pt, verticalAlign?: top|center|bottom|justify, flow?: columns|rows (Text Flow: down each column's rows, then across, the default; or across each row's columns, then down), fit?: none|autoHeight|shrinkText, fitMinPercent?: 10..100, …the keys of its own reply (it can be passed back; the values it only reports are ignored)} set the selected area type's options; width and height size the type area from its top-left corner, along the type's own axes, and the text reflows at its size (with fit autoHeight, Auto Size, the frame's height follows the text after every edit and height is ignored; setting the height by hand turns it off); fit shrinkText scales overflowing text down (size, leading, baseline shift) by the largest factor down to fitMinPercent % (default 50) that makes it fit (none given: query) → the first object's options, plus overflow (the text doesn't fit its frame) and fitScale (Shrink Text's factor, 1 unshrunk)", has_selection, area_options ), @@ -1006,6 +1006,7 @@ pub(crate) fn area_options_json(t: &TextObject) -> Result { let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t); if let Some(o) = v.as_object_mut() { o.insert("fit".into(), json!(t.area.fit.id())); + o.insert("flow".into(), json!(t.area.flow)); let min = match t.area.fit { AreaFit::ShrinkText { min_percent } => min_percent, _ => AreaFit::DEFAULT_MIN_PERCENT, @@ -1022,7 +1023,7 @@ pub(crate) fn area_options_json(t: &TextObject) -> Result { } /// The [`vectorcraft_doc::AreaOptions`] keys `text.areaOptions` merges as they are. -const AREA_KEYS: [&str; 7] = ["rows", "columns", "gutter", "inset", "firstBaseline", "firstBaselineMin", "verticalAlign"]; +const AREA_KEYS: [&str; 8] = ["rows", "columns", "gutter", "inset", "firstBaseline", "firstBaselineMin", "verticalAlign", "flow"]; fn area_options(s: &mut Session, p: &Value) -> Result { const C: &str = "text.areaOptions"; diff --git a/crates/engine/src/tests/typearea.rs b/crates/engine/src/tests/typearea.rs index 430074433..623eaa5a4 100644 --- a/crates/engine/src/tests/typearea.rs +++ b/crates/engine/src/tests/typearea.rs @@ -476,3 +476,35 @@ fn area_type_options_align_the_lines_vertically() { assert_eq!(text(&s, id).cached_bounds, before); assert!(cmd::find_command("text.areaOptions").unwrap().params.contains("verticalAlign?: top|center|bottom|justify")); } + +/// Area Type Options › Text Flow (#4978): `flow` fills the cells by columns (the default) or by +/// rows, is reported back, refuses other values and undoes. +#[test] +fn area_type_options_set_the_text_flow() { + use vectorcraft_doc::TextFlow; + let mut s = session(); + let id = area(&mut s, 0.0, 0.0, 420.0, 300.0, &"Words flow from cell to cell in order. ".repeat(40)); + sel(&mut s, &[id]); + let r = s.execute("text.areaOptions", &json!({"rows": 2, "columns": 2, "gutter": 20})).unwrap(); + assert_eq!(r["flow"], "columns"); + // Where the second cell sits from the first. + let second = |s: &Session| { + let f = layout(s, id).frames; + (f[1].x0 - f[0].x0, f[1].y0 - f[0].y0) + }; + assert_eq!(second(&s), (0.0, 160.0), "by columns the second cell is below the first"); + let n = undo_len(&s); + assert_eq!(s.execute("text.areaOptions", &json!({"flow": "rows"})).unwrap()["flow"], "rows"); + assert_eq!(text(&s, id).area.flow, TextFlow::Rows); + assert_eq!(second(&s), (220.0, 0.0), "by rows the second cell is beside the first"); + let l = layout(&s, id); + assert!(l.lines.iter().any(|li| li.region == 1 && li.x0 >= l.frames[1].x0 - 1e-6), "text reaches it"); + assert_eq!((undo_len(&s), last_step(&s).as_str()), (n + 1, "Area Type Options")); + for bad in [json!({"flow": "diagonal"}), json!({"flow": "Rows"}), json!({"flow": 1}), json!({"flow": null})] { + assert!(s.execute("text.areaOptions", &bad).is_err(), "{bad}"); + } + assert_eq!((undo_len(&s), text(&s, id).area.flow), (n + 1, TextFlow::Rows)); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!((text(&s, id).area.flow, second(&s)), (TextFlow::Columns, (0.0, 160.0))); + assert!(cmd::find_command("text.areaOptions").unwrap().params.contains("flow?: columns|rows")); +} diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index ae69993f7..fd879a301 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -720,7 +720,7 @@ fn shape_builder_options_junk() { assert!(failures.is_empty(), "{} failures:\n{}", failures.len(), failures.join("\n")); } -/// Area Type Options with junk fit values, on selected area type (the fixtures have none), with +/// Area Type Options with junk fit and flow values, on selected area type (the fixtures have none), with /// text that overflows: never a panic, the document stays sound, and editing still works. #[test] fn area_options_fit_junk() { @@ -741,6 +741,11 @@ fn area_options_fit_junk() { json!({"fit": "shrinkText", "columns": 100, "gutter": 0, "inset": 10000}), json!({"fitMinPercent": 50}), json!({"fit": "none", "fitMinPercent": 1e-308}), + json!({"flow": "rows", "rows": u64::MAX, "columns": u64::MAX, "gutter": 1e308}), + json!({"flow": "rows", "fit": "shrinkText", "rows": 3, "columns": 3}), + json!({"flow": "rows", "fit": "autoHeight", "columns": 4}), + json!({"flow": {"rows": 1}}), + json!({"flow": [1]}), ]; let story = "Words that overflow a small frame. ".repeat(40); let mut failures = vec![]; diff --git a/crates/text/src/layout/driver.rs b/crates/text/src/layout/driver.rs index 33d25f34f..03a5d6617 100644 --- a/crates/text/src/layout/driver.rs +++ b/crates/text/src/layout/driver.rs @@ -18,6 +18,7 @@ pub fn layout(db: &FontDb, t: &TextObject) -> TextLayout { first_baseline: a.first_baseline, first_baseline_min: a.first_baseline_min, vertical_align: a.vertical_align, + flow: a.flow, fit: a.fit, ..LayoutOptions::default() }; diff --git a/crates/text/src/layout/region.rs b/crates/text/src/layout/region.rs index e0423e1dd..e702c6eb6 100644 --- a/crates/text/src/layout/region.rs +++ b/crates/text/src/layout/region.rs @@ -142,9 +142,13 @@ impl Region { let cw = ((bbox.width() - gutter * (cols - 1) as f64) / cols as f64).max(0.0); let rh = ((bbox.height() - gutter * (rows - 1) as f64) / rows as f64).max(0.0); let mut out = Vec::with_capacity(rows * cols); - // Text flows down each column, then across (Illustrator's default "by columns"). - for c in 0..cols { - for r in 0..rows { + // Text Flow: down each column, then across (by columns, the default), or across each row, + // then down (by rows). + let by_rows = opts.flow == vectorcraft_doc::TextFlow::Rows; + let (outer, inner) = if by_rows { (rows, cols) } else { (cols, rows) }; + for o in 0..outer { + for i in 0..inner { + let (r, c) = if by_rows { (o, i) } else { (i, o) }; let x0 = bbox.x0 + c as f64 * (cw + gutter); let y0 = bbox.y0 + r as f64 * (rh + gutter); let cell = if rows * cols == 1 { bbox } else { Rect::new(x0, y0, x0 + cw, y0 + rh) }; diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 8524a5962..a382eabce 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -47,7 +47,7 @@ use kurbo::{Affine, BezPath, Point, Rect, Shape, Vec2}; pub use layout::{decorations, fit_first_line, layout, layout_with, scaled}; pub use vectorcraft_doc::TextObject; -pub use vectorcraft_doc::{AreaFit, FirstBaseline, VerticalAlign}; +pub use vectorcraft_doc::{AreaFit, FirstBaseline, TextFlow, VerticalAlign}; /// Paragraph composer; stored per text object in [`vectorcraft_doc::ParaStyle::composer`]. pub use vectorcraft_doc::Composer; @@ -56,7 +56,7 @@ pub use vectorcraft_doc::Composer; /// composer, OpenType features). [`layout`] takes rows/columns/inset/first baseline from the object. #[derive(Clone, Debug, PartialEq)] pub struct LayoutOptions { - /// Area type rows and columns (text flows down each column, then across). Layout clamps each + /// Area type rows and columns (text fills them in [`LayoutOptions::flow`] order). Layout clamps each /// count to 1..=[`vectorcraft_doc::AreaOptions::MAX_ROWS_COLUMNS`]. pub rows: usize, pub columns: usize, @@ -69,6 +69,8 @@ pub struct LayoutOptions { pub first_baseline_min: f64, /// Vertical alignment of the lines in each row/column (Area Type Options "Align"). pub vertical_align: VerticalAlign, + /// Area Type Options "Text Flow": by columns (down each column, then across) or by rows. + pub flow: TextFlow, /// Area type only: Shrink Text to Fit scales overflowing text down at layout time (see /// [`TextLayout::fit_scale`]); the other fits are the engine's business and lay out as `None`. pub fit: AreaFit, @@ -87,6 +89,7 @@ impl Default for LayoutOptions { first_baseline: FirstBaseline::Ascent, first_baseline_min: 0.0, vertical_align: VerticalAlign::Top, + flow: TextFlow::Columns, fit: AreaFit::None, composer: None, features: OtFeatures::default(), diff --git a/crates/text/src/tests/typo.rs b/crates/text/src/tests/typo.rs index 105c1b424..389fb985d 100644 --- a/crates/text/src/tests/typo.rs +++ b/crates/text/src/tests/typo.rs @@ -457,6 +457,26 @@ fn hit_testing_picks_the_right_column() { } } +/// Area Type Options › Text Flow (#4978): by columns (the default) the second cell is the one +/// below the first; by rows it is the one beside it. +#[test] +fn text_flow_fills_the_cells_by_columns_or_by_rows() { + let t = area(©.repeat(8), style(12.0), Rect::new(0.0, 0.0, 420.0, 300.0), Justify::Left); + let grid = LayoutOptions { rows: 2, columns: 2, gutter: 20.0, ..Default::default() }; + for (flow, second) in [(TextFlow::Columns, (0.0, 160.0)), (TextFlow::Rows, (220.0, 0.0))] { + let l = layout_with(db(), &t, &LayoutOptions { flow, ..grid.clone() }); + let frame = l.frames.get(1).copied().unwrap(); + assert_eq!((frame.x0, frame.y0), second, "{flow:?}"); + let line = l.lines.iter().find(|li| li.region == 1).expect("text reaches the second cell"); + assert!(line.x0 >= second.0 - 1e-6 && line.baseline >= second.1, "{flow:?}: {line:?}"); + assert!(l.lines.iter().all(|li| l.lines.iter().all(|m| m.region >= li.region || m.start <= li.start)), "{flow:?}: text runs in cell order"); + } + // The object's own option lays out the same way. + let mut by_rows = t.clone(); + (by_rows.area.rows, by_rows.area.columns, by_rows.area.gutter, by_rows.area.flow) = (2, 2, 20.0, TextFlow::Rows); + assert_eq!(layout(db(), &by_rows).frames.get(1).map(|f| (f.x0, f.y0)), Some((220.0, 0.0))); +} + // ---------- area flow ---------- #[test] diff --git a/crates/ui-egui/src/dialogs/command.rs b/crates/ui-egui/src/dialogs/command.rs index 89ef90c88..772833f4b 100644 --- a/crates/ui-egui/src/dialogs/command.rs +++ b/crates/ui-egui/src/dialogs/command.rs @@ -3,11 +3,12 @@ use std::sync::LazyLock; -use serde_json::Value; +use serde_json::{Value, json}; use super::{DialogSpec, form}; -use crate::VectorcraftApp; use crate::state::Dialog; +use crate::theme::Tokens; +use crate::{VectorcraftApp, widgets}; pub(super) const SPEC: DialogSpec = DialogSpec { heading: |d| tl!(&d.str("__label")).to_string(), @@ -15,7 +16,28 @@ pub(super) const SPEC: DialogSpec = DialogSpec { let command = d.str("__command"); let (lengths, order) = (lengths(&command), order(&command)); let rank = |k: &str| order.iter().position(|o| *o == k).unwrap_or(order.len()); + // Area Type Options › Text Flow is a pair of radio buttons after the other options. + let flow = if command == "text.areaOptions" { d.fields.remove("flow") } else { None }; form::param_fields(ui, d, &|k| lengths.contains(&k), &|k| choices(&command, k), &rank, app.session.general_unit()); + if let Some(v) = flow { + let mut rows = v.as_str() == Some("rows"); + ui.add_space(4.0); + egui::Grid::new("text-flow").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| { + let row = ui.label(egui::RichText::new(format!("{}:", tl!("Text Flow"))).color(Tokens::get(ui.ctx()).text)); + ui.label_inputs_by(row.id, |ui| { + ui.horizontal(|ui| { + if widgets::radio(ui, tl!("By Rows"), rows, true) { + rows = true; + } + if widgets::radio(ui, tl!("By Columns"), !rows, true) { + rows = false; + } + }); + }); + ui.end_row(); + }); + d.fields.insert("flow".into(), json!(if rows { "rows" } else { "columns" })); + } if previews(d) { let (cmd, params) = call(d); form::preview(app, ui, d, &d.str("__label"), &cmd, params); @@ -177,6 +199,28 @@ mod tests { assert_eq!(t.area.vertical_align, VerticalAlign::Center); } + /// Area Type Options › Text Flow (#4978): two radio buttons, By Columns as new area type has + /// it, and OK fills the cells by rows when By Rows is picked. + #[test] + fn area_type_options_dialog_sets_the_text_flow() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 400})).unwrap(); + let id = app.run("text.create", json!({"x": 20, "y": 20, "text": "Hello", "area": {"width": 200, "height": 200}})).unwrap()["id"] + .as_u64() + .unwrap(); + app.run("select.set", json!({"ids": [id]})).unwrap(); + crate::menus::invoke(&mut app, "text.areaOptions", json!({})); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| crate::dialogs::show(&mut app, ui.ctx())); + out.textures_delta.clear(); + assert_eq!(app.ui.dialog.as_ref().unwrap().str("flow"), "columns", "drawing the radio buttons keeps the value"); + app.ui.dialog.as_mut().unwrap().fields.insert("flow".into(), json!("rows")); + crate::dialogs::confirm(&mut app).unwrap(); + let NodeKind::Text(t) = &app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().kind else { panic!("text") }; + assert_eq!(t.area.flow, vectorcraft_doc::TextFlow::Rows); + } + /// Graph Type › Value Axis is a dropdown of the values `graph.setType` takes, and OK puts the /// value axis on the side picked. #[test] diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index e2b237f2a..cb018daf7 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5988,3 +5988,6 @@ breakText Glyphs Глифи @msg no object at path {shown} in the symbol's art в графиката на символа няма обект на пътя {shown} @msg give fill, stroke, opacity and/or blend задайте fill, stroke, opacity и/или blend @msg pass newName, registration, exportType, nineSlice and/or type задайте newName, registration, exportType, nineSlice и/или type + Text Flow Поток на текста + By Rows По редове + By Columns По колони diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 879566d12..aad1ce608 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -6021,3 +6021,6 @@ shape Open Otvoren @msg no object at path {shown} in the symbol's art nema objekta na putanji {shown} u grafici simbola @msg give fill, stroke, opacity and/or blend navedite fill, stroke, opacity i/ili blend @msg pass newName, registration, exportType, nineSlice and/or type navedite newName, registration, exportType, nineSlice i/ili type + Text Flow Tok teksta + By Rows Po redovima + By Columns Po kolonama diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index d0316d802..c874a4e36 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -6094,3 +6094,6 @@ breakText Glyphs Glifs @msg no object at path {shown} in the symbol's art no hi ha cap objecte al camí {shown} de l’art del símbol @msg give fill, stroke, opacity and/or blend indiqueu fill, stroke, opacity i/o blend @msg pass newName, registration, exportType, nineSlice and/or type indiqueu newName, registration, exportType, nineSlice i/o type + Text Flow Flux del text + By Rows Per files + By Columns Per columnes diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 3e441728a..1d1869553 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6104,3 +6104,6 @@ breakText Glyphs Glyfy @msg no object at path {shown} in the symbol's art v kresbě symbolu není na cestě {shown} žádný objekt @msg give fill, stroke, opacity and/or blend zadejte fill, stroke, opacity a/nebo blend @msg pass newName, registration, exportType, nineSlice and/or type zadejte newName, registration, exportType, nineSlice a/nebo type + Text Flow Tok textu + By Rows Po řádcích + By Columns Po sloupcích diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 36f923e49..399c067ad 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -6002,3 +6002,6 @@ breakText Glyphs Glyffer @msg no object at path {shown} in the symbol's art intet objekt ved stien {shown} i symbolets grafik @msg give fill, stroke, opacity and/or blend angiv fill, stroke, opacity og/eller blend @msg pass newName, registration, exportType, nineSlice and/or type angiv newName, registration, exportType, nineSlice og/eller type + Text Flow Tekstflow + By Rows Efter rækker + By Columns Efter kolonner diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 6e6dc2393..80adbe117 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5689,3 +5689,6 @@ breakText Glyphs Glyphen @msg no object at path {shown} in the symbol's art kein Objekt am Pfad {shown} in der Grafik des Symbols @msg give fill, stroke, opacity and/or blend geben Sie fill, stroke, opacity und/oder blend an @msg pass newName, registration, exportType, nineSlice and/or type geben Sie newName, registration, exportType, nineSlice und/oder type an + Text Flow Textfluss + By Rows Nach Zeilen + By Columns Nach Spalten diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 160be5842..535bc98be 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6056,3 +6056,6 @@ breakText Glyphs Γλύφοι @msg no object at path {shown} in the symbol's art δεν υπάρχει αντικείμενο στη διαδρομή {shown} στο γραφικό του συμβόλου @msg give fill, stroke, opacity and/or blend δώστε fill, stroke, opacity ή/και blend @msg pass newName, registration, exportType, nineSlice and/or type δώστε newName, registration, exportType, nineSlice ή/και type + Text Flow Ροή κειμένου + By Rows Κατά γραμμές + By Columns Κατά στήλες diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 8a682cdc9..5a56e70b0 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6046,3 +6046,6 @@ breakText Glyphs Glifos @msg no object at path {shown} in the symbol's art no hay ningún objeto en la ruta {shown} del arte del símbolo @msg give fill, stroke, opacity and/or blend indique fill, stroke, opacity y/o blend @msg pass newName, registration, exportType, nineSlice and/or type indique newName, registration, exportType, nineSlice y/o type + Text Flow Flujo de texto + By Rows Por filas + By Columns Por columnas diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 6bd598a4b..2a4564b93 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6134,3 +6134,6 @@ breakText Glyphs Merkit @msg no object at path {shown} in the symbol's art symbolin kuvituksessa ei ole objektia polussa {shown} @msg give fill, stroke, opacity and/or blend anna fill, stroke, opacity ja/tai blend @msg pass newName, registration, exportType, nineSlice and/or type anna newName, registration, exportType, nineSlice ja/tai type + Text Flow Tekstin kulku + By Rows Riveittäin + By Columns Sarakkeittain diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 9e232d218..a7a46033a 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6101,3 +6101,6 @@ breakText Glyphs Glyphes @msg no object at path {shown} in the symbol's art aucun objet au chemin {shown} dans l'illustration du symbole @msg give fill, stroke, opacity and/or blend indiquez fill, stroke, opacity et/ou blend @msg pass newName, registration, exportType, nineSlice and/or type indiquez newName, registration, exportType, nineSlice et/ou type + Text Flow Flux du texte + By Rows Par lignes + By Columns Par colonnes diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index ffe3360fc..eec246113 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6090,3 +6090,6 @@ breakText Glyphs Glifák @msg no object at path {shown} in the symbol's art a szimbólum grafikájában nincs objektum a(z) {shown} útvonalon @msg give fill, stroke, opacity and/or blend adja meg a fill, stroke, opacity és/vagy blend értékét @msg pass newName, registration, exportType, nineSlice and/or type adja meg a newName, registration, exportType, nineSlice és/vagy type értékét + Text Flow Szövegfolyás + By Rows Soronként + By Columns Oszloponként diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index d38b743a6..e5904b11c 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -6002,3 +6002,6 @@ breakText Glyphs Glif @msg no object at path {shown} in the symbol's art tidak ada objek di jalur {shown} dalam karya seni simbol @msg give fill, stroke, opacity and/or blend berikan fill, stroke, opacity dan/atau blend @msg pass newName, registration, exportType, nineSlice and/or type berikan newName, registration, exportType, nineSlice dan/atau type + Text Flow Alur teks + By Rows Per baris + By Columns Per kolom diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index e1f375654..bf53ef52d 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5723,3 +5723,6 @@ breakText Glyphs Glifi @msg no object at path {shown} in the symbol's art nessun oggetto al percorso {shown} nella grafica del simbolo @msg give fill, stroke, opacity and/or blend specifica fill, stroke, opacity e/o blend @msg pass newName, registration, exportType, nineSlice and/or type specifica newName, registration, exportType, nineSlice e/o type + Text Flow Flusso del testo + By Rows Per righe + By Columns Per colonne diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 8fdcd35c3..718561e26 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5790,3 +5790,6 @@ breakText Glyphs 字形 @msg no object at path {shown} in the symbol's art シンボルのアートのパス {shown} にオブジェクトがありません @msg give fill, stroke, opacity and/or blend fill、stroke、opacity、blend のいずれかを指定してください @msg pass newName, registration, exportType, nineSlice and/or type newName、registration、exportType、nineSlice、type のいずれかを指定してください + Text Flow テキストの流れ + By Rows 行方向 + By Columns 列方向 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index b091969fd..9091ca509 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6093,3 +6093,6 @@ breakText Glyphs გლიფები @msg no object at path {shown} in the symbol's art სიმბოლოს გრაფიკაში გზაზე {shown} ობიექტი არ არის @msg give fill, stroke, opacity and/or blend მიუთითეთ fill, stroke, opacity და/ან blend @msg pass newName, registration, exportType, nineSlice and/or type მიუთითეთ newName, registration, exportType, nineSlice და/ან type + Text Flow ტექსტის დინება + By Rows სტრიქონების მიხედვით + By Columns სვეტების მიხედვით diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index eac09b1b8..cdf3b7937 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5686,3 +5686,6 @@ breakText Glyphs 글리프 @msg no object at path {shown} in the symbol's art 심볼 아트의 경로 {shown}에 개체가 없습니다 @msg give fill, stroke, opacity and/or blend fill, stroke, opacity 및/또는 blend를 지정하세요 @msg pass newName, registration, exportType, nineSlice and/or type newName, registration, exportType, nineSlice 및/또는 type을 지정하세요 + Text Flow 텍스트 흐름 + By Rows 행 방향 + By Columns 열 방향 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 88652875a..1bd09afd9 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -6046,3 +6046,6 @@ breakText Glyphs Glyphs @msg no object at path {shown} in the symbol's art geen object op pad {shown} in de illustratie van het symbool @msg give fill, stroke, opacity and/or blend geef fill, stroke, opacity en/of blend op @msg pass newName, registration, exportType, nineSlice and/or type geef newName, registration, exportType, nineSlice en/of type op + Text Flow Tekstdoorloop + By Rows Per rij + By Columns Per kolom diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 2761b1fb3..9e6894342 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5959,3 +5959,6 @@ breakText Glyphs Glify @msg no object at path {shown} in the symbol's art brak obiektu w ścieżce {shown} w grafice symbolu @msg give fill, stroke, opacity and/or blend podaj fill, stroke, opacity i/lub blend @msg pass newName, registration, exportType, nineSlice and/or type podaj newName, registration, exportType, nineSlice i/lub type + Text Flow Przepływ tekstu + By Rows Wierszami + By Columns Kolumnami diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 33e8b45d5..54d1a792e 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -6023,3 +6023,6 @@ breakText Glyphs Glifos @msg no object at path {shown} in the symbol's art nenhum objeto no caminho {shown} na arte do símbolo @msg give fill, stroke, opacity and/or blend informe fill, stroke, opacity e/ou blend @msg pass newName, registration, exportType, nineSlice and/or type informe newName, registration, exportType, nineSlice e/ou type + Text Flow Fluxo do texto + By Rows Por linhas + By Columns Por colunas diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 8bab68b83..93b2543de 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -6081,3 +6081,6 @@ breakText Glyphs Глифы @msg no object at path {shown} in the symbol's art в графике символа нет объекта по пути {shown} @msg give fill, stroke, opacity and/or blend укажите fill, stroke, opacity и/или blend @msg pass newName, registration, exportType, nineSlice and/or type укажите newName, registration, exportType, nineSlice и/или type + Text Flow Поток текста + By Rows По строкам + By Columns По столбцам diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 572afa011..ef3a7b65c 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6156,3 +6156,6 @@ breakText Glyphs Glyfer @msg no object at path {shown} in the symbol's art inget objekt vid sökvägen {shown} i symbolens bild @msg give fill, stroke, opacity and/or blend ange fill, stroke, opacity och/eller blend @msg pass newName, registration, exportType, nineSlice and/or type ange newName, registration, exportType, nineSlice och/eller type + Text Flow Textflöde + By Rows Radvis + By Columns Kolumnvis diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index ffb90dbb9..85abd24cc 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6061,3 +6061,6 @@ breakText Glyphs Glifler @msg no object at path {shown} in the symbol's art sembolün çiziminde {shown} yolunda nesne yok @msg give fill, stroke, opacity and/or blend fill, stroke, opacity ve/veya blend verin @msg pass newName, registration, exportType, nineSlice and/or type newName, registration, exportType, nineSlice ve/veya type verin + Text Flow Metin Akışı + By Rows Satırlara Göre + By Columns Sütunlara Göre diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 543e4834c..686d8c4a1 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5703,3 +5703,6 @@ breakText Glyphs Гліфи @msg no object at path {shown} in the symbol's art у графіці символу немає об’єкта за шляхом {shown} @msg give fill, stroke, opacity and/or blend вкажіть fill, stroke, opacity та/або blend @msg pass newName, registration, exportType, nineSlice and/or type вкажіть newName, registration, exportType, nineSlice та/або type + Text Flow Потік тексту + By Rows За рядками + By Columns За стовпцями diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 78f16ce99..5b5ad327d 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5706,3 +5706,6 @@ breakText Glyphs Glyph @msg no object at path {shown} in the symbol's art không có đối tượng tại đường dẫn {shown} trong hình của ký hiệu @msg give fill, stroke, opacity and/or blend hãy cung cấp fill, stroke, opacity và/hoặc blend @msg pass newName, registration, exportType, nineSlice and/or type hãy cung cấp newName, registration, exportType, nineSlice và/hoặc type + Text Flow Luồng văn bản + By Rows Theo hàng + By Columns Theo cột diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 3249ed52f..f3246d7a4 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5946,3 +5946,6 @@ breakText Glyphs 字形 @msg no object at path {shown} in the symbol's art 符号图稿中路径 {shown} 处没有对象 @msg give fill, stroke, opacity and/or blend 请提供 fill、stroke、opacity 和/或 blend @msg pass newName, registration, exportType, nineSlice and/or type 请提供 newName、registration、exportType、nineSlice 和/或 type + Text Flow 文本流向 + By Rows 按行 + By Columns 按列 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 70c266d79..831924ac2 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5743,3 +5743,6 @@ breakText Glyphs 字符 @msg no object at path {shown} in the symbol's art 符號圖稿中路徑 {shown} 處沒有物件 @msg give fill, stroke, opacity and/or blend 請提供 fill、stroke、opacity 和/或 blend @msg pass newName, registration, exportType, nineSlice and/or type 請提供 newName、registration、exportType、nineSlice 和/或 type + Text Flow 文字流向 + By Rows 依列 + By Columns 依欄 diff --git a/docs/mcp.md b/docs/mcp.md index 16cadd2be..f40709cb3 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4593,6 +4593,17 @@ again, so SVG, PDF and EPS show the aligned text. {"name":"run_command","arguments":{"command":"text.areaOptions","params":{"verticalAlign":"center"}}} ``` +### Text flow + +`text.areaOptions {flow: columns|rows}` (Area Type Options › Text Flow) sets the order area type with several rows +and columns fills its cells: `columns` (the default) runs down each column's rows, then on to the next column; +`rows` runs across each row's columns, then on to the next row. The reply reports `flow`; other values are refused. +Native files save it only when it is `rows`, so files from before it open by columns. + +```json +{"name":"run_command","arguments":{"command":"text.areaOptions","params":{"rows":2,"columns":3,"flow":"rows"}}} +``` + ## Fitting area type: Auto Size, Shrink Text to Fit, overflow `text.areaOptions` (and `text.create`'s `area`, `text.createInPath` in area mode) take a `fit`: diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index d6fb2d9a0..e90c39ef7 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (dynamic symbols, #4938; score unchanged, hours 5–9 → 4–8) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Area Type Options › Text Flow, #4978; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -849,7 +849,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Optical kerning:** Character and Properties offer Optical beside Auto for automatic pair spacing. The side-profile spacing method is VectorCraft's own. Optical keeps its mode across cosmetic style runs, and leaves manual pair values and additive tracking intact. Native documents retain the mode and pair overrides; old documents use Auto. Behavior reference: [Line and character spacing](https://web.archive.org/web/20260916050333/https://helpx.adobe.com/illustrator/using/line-character-spacing.html) (public documentation). Tests: `crates/text/src/tests/typo.rs`, `crates/engine/src/tests/textedit.rs`. - **Kerning across style runs:** characters that differ only in fill, stroke, underline, strikethrough, overprint, character style name, leading, baseline shift, rotation, Character Alignment, kerning, or tracking that keeps or drops ligatures alike are shaped together, so restyling one character keeps the font's kerning pairs with its neighbors. A character with manual kerning that follows one with automatic kerning keeps the font's kerning with it (unless All Caps or Small Caps sets it as two letters, as ß becomes SS), and its value replaces the font's kerning after it. A ligature does not form across a change of style, and a change of font, size or OpenType features still shapes the characters apart. Behavior reference: [Line and character spacing](https://web.archive.org/web/20260916050333/https://helpx.adobe.com/illustrator/using/line-character-spacing.html) (public documentation). Tests: `crates/text/src/tests/typo.rs`, `crates/text/src/tests/bidi.rs`. - **Recent font pickers (#3515):** The shared Character, Properties, Glyphs and Find Font family picker lists recently applied families first, once each, in most-recent order and limited by Preferences › Type › Number of Recent Fonts. Search and filters apply to both sections; unavailable families are omitted. Section headings cannot be selected or previewed. Confirmed character-range font edits update the same saved history; previews and failed edits do not. Requirement: [issue #3515](https://github.com/storytold/vectorcraft/issues/3515). -- **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, 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 to the Selection tool (#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). +- **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 to the Selection tool (#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 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 rectangular area type, rows and columns included. In area type of another shape, or with text wrap, such a paragraph is broken with the Single-line Composer. The Character panel's Small Caps, Superscript and Subscript buttons and the panel menu's items of the same names turn small caps and the superscript or subscript position on and off for the selected type, or for the text the Type tool has selected (`text.setFormat` or `text.setRangeStyle` `{smallCaps, position}`), in Document Setup's proportions. Turning Superscript on turns Subscript off, and turning Subscript on turns Superscript off. A button or item shows as on when the first selected character (at a caret, the character before it) has its attribute. - **Paragraph alignment:** the shared Paragraph/Properties alignment row leaves every button unselected when the paragraphs it acts on differ. Auto resolves per paragraph; choosing an alignment uses the existing command. Headless paragraph_mixed_alignment_* regressions exercise mixed → Center → Undo, caret/uniform states, Unicode range boundaries, Auto/direction, split runs and grouped text. These regressions and full integration CI pass. Original captures confirm mixed → Center → Undo in the existing alignment-button layout. The [public Paragraph panel guide](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/paragraph-panel-overview.html) lists the seven controls, but does not specify mixed-state rendering. Other paragraph fields remain outside this change; scores and hours unchanged. @@ -1427,6 +1427,7 @@ Symbols area (#4938), from the public symbol documentation ([create and place sy | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Type core: Area Type Options › Text Flow (#4978): area type with several rows and columns fills them by columns (the default) or by rows, from the dialog's radio buttons or `text.areaOptions {flow}`; evidence updated; scores unchanged. | | 2026-11-01 | minor | Symbols: dynamic symbols (#4938): Symbol Options › Symbol Type, per-instance appearance for objects of a dynamic symbol's art that stays linked through redefinition, Break Link baking, Reset and Static dropping it; evidence added. Score unchanged at 87% (estimated), hours 5–9 → 4–8. | | 2026-11-01 | minor | Artboard background colors in output (#4937): raster exports (a transparent background too), PDF, SVG and EPS pages, `.ai` files' PDF pages, Export for Screens and print (and its preview) draw each artboard's color behind its art, inside the artboard and in the color's own model; Export for Screens has Include Artboard Fill beside Include Bleed (on by default, remembered; `includeArtboardFill`, also on `document.export` and `file.print`); editing data keeps the color as the artboard's, and Export for Screens' document rows keep it too; evidence updated; scores unchanged. | | 2026-11-01 | minor | Selection: Select › Start Global Edit on a group takes the groups like it (the same objects at the same places, moved or scaled; Appearance, Size and the artboard options apply) and gives an edit made to an object inside one to the object at its place in each of the others, in proportion, as one undo step (#4939); Global Edit of rotated copies and Shift-click exclusion remain. Score unchanged at 85% (estimated), hours unchanged. | diff --git a/docs/type-parity.md b/docs/type-parity.md index f6c4a2c7b..521d21685 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -1,6 +1,6 @@ # Type parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Digits: the native digits of the Indic scripts, #4942; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Area Type Options › Text Flow by rows or by columns, #4978; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in [target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the @@ -21,7 +21,7 @@ against Illustrator's public documentation; **estimated** unless noted. | 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 | -| Area Type Options (rows, columns, inset, first baseline, Auto Size, Shrink Text to Fit) | done | 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) | +| 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 | | Type on a Path effects and options; brackets | done | #429 | @@ -72,6 +72,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Area Type Options › Text Flow (#4978): By Rows / By Columns radio buttons and `text.areaOptions {flow}`; area type with several rows and columns fills them across each row first or down each column first (the default, so existing documents are unchanged; saved only when by rows); scores unchanged | | 2026-11-01 | minor | Digits (#4942): the native digits of Devanagari, Bengali, Gurmukhi, Gujarati, Odia, Tamil, Telugu, Kannada and Malayalam in the Character panel menu › Digits and `{digits}`; scores unchanged | | 2026-11-01 | minor | Language per character (#4946): Character panel › Language, `text.setFormat` / `text.setRangeStyle {language}`, character styles; text is shaped in its language (`locl`, #4940); new type takes the document's; Indian languages in the list (#4943); scores unchanged | | 2026-10-31 | minor | PDF import finds an installed font whose PDF name holds hyphens (`Noto-Sans-Bold` is Noto Sans, Bold: the longest part before a hyphen that names an installed family) and a composite font by its CIDFont's name rather than its base name, which ends in the CMap's (`Name-Identity-H`, [ISO 32000-1 §9.7.6.1](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf)); a missing font is split only before a style part (`Helvetica-BoldOblique`), so it is reported as `MS PGothic`, not `MS`; scores unchanged |