From 9798beef98a4506b7557ba3f16bc0cdf5d323934 Mon Sep 17 00:00:00 2001 From: Taha-Mahmoodi <85902429+Taha-Mahmoodi@users.noreply.github.com> Date: Sat, 14 Nov 2026 13:15:38 +0430 Subject: [PATCH] feat(text): Middle Eastern & South Asian and Standard composers per paragraph (#4935) Four paragraph composers: the existing Single-line and Every-line (Middle Eastern & South Asian, complex scripts shaped, the default) and Standard Single-line and Every-line, which break lines the same way and set Indic, Arabic and Hebrew type glyph by glyph as the font maps it. Paragraph panel menu, Paragraph Style Options, text.setFormat, native file and paragraph styles. .ai, EPS and PDF files carry no composer. --- crates/doc/src/text/para_style.rs | 38 +++++++- crates/engine/src/cmd/panelcmds.rs | 6 +- crates/engine/src/cmd/textstyles/tests.rs | 24 +++++ crates/engine/src/tests/panelcmds.rs | 29 ++++++ crates/format/tests/it/native_v3.rs | 38 ++++++++ crates/mcp/src/ui_catalog.rs | 2 +- crates/text/src/features.rs | 5 + crates/text/src/layout.rs | 9 +- crates/text/src/layout/flow.rs | 6 +- crates/text/src/layout/on_path.rs | 2 +- crates/text/src/shape.rs | 36 ++++++- crates/text/src/test_fonts.rs | 8 +- crates/text/src/tests.rs | 1 + crates/text/src/tests/composers.rs | 96 +++++++++++++++++++ .../ui-egui/src/dialogs/para_style_options.rs | 22 ++++- crates/ui-egui/src/i18n/bg.tsv | 7 +- crates/ui-egui/src/i18n/bs.tsv | 7 +- crates/ui-egui/src/i18n/ca.tsv | 7 +- crates/ui-egui/src/i18n/cs.tsv | 8 +- crates/ui-egui/src/i18n/da.tsv | 7 +- crates/ui-egui/src/i18n/de.tsv | 8 +- crates/ui-egui/src/i18n/el.tsv | 7 +- crates/ui-egui/src/i18n/es.tsv | 7 +- crates/ui-egui/src/i18n/fi.tsv | 7 +- crates/ui-egui/src/i18n/fr.tsv | 7 +- crates/ui-egui/src/i18n/hi.tsv | 8 +- crates/ui-egui/src/i18n/hu.tsv | 7 +- crates/ui-egui/src/i18n/id.tsv | 7 +- crates/ui-egui/src/i18n/it.tsv | 8 +- crates/ui-egui/src/i18n/ja.tsv | 7 +- crates/ui-egui/src/i18n/ka.tsv | 7 +- crates/ui-egui/src/i18n/ko.tsv | 7 +- crates/ui-egui/src/i18n/nl.tsv | 7 +- crates/ui-egui/src/i18n/pl.tsv | 8 +- crates/ui-egui/src/i18n/pt-br.tsv | 8 +- crates/ui-egui/src/i18n/ru.tsv | 7 +- crates/ui-egui/src/i18n/sv.tsv | 8 +- crates/ui-egui/src/i18n/tr.tsv | 7 +- crates/ui-egui/src/i18n/uk.tsv | 8 +- crates/ui-egui/src/i18n/vi.tsv | 7 +- crates/ui-egui/src/i18n/zh-hans.tsv | 8 +- crates/ui-egui/src/i18n/zh-hant.tsv | 8 +- crates/ui-egui/src/menus/ui_commands.rs | 2 +- crates/ui-egui/src/panels/paragraph.rs | 29 ++++-- crates/ui-egui/src/panels/paragraph/tests.rs | 63 ++++++++++++ docs/mcp.md | 22 +++-- docs/target-app-parity.md | 5 +- docs/type-parity.md | 8 +- 48 files changed, 541 insertions(+), 109 deletions(-) create mode 100644 crates/text/src/tests/composers.rs diff --git a/crates/doc/src/text/para_style.rs b/crates/doc/src/text/para_style.rs index 45cf181f18..c3db242ed0 100644 --- a/crates/doc/src/text/para_style.rs +++ b/crates/doc/src/text/para_style.rs @@ -17,22 +17,52 @@ pub enum Justify { JustifyAll, } -/// Paragraph composer (Paragraph panel menu): how lines are broken. +/// Paragraph composer (Paragraph panel menu): how lines are broken, and whether complex scripts +/// (Indic, Arabic, Hebrew) are shaped. `SingleLine` and `EveryLine` are the Middle Eastern & South +/// Asian composers: they shape complex scripts with the font's layout tables, as all type is set +/// here, and are what documents saved before the four composers existed use. The `Standard` +/// composers break lines the same way but set a complex script glyph by glyph as the font maps its +/// characters, with no conjuncts, marks placed or joining forms: what fonts that keep a script's +/// glyphs on Latin character codes (legacy ANSI Indic fonts) need. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum Composer { - /// Break each line as soon as it is full. + /// Middle Eastern & South Asian Single-line Composer: break each line as soon as it is full. SingleLine, - /// Total fit over the whole paragraph (Illustrator's default): justified lines get even word - /// spacing, ragged lines an even rag. + /// Middle Eastern & South Asian Every-line Composer: total fit over the whole paragraph + /// (Illustrator's default): justified lines get even word spacing, ragged lines an even rag. #[default] EveryLine, + /// Standard Single-line Composer: [`Composer::SingleLine`]'s breaks, complex scripts not shaped. + StandardSingleLine, + /// Standard Every-line Composer: [`Composer::EveryLine`]'s breaks, complex scripts not shaped. + StandardEveryLine, } impl Composer { fn is_default(&self) -> bool { *self == Self::default() } + + /// Does this composer break lines one at a time (as opposed to a total fit)? + pub fn is_single_line(self) -> bool { + matches!(self, Self::SingleLine | Self::StandardSingleLine) + } + + /// Does this composer shape complex scripts ([`Composer::SingleLine`], [`Composer::EveryLine`])? + pub fn shapes_complex_scripts(self) -> bool { + matches!(self, Self::SingleLine | Self::EveryLine) + } + + /// The same shaping with the other line breaking (`single_line`: one line at a time). + pub fn with_breaking(self, single_line: bool) -> Self { + match (single_line, self.shapes_complex_scripts()) { + (true, true) => Self::SingleLine, + (true, false) => Self::StandardSingleLine, + (false, true) => Self::EveryLine, + (false, false) => Self::StandardEveryLine, + } + } } /// A paragraph's hyphenation options (Paragraph panel menu › Hyphenation…): which words automatic diff --git a/crates/engine/src/cmd/panelcmds.rs b/crates/engine/src/cmd/panelcmds.rs index f9c4b10049..10310e8206 100644 --- a/crates/engine/src/cmd/panelcmds.rs +++ b/crates/engine/src/cmd/panelcmds.rs @@ -67,7 +67,7 @@ pub fn specs() -> Vec { "Character / Paragraph", [], None, - "{ids?|id?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg counter-clockwise (0: upright), underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), antiAlias?: \"none\"|\"sharp\"|\"crisp\"|\"strong\" (Character panel › Anti-aliasing Method, how the type's edges are drawn as pixels; it sets each targeted type object, with or without a range; each frame of a thread keeps its own method; graph labels keep it for every label of their kind when the graph is drawn again; new type: \"sharp\"), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt (±1296), list?: \"none\"|\"bullet\"|\"number\", bullet?: \"none\"|\"dot\"|\"dash\"|\"circle\"|\"square\", number?: \"none\"|\"arabic\"|\"upperLetters\"|\"lowerLetters\"|\"upperRoman\"|\"lowerRoman\"|\"twoDigits\"|\"threeDigits\", listAlign?: \"left\"|\"right\", listLevel?: 1..9 (Paragraph panel › Bullets and Numbered List: each paragraph of a list starts with its marker, drawn in the font, size and color of the paragraph's first character, and its text starts at the next tab stop after the marker, the left indent counting as a stop; a bulleted list's marker is its bullet, and a numbered list's is the paragraph's number and a period: 1, 2, 3; A to Z, then AA, BB; a to z, then aa, bb; I, II, III; i, ii, iii; 01 to 99, then 100; 001 to 999, then 1000; letters and roman numerals past 3999 in Arabic digits; the numbers count the story's numbered paragraphs, its threaded frames included, from 1 after a paragraph in no list, and an empty or bulleted paragraph is not counted; list \"bullet\" keeps a list's bullet, else a dot, and list \"number\" keeps a numbered list's style, else arabic; without list, a number style makes a numbered list and a bullet makes a bulleted one; \"none\" in any of the three turns the list off; a paragraph that joins a list with left and first-line indents of 0 gets 18 and -18 unless the call sets an indent; a horizontal right-to-left list paragraph has its indents mirrored, measured from the right; listAlign \"right\" (Bullets and Numbering…'s Alignment) puts the marker against the text, and \"left\" (the default) puts it at the first line's start, the text starting at the same place with either; it sets the paragraphs that are in a list or that the call puts in one, and a list keeps it when it changes between bullets and numbers; listLevel (Tab and Shift+Tab at the start of a list paragraph, Bullets and Numbering…'s Level) is a whole number from 1 to 9 that sets the paragraphs in a list or that the call puts in one, a list keeping it when it changes between bullets and numbers, and a list turned on without listLevel starting at level 1; each level past the first sets the paragraph 18 pt further in than its left indent, and each level counts its own numbers: a list paragraph with text sets the counts of the levels deeper than its own back to 0, so a deeper numbered paragraph after it counts from 1, and a paragraph back at a shallower level goes on with that level's count), hyphenate?: bool, wordsLongerThan?: letters 1..25 (5), afterFirst?, beforeLast?: letters 1..15 (3), hyphenLimit?: lines 0..25 (0: no limit), hyphenationZone?: pt 0..1296 (0; Single-line Composer only), hyphenateCapitalized?: bool (true) (Paragraph panel menu › Hyphenation…: only words longer than wordsLongerThan break, keeping afterFirst letters before the hyphen and beforeLast after it; at most hyphenLimit lines in a row end with a hyphen; a line whose text already ends within hyphenationZone of its right edge breaks before the next word instead of hyphenating it; the counts are rounded, and every value is clamped to its range), wordSpacing?: % 0..1000 (80, 100, 133), letterSpacing?: % of a word space -100..500 (0, 0, 0), glyphScaling?: % 50..200 (100, 100, 100): each [min, desired, max] or {min?, desired?, max?} (Paragraph panel menu › Justification…: Desired applies to every line, Minimum and Maximum bound what justified lines may use; values are clamped to their range, and Minimum ≤ Desired ≤ Maximum must hold afterwards or nothing changes), autoLeading?: % of the size 0..500 (120; the leading of characters whose leading is Auto), singleWordJustification?: \"fullJustify\"|\"alignLeft\"|\"alignCenter\"|\"alignRight\" (a justified line holding one word), mojikumi?: \"none\"|\"yakumonoHankaku\"|\"gyomatsuYakumonoHankaku\"|\"gyomatsuYakumonoZenkaku\"|\"yakumonoZenkaku\"|\"lineEndHalf\" (Mojikumi Set: Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), kinsokuType?: \"pushInFirst\"|\"pushOutFirst\"|\"pushOutOnly\" (Paragraph panel menu › Kinsoku Shori Type: how area type keeps those characters off a line's ends; pushInFirst sets them on the line when its gaps next to CJK characters can close up enough, by at most a tenth of an em each, pushOutFirst does so only when moving the character before them to the next line would leave the line more than an em short, and pushOutOnly, the default, moves it unless Burasagari hangs an overflowing comma or full stop; no effect with kinsoku \"none\"), bunriKinshi?: bool (Paragraph panel menu › Bunri-Kinshi: a line never breaks inside an inseparable pair, two em dashes, two horizontal ellipses, two two-dot leaders or the two halves of a vertical kana repeat mark, and justification adds no space inside one; kept but without effect with kinsoku \"none\"; off by default), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (where characters smaller than the largest on their line line up with it), proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), tateChuYoko?: bool (Character panel menu › Tate-chu-yoko, with Show East Asian Options: in vertical type those characters stand side by side across one em of the column; kept on the characters and ignored in horizontal type), tcyUpDown?, tcyLeftRight?: pt -1296..1296 (Character panel menu › Tate-chu-yoko Settings, as text.setRangeStyle: a tate-chu-yoko block moves up or down, right or left, and on a vertical path toward or away from the path's start, to its left or right), warichu?: bool, warichuLines?: 1..6, warichuGap?: pt 0..100, warichuScale?: % 10..100, warichuAlign?: \"auto\"|\"left\"|\"center\"|\"right\"|\"justifyLeft\"|\"justifyCenter\"|\"justifyRight\"|\"justifyAll\" (Character panel menu › Warichu and Warichu Settings, as text.setRangeStyle: the characters set smaller and stacked on lines inside their line), akiLeft?, akiRight?: 1/1000 em 0..1000 (Character panel › Insert Aki Left and Right, with Show East Asian Options, as text.setRangeStyle: space before and after each character in reading order), standardVerticalRoman?: bool (Character panel menu › Standard Vertical Roman Alignment, as text.setRangeStyle), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), kashidas?: bool (Character panel menu › Kashidas: selected characters allow insertion only when both joined neighbors have it on; on by default), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), fontHeightReference?: \"emBox\"|\"capHeight\"|\"xHeight\"|\"icfBox\" (Character panel › Set Font Height Reference, as text.setRangeStyle: the height of the font that the size measures), language?: name|tag (Character panel › Language, as text.setRangeStyle), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font, as text.setRangeStyle: each axis named set in its units and clamped to its range, null back to the style's own setting), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\"), romanHangingPunctuation?: bool (Paragraph panel menu › Roman Hanging Punctuation: quotation marks, hyphens, full stops and commas starting or ending a line hang outside the margin, asterisks, tildes, ellipses, dashes, colons and semicolons by half; off by default), koreanLineBreakAnywhere?: bool (Paragraph panel menu › Korean Line Break Anywhere: a line may break between any two Hangul syllables; off, a word with Hangul in it breaks only at its spaces and punctuation, or where it is wider than the line; new type: false, paragraphs saved before the option: true), kashida?: \"none\"|\"short\"|\"medium\"|\"long\"|\"stylistic\" (Paragraph panel › Insert Kashidas: justified lines of horizontal area type lengthen each Arabic word with a kashida between its last pair of joined letters, at most a quarter, half or whole em long, or whole tatweels up to an em for stylistic, before they widen their word spaces; none by default), composer?: \"singleLine\"|\"everyLine\" (line breaking, Every-line by default), start?: byte, end?: byte} (baselineShift is in document points and hScale includes the object transform; groups include their text descendants; with a range: the character attributes style that range and the paragraph attributes apply to the paragraphs it touches; without: all the text, and graph labels keep the character and paragraph attributes for every label of their kind when the graph is drawn again, the graph still setting each label's alignment; a number or switch given as another type fails the call, and a null value leaves its attribute as it is, so a call with only null values fails with nothing to change; a call that gives the values the text already has records no undo step). Or {paragraphIndents: [{id, start: byte where the paragraph starts, leftIndent?, firstLineIndent?: pt -1296..1296}…]} alone (Tabs panel indent markers over paragraphs with indents of their own): each entry sets one paragraph of a type object, at least one indent each, at most 100000 entries; every entry is checked before anything changes, and an unknown key, a value out of range, an object that is not type, a byte where no paragraph starts or two entries for one paragraph with other values fail the call; all of it is one edit, a threaded story flowing again once, and when no paragraph's indents change nothing is recorded → {ids}", + "{ids?|id?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg counter-clockwise (0: upright), underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), antiAlias?: \"none\"|\"sharp\"|\"crisp\"|\"strong\" (Character panel › Anti-aliasing Method, how the type's edges are drawn as pixels; it sets each targeted type object, with or without a range; each frame of a thread keeps its own method; graph labels keep it for every label of their kind when the graph is drawn again; new type: \"sharp\"), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt (±1296), list?: \"none\"|\"bullet\"|\"number\", bullet?: \"none\"|\"dot\"|\"dash\"|\"circle\"|\"square\", number?: \"none\"|\"arabic\"|\"upperLetters\"|\"lowerLetters\"|\"upperRoman\"|\"lowerRoman\"|\"twoDigits\"|\"threeDigits\", listAlign?: \"left\"|\"right\", listLevel?: 1..9 (Paragraph panel › Bullets and Numbered List: each paragraph of a list starts with its marker, drawn in the font, size and color of the paragraph's first character, and its text starts at the next tab stop after the marker, the left indent counting as a stop; a bulleted list's marker is its bullet, and a numbered list's is the paragraph's number and a period: 1, 2, 3; A to Z, then AA, BB; a to z, then aa, bb; I, II, III; i, ii, iii; 01 to 99, then 100; 001 to 999, then 1000; letters and roman numerals past 3999 in Arabic digits; the numbers count the story's numbered paragraphs, its threaded frames included, from 1 after a paragraph in no list, and an empty or bulleted paragraph is not counted; list \"bullet\" keeps a list's bullet, else a dot, and list \"number\" keeps a numbered list's style, else arabic; without list, a number style makes a numbered list and a bullet makes a bulleted one; \"none\" in any of the three turns the list off; a paragraph that joins a list with left and first-line indents of 0 gets 18 and -18 unless the call sets an indent; a horizontal right-to-left list paragraph has its indents mirrored, measured from the right; listAlign \"right\" (Bullets and Numbering…'s Alignment) puts the marker against the text, and \"left\" (the default) puts it at the first line's start, the text starting at the same place with either; it sets the paragraphs that are in a list or that the call puts in one, and a list keeps it when it changes between bullets and numbers; listLevel (Tab and Shift+Tab at the start of a list paragraph, Bullets and Numbering…'s Level) is a whole number from 1 to 9 that sets the paragraphs in a list or that the call puts in one, a list keeping it when it changes between bullets and numbers, and a list turned on without listLevel starting at level 1; each level past the first sets the paragraph 18 pt further in than its left indent, and each level counts its own numbers: a list paragraph with text sets the counts of the levels deeper than its own back to 0, so a deeper numbered paragraph after it counts from 1, and a paragraph back at a shallower level goes on with that level's count), hyphenate?: bool, wordsLongerThan?: letters 1..25 (5), afterFirst?, beforeLast?: letters 1..15 (3), hyphenLimit?: lines 0..25 (0: no limit), hyphenationZone?: pt 0..1296 (0; Single-line Composer only), hyphenateCapitalized?: bool (true) (Paragraph panel menu › Hyphenation…: only words longer than wordsLongerThan break, keeping afterFirst letters before the hyphen and beforeLast after it; at most hyphenLimit lines in a row end with a hyphen; a line whose text already ends within hyphenationZone of its right edge breaks before the next word instead of hyphenating it; the counts are rounded, and every value is clamped to its range), wordSpacing?: % 0..1000 (80, 100, 133), letterSpacing?: % of a word space -100..500 (0, 0, 0), glyphScaling?: % 50..200 (100, 100, 100): each [min, desired, max] or {min?, desired?, max?} (Paragraph panel menu › Justification…: Desired applies to every line, Minimum and Maximum bound what justified lines may use; values are clamped to their range, and Minimum ≤ Desired ≤ Maximum must hold afterwards or nothing changes), autoLeading?: % of the size 0..500 (120; the leading of characters whose leading is Auto), singleWordJustification?: \"fullJustify\"|\"alignLeft\"|\"alignCenter\"|\"alignRight\" (a justified line holding one word), mojikumi?: \"none\"|\"yakumonoHankaku\"|\"gyomatsuYakumonoHankaku\"|\"gyomatsuYakumonoZenkaku\"|\"yakumonoZenkaku\"|\"lineEndHalf\" (Mojikumi Set: Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), kinsokuType?: \"pushInFirst\"|\"pushOutFirst\"|\"pushOutOnly\" (Paragraph panel menu › Kinsoku Shori Type: how area type keeps those characters off a line's ends; pushInFirst sets them on the line when its gaps next to CJK characters can close up enough, by at most a tenth of an em each, pushOutFirst does so only when moving the character before them to the next line would leave the line more than an em short, and pushOutOnly, the default, moves it unless Burasagari hangs an overflowing comma or full stop; no effect with kinsoku \"none\"), bunriKinshi?: bool (Paragraph panel menu › Bunri-Kinshi: a line never breaks inside an inseparable pair, two em dashes, two horizontal ellipses, two two-dot leaders or the two halves of a vertical kana repeat mark, and justification adds no space inside one; kept but without effect with kinsoku \"none\"; off by default), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (where characters smaller than the largest on their line line up with it), proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), tateChuYoko?: bool (Character panel menu › Tate-chu-yoko, with Show East Asian Options: in vertical type those characters stand side by side across one em of the column; kept on the characters and ignored in horizontal type), tcyUpDown?, tcyLeftRight?: pt -1296..1296 (Character panel menu › Tate-chu-yoko Settings, as text.setRangeStyle: a tate-chu-yoko block moves up or down, right or left, and on a vertical path toward or away from the path's start, to its left or right), warichu?: bool, warichuLines?: 1..6, warichuGap?: pt 0..100, warichuScale?: % 10..100, warichuAlign?: \"auto\"|\"left\"|\"center\"|\"right\"|\"justifyLeft\"|\"justifyCenter\"|\"justifyRight\"|\"justifyAll\" (Character panel menu › Warichu and Warichu Settings, as text.setRangeStyle: the characters set smaller and stacked on lines inside their line), akiLeft?, akiRight?: 1/1000 em 0..1000 (Character panel › Insert Aki Left and Right, with Show East Asian Options, as text.setRangeStyle: space before and after each character in reading order), standardVerticalRoman?: bool (Character panel menu › Standard Vertical Roman Alignment, as text.setRangeStyle), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), kashidas?: bool (Character panel menu › Kashidas: selected characters allow insertion only when both joined neighbors have it on; on by default), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), fontHeightReference?: \"emBox\"|\"capHeight\"|\"xHeight\"|\"icfBox\" (Character panel › Set Font Height Reference, as text.setRangeStyle: the height of the font that the size measures), language?: name|tag (Character panel › Language, as text.setRangeStyle), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font, as text.setRangeStyle: each axis named set in its units and clamped to its range, null back to the style's own setting), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\"), romanHangingPunctuation?: bool (Paragraph panel menu › Roman Hanging Punctuation: quotation marks, hyphens, full stops and commas starting or ending a line hang outside the margin, asterisks, tildes, ellipses, dashes, colons and semicolons by half; off by default), koreanLineBreakAnywhere?: bool (Paragraph panel menu › Korean Line Break Anywhere: a line may break between any two Hangul syllables; off, a word with Hangul in it breaks only at its spaces and punctuation, or where it is wider than the line; new type: false, paragraphs saved before the option: true), kashida?: \"none\"|\"short\"|\"medium\"|\"long\"|\"stylistic\" (Paragraph panel › Insert Kashidas: justified lines of horizontal area type lengthen each Arabic word with a kashida between its last pair of joined letters, at most a quarter, half or whole em long, or whole tatweels up to an em for stylistic, before they widen their word spaces; none by default), composer?: \"singleLine\"|\"everyLine\"|\"standardSingleLine\"|\"standardEveryLine\" (line breaking, Every-line by default; singleLine and everyLine are the Middle Eastern & South Asian composers, which shape Indic, Arabic and Hebrew type; the standard ones set it glyph by glyph as the font maps its characters, for legacy fonts that keep a script on Latin character codes), start?: byte, end?: byte} (baselineShift is in document points and hScale includes the object transform; groups include their text descendants; with a range: the character attributes style that range and the paragraph attributes apply to the paragraphs it touches; without: all the text, and graph labels keep the character and paragraph attributes for every label of their kind when the graph is drawn again, the graph still setting each label's alignment; a number or switch given as another type fails the call, and a null value leaves its attribute as it is, so a call with only null values fails with nothing to change; a call that gives the values the text already has records no undo step). Or {paragraphIndents: [{id, start: byte where the paragraph starts, leftIndent?, firstLineIndent?: pt -1296..1296}…]} alone (Tabs panel indent markers over paragraphs with indents of their own): each entry sets one paragraph of a type object, at least one indent each, at most 100000 entries; every entry is checked before anything changes, and an unknown key, a value out of range, an object that is not type, a byte where no paragraph starts or two entries for one paragraph with other values fail the call; all of it is one edit, a threaded story flowing again once, and when no paragraph's indents change nothing is recorded → {ids}", has_doc, set_format ), @@ -618,7 +618,9 @@ fn set_format(s: &mut Session, p: &Value) -> Result { Some(v) => Some(match v.as_str() { Some("singleLine") => Composer::SingleLine, Some("everyLine") => Composer::EveryLine, - _ => return Err(bad(C, "composer must be \"singleLine\" or \"everyLine\"")), + Some("standardSingleLine") => Composer::StandardSingleLine, + Some("standardEveryLine") => Composer::StandardEveryLine, + _ => return Err(bad(C, "composer must be \"singleLine\", \"everyLine\", \"standardSingleLine\" or \"standardEveryLine\"")), }), }; let (position, small_caps) = super::docsetup::script_params(p, &s.doc()?.doc.setup, C)?; diff --git a/crates/engine/src/cmd/textstyles/tests.rs b/crates/engine/src/cmd/textstyles/tests.rs index 3e0727ce4a..ee13522073 100644 --- a/crates/engine/src/cmd/textstyles/tests.rs +++ b/crates/engine/src/cmd/textstyles/tests.rs @@ -1649,6 +1649,30 @@ fn paragraph_style_options_edit_applies_to_users_and_is_one_saved_undo_step() { } } +/// A paragraph style can hold a Standard composer: it applies to its paragraphs, redefining and +/// resetting treat it like any attribute, and the style and the paragraphs keep it through a save. +#[test] +fn paragraph_styles_keep_a_standard_composer_through_a_save() { + use vectorcraft_doc::Composer; + let (mut s, id) = session_with_text("One\nTwo"); + s.execute("paraStyle.new", &json!({"name": "Legacy", "attrs": {"composer": "standardSingleLine"}})).unwrap(); + s.execute("paraStyle.apply", &json!({"name": "Legacy", "id": id, "start": 0, "end": 0})).unwrap(); + assert_eq!(paras(&s, id).iter().map(|p| p.composer).collect::>(), [Composer::StandardSingleLine, Composer::EveryLine]); + let doc = s.doc().unwrap().doc.clone(); + let saved = vectorcraft_format::load(&vectorcraft_format::save(&doc, false)).unwrap(); + let style = saved.para_styles.iter().find(|d| d.name == "Legacy").unwrap(); + assert_eq!(style.attrs["composer"], json!("standardSingleLine")); + let mut composers = vec![]; + saved.walk(|n| { + if let vectorcraft_doc::NodeKind::Text(t) = &n.kind { + composers.extend(t.paragraph_styles().iter().map(|p| p.composer)); + } + }); + assert_eq!(composers, [Composer::StandardSingleLine, Composer::EveryLine]); + s.execute("paraStyle.setAttrs", &json!({"name": "Legacy", "attrs": {"composer": "standardEveryLine"}})).unwrap(); + assert_eq!(paras(&s, id)[0].composer, Composer::StandardEveryLine, "an edited style reaches its paragraphs"); +} + /// A changed paragraph attribute outside the range `text.setFormat` keeps it to is an error for /// `paraStyle.edit`, `paraStyle.setAttrs` and `paraStyle.new`, with no change to the document; the /// ends of each range are accepted, and an out-of-range value the style already has can be kept. diff --git a/crates/engine/src/tests/panelcmds.rs b/crates/engine/src/tests/panelcmds.rs index 8174cf36d1..383c1f7db1 100644 --- a/crates/engine/src/tests/panelcmds.rs +++ b/crates/engine/src/tests/panelcmds.rs @@ -1624,6 +1624,35 @@ fn text_set_format() { assert_eq!(t.para.composer, vectorcraft_doc::Composer::EveryLine); } +/// `text.setFormat {composer}` takes the two Middle Eastern & South Asian and the two Standard +/// composers, each per paragraph and one undo step; any other value is refused and names them all. +#[test] +fn text_set_format_takes_the_four_composers() { + use vectorcraft_doc::Composer; + let mut s = session(); + let r = s.execute("text.create", &json!({"x": 10, "y": 100, "text": "One\nTwo"})).unwrap(); + let id = NodeId(r["id"].as_u64().unwrap()); + let composers = |s: &Session| { + let NodeKind::Text(t) = node(s, id).kind else { panic!() }; + [t.para_at(0).composer, t.para_at(1).composer] + }; + assert_eq!(composers(&s), [Composer::EveryLine; 2], "new type is ME&SA Every-line"); + for (value, want) in [ + ("singleLine", Composer::SingleLine), + ("standardSingleLine", Composer::StandardSingleLine), + ("standardEveryLine", Composer::StandardEveryLine), + ("everyLine", Composer::EveryLine), + ] { + s.execute("text.setFormat", &json!({"ids": [id.0], "composer": value, "start": 4, "end": 4})).unwrap(); + assert_eq!(composers(&s), [Composer::EveryLine, want], "{value}: only the second paragraph"); + } + s.execute("text.setFormat", &json!({"ids": [id.0], "composer": "standardEveryLine"})).unwrap(); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(composers(&s), [Composer::EveryLine; 2], "one undo step"); + let e = s.execute("text.setFormat", &json!({"ids": [id.0], "composer": "standard"})).unwrap_err().to_string(); + assert!(e.contains("standardSingleLine") && e.contains("standardEveryLine"), "{e}"); +} + #[test] fn stroke_advanced_scale_swap_flip() { let mut s = session(); diff --git a/crates/format/tests/it/native_v3.rs b/crates/format/tests/it/native_v3.rs index 512025375e..4dbd33c196 100644 --- a/crates/format/tests/it/native_v3.rs +++ b/crates/format/tests/it/native_v3.rs @@ -212,6 +212,44 @@ fn text_composer_round_trips() { assert!(!String::from_utf8_lossy(&save(&d2, false)).contains("composer")); } +/// Every composer reads back as it was saved (the two Standard ones as their own values), and the +/// ME&SA names are the ones files have always used: `everyLine` stays the unwritten default and +/// `singleLine` keeps its spelling, so a document that never picks a Standard composer is unchanged. +#[test] +fn all_four_composers_round_trip() { + use vectorcraft_doc::{TextKind, TextObject}; + let one = |c: Composer| { + let mut d = Document::new(200.0, 200.0); + let mut t = TextObject::point(kurbo::Point::ZERO, "ક\u{ACD}ક", Default::default()); + t.para.composer = c; + t.kind = TextKind::Point; + let id = d.alloc_id(); + d.insert(None, usize::MAX, Node::new(id, NodeKind::Text(Box::new(t)))).unwrap(); + d + }; + for (c, written) in [ + (Composer::SingleLine, Some("singleLine")), + (Composer::EveryLine, None), + (Composer::StandardSingleLine, Some("standardSingleLine")), + (Composer::StandardEveryLine, Some("standardEveryLine")), + ] { + let bytes = save(&one(c), false); + let text = String::from_utf8_lossy(&bytes); + match written { + Some(w) => assert!(text.contains(&format!("\"composer\":\"{w}\"")), "{c:?}"), + None => assert!(!text.contains("\"composer\""), "{c:?}"), + } + let back = load(&bytes).unwrap(); + let mut got = None; + back.walk(|n| { + if let NodeKind::Text(t) = &n.kind { + got = Some(t.para.composer); + } + }); + assert_eq!(got, Some(c)); + } +} + /// A document as the first version wrote it (format name `drawcraft`, anchors as maps). const V1_FILE: &str = r#"{"format":"drawcraft","version":1,"generator":"DrawCraft 0.1.0","document":{"version":1,"title":"Old","artboards":[{"id":1,"name":"Artboard 1","rect":{"x0":0.0,"y0":0.0,"x1":100.0,"y1":80.0}}], "layers":[{"id":1,"name":"Layer 1","visible":true,"locked":false,"opacity":1.0,"kind":{"type":"layer","children":[ diff --git a/crates/mcp/src/ui_catalog.rs b/crates/mcp/src/ui_catalog.rs index f22b9dfd41..deb43ae968 100644 --- a/crates/mcp/src/ui_catalog.rs +++ b/crates/mcp/src/ui_catalog.rs @@ -392,7 +392,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "type.lineComposer", "Toggle Line Composer", "Cmd+Alt+Shift+C", - "{} the selected type's paragraphs (or those the Type tool's selection touches) use the Single-line Composer when they all use the Every-line Composer, else the Every-line Composer (text.setFormat {composer}) → \"singleLine\" | \"everyLine\"", + "{} the selected type's paragraphs (or those the Type tool's selection touches) use the Single-line Composer when they all use the Every-line Composer, else the Every-line Composer, each keeping its shaping: Middle Eastern & South Asian or Standard (text.setFormat {composer}) → \"singleLine\" | \"everyLine\" | \"standardSingleLine\" | \"standardEveryLine\"", ), ( "type.hyphenate", diff --git a/crates/text/src/features.rs b/crates/text/src/features.rs index 9f2b555e23..e25d225dc1 100644 --- a/crates/text/src/features.rs +++ b/crates/text/src/features.rs @@ -52,6 +52,10 @@ pub struct OtFeatures { pub extra_values: [u16; EXTRA_FEATURES.len()], /// The value `swsh=N` gives swashes ([`Self::swash`]), as [`Self::extra_values`] for `salt`. pub swash_value: u16, + /// Set a complex script (Indic, Arabic, Hebrew) glyph by glyph as the font maps its + /// characters, with none of its layout tables: the Standard composers. Set per paragraph by + /// layout from the paragraph's composer, not by a style. + pub unshaped_complex: bool, } /// The largest value a `tag=N` feature takes: a font's glyph ids are 16 bits, so is an alternate's @@ -87,6 +91,7 @@ impl Default for OtFeatures { extra: 0, extra_values: [0; EXTRA_FEATURES.len()], swash_value: 0, + unshaped_complex: false, } } } diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index a7d7d1d3f3..5ff18b4076 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -229,11 +229,14 @@ impl Ctx<'_> { out } - /// Shape paragraph `r` (its bidi resolution `bidi`). - fn shape_para(&self, r: Range, bidi: Option<&BidiInfo<'_>>) -> Vec { + /// Shape paragraph `r` (its bidi resolution `bidi`, its attributes `para`). A Standard composer + /// sets a complex script glyph by glyph ([`OtFeatures::unshaped_complex`]). + fn shape_para(&self, r: Range, bidi: Option<&BidiInfo<'_>>, para: &ParaStyle) -> Vec { let mut v = Vec::with_capacity(r.len()); let levels = bidi.map_or(&[][..], |b| &b.levels); - shape_runs(self.db, self.text, r, &self.runs, &self.inlines, &self.faces, &self.opts.features, levels, &mut v); + let standard = !self.opts.composer.unwrap_or(para.composer).shapes_complex_scripts(); + let feats = OtFeatures { unshaped_complex: standard, ..self.opts.features }; + shape_runs(self.db, self.text, r, &self.runs, &self.inlines, &self.faces, &feats, levels, &mut v); self.finish_shaping(&mut v, |g| self.style_at(g.byte)); v } diff --git a/crates/text/src/layout/flow.rs b/crates/text/src/layout/flow.rs index 1ee9a3fe50..38df882e07 100644 --- a/crates/text/src/layout/flow.rs +++ b/crates/text/src/layout/flow.rs @@ -58,7 +58,7 @@ fn compose_para( // The tab of a list marker is on the first line, with the marker, and has a fixed width. let has_tab = sg.iter().any(|g| g.ch == '\t' && g.len > 0); let has_soft_break = sg.iter().any(|g| g.ch == '\u{2028}'); - if composer != Composer::EveryLine + if composer.is_single_line() || pen.regions.is_none() || sg.len() < 2 || sg.len() > MAX_COMPOSE_GLYPHS @@ -242,7 +242,7 @@ fn prepare_paragraph<'a, 'text>( let text = cx.text; let bidi = para_bidi_with(text.get(pr.clone()).unwrap_or_default(), para.direction, &cx.char_directions(&pr)); let rtl = is_rtl(bidi.as_ref()); - let mut sg = cx.shape_para(pr.clone(), bidi.as_ref()); + let mut sg = cx.shape_para(pr.clone(), bidi.as_ref(), para); // Thai, Lao, Khmer and Myanmar break lines between words, not at spaces. mark_word_ends(text.get(pr.clone()).unwrap_or_default(), pr.start, &mut sg); // Korean breaks between words, unless Korean Line Break Anywhere lets it break between syllables. @@ -316,7 +316,7 @@ fn prepare_paragraph<'a, 'text>( let composed = compose_para(cx, &sg, &flex, &line_end, para, pen, rtl, continued); // Hyphenation options: the Hyphenation Zone applies with the Single-line Composer, and the // Hyphen Limit counts the lines in a row that end with a hyphen. - let zone = if cx.opts.composer.unwrap_or(para.composer) == Composer::SingleLine { para.hyphenation.zone } else { 0.0 }; + let zone = if cx.opts.composer.unwrap_or(para.composer).is_single_line() { para.hyphenation.zone } else { 0.0 }; let hyphen_limit = usize::try_from(para.hyphenation.hyphen_limit).unwrap_or(usize::MAX); let mirror = mirrors_indents(para, rtl, cx.vertical); ParagraphData { diff --git a/crates/text/src/layout/on_path.rs b/crates/text/src/layout/on_path.rs index d119f5d780..e0b8980358 100644 --- a/crates/text/src/layout/on_path.rs +++ b/crates/text/src/layout/on_path.rs @@ -393,7 +393,7 @@ pub(super) fn on_path(cx: &mut Ctx<'_>, paras: &[Range], t: &TextObject, let text = cx.text; let bidi = para_bidi_with(text.get(pr.clone()).unwrap_or_default(), para.direction, &cx.char_directions(pr)); rtl.get_or_insert(is_rtl(bidi.as_ref()) && !cx.vertical); - let mut glyphs = cx.shape_para(pr.clone(), bidi.as_ref()); + let mut glyphs = cx.shape_para(pr.clone(), bidi.as_ref(), t.para_at(pi)); // Bullets and numbers: a list paragraph's text starts after its marker here too, unless // the paragraph continues from the frame before in its thread. if pi > 0 || !t.first_para_continues { diff --git a/crates/text/src/shape.rs b/crates/text/src/shape.rs index ff79ce9a4c..16809291bc 100644 --- a/crates/text/src/shape.rs +++ b/crates/text/src/shape.rs @@ -122,6 +122,26 @@ pub(crate) fn joins(script: Script) -> bool { ) } +/// Is `script` one a Standard composer leaves unshaped: Indic (conjuncts, half forms, marks placed +/// by the font's layout tables), Arabic-like ([`joins`]) and Hebrew? +fn is_complex_script(script: Script) -> bool { + joins(script) + || matches!( + script, + Script::Hebrew + | Script::Devanagari + | Script::Bengali + | Script::Gurmukhi + | Script::Gujarati + | Script::Oriya + | Script::Tamil + | Script::Telugu + | Script::Kannada + | Script::Malayalam + | Script::Sinhala + ) +} + /// How `vpal` and the vertical kerning with it move an upright glyph in vertical type and change /// its length down the column, in points: `advance` down the column, `dx` and `dy` in glyph space /// (y down; `dy` is down the column once the glyph stands upright). `proportional`: `vpal` itself @@ -1374,7 +1394,12 @@ fn shape_segment(text: &str, context: &Range, seg: &Segment, feats: &OtFe let source_runs = &(source_runs.start + first..source_runs.start + last); let text_seg = &text[range.clone()]; // The segment's script: that of its first character of a script of its own. - let cursive = text_seg.chars().map(|c| c.script()).find(|s| !matches!(s, Script::Common | Script::Inherited)).is_some_and(joins); + let first_script = text_seg.chars().map(|c| c.script()).find(|s| !matches!(s, Script::Common | Script::Inherited)); + let cursive = first_script.is_some_and(joins); + // A Standard composer sets a complex script glyph by glyph (no layout tables, nominal glyphs). + // shortcut: decided for the whole segment by its first strong script, as `cursive` is; segments + // already split where the script changes. + let unshaped = feats.unshaped_complex && first_script.is_some_and(is_complex_script); let full = st.size.max(0.0); // Superscript/subscript and small capitals shrink the glyphs; line metrics keep the full size. let (_, pos_shift) = st.position.scale_shift(full); @@ -1453,7 +1478,7 @@ fn shape_segment(text: &str, context: &Range, seg: &Segment, feats: &OtFe .and_then(|l| l.parse::().ok()); let mut shaped = Shaped::default(); let Shaped { raw, proportional, vpal, chars } = &mut shaped; - let hb_shaped = face.hb().map(|hb| { + let hb_shaped = face.hb().filter(|_| !unshaped).map(|hb| { let shaper = face.shaper.shaper(&hb).instance(face.instance.as_ref()).build(); // The baseline and character-level pass use exactly the same resolved features. // Alternate positioning passes still resolve their own features, with `more` last. @@ -1558,7 +1583,8 @@ fn shape_segment(text: &str, context: &Range, seg: &Segment, feats: &OtFe } }); if hb_shaped.is_none() { - // Fallback: nominal glyphs and hmtx advances, no shaping. + // Fallback: nominal glyphs and hmtx advances, no shaping (also a complex script under a + // Standard composer). if let Some(f) = face.skrifa() { let cmap = f.charmap(); let gm = f.glyph_metrics(Size::unscaled(), face.location()); @@ -1572,6 +1598,10 @@ fn shape_segment(text: &str, context: &Range, seg: &Segment, feats: &OtFe raw.push((g.to_u32(), cl, adv.round() as i32, 0, 0)); } } + // Shaped right-to-left text comes out in visual order, which layout expects. + if unshaped && level.is_rtl() { + raw.reverse(); + } } } shaped diff --git a/crates/text/src/test_fonts.rs b/crates/text/src/test_fonts.rs index bdb818d3f3..a997617287 100644 --- a/crates/text/src/test_fonts.rs +++ b/crates/text/src/test_fonts.rs @@ -534,7 +534,8 @@ pub fn half_form_font(half: bool) -> Option> { pub const CONJUNCT_FAMILY: &str = "Conjuncttest Sans"; /// A test-only cmap over bundled Source Sans 3: Devanagari Ka uses K, the halant uses a period and -/// the vowel sign I (drawn before its syllable) uses i. The `GSUB` table's `dev2` script has an +/// the vowel sign I (drawn before its syllable) uses i; Gujarati Ka, virama and vowel sign I use the +/// same glyphs. The `GSUB` table's `dev2` and `gjr2` scripts have an /// `aalt` single substitution of K by k and a `salt` one of K by i, which list k and the vowel /// sign's glyph as alternates of Ka, and a `half` ligature of K and the period into k (Ka's half /// form). Family [`CONJUNCT_FAMILY`]. `None` when it can't be built. @@ -549,7 +550,7 @@ pub fn conjunct_font() -> Option> { const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf"); let font = skrifa::FontRef::new(BASE).ok()?; let charmap = font.charmap(); - let mapping = [('क', 'K'), ('\u{094D}', '.'), ('\u{093F}', 'i')]; + let mapping = [('क', 'K'), ('\u{094D}', '.'), ('\u{093F}', 'i'), ('ક', 'K'), ('\u{0ACD}', '.'), ('\u{0ABF}', 'i')]; let mappings = mapping.into_iter().map(|(c, source)| Some((c, write_fonts::types::GlyphId::new(charmap.map(source)?.to_u32())))); let cmap = Cmap::from_mappings(mappings.collect::>>()?).ok()?; let family = CONJUNCT_FAMILY; @@ -574,7 +575,8 @@ pub fn conjunct_font() -> Option> { SubstitutionLookup::Ligature(Lookup::new(Default::default(), vec![half])), SubstitutionLookup::Single(Lookup::new(Default::default(), vec![salt.into()])), ]); - let scripts = ScriptList::new(vec![ScriptRecord::new(Tag::new(b"dev2"), Script::new(Some(LangSys::new(vec![0, 1, 2])), vec![]))]); + let langsys = || Script::new(Some(LangSys::new(vec![0, 1, 2])), vec![]); + let scripts = ScriptList::new(vec![ScriptRecord::new(Tag::new(b"dev2"), langsys()), ScriptRecord::new(Tag::new(b"gjr2"), langsys())]); builder.add_table(&Gsub::new(scripts, features, lookups)).ok()?; for r in font.table_directory.table_records() { let tag = Tag::new(&r.tag().to_be_bytes()); diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index e7e4f33ff6..6d32831d8e 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -10,6 +10,7 @@ mod bidi; mod bitmaps; mod colr; mod combos; +mod composers; mod composite; mod devanagari; mod embed; diff --git a/crates/text/src/tests/composers.rs b/crates/text/src/tests/composers.rs new file mode 100644 index 0000000000..7b4988e822 --- /dev/null +++ b/crates/text/src/tests/composers.rs @@ -0,0 +1,96 @@ +//! The four composers: the Middle Eastern & South Asian ones shape complex scripts with the font's +//! layout tables, the Standard ones set them glyph by glyph as the font maps their characters. +//! Uses a font built in code ([`conjunct_font`]): Ka K, halant a period and vowel sign I i, in +//! Devanagari and Gujarati, with a `half` ligature of K and the period into k. +use crate::test_fonts::{CONJUNCT_FAMILY, conjunct_font, source_sans_gid}; +use crate::*; +use kurbo::Point; +use vectorcraft_doc::{CharStyle, Composer, TextObject}; + +/// Ka, halant, Ka, vowel sign I in Devanagari, then in Gujarati. +const DEVANAGARI: &str = "क\u{94D}क\u{93F}"; +const GUJARATI: &str = "ક\u{ACD}ક\u{ABF}"; + +fn db() -> FontDb { + let db = FontDb::with_font_dirs(vec![]); + db.add_font(conjunct_font().expect("the test font builds")); + db +} + +fn object(text: &str, composer: Composer) -> TextObject { + let st = CharStyle { font_family: CONJUNCT_FAMILY.into(), font_style: "Regular".into(), size: 20.0, ..CharStyle::default() }; + let mut t = TextObject::point(Point::ZERO, text, st); + t.para.composer = composer; + t +} + +/// The glyph ids drawn for `text` with `composer`, and each glyph's source cluster length. +fn drawn(db: &FontDb, text: &str, composer: Composer) -> Vec<(u32, usize)> { + layout(db, &object(text, composer)).glyphs.iter().map(|g| (g.gid, g.len)).collect() +} + +fn g(c: char) -> u32 { + u32::from(source_sans_gid(c).expect("a Source Sans glyph")) +} + +/// Under the ME&SA composers the conjunct forms (Devanagari: the vowel sign I drawn before its +/// syllable and Ka's half form; Gujarati: the half form); under the Standard ones each character +/// has its own glyph, in text order, as the font maps it. +#[test] +fn standard_composers_set_indic_text_glyph_by_glyph() { + let db = db(); + let ids = |v: Vec<(u32, usize)>| v.into_iter().map(|(gid, _)| gid).collect::>(); + for composer in [Composer::SingleLine, Composer::EveryLine] { + assert_eq!(ids(drawn(&db, DEVANAGARI, composer)), [g('i'), g('k'), g('K')], "{composer:?} Devanagari"); + assert_eq!(ids(drawn(&db, GUJARATI, composer)), [g('k'), g('K'), g('i')], "{composer:?} Gujarati"); + } + for composer in [Composer::StandardSingleLine, Composer::StandardEveryLine] { + let plain = [g('K'), g('.'), g('K'), g('i')]; + let devanagari = drawn(&db, DEVANAGARI, composer); + assert_eq!(ids(devanagari.clone()), plain, "{composer:?} Devanagari"); + assert_eq!(ids(drawn(&db, GUJARATI, composer)), plain, "{composer:?} Gujarati"); + assert!(devanagari.iter().all(|&(_, len)| len == 3), "each glyph is one character's own cluster: {devanagari:?}"); + } +} + +/// The composer is per paragraph: of two paragraphs of the same text, each is shaped by its own. +#[test] +fn each_paragraph_follows_its_own_composer() { + let db = db(); + let mut t = object(&format!("{DEVANAGARI}\n{DEVANAGARI}"), Composer::EveryLine); + let standard = vectorcraft_doc::ParaStyle { composer: Composer::StandardEveryLine, ..t.para.clone() }; + t.set_paragraph_styles(vec![t.para.clone(), standard]); + let l = layout(&db, &t); + let second = DEVANAGARI.len() + 1; + let of = |from: usize, to: usize| l.glyphs.iter().filter(|g| (from..to).contains(&g.byte) && g.len > 0).map(|g| g.gid).collect::>(); + assert_eq!(of(0, second), [g('i'), g('k'), g('K')], "ME&SA paragraph"); + assert_eq!(of(second, second + DEVANAGARI.len()), [g('K'), g('.'), g('K'), g('i')], "Standard paragraph"); +} + +/// Layout's composer override ([`LayoutOptions::composer`]) decides the shaping too. +#[test] +fn the_layout_composer_override_decides_the_shaping() { + let db = db(); + let t = object(DEVANAGARI, Composer::EveryLine); + let with = |c| layout_with(&db, &t, &LayoutOptions { composer: Some(c), ..Default::default() }).glyphs.len(); + assert_eq!((with(Composer::SingleLine), with(Composer::StandardSingleLine)), (3, 4)); +} + +/// Standard composers break lines as the ME&SA ones do (`is_single_line`), and old files' composers +/// keep their meaning. +#[test] +fn composers_split_into_breaking_and_shaping() { + use Composer::*; + for (c, single, shapes) in + [(SingleLine, true, true), (EveryLine, false, true), (StandardSingleLine, true, false), (StandardEveryLine, false, false)] + { + assert_eq!((c.is_single_line(), c.shapes_complex_scripts()), (single, shapes), "{c:?}"); + assert_eq!(c.with_breaking(single), c, "{c:?}"); + assert_eq!( + (c.with_breaking(!single).is_single_line(), c.with_breaking(!single).shapes_complex_scripts()), + (!single, shapes), + "{c:?} toggled" + ); + } + assert_eq!(Composer::default(), EveryLine, "new type and old documents are ME&SA Every-line"); +} diff --git a/crates/ui-egui/src/dialogs/para_style_options.rs b/crates/ui-egui/src/dialogs/para_style_options.rs index a78790d692..d63d43be89 100644 --- a/crates/ui-egui/src/dialogs/para_style_options.rs +++ b/crates/ui-egui/src/dialogs/para_style_options.rs @@ -361,7 +361,12 @@ pub(crate) fn edited_attrs(d: &Dialog) -> Result, String> { } let same = unchanged("composer", "__resetComposition"); - keep_or(&mut attrs, "composer", same, choice("composer").filter(|v| matches!(v.as_str(), Some("singleLine" | "everyLine")))); + keep_or( + &mut attrs, + "composer", + same, + choice("composer").filter(|v| matches!(v.as_str(), Some("singleLine" | "everyLine" | "standardSingleLine" | "standardEveryLine"))), + ); let same = unchanged("roman_hanging", "__resetComposition"); keep_or(&mut attrs, "roman_hanging", same, switch("roman_hanging")); @@ -637,7 +642,9 @@ fn choice(ui: &mut egui::Ui, d: &mut Dialog, key: &str, options: &[(&str, &str)] let shown = options.iter().find(|(value, _)| *value == selected).map_or("", |(_, label)| *label); let labels: Vec<&str> = std::iter::once(" ").chain(options.iter().map(|(_, label)| *label)).collect(); ui.add_enabled_ui(enabled, |ui| { - if let Some(i) = crate::widgets::dropdown(ui, (KIND, key), shown, &labels, 190.0) { + // shortcut: 7 pt per character approximates the widest label; measure the text when a label wraps. + let width = (labels.iter().map(|l| l.chars().count()).max().unwrap_or(0) as f32 * 7.0).max(190.0); + if let Some(i) = crate::widgets::dropdown(ui, (KIND, key), shown, &labels, width) { d.fields.insert(key.into(), if i == 0 { Value::Null } else { options.get(i - 1).map_or(Value::Null, |(value, _)| json!(value)) }); } }); @@ -842,11 +849,16 @@ pub(crate) fn clear_tabs(d: &mut Dialog) { d.fields.remove("__tabEdited"); } -/// Composition: the Single-line or Every-line Composer, and Roman Hanging Punctuation. +/// Composition: the Middle Eastern & South Asian or Standard, Single-line or Every-line Composer, and Roman Hanging Punctuation. pub(crate) fn composition_body(ui: &mut egui::Ui, d: &mut Dialog) { egui::Grid::new("paragraph-style-composition").num_columns(2).spacing([18.0, 8.0]).show(ui, |ui| { let row = ui.label(tl!("Composition")); - let composers = [("singleLine", tl!("Single-line Composer")), ("everyLine", tl!("Every-line Composer"))]; + let composers = [ + ("singleLine", tl!("Middle Eastern & South Asian Single-line Composer")), + ("everyLine", tl!("Middle Eastern & South Asian Every-line Composer")), + ("standardSingleLine", tl!("Standard Single-line Composer")), + ("standardEveryLine", tl!("Standard Every-line Composer")), + ]; ui.label_inputs_by(row.id, |ui| choice(ui, d, "composer", &composers, true)); ui.end_row(); switch(ui, d, "roman_hanging", tl!("Roman Hanging Punctuation"), true); @@ -857,7 +869,7 @@ pub(crate) fn composition_body(ui: &mut egui::Ui, d: &mut Dialog) { /// unset; Hyphenation Zone is also inactive with the Every-line Composer, which does not use it. pub(crate) fn hyphenation_body(ui: &mut egui::Ui, d: &mut Dialog) { let on = d.fields.get("hyphenate").and_then(Value::as_bool) == Some(true); - let single_line = d.fields.get("composer").and_then(Value::as_str) == Some("singleLine"); + let single_line = matches!(d.fields.get("composer").and_then(Value::as_str), Some("singleLine" | "standardSingleLine")); egui::Grid::new("paragraph-style-hyphenation").num_columns(2).spacing([18.0, 8.0]).show(ui, |ui| { switch(ui, d, "hyphenate", tl!("Hyphenation"), true); for (key, suffix) in [ diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 4b5982cecd..a84162840f 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -1046,7 +1046,8 @@ font-technology Variable Променливи Every (minutes) На всеки (минути) Every option is at its default. Всички опции са в състояние по подразбиране. Every option matches {base}. Всички опции съответстват на {base}. - Every-line Composer Композитор за всеки ред + Middle Eastern & South Asian Every-line Composer Композитор за всеки ред (Близък изток и Южна Азия) + Standard Every-line Composer Стандартен композитор за всеки ред Exact dash lengths Точни дължини на тиретата Exceptions (comma separated) Изключения (разделени със запетаи) Exclude Изключване @@ -3310,7 +3311,8 @@ digits Malayalam Малаялам Single Left Quotation Mark Лява единична кавичка Single Quotes: Единични кавички: Single Right Quotation Mark Дясна единична кавичка - Single-line Composer Ред по ред композитор + Middle Eastern & South Asian Single-line Composer Ред по ред композитор (Близък изток и Южна Азия) + Standard Single-line Composer Стандартен ред по ред композитор Toggle Line Composer Превключване на композитора на редове Sixth Space Шесто разстояние Size Размер @@ -6884,3 +6886,4 @@ breakText Glyphs Глифи Grommets Капси Grommet Inset Отстъп на капсите от ръба Diameter Диаметър +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer трябва да е "singleLine", "everyLine", "standardSingleLine" или "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 1576b72373..9f6702e229 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -1998,7 +1998,8 @@ digits Malayalam Malajalam Tabular Lining Tabelarne brojke Tabular Oldstyle Tabelarne stare brojke Titling Alternates Alternative za naslove - Every-line Composer Slagač svih redova + Middle Eastern & South Asian Every-line Composer Slagač svih redova (Bliski istok i Južna Azija) + Standard Every-line Composer Standardni slagač svih redova First-line Left Indent Lijeva uvlaka prvog reda Hyphenation… Rastavljanje riječi… Words Longer Than: Riječi duže od: @@ -2038,7 +2039,8 @@ digits Malayalam Malajalam East Asian Composition Settings Postavke istočnoazijske kompozicije Korean Composition Settings Postavke korejske kompozicije Select a text object to edit its paragraph attributes. Odaberite tekstualni objekat da uredite njegove atribute pasusa. - Single-line Composer Slagač jednog reda + Middle Eastern & South Asian Single-line Composer Slagač jednog reda (Bliski istok i Južna Azija) + Standard Single-line Composer Standardni slagač jednog reda Toggle Line Composer Uključi/isključi Line Composer Space After Paragraph Razmak nakon pasusa Space Before Paragraph Razmak prije pasusa @@ -6937,3 +6939,4 @@ shape Open Otvoren Grommets Ušice Grommet Inset Udaljenost ušica od ruba Diameter Prečnik +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer mora biti "singleLine", "everyLine", "standardSingleLine" ili "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index c098a7cb4b..cb3cbdd80a 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -1525,7 +1525,8 @@ antiAlias Sharp Nítid Every (minutes) Cada (minuts) Every option is at its default. Totes les opcions són al valor predeterminat. Every option matches {base}. Totes les opcions coincideixen amb {base}. - Every-line Composer Composició línia per línia + Middle Eastern & South Asian Every-line Composer Composició línia per línia (Orient Pròxim i Àsia Meridional) + Standard Every-line Composer Composició estàndard línia per línia Exact Exacte Exact dash lengths Longituds de guió exactes Example: {name} Exemple: {name} @@ -4326,7 +4327,8 @@ digits Malayalam Malaiàlam Single Quotes: Cometes simples: Single Right Quotation Mark Cometa simple dreta Single Word Justification: Justificació d’una sola paraula: - Single-line Composer Compositor d’una línia + Middle Eastern & South Asian Single-line Composer Compositor d’una línia (Orient Pròxim i Àsia Meridional) + Standard Single-line Composer Compositor estàndard d’una línia Sixth Space Espai de sisena part Size Mida Size Tile to Art Ajusta la mida del mosaic a l’art @@ -7476,3 +7478,4 @@ breakText Glyphs Glifs Grommets Ullets Grommet Inset Distància dels ullets a la vora Diameter Diàmetre +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer ha de ser "singleLine", "everyLine", "standardSingleLine" o "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index c83c9945dc..7dd2192eca 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2029,7 +2029,8 @@ digits Malayalam Malajálamské Tabular Lining Tabulkové verzálkové Tabular Oldstyle Tabulkové minuskové Titling Alternates Titulkové alternativy - Every-line Composer Sazba všech řádků + Middle Eastern & South Asian Every-line Composer Sazba všech řádků (Blízký východ a Jižní Asie) + Standard Every-line Composer Standardní sazba všech řádků First-line Left Indent Odsazení prvního řádku zleva Hyphenation… Dělení slov… Words Longer Than: Slova delší než: @@ -2069,7 +2070,8 @@ digits Malayalam Malajálamské East Asian Composition Settings Nastavení východoasijské sazby Korean Composition Settings Nastavení korejské sazby Select a text object to edit its paragraph attributes. Vyberte textový objekt, abyste mohli upravit atributy jeho odstavce. - Single-line Composer Sazba jednoho řádku + Middle Eastern & South Asian Single-line Composer Sazba jednoho řádku (Blízký východ a Jižní Asie) + Standard Single-line Composer Standardní sazba jednoho řádku Toggle Line Composer Přepnout sazbu řádků Space After Paragraph Mezera za odstavcem Space Before Paragraph Mezera před odstavcem @@ -8114,7 +8116,7 @@ breakText Glyphs Glyfy @msg column: true floats the whole column and takes no onto, above or below column: true uvolní celý sloupec jako plovoucí a nepřijímá onto, above ani below @msg command must be a shape.* command command musí být příkaz shape.* @msg commands[{i}]: missing `command` commands[{i}]: chybí `command` -@msg composer must be "singleLine" or "everyLine" composer musí být "singleLine" nebo "everyLine" +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer musí být "singleLine", "everyLine", "standardSingleLine" nebo "standardEveryLine" @msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes složené tvary přijímají cesty, složené cesty, skupiny, složené tvary, text, prolnutí a obálky @msg compound {_1} must contain exactly one open path to join složený objekt {_1} musí ke spojení obsahovat právě jednu otevřenou cestu @msg connect needs a path other than the one extended connect vyžaduje jinou cestu než tu prodlužovanou diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 5fad269299..35550bfebd 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -1837,7 +1837,8 @@ digits Malayalam Malayalam Tabular Lining Tabulære versalcifre Tabular Oldstyle Tabulære mediævalcifre Titling Alternates Titelalternativer - Every-line Composer Hverlinjesætter + Middle Eastern & South Asian Every-line Composer Hverlinjesætter (Mellemøsten og Sydasien) + Standard Every-line Composer Standard-hverlinjesætter First-line Left Indent Venstre indrykning af første linje Hyphenation… Orddeling… Justification… Justering… @@ -1869,7 +1870,8 @@ digits Malayalam Malayalam East Asian Composition Settings Indstillinger for østasiatisk komposition Korean Composition Settings Indstillinger for koreansk komposition Select a text object to edit its paragraph attributes. Markér et tekstobjekt for at redigere dets afsnitsattributter. - Single-line Composer Enlinjesætter + Middle Eastern & South Asian Single-line Composer Enlinjesætter (Mellemøsten og Sydasien) + Standard Single-line Composer Standard-enlinjesætter Toggle Line Composer Skift linjesætter Space After Paragraph Mellemrum efter afsnit Space Before Paragraph Mellemrum før afsnit @@ -6899,3 +6901,4 @@ breakText Glyphs Glyffer Grommets Maljer Grommet Inset Maljernes afstand fra kanten Diameter Diameter +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer skal være "singleLine", "everyLine", "standardSingleLine" eller "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index e33ed2a7a9..8fd441aea9 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1048,7 +1048,8 @@ font-technology Variable Variabel Every (minutes) Alle (Minuten) Every option is at its default. Alle Optionen haben ihren Standardwert. Every option matches {base}. Alle Optionen entsprechen {base}. - Every-line Composer Alle-Zeilen-Setzer + Middle Eastern & South Asian Every-line Composer Alle-Zeilen-Setzer (Naher Osten und Südasien) + Standard Every-line Composer Standard-Alle-Zeilen-Setzer Exact dash lengths Exakte Strichlängen Exceptions (comma separated) Ausnahmen (durch Kommas getrennt) Exclude Schnittmenge entfernen @@ -3372,7 +3373,8 @@ digits Malayalam Malayalam Single Left Quotation Mark Einfaches öffnendes Anführungszeichen Single Quotes: Einfache Anführungszeichen: Single Right Quotation Mark Einfaches schließendes Anführungszeichen - Single-line Composer Einzeilen-Setzer + Middle Eastern & South Asian Single-line Composer Einzeilen-Setzer (Naher Osten und Südasien) + Standard Single-line Composer Standard-Einzeilen-Setzer Toggle Line Composer Zeilensetzer umschalten Sixth Space Sechstelgeviert-Leerzeichen Size Größe @@ -7918,7 +7920,7 @@ breakText Glyphs Glyphen @msg column: true floats the whole column and takes no onto, above or below column: true macht die ganze Spalte schwebend und nimmt kein onto, above oder below @msg command must be a shape.* command command muss ein shape.*-Befehl sein @msg commands[{i}]: missing `command` commands[{i}]: `command` fehlt -@msg composer must be "singleLine" or "everyLine" composer muss „singleLine“ oder „everyLine“ sein +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer muss „singleLine“, „everyLine“, „standardSingleLine“ oder „standardEveryLine“ sein @msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes zusammengesetzte Formen nehmen Pfade, zusammengesetzte Pfade, Gruppen, zusammengesetzte Formen, Text, Angleichungen und Verzerrungshüllen @msg compound {_1} must contain exactly one open path to join zusammengesetzter Pfad {_1} muss genau einen offenen Pfad zum Zusammenfügen enthalten @msg connect end must be "start" or "end" connect end muss „start“ oder „end“ sein diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 9ff2eae9a2..ce41492ac8 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1013,7 +1013,8 @@ font-technology Variable Μεταβλητές Every (minutes) Κάθε (λεπτά) Every option is at its default. Όλες οι επιλογές έχουν τις προεπιλεγμένες τιμές τους. Every option matches {base}. Όλες οι επιλογές ταιριάζουν με το {base}. - Every-line Composer Σύνθεση όλων των γραμμών + Middle Eastern & South Asian Every-line Composer Σύνθεση όλων των γραμμών (Μέση Ανατολή και Νότια Ασία) + Standard Every-line Composer Τυπική σύνθεση όλων των γραμμών Exact dash lengths Ακριβή μήκη παυλών Exceptions (comma separated) Εξαιρέσεις (χωρισμένες με κόμμα) Exclude Εξαίρεση @@ -3954,7 +3955,8 @@ digits Malayalam Μαλαγιάλαμ Single Left Quotation Mark Μονό αριστερό εισαγωγικό Single Quotes: Μονά εισαγωγικά: Single Right Quotation Mark Μονό δεξί εισαγωγικό - Single-line Composer Σύνθεση μονής γραμμής + Middle Eastern & South Asian Single-line Composer Σύνθεση μονής γραμμής (Μέση Ανατολή και Νότια Ασία) + Standard Single-line Composer Τυπική σύνθεση μονής γραμμής Toggle Line Composer Εναλλαγή σύνθεσης γραμμών Sixth Space Διάστημα ενός έκτου Size Tile to Art Προσαρμογή μεγέθους πλακιδίου στο σχέδιο @@ -7448,3 +7450,4 @@ breakText Glyphs Γλύφοι Grommets Τρουκς Grommet Inset Απόσταση τρουκς από την άκρη Diameter Διάμετρος +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" το composer πρέπει να είναι "singleLine", "everyLine", "standardSingleLine" ή "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index e51ba240a8..600863255e 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1069,7 +1069,8 @@ font-technology Variable Variables Every (minutes) Cada (minutos) Every option is at its default. Todas las opciones tienen su valor predeterminado. Every option matches {base}. Todas las opciones coinciden con {base}. - Every-line Composer Composición de todas las líneas + Middle Eastern & South Asian Every-line Composer Composición de todas las líneas (Oriente Medio y Sur de Asia) + Standard Every-line Composer Composición estándar de todas las líneas Exact dash lengths Longitudes exactas de guion Exceptions (comma separated) Excepciones (separadas por comas) Exclude Excluir @@ -3403,7 +3404,8 @@ digits Malayalam Malayálam Single Left Quotation Mark Comilla simple de apertura Single Quotes: Comillas simples: Single Right Quotation Mark Comilla simple de cierre - Single-line Composer Composición de una línea + Middle Eastern & South Asian Single-line Composer Composición de una línea (Oriente Medio y Sur de Asia) + Standard Single-line Composer Composición estándar de una línea Toggle Line Composer Alternar composición de líneas Sixth Space Sexto de espacio Size Tamaño @@ -7439,3 +7441,4 @@ breakText Glyphs Glifos Grommets Ojales Grommet Inset Distancia de los ojales al borde Diameter Diámetro +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer debe ser "singleLine", "everyLine", "standardSingleLine" o "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 36f4c9f5a9..48f8771fb2 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -1990,7 +1990,8 @@ digits Malayalam Malajalam Tabular Lining Taulukkonumerot Tabular Oldstyle Vanhanaikaiset taulukkonumerot Titling Alternates Otsikkovaihtoehdot - Every-line Composer Kaikkien rivien asettelu + Middle Eastern & South Asian Every-line Composer Kaikkien rivien asettelu (Lähi-itä ja Etelä-Aasia) + Standard Every-line Composer Tavallinen kaikkien rivien asettelu First-line Left Indent Ensimmäisen rivin vasen sisennys Hyphenation… Tavutus… Words Longer Than: Sanat, jotka ovat pidempiä kuin: @@ -2030,7 +2031,8 @@ digits Malayalam Malajalam East Asian Composition Settings Itäaasialaisen ladonnan asetukset Korean Composition Settings Korean ladonnan asetukset Select a text object to edit its paragraph attributes. Valitse tekstiobjekti, jonka kappalemääritteitä haluat muokata. - Single-line Composer Yhden rivin asettelu + Middle Eastern & South Asian Single-line Composer Yhden rivin asettelu (Lähi-itä ja Etelä-Aasia) + Standard Single-line Composer Tavallinen yhden rivin asettelu Toggle Line Composer Vaihda rivien asettelu Space After Paragraph Väli kappaleen jälkeen Space Before Paragraph Väli ennen kappaletta @@ -7530,3 +7532,4 @@ breakText Glyphs Merkit Grommets Purjerenkaat Grommet Inset Purjerenkaiden etäisyys reunasta Diameter Halkaisija +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer on oltava "singleLine", "everyLine", "standardSingleLine" tai "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index a3d1c9b785..a8b4313e28 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1064,7 +1064,8 @@ font-technology Variable Variables Every (minutes) Toutes les (minutes) Every option is at its default. Toutes les options ont leur valeur par défaut. Every option matches {base}. Toutes les options correspondent à {base}. - Every-line Composer Compositeur multiligne + Middle Eastern & South Asian Every-line Composer Compositeur multiligne (Moyen-Orient et Asie du Sud) + Standard Every-line Composer Compositeur standard multiligne Exact dash lengths Longueurs de tirets exactes Exceptions (comma separated) Exceptions (séparées par des virgules) Exclude Exclusion @@ -3395,7 +3396,8 @@ digits Malayalam Malayalam Single Left Quotation Mark Guillemet simple ouvrant Single Quotes: Guillemets simples : Single Right Quotation Mark Guillemet simple fermant - Single-line Composer Compositeur ligne par ligne + Middle Eastern & South Asian Single-line Composer Compositeur ligne par ligne (Moyen-Orient et Asie du Sud) + Standard Single-line Composer Compositeur standard ligne par ligne Toggle Line Composer Basculer le compositeur de lignes Sixth Space Espace d’un sixième Size Taille @@ -7490,3 +7492,4 @@ breakText Glyphs Glyphes Grommets Œillets Grommet Inset Distance des œillets au bord Diameter Diamètre +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer doit être "singleLine", "everyLine", "standardSingleLine" ou "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index dff4c3503c..62b9be659b 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -1699,7 +1699,8 @@ antiAlias Sharp शार्प Every (minutes) हर (मिनट) Every option is at its default. प्रत्येक ऑप्शन इसके डिफ़ॉल्ट रूप से होता है। Every option matches {base}. प्रत्येक ऑप्शन {base} से मेल खाता है। - Every-line Composer हर लाइन संगीतकार + Middle Eastern & South Asian Every-line Composer हर लाइन संगीतकार (मध्य पूर्व और दक्षिण एशिया) + Standard Every-line Composer मानक हर लाइन संगीतकार Exact सटीक Exact dash lengths सटीक डैश लंबाई Example: {name} उदाहरण: {name} @@ -4673,7 +4674,8 @@ antiAlias Sharp शार्प Single Quotes: एकल उद्धरण: Single Right Quotation Mark सिंगल राइट कोटेशन मार्क Single Word Justification: एकल शब्द जस्टिफ़िकेशन: - Single-line Composer एकल लाइन संगीतकार + Middle Eastern & South Asian Single-line Composer एकल लाइन संगीतकार (मध्य पूर्व और दक्षिण एशिया) + Standard Single-line Composer मानक एकल लाइन संगीतकार Sixth Space छठे हिस्से जितना स्पेस Size आकार Size Tile to Art आकार टाइल कला के लिए @@ -6567,7 +6569,7 @@ antiAlias Sharp शार्प @msg command `{id}` is not available right now आदेश `{id}` अभी उपलब्ध नहीं है @msg command must be a shape.* command command कोई shape.* कमांड होना चाहिए @msg commands[{i}]: missing `command` आदेश [{i}]: लापता `command` -@msg composer must be "singleLine" or "everyLine" composer "singleLine" या "everyLine" होना चाहिए +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer "singleLine", "everyLine", "standardSingleLine" या "standardEveryLine" होना चाहिए @msg compound {_1} must contain exactly one open path to join जोड़ने के लिए कंपाउंड {_1} में ठीक एक खुला पाथ होना चाहिए @msg compound paths can only contain paths यौगिक पाथ में केवल पाथ हो सकते हैं @msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes कंपाउंड शेप पाथ, कंपाउंड पाथ, समूह, कंपाउंड शेप, टाइप, ब्लेंड और एन्वेलप लेते हैं diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 8549bd5d63..22842926eb 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -1356,7 +1356,8 @@ font-technology Variable Változó Every (minutes) Gyakoriság (perc) Every option is at its default. Minden beállítás az alapértéken áll. Every option matches {base}. Minden beállítás egyezik: {base}. - Every-line Composer Többsoros tördelő + Middle Eastern & South Asian Every-line Composer Többsoros tördelő (Közel-Kelet és Dél-Ázsia) + Standard Every-line Composer Normál többsoros tördelő Exact Pontos Exact dash lengths Pontos vonalhosszak Exceptions (comma separated) Kivételek (vesszővel elválasztva) @@ -4052,7 +4053,8 @@ mark-style Japanese Japán Single Left Quotation Mark Bal oldali szimpla idézőjel Single Quotes: Szimpla idézőjelek: Single Right Quotation Mark Jobb oldali szimpla idézőjel - Single-line Composer Egysoros tördelő + Middle Eastern & South Asian Single-line Composer Egysoros tördelő (Közel-Kelet és Dél-Ázsia) + Standard Single-line Composer Normál egysoros tördelő Toggle Line Composer Sortördelő váltása Sixth Space Hatodkvirt-szóköz Size Méret @@ -7479,3 +7481,4 @@ breakText Glyphs Glifák Grommets Ringlik Grommet Inset Ringlik távolsága a széltől Diameter Átmérő +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer értéke "singleLine", "everyLine", "standardSingleLine" vagy "standardEveryLine" lehet diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index e27253376e..1ca61352cf 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -1837,7 +1837,8 @@ digits Malayalam Malayalam Tabular Lining Lining tabular Tabular Oldstyle Oldstyle tabular Titling Alternates Alternatif judul - Every-line Composer Komposer tiap baris + Middle Eastern & South Asian Every-line Composer Komposer tiap baris (Timur Tengah dan Asia Selatan) + Standard Every-line Composer Komposer standar tiap baris First-line Left Indent Indentasi kiri baris pertama Hyphenation… Pemenggalan kata… Justification… Justifikasi… @@ -1869,7 +1870,8 @@ digits Malayalam Malayalam East Asian Composition Settings Pengaturan komposisi Asia Timur Korean Composition Settings Pengaturan komposisi Korea Select a text object to edit its paragraph attributes. Pilih objek teks untuk mengedit atribut paragrafnya. - Single-line Composer Komposer satu baris + Middle Eastern & South Asian Single-line Composer Komposer satu baris (Timur Tengah dan Asia Selatan) + Standard Single-line Composer Komposer standar satu baris Toggle Line Composer Alihkan komposer baris Space After Paragraph Spasi setelah paragraf Space Before Paragraph Spasi sebelum paragraf @@ -6899,3 +6901,4 @@ breakText Glyphs Glif Grommets Mata ayam Grommet Inset Jarak mata ayam dari tepi Diameter Diameter +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer harus "singleLine", "everyLine", "standardSingleLine", atau "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 2381051c9b..b8b2cf14b4 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1078,7 +1078,8 @@ font-technology Variable Variabili Every (minutes) Ogni (minuti) Every option is at its default. Tutte le opzioni hanno il valore predefinito. Every option matches {base}. Tutte le opzioni corrispondono a {base}. - Every-line Composer Composizione su tutte le righe + Middle Eastern & South Asian Every-line Composer Composizione su tutte le righe (Medio Oriente e Asia meridionale) + Standard Every-line Composer Composizione standard su tutte le righe Exact dash lengths Lunghezze esatte dei trattini Exceptions (comma separated) Eccezioni (separate da virgole) Exclude Escludi @@ -3448,7 +3449,8 @@ digits Malayalam Malayalam Single Left Quotation Mark Virgoletta singola di apertura Single Quotes: Virgolette singole: Single Right Quotation Mark Virgoletta singola di chiusura - Single-line Composer Composizione su riga singola + Middle Eastern & South Asian Single-line Composer Composizione su riga singola (Medio Oriente e Asia meridionale) + Standard Single-line Composer Composizione standard su riga singola Toggle Line Composer Alterna composizione delle righe Sixth Space Spazio di un sesto Size Dimensione @@ -7966,7 +7968,7 @@ breakText Glyphs Glifi @msg column: true floats the whole column and takes no onto, above or below column: true rende mobile l'intera colonna e non accetta onto, above o below @msg command must be a shape.* command command deve essere un comando shape.* @msg commands[{i}]: missing `command` commands[{i}]: `command` mancante -@msg composer must be "singleLine" or "everyLine" composer deve essere "singleLine" o "everyLine" +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer deve essere "singleLine", "everyLine", "standardSingleLine" o "standardEveryLine" @msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes le forme composte accettano tracciati, tracciati composti, gruppi, forme composte, testo, fusioni e involucri @msg compound {_1} must contain exactly one open path to join il composto {_1} deve contenere esattamente un tracciato aperto da unire @msg connect end must be "start" or "end" end di connect deve essere "start" o "end" diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 1458bf2aa1..e326aaae1f 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1666,7 +1666,8 @@ antiAlias Sharp シャープ Every (minutes) 間隔 (分) Every option is at its default. すべてのオプションが初期値です。 Every option matches {base}. すべてのオプションが {base} と同じです。 - Every-line Composer すべての行を一括処理 + Middle Eastern & South Asian Every-line Composer すべての行を一括処理 (中東・南アジア) + Standard Every-line Composer すべての行を一括処理 (標準) Exact dash lengths 破線の長さを正確に保つ Exceptions (comma separated) 例外 (カンマ区切り) Exclusion 除外 @@ -3256,7 +3257,8 @@ mark-style Japanese 日本式 Simulate Colored Paper 色紙をシミュレート Simulate Paper 紙色をシミュレート Single Quotes: 一重引用符: - Single-line Composer 1 行ずつ組む + Middle Eastern & South Asian Single-line Composer 1 行ずつ組む (中東・南アジア) + Standard Single-line Composer 1 行ずつ組む (標準) Toggle Line Composer 行の組版方式を切り替え Size Tile to Art タイルをアートワークのサイズに合わせる Size/Leading サイズ/行送り @@ -7191,3 +7193,4 @@ breakText Glyphs 字形 Grommets ハトメ Grommet Inset 端からハトメまでの距離 Diameter 直径 +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer は "singleLine"、"everyLine"、"standardSingleLine"、"standardEveryLine" のいずれかにしてください diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 9105fe50e7..e339971692 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -1766,7 +1766,8 @@ digits Malayalam მალაიალამური Tabular Lining ცხრილური ხაზოვანი Tabular Oldstyle ცხრილური ძველებური Titling Alternates სათაურის ალტერნატივები - Every-line Composer აბზაცის მიხედვით აწყობა + Middle Eastern & South Asian Every-line Composer აბზაცის მიხედვით აწყობა (შუა აღმოსავლეთი და სამხრეთ აზია) + Standard Every-line Composer სტანდარტული აბზაცის მიხედვით აწყობა First-line Left Indent პირველი სტრიქონის მარცხენა შეწევა Hyphenation… სიტყვების გადატანა… Justification… სწორება… @@ -1798,7 +1799,8 @@ digits Malayalam მალაიალამური East Asian Composition Settings აღმოსავლეთ აზიური აწყობის პარამეტრები Korean Composition Settings კორეული აწყობის პარამეტრები Select a text object to edit its paragraph attributes. აბზაცის ატრიბუტების რედაქტირებისთვის მონიშნეთ ტექსტური ობიექტი. - Single-line Composer სტრიქონის მიხედვით აწყობა + Middle Eastern & South Asian Single-line Composer სტრიქონის მიხედვით აწყობა (შუა აღმოსავლეთი და სამხრეთ აზია) + Standard Single-line Composer სტანდარტული სტრიქონის მიხედვით აწყობა Toggle Line Composer აწყობის რეჟიმის გადართვა Space After Paragraph ინტერვალი აბზაცის შემდეგ Space Before Paragraph ინტერვალი აბზაცის წინ @@ -7609,3 +7611,4 @@ font-script Japanese იაპონური Grommets ლუვერები Grommet Inset ლუვერების დაშორება კიდიდან Diameter დიამეტრი +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer უნდა იყოს "singleLine", "everyLine", "standardSingleLine" ან "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index fd75f983ce..d606239f79 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1656,7 +1656,8 @@ antiAlias Sharp 날카롭게 Every (minutes) 간격 (분) Every option is at its default. 모든 옵션이 기본값입니다. Every option matches {base}. 모든 옵션이 {base}와(과) 같습니다. - Every-line Composer 전체 줄 컴포저 + Middle Eastern & South Asian Every-line Composer 전체 줄 컴포저 (중동·남아시아) + Standard Every-line Composer 표준 전체 줄 컴포저 Exact dash lengths 정확한 대시 길이 Exceptions (comma separated) 예외 (쉼표로 구분) Exclusion 제외 @@ -3245,7 +3246,8 @@ mark-style Japanese 일본식 Simulate Colored Paper 색 용지 시뮬레이션 Simulate Paper 용지 시뮬레이션 Single Quotes: 작은따옴표: - Single-line Composer 한 줄 조판 + Middle Eastern & South Asian Single-line Composer 한 줄 조판 (중동·남아시아) + Standard Single-line Composer 표준 한 줄 조판 Toggle Line Composer 줄 조판 방식 전환 Size Tile to Art 타일을 아트웍 크기에 맞춤 Size/Leading 크기/행간 @@ -7086,3 +7088,4 @@ breakText Glyphs 글리프 Grommets 아일렛 Grommet Inset 가장자리에서 아일렛까지 거리 Diameter 지름 +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer는 "singleLine", "everyLine", "standardSingleLine" 또는 "standardEveryLine"이어야 합니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index cd3aa43966..8170f18e3f 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -2354,7 +2354,8 @@ digits Malayalam Malayalam Tabular Lining Tabulaire lining Tabular Oldstyle Tabulaire oldstyle Titling Alternates Titelalternatieven - Every-line Composer Compositie per alinea + Middle Eastern & South Asian Every-line Composer Compositie per alinea (Midden-Oosten en Zuid-Azië) + Standard Every-line Composer Standaardcompositie per alinea First-line Left Indent Eerste regel links inspringen Hyphenation… Afbreken… Justification… Uitvullen… @@ -2386,7 +2387,8 @@ digits Malayalam Malayalam East Asian Composition Settings Instellingen voor Oost-Aziatische zetting Korean Composition Settings Instellingen voor Koreaanse zetting Select a text object to edit its paragraph attributes. Selecteer een tekstobject om de alineakenmerken ervan te bewerken. - Single-line Composer Compositie per regel + Middle Eastern & South Asian Single-line Composer Compositie per regel (Midden-Oosten en Zuid-Azië) + Standard Single-line Composer Standaardcompositie per regel Toggle Line Composer Compositiemethode wisselen Space After Paragraph Ruimte na alinea Space Before Paragraph Ruimte voor alinea @@ -7522,3 +7524,4 @@ breakText Glyphs Glyphs Grommets Zeilringen Grommet Inset Afstand van zeilringen tot de rand Diameter Diameter +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer moet "singleLine", "everyLine", "standardSingleLine" of "standardEveryLine" zijn diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 5f274cef09..85d95d8134 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -1947,7 +1947,8 @@ digits Malayalam Malajalam Tabular Lining Tabelaryczne nowoczesne Tabular Oldstyle Tabelaryczne nautyczne Titling Alternates Alternatywy tytułowe - Every-line Composer Kompozytor wielowierszowy + Middle Eastern & South Asian Every-line Composer Kompozytor wielowierszowy (Bliski Wschód i Azja Południowa) + Standard Every-line Composer Standardowy kompozytor wielowierszowy First-line Left Indent Wcięcie pierwszego wiersza Hyphenation… Dzielenie wyrazów… Words Longer Than: Wyrazy dłuższe niż: @@ -1987,7 +1988,8 @@ digits Malayalam Malajalam East Asian Composition Settings Ustawienia składu wschodnioazjatyckiego Korean Composition Settings Ustawienia składu koreańskiego Select a text object to edit its paragraph attributes. Zaznacz obiekt tekstowy, aby edytować atrybuty akapitu. - Single-line Composer Kompozytor jednowierszowy + Middle Eastern & South Asian Single-line Composer Kompozytor jednowierszowy (Bliski Wschód i Azja Południowa) + Standard Single-line Composer Standardowy kompozytor jednowierszowy Toggle Line Composer Przełącz kompozytor wierszy Space After Paragraph Odstęp po akapicie Space Before Paragraph Odstęp przed akapitem @@ -8205,7 +8207,7 @@ breakText Glyphs Glify @msg column: true floats the whole column and takes no onto, above or below column: true odłącza całą kolumnę i nie przyjmuje onto, above ani below @msg command must be a shape.* command command musi być poleceniem shape.* @msg commands[{i}]: missing `command` commands[{i}]: brak `command` -@msg composer must be "singleLine" or "everyLine" composer musi mieć wartość "singleLine" lub "everyLine" +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer musi mieć wartość "singleLine", "everyLine", "standardSingleLine" lub "standardEveryLine" @msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes kształty złożone przyjmują ścieżki, ścieżki złożone, grupy, kształty złożone, tekst, przejścia i koperty @msg compound {_1} must contain exactly one open path to join obiekt złożony {_1} musi zawierać dokładnie jedną otwartą ścieżkę do połączenia @msg connect needs a path other than the one extended connect wymaga ścieżki innej niż przedłużana diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index a9e56041f4..0203e3f850 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -960,7 +960,8 @@ antiAlias Sharp Nítido Every (minutes) A cada (minutos) Every option is at its default. Todas as opções estão no padrão. Every option matches {base}. Todas as opções correspondem a {base}. - Every-line Composer Compositor de Todas as Linhas + Middle Eastern & South Asian Every-line Composer Compositor de Todas as Linhas (Oriente Médio e Sul da Ásia) + Standard Every-line Composer Compositor Padrão de Todas as Linhas Exact dash lengths Comprimentos de tracejado exatos Exceptions (comma separated) Exceções (separadas por vírgula) Exclude Excluir @@ -2877,7 +2878,8 @@ digits Malayalam Malaiala Single Left Quotation Mark Aspa Esquerda Única Single Quotes: Aspas Simples: Single Right Quotation Mark Aspa Direita Única - Single-line Composer Compositor de Linha Única + Middle Eastern & South Asian Single-line Composer Compositor de Linha Única (Oriente Médio e Sul da Ásia) + Standard Single-line Composer Compositor Padrão de Linha Única Toggle Line Composer Alternar Compositor de Linhas Sixth Space Espaço de Sexto Size Tamanho @@ -8181,7 +8183,7 @@ swatch Yellow Green Verde-amarelado @msg column: true floats the whole column and takes no onto, above or below column: true torna flutuante a coluna inteira e não aceita onto, above nem below @msg command must be a shape.* command command deve ser um comando shape.* @msg commands[{i}]: missing `command` commands[{i}]: falta `command` -@msg composer must be "singleLine" or "everyLine" composer deve ser "singleLine" ou "everyLine" +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer deve ser "singleLine", "everyLine", "standardSingleLine" ou "standardEveryLine" @msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes formas compostas aceitam caminhos, caminhos compostos, grupos, formas compostas, texto, mesclagens e envelopes @msg compound {_1} must contain exactly one open path to join o composto {_1} deve conter exatamente um caminho aberto para unir @msg connect needs a path other than the one extended connect precisa de um caminho diferente do que foi estendido diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 4ea3511c36..74e6e6ea40 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1067,7 +1067,8 @@ font-technology Variable Вариативные Every (minutes) Каждые (минут) Every option is at its default. Все параметры имеют значения по умолчанию. Every option matches {base}. Все параметры совпадают с {base}. - Every-line Composer Компоновщик всех строк + Middle Eastern & South Asian Every-line Composer Компоновщик всех строк (Ближний Восток и Южная Азия) + Standard Every-line Composer Стандартный компоновщик всех строк Exact dash lengths Точная длина штрихов Exceptions (comma separated) Исключения (через запятую) Exclude Исключить @@ -3435,7 +3436,8 @@ digits Malayalam Малаялам Single Left Quotation Mark Одинарная открывающая кавычка Single Quotes: Одинарные кавычки: Single Right Quotation Mark Одинарная закрывающая кавычка - Single-line Composer Однострочный компоновщик + Middle Eastern & South Asian Single-line Composer Однострочный компоновщик (Ближний Восток и Южная Азия) + Standard Single-line Composer Стандартный однострочный компоновщик Toggle Line Composer Переключить компоновщик строк Sixth Space Шестая шпация Size Размер @@ -7584,3 +7586,4 @@ shape Open Открытый Grommets Люверсы Grommet Inset Отступ люверсов от края Diameter Диаметр +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer должен быть "singleLine", "everyLine", "standardSingleLine" или "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 9a0de40468..67604f634b 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -1048,7 +1048,8 @@ font-technology Variable Variabla Every (minutes) Intervall (minuter) Every option is at its default. Alla alternativ har standardvärden. Every option matches {base}. Alla alternativ matchar {base}. - Every-line Composer Flerradssättning + Middle Eastern & South Asian Every-line Composer Flerradssättning (Mellanöstern och Sydasien) + Standard Every-line Composer Standard-flerradssättning Exact dash lengths Exakta strecklängder Exceptions (comma separated) Undantag (kommaavgränsade) Exclude Uteslut @@ -3325,7 +3326,8 @@ digits Malayalam Malayalam Single Left Quotation Mark Enkelt vänster citattecken Single Quotes: Enkla citattecken: Single Right Quotation Mark Enkelt höger citattecken - Single-line Composer Enradssättare + Middle Eastern & South Asian Single-line Composer Enradssättare (Mellanöstern och Sydasien) + Standard Single-line Composer Standard-enradssättare Toggle Line Composer Växla radsättare Sixth Space Sjättedelsblanksteg Size Storlek @@ -7874,7 +7876,7 @@ digits Hindi Hindi @msg column: true floats the whole column and takes no onto, above or below column: true gör hela kolumnen flytande och tar inte onto, above eller below @msg command must be a shape.* command command måste vara ett shape.*-kommando @msg commands[{i}]: missing `command` commands[{i}]: `command` saknas -@msg composer must be "singleLine" or "everyLine" composer måste vara "singleLine" eller "everyLine" +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer måste vara "singleLine", "everyLine", "standardSingleLine" eller "standardEveryLine" @msg compound {_1} must contain exactly one open path to join sammansatt {_1} måste innehålla exakt en öppen bana att foga samman @msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes sammansatta former tar banor, sammansatta banor, grupper, sammansatta former, text, övergångar och kuvert @msg connect end must be "start" or "end" connect end måste vara "start" eller "end" diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index a457467ef6..4e086833ab 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -1040,7 +1040,8 @@ font-technology Variable Değişken Every (minutes) Her (dakika) Every option is at its default. Her seçenek varsayılan değerdedir. Every option matches {base}. Her seçenek {base} ile eşleşir. - Every-line Composer Her Satır Bestecisi + Middle Eastern & South Asian Every-line Composer Her Satır Bestecisi (Orta Doğu ve Güney Asya) + Standard Every-line Composer Standart Her Satır Bestecisi Exact dash lengths Tam çizgi uzunlukları Exceptions (comma separated) İstisnalar (virgülle ayrılmış) Exclude Hariç tut @@ -3201,7 +3202,8 @@ digits Malayalam Malayalam Single Left Quotation Mark Tek Sol Tırnak İşareti Single Quotes: Tek Alıntılar: Single Right Quotation Mark Tek Sağ Tırnak İşareti - Single-line Composer Tek Satır Oluşturucu + Middle Eastern & South Asian Single-line Composer Tek Satır Oluşturucu (Orta Doğu ve Güney Asya) + Standard Single-line Composer Standart Tek Satır Oluşturucu Toggle Line Composer Satır Oluşturucuyu Değiştir Sixth Space Altıncı Uzay Size Boyut @@ -7451,3 +7453,4 @@ breakText Glyphs Glifler Grommets Kuşgözleri Grommet Inset Kuşgözlerinin kenara uzaklığı Diameter Çap +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer "singleLine", "everyLine", "standardSingleLine" veya "standardEveryLine" olmalıdır diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 18a118822f..1de2f2b880 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1072,7 +1072,8 @@ font-technology Variable Варіативні Every (minutes) Кожен (хвилин) Every option is at its default. Усі параметри мають значення за промовчанням. Every option matches {base}. Усі параметри збігаються з {base}. - Every-line Composer Компонувальник всіх рядків + Middle Eastern & South Asian Every-line Composer Компонувальник всіх рядків (Близький Схід і Південна Азія) + Standard Every-line Composer Стандартний компонувальник всіх рядків Exact dash lengths Точна довжина штрихів Exceptions (comma separated) Винятки (через кому) Exclude Виключити @@ -3440,7 +3441,8 @@ digits Malayalam Малаялам Single Left Quotation Mark Одинарна лапка, що відкриває Single Quotes: Одинарні лапки: Single Right Quotation Mark Одинарна лапка, що закриває - Single-line Composer Однорядковий компонувальник + Middle Eastern & South Asian Single-line Composer Однорядковий компонувальник (Близький Схід і Південна Азія) + Standard Single-line Composer Стандартний однорядковий компонувальник Toggle Line Composer Переключити компонувальник рядків Sixth Space Шоста шпація Size Розмір @@ -7715,7 +7717,7 @@ breakText Glyphs Гліфи @msg column: true floats the whole column and takes no onto, above or below column: true робить плаваючим увесь стовпець і не приймає onto, above або below @msg command must be a shape.* command command має бути командою shape.* @msg commands[{i}]: missing `command` commands[{i}]: відсутній `command` -@msg composer must be "singleLine" or "everyLine" composer має бути "singleLine" або "everyLine" +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer має бути "singleLine", "everyLine", "standardSingleLine" або "standardEveryLine" @msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes складені фігури приймають контури, складені контури, групи, складені фігури, текст, переходи й оболонки @msg compound {_1} must contain exactly one open path to join складений об’єкт {_1} має містити рівно один відкритий контур для з’єднання @msg connect needs a path other than the one extended для connect потрібен контур, відмінний від того, що продовжується diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index db36624af7..5e091c8288 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1245,7 +1245,8 @@ font-technology Variable Biến thiên Every (minutes) Mỗi (phút) Every option is at its default. Mọi tùy chọn đang ở mặc định. Every option matches {base}. Mọi tùy chọn khớp với {base}. - Every-line Composer Soạn theo từng dòng + Middle Eastern & South Asian Every-line Composer Soạn theo từng dòng (Trung Đông và Nam Á) + Standard Every-line Composer Soạn theo từng dòng tiêu chuẩn Exact dash lengths Độ dài nét đứt chính xác Exceptions (comma separated) Ngoại lệ (phân cách bằng dấu phẩy) Exclude Loại trừ @@ -3795,7 +3796,8 @@ mark-style Japanese Kiểu Nhật Single Left Quotation Mark Dấu nháy đơn mở Single Quotes: Dấu nháy đơn: Single Right Quotation Mark Dấu nháy đơn đóng - Single-line Composer Soạn thảo một dòng + Middle Eastern & South Asian Single-line Composer Soạn thảo một dòng (Trung Đông và Nam Á) + Standard Single-line Composer Soạn thảo một dòng tiêu chuẩn Toggle Line Composer Chuyển đổi cách soạn dòng Sixth Space Dấu cách 1/6 Size Kích thước @@ -7106,3 +7108,4 @@ breakText Glyphs Glyph Grommets Khoen Grommet Inset Khoảng cách từ mép đến khoen Diameter Đường kính +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer phải là "singleLine", "everyLine", "standardSingleLine" hoặc "standardEveryLine" diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 0b0639e602..0a390e9e6a 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1012,7 +1012,8 @@ antiAlias Sharp 锐利 Every (minutes) 每隔 (分钟) Every option is at its default. 所有选项皆为默认值。 Every option matches {base}. 所有选项皆与{base}相同。 - Every-line Composer 逐行排版器 + Middle Eastern & South Asian Every-line Composer 逐行排版器(中东和南亚) + Standard Every-line Composer 标准逐行排版器 Exact dash lengths 精确虚线长度 Exceptions (comma separated) 例外词 (逗号分隔) Exclude 差集 @@ -3264,7 +3265,8 @@ digits Malayalam 马拉雅拉姆数字 Single Left Quotation Mark 左单引号 Single Quotes: 单引号: Single Right Quotation Mark 右单引号 - Single-line Composer 单行排版器 + Middle Eastern & South Asian Single-line Composer 单行排版器(中东和南亚) + Standard Single-line Composer 标准单行排版器 Toggle Line Composer 切换行排版器 Sixth Space 六分之一空格 Size 大小 @@ -8191,7 +8193,7 @@ breakText Glyphs 字形 @msg column: true floats the whole column and takes no onto, above or below column: true 会使整列浮动,不能再给出 onto、above 或 below @msg command must be a shape.* command command 必须是 shape.* 命令 @msg commands[{i}]: missing `command` commands[{i}]:缺少 `command` -@msg composer must be "singleLine" or "everyLine" composer 必须是 "singleLine" 或 "everyLine" +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer 必须是 "singleLine"、"everyLine"、"standardSingleLine" 或 "standardEveryLine" @msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes 复合形状接受路径、复合路径、编组、复合形状、文字、混合和封套 @msg compound {_1} must contain exactly one open path to join 复合对象 {_1} 必须恰好包含一条要连接的开放路径 @msg connect needs a path other than the one extended connect 需要一条不同于被延伸路径的路径 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index e72c0d1a0d..bd89fd021f 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1042,7 +1042,8 @@ font-technology Variable 可變 Every (minutes) 每隔 (分鐘) Every option is at its default. 所有選項皆為預設值。 Every option matches {base}. 所有選項皆與{base}相同。 - Every-line Composer 逐行排版程式 + Middle Eastern & South Asian Every-line Composer 逐行排版程式(中東與南亞) + Standard Every-line Composer 標準逐行排版程式 Exact dash lengths 精確虛線長度 Exceptions (comma separated) 例外 (以逗號分隔) Exclude 差集 @@ -4517,7 +4518,8 @@ digits Malayalam 馬來亞拉姆數字 Single Left Quotation Mark 左單引號 Single Quotes: 單引號: Single Right Quotation Mark 右單引號 - Single-line Composer 單行排版程式 + Middle Eastern & South Asian Single-line Composer 單行排版程式(中東與南亞) + Standard Single-line Composer 標準單行排版程式 Toggle Line Composer 切換行排版程式 Sixth Space 六分之一空格 Size 大小 @@ -7999,7 +8001,7 @@ breakText Glyphs 字符 @msg column: true floats the whole column and takes no onto, above or below column: true 會讓整欄浮動,不接受 onto、above 或 below @msg command must be a shape.* command command 必須是 shape.* 指令 @msg commands[{i}]: missing `command` commands[{i}]:未提供 `command` -@msg composer must be "singleLine" or "everyLine" composer 必須是 "singleLine" 或 "everyLine" +@msg composer must be "singleLine", "everyLine", "standardSingleLine" or "standardEveryLine" composer 必須是 "singleLine"、"everyLine"、"standardSingleLine" 或 "standardEveryLine" @msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes 複合形狀接受路徑、複合路徑、群組、複合形狀、文字、漸變物件和封套 @msg compound {_1} must contain exactly one open path to join 複合路徑 {_1} 必須剛好包含一條可連接的開放路徑 @msg connect needs a path other than the one extended connect 需要延伸路徑以外的另一條路徑 diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 255d0836b5..029ddf955a 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -393,7 +393,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "type.lineComposer", "Toggle Line Composer", "Cmd+Alt+Shift+C", - "{} the selected type's paragraphs (or those the Type tool's selection touches) use the Single-line Composer when they all use the Every-line Composer, else the Every-line Composer (text.setFormat {composer}) → \"singleLine\" | \"everyLine\"", + "{} the selected type's paragraphs (or those the Type tool's selection touches) use the Single-line Composer when they all use the Every-line Composer, else the Every-line Composer, each keeping its shaping: Middle Eastern & South Asian or Standard (text.setFormat {composer}) → \"singleLine\" | \"everyLine\" | \"standardSingleLine\" | \"standardEveryLine\"", ), ( "type.hyphenate", diff --git a/crates/ui-egui/src/panels/paragraph.rs b/crates/ui-egui/src/panels/paragraph.rs index e02d81e14c..3c1a0fe9c8 100644 --- a/crates/ui-egui/src/panels/paragraph.rs +++ b/crates/ui-egui/src/panels/paragraph.rs @@ -54,15 +54,27 @@ pub(crate) fn align(app: &mut VectorcraftApp, justify: &str) -> Result Result { text_style(app).ok_or("select some type first")?; - let id = if shared_composer(app) == Some(Composer::EveryLine) { "singleLine" } else { "everyLine" }; + let shared = shared_composer(app); + let to_single = shared.is_some_and(|c| !c.is_single_line()); + let id = composer_id(shared.unwrap_or_default().with_breaking(to_single)); format(app, json!({ "composer": id })); Ok(json!(id)) } +/// The `text.setFormat` value of `composer`. +fn composer_id(composer: Composer) -> &'static str { + match composer { + Composer::SingleLine => "singleLine", + Composer::EveryLine => "everyLine", + Composer::StandardSingleLine => "standardSingleLine", + Composer::StandardEveryLine => "standardEveryLine", + } +} + /// `type.hyphenate` (Cmd+Alt+Shift+H): what the Hyphenate checkbox does, on the paragraphs the /// panel acts on. Returns whether they now hyphenate. pub(crate) fn toggle_hyphenate(app: &mut VectorcraftApp) -> Result { @@ -876,11 +888,14 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { } ui.separator(); let composer = shared_composer(app); - for (c, label, id) in - [(Composer::SingleLine, tl!("Single-line Composer"), "singleLine"), (Composer::EveryLine, tl!("Every-line Composer"), "everyLine")] - { + for (c, label) in [ + (Composer::SingleLine, tl!("Middle Eastern & South Asian Single-line Composer")), + (Composer::EveryLine, tl!("Middle Eastern & South Asian Every-line Composer")), + (Composer::StandardSingleLine, tl!("Standard Single-line Composer")), + (Composer::StandardEveryLine, tl!("Standard Every-line Composer")), + ] { if menu_item(ui, label, has, composer == Some(c)) { - format(app, json!({ "composer": id })); + format(app, json!({ "composer": composer_id(c) })); } } ui.separator(); diff --git a/crates/ui-egui/src/panels/paragraph/tests.rs b/crates/ui-egui/src/panels/paragraph/tests.rs index 180f0b1fa1..518df239ae 100644 --- a/crates/ui-egui/src/panels/paragraph/tests.rs +++ b/crates/ui-egui/src/panels/paragraph/tests.rs @@ -82,6 +82,69 @@ fn paragraph_mixed_composer_checks_neither_menu_item() { assert_eq!(shared_composer(&app), Some(Composer::EveryLine)); } +/// Cmd+Alt+Shift+C switches between Single-line and Every-line and keeps what each paragraph's +/// composer does with complex scripts: a Standard composer toggles to the other Standard one. +#[test] +fn toggling_the_line_composer_keeps_the_shaping() { + let (mut app, id) = editing_text("A\nB\n"); + for (from, first, second) in [ + ("standardEveryLine", "standardSingleLine", "standardEveryLine"), + ("everyLine", "singleLine", "everyLine"), + ("standardSingleLine", "standardEveryLine", "standardSingleLine"), + ] { + app.run("text.setFormat", json!({"id": id, "composer": from})).unwrap(); + app.session.set_tool_option("selectAll", &json!(true)); + assert_eq!(toggle_composer(&mut app).unwrap(), json!(first), "{from}"); + assert_eq!(toggle_composer(&mut app).unwrap(), json!(second), "{from} again"); + } + // Paragraphs that differ take Every-line, as before the Standard composers existed. + app.run("text.setFormat", json!({"id": id, "composer": "standardSingleLine"})).unwrap(); + app.run("text.setFormat", json!({"id": id, "start": 2, "end": 3, "composer": "singleLine"})).unwrap(); + app.session.set_tool_option("selectAll", &json!(true)); + assert_eq!(toggle_composer(&mut app).unwrap(), json!("everyLine")); +} + +/// The Paragraph panel menu lists the four composers, and ticks the one every selected paragraph has. +#[test] +fn the_paragraph_menu_sets_each_of_the_four_composers() { + let (mut app, id) = editing_text("A\nB\n"); + app.session.set_tool_option("selectAll", &json!(true)); + let ctx = egui::Context::default(); + let items = |app: &mut VectorcraftApp| { + let mut seen = vec![]; + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| menu(app, ui)); + out.textures_delta.clear(); + for shape in out.shapes { + collect_texts(&shape.shape, &mut seen); + } + seen + }; + fn collect_texts(shape: &egui::Shape, out: &mut Vec) { + match shape { + egui::Shape::Vec(v) => v.iter().for_each(|s| collect_texts(s, out)), + egui::Shape::Text(t) => out.push(t.galley.text().to_string()), + _ => {} + } + } + let texts = items(&mut app); + for label in [ + "Middle Eastern & South Asian Single-line Composer", + "Middle Eastern & South Asian Every-line Composer", + "Standard Single-line Composer", + "Standard Every-line Composer", + ] { + assert!(texts.iter().any(|t| t == label), "{label} in {texts:?}"); + } + for (value, want) in [ + ("standardEveryLine", Composer::StandardEveryLine), + ("singleLine", Composer::SingleLine), + ("standardSingleLine", Composer::StandardSingleLine), + ] { + app.run("text.setFormat", json!({"id": id, "composer": value})).unwrap(); + assert_eq!(shared_composer(&app), Some(want), "{value}"); + } +} + #[test] fn paragraph_mixed_hyphenate_checks_off() { let (mut app, id) = editing_text("A\nB\n"); diff --git a/docs/mcp.md b/docs/mcp.md index 1ad05ba3df..e435dfb1a2 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1,6 +1,6 @@ # VectorCraft MCP server -> **Last updated:** 2026-11-13 · **Change:** Snap to Point with Smart Guides off draws no Smart Guides labels or lines, #7250; no response-shape change. Previously: Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter (#7097); existing response shapes retained. +> **Last updated:** 2026-11-14 · **Change:** `text.setFormat {composer}` and paragraph styles also take `standardSingleLine` and `standardEveryLine` (#4935); existing values and responses unchanged. Previously: Snap to Point with Smart Guides off draws no Smart Guides labels or lines, #7250; no response-shape change. Previously: Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter (#7097); existing response shapes retained. `vectorcraft-cli mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server on stdio (newline-delimited JSON-RPC 2.0; see [Protocol](#protocol) for version negotiation). Agents use it to @@ -9553,7 +9553,7 @@ The `paraStyleOptions` dialog exposes `name`, `section` (`general`, `basic`, `ta 100000 pt); it is disabled at 256 stops or when a stop is already at that position. Delete Tab removes the selected stop, and Clear All Tabs leaves the style with no stops, which it then sets. OK sorts the stops by position; of two stops at one position it keeps the one edited last. -- Composition: `composer` (`singleLine` or `everyLine`) and the `roman_hanging` checkbox. +- Composition: `composer` (`singleLine`, `everyLine`, `standardSingleLine` or `standardEveryLine`) and the `roman_hanging` checkbox. - Hyphenation: the `hyphenate` checkbox, `words_longer_than`, `after_first`, `before_last`, `hyphen_limit`, `hyphenation_zone` and the `hyphenate_capitalized` checkbox. The options are inactive while Hyphenation is off, and Hyphenation Zone is inactive with the Every-line Composer. @@ -10478,9 +10478,16 @@ of the group in the Layers panel (`layer.move`). ## Paragraph composer -`text.setFormat {ids?, composer: "singleLine"|"everyLine"}` sets how a text object's lines are broken (the Paragraph -panel menu's Single-line and Every-line Composer; stored in the paragraph attributes as `para.composer`, saved only -when it is Single-line). Every-line, the default, picks the breaks of the whole paragraph together in area type: +`text.setFormat {ids?, composer: "singleLine"|"everyLine"|"standardSingleLine"|"standardEveryLine"}` sets how a text +object's lines are broken and whether complex scripts are shaped (the Paragraph panel menu's four composers: Middle +Eastern & South Asian Single-line and Every-line, and Standard Single-line and Every-line; stored in the paragraph +attributes as `para.composer`, saved only when it is not Every-line, which is the default and what all type used before the +Standard composers existed). `singleLine` and `everyLine` shape Indic, Arabic and Hebrew type with the font's layout +tables, as all type is set; `standardSingleLine` and `standardEveryLine` break lines the same way but set such type +glyph by glyph as the font maps its characters (no conjuncts, reordering, mark placement or joining forms), for legacy +fonts that keep a script on Latin character codes. Latin and other scripts keep their kerning and ligatures under every +composer. An older VectorCraft cannot open a file that uses a Standard composer. `.ai`, EPS and PDF files carry no composer. +Every-line, the default, picks the breaks of the whole paragraph together in area type: justified text gets even word spacing, ragged text (left, centre or right aligned) an even rag, so it may move a word that fits to the next line to avoid a lone short word on the last line or a full line next to a short one. Single-line fills each line as far as it goes. Point type and type on a path have no line width, so both composers @@ -10492,8 +10499,9 @@ narrower, or beside a wrap object, gets the room there. Such a paragraph falls b no room for all of it, 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). 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. +shift: true}}` or `invoke_menu`) sets the Single-line form when every paragraph of the selected type (or every paragraph the +Type tool's selection touches) uses an Every-line one, else the Every-line form, keeping each paragraph's shaping (`singleLine` +or `everyLine`; a Standard composer toggles between `standardSingleLine` and `standardEveryLine`), and returns the composer it set. ```json {"name":"run_command","arguments":{"command":"text.setFormat","params":{"ids":[7],"composer":"singleLine"}}} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 2c74302900..5af4c94ca7 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-14 · **Change:** minor (a closed Pencil stroke closes with a straight segment; previously: Snap to Point with Smart Guides off draws no Smart Guides labels or lines, #7250; previously: 3D: the seams of Extrude & Bevel, Inflate and Rotate under a white light, and of the faces of Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic), went from miter to round joins, so white-light 3D output is expected to differ from the 2026-10-17 comparison at facet corners (not measured: the scenes were not exported again); previously: `.ai` and EPS import: in layers that don't draw as the page, art that differs away from an envelope of type no longer costs it its editable type; it keeps its drawn art where, in the pixels it and its drawn art ink, the layers with it differ from the page more than its own check allows, or past the envelopes' share of the page check, #7141; Character panel: the Set Font Height Reference dropdown shows an icon before each reference, #6304; scores and estimates unchanged; previously: Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter, so a top object saved with its sides' curves in pieces draws as its `.ai` drawn art, #7097; scores and estimates unchanged; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously minor: Object › Rasterize with the Grayscale or Bitmap colour model makes a grayscale image, #6969; scores and estimates unchanged; previously minor: `.ai` and EPS import: type in an envelope with Distort Appearance is also tried mapped from the box of what it paints, miter tips and moved strokes included, after its glyphs grown by half its strokes' weight (#6824; #6617: Swatches panel menu: Show Recent Colors; scores unchanged; previously minor: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; macOS: the canvas cursor stays after the pointer moves, #6362; scores and estimates unchanged; previously minor: Type tool: kerning at a caret at the start of a line moves the character after it, #5111; scores and estimates unchanged; previously minor: Rulers: the zero point dragged out of the rulers' corner snaps as a drawn point does; previously minor: Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: Envelopes: with Distort Appearance, type's strokes paint along the bent glyph outlines at their own weight, as users' `.ai` files draw them, which changes how VectorCraft's own envelopes of stroked type draw (#6644; scores and estimates unchanged); previously minor: G5: Energy Beams brush library; bundled content ~44% → ~46%; previously minor: G5: Skill Icons symbol library; bundled content ~42% → ~44%; previously minor: `.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art (#6570; scores and estimates unchanged); previously minor: ruler double-click adds a guide at the pointer, #5239; G5: Status Effects graphic style library; bundled content ~40% → ~42%; MCP symbol libraries list UI Frames and Map Markers; previously minor: G5: Inventory Icons built-in symbol library; bundled content ~38% → ~40%; previously minor: `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke (#3179; scores and estimates unchanged); previously minor: `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid where VectorCraft's warp doesn't draw as their drawn art (#3179; scores and estimates unchanged); G5: Game Trails brush library; bundled content ~36% → ~38%; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page (#3179; scores and estimates unchanged); G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-14 · **Change:** minor (Paragraph composers (#4935): the Paragraph panel menu and paragraph styles offer four composers, the Middle Eastern & South Asian Single-line and Every-line (how all type is set, and the default) and a Standard Single-line and Every-line that set Indic, Arabic and Hebrew type glyph by glyph as the font maps it, for legacy fonts that keep a script on Latin character codes; `.ai` and EPS files do not carry the choice; scores and estimates unchanged; previously minor: a closed Pencil stroke closes with a straight segment; previously: Snap to Point with Smart Guides off draws no Smart Guides labels or lines, #7250; previously: 3D: the seams of Extrude & Bevel, Inflate and Rotate under a white light, and of the faces of Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic), went from miter to round joins, so white-light 3D output is expected to differ from the 2026-10-17 comparison at facet corners (not measured: the scenes were not exported again); previously: `.ai` and EPS import: in layers that don't draw as the page, art that differs away from an envelope of type no longer costs it its editable type; it keeps its drawn art where, in the pixels it and its drawn art ink, the layers with it differ from the page more than its own check allows, or past the envelopes' share of the page check, #7141; Character panel: the Set Font Height Reference dropdown shows an icon before each reference, #6304; scores and estimates unchanged; previously: Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter, so a top object saved with its sides' curves in pieces draws as its `.ai` drawn art, #7097; scores and estimates unchanged; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously minor: Object › Rasterize with the Grayscale or Bitmap colour model makes a grayscale image, #6969; scores and estimates unchanged; previously minor: `.ai` and EPS import: type in an envelope with Distort Appearance is also tried mapped from the box of what it paints, miter tips and moved strokes included, after its glyphs grown by half its strokes' weight (#6824; #6617: Swatches panel menu: Show Recent Colors; scores unchanged; previously minor: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; macOS: the canvas cursor stays after the pointer moves, #6362; scores and estimates unchanged; previously minor: Type tool: kerning at a caret at the start of a line moves the character after it, #5111; scores and estimates unchanged; previously minor: Rulers: the zero point dragged out of the rulers' corner snaps as a drawn point does; previously minor: Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: Envelopes: with Distort Appearance, type's strokes paint along the bent glyph outlines at their own weight, as users' `.ai` files draw them, which changes how VectorCraft's own envelopes of stroked type draw (#6644; scores and estimates unchanged); previously minor: G5: Energy Beams brush library; bundled content ~44% → ~46%; previously minor: G5: Skill Icons symbol library; bundled content ~42% → ~44%; previously minor: `.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art (#6570; scores and estimates unchanged); previously minor: ruler double-click adds a guide at the pointer, #5239; G5: Status Effects graphic style library; bundled content ~40% → ~42%; MCP symbol libraries list UI Frames and Map Markers; previously minor: G5: Inventory Icons built-in symbol library; bundled content ~38% → ~40%; previously minor: `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke (#3179; scores and estimates unchanged); previously minor: `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid where VectorCraft's warp doesn't draw as their drawn art (#3179; scores and estimates unchanged); G5: Game Trails brush library; bundled content ~36% → ~38%; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page (#3179; scores and estimates unchanged); G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) > > Oversized artboard import update (2026-11-12): Explicit editing-data import retains a single artboard beyond a capped 14,400 pt PDF page (#6865). > @@ -56,6 +56,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-14 | minor | Paragraph composers (#4935): `text.setFormat {composer}` and paragraph styles take `singleLine` and `everyLine` (the Middle Eastern & South Asian composers, what all type used and the default) and `standardSingleLine` and `standardEveryLine`, which break lines the same way and set a complex script (Indic, Arabic, Hebrew) glyph by glyph as the font maps its characters, with no conjuncts, reordering, marks placed or joining forms. The choice is per paragraph, saved in the native file only when it is not Every-line, and not written to `.ai`, EPS or PDF files, whose composer setting this app has no recorded format for. Scores and estimates unchanged. Tests `tests::composers`. | | 2026-11-14 | minor | Pencil and Paintbrush: a closed stroke (Alt at the release, or ending within Close Paths When Ends Are Within of its start) is fitted as the open stroke would be and closed by a straight segment back to its start, instead of curving on past both ends (`pencil_alt_closes_a_stroke_straight_back_to_its_start` in `crates/engine/src/tests/draw2.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | Snap to Point: with Smart Guides off it still pulls drawn and dragged points onto anchors and ruler guides, but draws none of the Smart Guides labels ("anchor") or lines (#7250; `guides::tests::snap_to_point_without_smart_guides_shows_no_guides`). Scores and estimates unchanged. | | 2026-11-13 | minor | Graphic Styles: in a panel made taller the list fills the space above the footer instead of stopping at 220 pt (#2992; `the_list_fills_a_tall_panel_above_its_footer` in `crates/ui-egui/src/panels/graphic_styles.rs`). Scores and estimates unchanged. | @@ -513,7 +514,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale) | Raster effects (Effect Gallery, Document Raster Effects Settings) | 4 | 35% | ~36 Photoshop-style filters (Artistic, Brush Strokes, Sketch, Stylize) and Texturizer's Load Texture (since done, see the area table above, as is the Effect Gallery, #2883); Document Raster Effects Settings, raster effects in PDF, the pixel-filter pipeline, Radial Blur, Smart Blur (Normal, Edge Only and Overlay Edge), Unsharp Mask, Glowing Edges, Distort (Diffuse Glow, Glass, Ocean Ripple), Pixelate (Color Halftone, Crystallize, Mezzotint, Pointillize), Texture (Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass, Texturizer) and Video (De-Interlace, NTSC Colors) are done | 8–14 | | 3D and Materials | 4 | ~25% | Initial Revolve, Extrude & Bevel, Inflate and Rotate, with simple lighting and four original lighting presets; materials, shadows, ray tracing, Rotate (Classic), Map Art and more complete visibility remain | 25–45 | | Type advanced | 4 | 50% | CJK composition for vertical type (mojikumi beyond Line-end Punctuation Half Width — the other sets (and their paragraph-start bracket and indent rules), custom kinsoku sets (beyond Hard, Soft and None) and the Mojikumi Settings dialog —, manual tate-chu-yoko is done, its up/down settings remain; No Break is done), tab leaders, spell check (its engine, custom dictionary commands, the Edit Custom Dictionary dialog and Auto Spell Check are done, #4936: the Check Spelling dialog remains; a right-click on an underlined word offers its suggestions, Ignore All and Add to Dictionary, and a right-click on type has Spelling › Auto Spell Check), Touch Type, Retype; vertical point/area/path type and Type Orientation have initial support (beyond parity, not scored: inline symbols in text) | 20–28 | -| Type core | 9 | 80% | Middle Eastern features beyond bidi layout, Paragraph Direction, digit types and Kashidas (the Middle Eastern composers, editable bidirectional SVG text, a split caret where directions meet); paint-only harakat keep joining with conditional GDEF-mark colour ownership, but fonts without GDEF mark classes and some AAT/`morx` ligatures can still leave marks painted as their letters; Every-line Composer fidelity for justified and ragged text (available per object; mixed type sizes in a paragraph still break single-line in non-rectangular area type and with text wrap), Optical Margin Alignment, paragraph spacing limit for vertically justified area type, hidden characters, paragraph attributes in the Type tool's copy and paste (the characters keep their styles, the paragraphs take the destination's), mixed-value display in the Paragraph panel for a selection across differing paragraphs, partial selection of type-area anchors by marquee, optional font-size preservation during group scaling (scaled character controls fixed in #1034). Hyphenation and Justification options, Fractional Widths, System Layout, styled Type-range canvas Paste (#5636), and pattern hyphenation for eleven Indian languages with grapheme/cluster protection (#4947) are done; Sanskrit automatic patterns remain pending an allowlisted source | 14–19 | +| Type core | 9 | 80% | Middle Eastern features beyond bidi layout, Paragraph Direction, digit types and Kashidas (editable bidirectional SVG text, a split caret where directions meet; the Middle Eastern & South Asian composers are done, #4935, but `.ai`/EPS/PDF files carry no composer); paint-only harakat keep joining with conditional GDEF-mark colour ownership, but fonts without GDEF mark classes and some AAT/`morx` ligatures can still leave marks painted as their letters; Every-line Composer fidelity for justified and ragged text (available per object; mixed type sizes in a paragraph still break single-line in non-rectangular area type and with text wrap), Optical Margin Alignment, paragraph spacing limit for vertically justified area type, hidden characters, paragraph attributes in the Type tool's copy and paste (the characters keep their styles, the paragraphs take the destination's), mixed-value display in the Paragraph panel for a selection across differing paragraphs, partial selection of type-area anchors by marquee, optional font-size preservation during group scaling (scaled character controls fixed in #1034). Hyphenation and Justification options, Fractional Widths, System Layout, styled Type-range canvas Paste (#5636), and pattern hyphenation for eleven Indian languages with grapheme/cluster protection (#4947) are done; Sanskrit automatic patterns remain pending an allowlisted source | 14–19 | | Symbols, blends, envelopes, Repeat, perspective | 5 | 90% | further built-in symbol libraries (Basic Shapes, UI Frames with default 9-slice, Map Markers, Inventory Icons and Skill Icons are done), dynamic symbols' `.ai` round trip, envelope Anti-Alias/Preserve Shape output and warp point editing, brush interpolation in blends (M8 fidelity pass done); Symbol Options › Registration and Export Type and on-canvas dynamic part paint (gradient/pattern, opacity, blend) are done; symbol editing shows draggable 9-slice guides (#5280), while guides outside the artwork remain unsupported | 4–7 | | Image Trace, graphs, image tools | 3 | 72% | graph Column designs (Graph Design, Marker designs and series colours set with Group Selection are done), Create Object Mosaic, per-feature stroke/fill choice and varying widths in Image Trace's Create Strokes (Vector Halftone and the Image Trace View modes, Palette and Create Strokes are done) | 6–10 | | Layers, artboards, document setup | 5 | 82% | Layers panel: a confirmation before deleting layers that hold art, row menu commands beyond Collect for Export (now on every layer, sublayer, group and object row), menus on guide and blend spine rows, the Layer Options dialog for several rows with mixed values, Paste Remembers Layers for sublayers; artboard preset polish (artboard escape routing and document setup modal handoff done; Move Artwork for artboard X/Y edits, Rearrange All Artboards, Document Setup, New Document and the rest of the Layers panel are done) | 4–7 | diff --git a/docs/type-parity.md b/docs/type-parity.md index 308a395670..f49594adc5 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-11 · **Change:** minor (kerning at the Type tool's caret at the start of a line sets the space before the character after it, #5111; previously minor: type's strokes in an envelope paint along its bent glyph outlines at their own weight, Distort Appearance on or off, as users' `.ai` files draw them, #6644; previously minor: a centred or right-aligned line of tracked type is aligned without the tracking after its last character, as the page of users' `.ai` files aligns it, #6610; previously minor: tracking and manual kerning in horizontally scaled type are thousandths of the em as the Horizontal Scale draws it, as the page of users' `.ai` files spaces such type; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: Direct Selection shows and drags the direction handles of type on a path's path, #3565; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; paint-only Arabic harakat keep joining with GDEF-mark ownership and Insert Kashidas base-letter paint; scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: right-aligned area type sets the text after a line's last tab against the right edge; a soft return in justified type no longer keeps its line from being justified; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-14 · **Change:** minor (Paragraph composers (#4935): the Paragraph panel menu and paragraph styles offer four composers, the Middle Eastern & South Asian Single-line and Every-line (how all type is set, and the default) and a Standard Single-line and Every-line that set Indic, Arabic and Hebrew type glyph by glyph as the font maps it, for legacy fonts that keep a script on Latin character codes; `.ai` and EPS files do not carry the choice; scores and estimates unchanged; previously minor: kerning at the Type tool's caret at the start of a line sets the space before the character after it, #5111; previously minor: type's strokes in an envelope paint along its bent glyph outlines at their own weight, Distort Appearance on or off, as users' `.ai` files draw them, #6644; previously minor: a centred or right-aligned line of tracked type is aligned without the tracking after its last character, as the page of users' `.ai` files aligns it, #6610; previously minor: tracking and manual kerning in horizontally scaled type are thousandths of the em as the Horizontal Scale draws it, as the page of users' `.ai` files spaces such type; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: Direct Selection shows and drags the direction handles of type on a path's path, #3565; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; paint-only Arabic harakat keep joining with GDEF-mark ownership and Insert Kashidas base-letter paint; scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: right-aligned area type sets the text after a line's last tab against the right edge; a soft return in justified type no longer keeps its line from being justified; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen) · **Target:** Adobe Illustrator 2026 (30.x) Glyphs review (#6084): a positional variant no supported feature draws is accepted as a glyph choice. Entire Font respects the selection's variable axes and font version; the default cell can reset an unlisted choice without GSUB alternates. Contextual choices use the supplying face, including fallback. Feature-only boundaries preserve ligature clusters and Arabic mark attachment. Encoded characters insert unchanged except Arabic presentation forms with a joining base letter; normalization reports success only after insertion succeeds. Latest follow-up regressions await runtime verification. Scores and estimates unchanged. @@ -20,6 +20,7 @@ against Illustrator's public documentation; **estimated** unless noted. | Feature | Status | Notes | |---|---|---| +| 2026-11-14 | minor | Paragraph composers (#4935): `text.setFormat {composer}` and paragraph styles take `singleLine` and `everyLine` (the Middle Eastern & South Asian composers, what all type used and the default) and `standardSingleLine` and `standardEveryLine`, which break lines the same way and set a complex script (Indic, Arabic, Hebrew) glyph by glyph as the font maps its characters, with no conjuncts, reordering, marks placed or joining forms. The choice is per paragraph, saved in the native file only when it is not Every-line, and not written to `.ai`, EPS or PDF files, whose composer setting this app has no recorded format for. Scores and estimates unchanged. Tests `tests::composers`. | | 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. The field takes the wheel only when the pointer was moved onto it after the field last moved or was covered by a window or overlay, and the wheel stream began there; a stream that began elsewhere stays there until 0.5 s pass with no wheel input. Order, bounds, Undo grouping, this rule and its 0.5 s 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; in justified text a line a soft return ends is justified as a wrapped line is, only the paragraph's last line keeping its alignment, as Illustrator's own layout in a user's `.ai` sets it (evidence: `tests::typo::soft_return_in_justified_text_spreads_like_a_wrapped_line`; first done the other way in #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. | @@ -40,7 +41,7 @@ against Illustrator's public documentation; **estimated** unless noted. | Variable fonts | done | named instances as styles; Character panel › Variable Font sets each axis (`fontAxes`); the font menu's Font Technology › Variable lists only variable font families, with the script, kind and ★ filters ([Find, apply, and adjust variable fonts](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/find-apply-and-adjust-variable-fonts.html)); a family with a variable file beside static ones counts as variable. Evidence: `font_menu::tests::the_variable_filter_lists_only_variable_families`, `tests_axes::a_family_with_variation_axes_is_variable`, `tests_sysfonts::a_family_with_a_variable_file_beside_static_ones_is_variable` | | Fallback glyphs keep the text's weight and slant | done | #2491: a character from a fallback face is set in that family's face nearest the run's weight and slant; Bold Italic of a missing family is drawn Bold (Source Sans 3 has no Bold Italic) rather than Italic; a letter and its combining marks (a grapheme cluster) are drawn in one face, the first that has all of them, so marks the text's font lacks attach to their letter instead of drawing as missing glyphs (#4491). Vietnamese letters with two marks (Ể, Ứ, Ự) sit on their letter whether typed precomposed, decomposed or as a vowel plus a tone mark, built from GPOS mark-to-mark in fonts without the precomposed letters. Evidence: `tests::vietnamese` | | Colour fonts (bitmap, COLR, OpenType SVG) | partial | bitmap glyphs (`sbix`, `CBDT`) done: the largest strike's PNG, transformed with the type, fill and stroke ignored; PDF Type 3 glyphs or images, SVG images with outlined text (live text leaves emoji to the viewer, their fonts not embedded), EPS and legacy `.ai` images, Create Outlines images; not `sbix` duplicate, flipped, JPEG or TIFF glyphs, or `CBDT` BGRA or mask data, or ruby; in envelopes they warp as images do, and a Warp effect moves them without bending them. COLR (v0 and v1) done: a glyph's layers as vector art (solid colours, linear and radial gradients, clip glyphs and boxes, blend modes, transforms, palette 0, the foreground colour from the run's fill, variable fonts at the face's instance), transformed with the type; drawn on the canvas, written as paths in SVG, EPS, legacy `.ai` and PDF with outlined text, as text in PDF (Type 3 glyphs), and Create Outlines makes groups of editable paths. Paints documents have no form for draw the glyph as an image (256 px per em): sweep gradients, Porter-Duff compositing, radial gradients from a circle that isn't the centre, repeating or reflecting gradients. Not ruby. COLR and SVG glyphs also go with type that geometry effects reshape (on the canvas and in exports), into Expand Appearance and EMF/WMF export, as their art, and bitmap glyphs as images. OpenType SVG done: a glyph's SVG document (its element `glyph`) is read by the SVG importer into vector art in font units, `currentColor` in the run's fill (and the legacy `context-fill` and `context-stroke` in its fill and stroke), a `viewBox` mapped onto the em square, and then drawn, exported and outlined as COLR glyphs are; in PDF real text such glyphs are text that draws nothing, with their art drawn where they sit. Images in glyph documents are in the art, holding their pixels until a document takes them in; text, scripts and embedded content are left out, as the table rules them out. Not CPAL palette variables. Evidence: `tests_svg_glyphs`, `svg/tests/it/svg_glyphs.rs`, `tests_colourfonts::svg_glyph_images_draw_and_outline`, `tests_colourfonts::create_outlines_turns_svg_glyphs_into_groups`, `tests_colourfonts::pdf_real_text_draws_svg_glyphs_as_art_and_keeps_their_characters`, `tests_colourfonts::expand_appearance_keeps_the_colour_glyphs_of_type_with_effects`, `render::tests_colrglyphs::colour_glyphs_go_with_type_that_a_geometry_effect_reshapes`, `tests_bitmaps`, `tests_colr`, `render::tests_bitmapglyphs`, `render::tests_colrglyphs`, `pdf::tests::colour_bitmap_glyphs_export_as_images`, `pdf::tests::colr_glyphs_export_as_vector_art`, `svg/tests/it/colour_glyphs.rs`, `eps` `colour_bitmap_glyphs_*` and `colr_glyphs_*`, `tests_pathops::create_outlines_turns_bitmap_glyphs_into_images`, `tests_pathops::create_outlines_turns_colr_glyphs_into_layer_groups` | -| Every-line and Single-line composers | done | tuned by eye, never compared side by side | +| Every-line and Single-line composers | done | tuned by eye, never compared side by side; each in a Middle Eastern & South Asian and a Standard form (#4935), see below | | Optical Margin Alignment | done | Type › Optical Margin Alignment, `type.opticalMarginAlignment`: per type object, with one setting for all the frames of a thread, in point and area type of left-to-right paragraphs, and at the brackets of type on a path whose first paragraph runs left to right; right-to-left paragraphs of point and area type, and type on a path whose first paragraph runs right to left, do not hang | | System Layout | done | Character panel menu › System Layout, `type.systemLayout`: the whole document, off by default and for documents from before it, saved only when on. On, every text object advances by whole points (at least one when a character takes space). New point, area and path type, threaded frames, placed text, graph and dimension labels and pasted plain text inherit it. Evidence: `tests::system_layout_rounds_positive_advances_to_whole_points`, `typecmd::area_tests::system_layout_sets_the_document_and_every_text_object`, `panels::character::tests::system_layout_menu_item_toggles_the_document` | | Fractional Widths | done | Character panel menu › Fractional Widths, `type.fractionalWidths`: per type object, and every frame of its threads. On by default and for documents from before it; saved only when off. Off, each character that takes space advances by a whole number of points, at least one, so small type does not run together. Evidence: `tests::fractional_widths_off_rounds_positive_advances_to_whole_points`, `typecmd::area_tests::fractional_widths_is_set_per_object_and_saved_only_when_off`, `panels::character::tests::fractional_widths_menu_item_toggles_the_type_object` | @@ -61,7 +62,8 @@ against Illustrator's public documentation; **estimated** unless noted. | Middle Eastern: bidirectional layout, Paragraph Direction | done | PDF export keeps harakat and zero-advance Noto Arabic dot glyphs in one compatible batch per layout line, with per-glyph vertical offsets (#6309). `crates/pdf/src/tests/realtext.rs` (`mark_text_*`) checks decoded cluster mappings, mixed Arabic/Latin/digits, contour placement against outline export, vertical columns, differing path rotations and PDF/X-1a/3/4. Editable-text import still needs ActualText support (#6165); separately styled or differently rotated clusters and external viewer search/copy remain outside this export-only evidence. | | Middle Eastern and Indic: digit types (Character panel menu › Digits: Default, Arabic, Hindi, Farsi; Devanagari, Bengali, Gurmukhi, Gujarati, Odia, Tamil, Telugu, Kannada, Malayalam) | done | `text.setFormat` / `text.setRangeStyle {digits}` (`"devanagari"`, `"bengali"`, `"gurmukhi"`, `"gujarati"`, `"odia"`, `"tamil"`, `"telugu"`, `"kannada"`, `"malayalam"` added, #4942); the text keeps the typed digits, the glyphs and SVG export follow the style; carried by character styles. Not yet: `.ai` export of the setting; Indic digits typed as such stay as typed under another system. Evidence: `text::tests::indic_digits_map_to_their_scripts_block`, `tests::bidi::indic_digits_draw_the_scripts_own_digits`, `typecmd::area_tests::indic_digits_round_trip_through_the_commands_and_the_file` | | Middle Eastern: Kashidas | done | Paragraph Insert Kashidas sets the length for justified Arabic area type; Character-menu Kashidas allows or excludes selected characters, on by default. A kashida is inserted only between two allowed joined characters. The character switch is retained in character styles and the native file; the length is retained in paragraph styles and the native file. Paint-only harakat (coloured or hidden marks) stay in one shaping cluster so letters keep joining; when the character-level pass matches, a glyph may take a later in-cluster character's run only if GDEF names it a mark (class 3) and that character is a harakat—otherwise the cluster start (a mark may miss its colour; a base never takes a mark's). Inserted tatweels take the base letter's paint—not a trailing mark's. Fonts without GDEF and some AAT/`morx` ligatures (e.g. Geeza Pro Allah) can leave a mark painted as its letter. No Character-panel harakat colour command yet. Evidence: `tests::harakat`. [Public help](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/justify-text-using-kashida-and-hyphenation.html). | -| Middle Eastern: Middle Eastern composers, split caret | missing | Character Direction is done; editable bidirectional SVG text remains incomplete. | +| Middle Eastern & South Asian composers and Standard composers (Paragraph panel menu, paragraph styles, `text.setFormat {composer}`) | done / partial | #4935: per paragraph, four choices. Middle Eastern & South Asian Single-line and Every-line are how all type was already set (complex-script shaping on) and stay the default, so existing documents do not change. Standard Single-line and Every-line break lines the same way and set Indic, Arabic and Hebrew type glyph by glyph as the font maps its characters (no conjuncts, reordering, mark placement or joining), for legacy fonts that keep a script on Latin character codes; Latin and other scripts keep their kerning and ligatures. The native file and paragraph styles keep the choice (`standardSingleLine` / `standardEveryLine`; an older VectorCraft cannot read a file that uses them). `.ai`, EPS and PDF import and export carry no composer: this app has no recorded format for it, so a Standard paragraph opened from or saved to those files comes back Middle Eastern & South Asian. Evidence: `tests::composers`, `tests::panelcmds::text_set_format_takes_the_four_composers`, `native_v3::all_four_composers_round_trip` | +| Middle Eastern: split caret | missing | Character Direction is done; editable bidirectional SVG text remains incomplete. | | CJK: input methods (marked text, candidate window) | done | | | CJK: kinsoku (Hard, Soft, None), the predefined mojikumi sets (None, YakumonoHankaku, GyomatsuYakumonoHankaku, GyomatsuYakumonoZenkaku, YakumonoZenkaku, with VectorCraft's own spacing for each) and Line-end Punctuation Half Width, tate-chu-yoko (auto) | done / partial | custom kinsoku sets, custom mojikumi sets and the Mojikumi Settings dialog missing | | CJK: Kinsoku Shori Type (Push In First, Push Out First, Push Out Only) | done | Paragraph panel menu › Kinsoku Shori Type (`text.setFormat {kinsokuType}`), with the East Asian options ([Formatting Asian characters](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)); Push Out Only is the default, and a push-in closes the line's gaps next to CJK characters by at most a tenth of an em each (VectorCraft's own limit). Evidence: `layout::wrapping_tests::kinsoku_shori_type_pushes_a_closing_bracket_in_or_out`, `tests::vertical::kinsoku_shori_type_pushes_a_closing_bracket_in_or_out`, `typecmd::area_tests::kinsoku_shori_type_is_set_per_paragraph_and_saved_only_when_not_push_out_only` |