diff --git a/crates/doc/src/text/char_style.rs b/crates/doc/src/text/char_style.rs index b5bc7e065..035b92e34 100644 --- a/crates/doc/src/text/char_style.rs +++ b/crates/doc/src/text/char_style.rs @@ -104,6 +104,17 @@ pub struct CharStyle { /// #[serde(default, rename = "tateChuYoko", skip_serializing_if = "crate::skip::is_default")] pub tate_chu_yoko: bool, + /// Tate-chu-yoko Settings › Up/Down (Character panel menu, East Asian options), in points: a + /// block of marked characters in vertical type moves up by a positive value and down by a + /// negative one. The block takes the value of its first character; automatic digit blocks and + /// horizontal type do not move. Saved only when not 0. + /// + #[serde(default, rename = "tcyUpDown", skip_serializing_if = "crate::skip::is_default")] + pub tcy_up_down: f64, + /// Tate-chu-yoko Settings › Left/Right, in points: a block of marked characters in vertical type + /// moves right by a positive value and left by a negative one, as [`Self::tcy_up_down`] does. + #[serde(default, rename = "tcyLeftRight", skip_serializing_if = "crate::skip::is_default")] + pub tcy_left_right: f64, /// Standard Vertical Roman Alignment (Character panel menu, East Asian options): in vertical /// type the half-width characters among these (Latin letters, digits and other characters that /// would lie on their side) stand upright one below the other instead of being rotated @@ -396,6 +407,8 @@ impl Default for CharStyle { char_direction: None, proportional_metrics: false, tate_chu_yoko: false, + tcy_up_down: 0.0, + tcy_left_right: 0.0, standard_vertical_roman: false, small_caps: None, digits: Digits::Default, @@ -414,7 +427,8 @@ impl CharStyle { /// or scale of 0 or less becomes the default new type takes (12 pt, 100%), a negative leading /// becomes Auto, and a value past [`crate::MAX_COORD`] (a baseline shift past it either way) /// becomes the nearer end. A leading of 0, which legacy `.ai` import stores, stays, and a - /// baseline shift that is not a number becomes 0. + /// baseline shift that is not a number becomes 0. The Tate-chu-yoko Up/Down and Left/Right + /// offsets are kept as the baseline shift is. pub fn keep_dimensions_in_range(&mut self) { let default = Self::default(); let within = |v: f64, or: f64| if v > 0.0 { v.min(crate::MAX_COORD) } else { or }; @@ -422,7 +436,10 @@ impl CharStyle { self.h_scale = within(self.h_scale, default.h_scale); self.v_scale = within(self.v_scale, default.v_scale); self.leading = self.leading.filter(|l| *l >= 0.0).map(|l| l.min(crate::MAX_COORD)); - self.baseline_shift = if self.baseline_shift.is_nan() { 0.0 } else { self.baseline_shift.clamp(-crate::MAX_COORD, crate::MAX_COORD) }; + let shift = |v: f64| if v.is_nan() { 0.0 } else { v.clamp(-crate::MAX_COORD, crate::MAX_COORD) }; + self.baseline_shift = shift(self.baseline_shift); + self.tcy_up_down = shift(self.tcy_up_down); + self.tcy_left_right = shift(self.tcy_left_right); } /// Do the characters draw a stroke (a paint and a positive weight)? pub fn has_stroke(&self) -> bool { diff --git a/crates/doc/src/text/object.rs b/crates/doc/src/text/object.rs index de77da298..5ba5669e6 100644 --- a/crates/doc/src/text/object.rs +++ b/crates/doc/src/text/object.rs @@ -32,8 +32,13 @@ impl TextObject { style.size *= scale.points; style.leading = style.leading.map(|v| v * scale.points); style.baseline_shift *= scale.points; + style.tcy_up_down *= scale.points; + style.tcy_left_right *= scale.points; style.h_scale *= scale.horizontal; - if ![style.size, style.leading.unwrap_or(0.0), style.baseline_shift, style.h_scale].iter().all(|v| v.is_finite()) { + if ![style.size, style.leading.unwrap_or(0.0), style.baseline_shift, style.tcy_up_down, style.tcy_left_right, style.h_scale] + .iter() + .all(|v| v.is_finite()) + { return None; } Some(style) diff --git a/crates/engine/src/cmd/panelcmds.rs b/crates/engine/src/cmd/panelcmds.rs index 6cab4b35b..40df8f377 100644 --- a/crates/engine/src/cmd/panelcmds.rs +++ b/crates/engine/src/cmd/panelcmds.rs @@ -43,7 +43,7 @@ pub fn specs() -> Vec { "Character / Paragraph", [], None, - "{ids?|id?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), 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\" (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), 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\"|\"lineEndHalf\" (Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), 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), 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), 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), 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)", + "{ids?|id?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), 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\" (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), 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\"|\"lineEndHalf\" (Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), 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), 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), 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), 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)", has_doc, set_format ), @@ -386,6 +386,8 @@ fn set_format(s: &mut Session, p: &Value) -> Result { "charDirection", "proportionalMetrics", "tateChuYoko", + "tcyUpDown", + "tcyLeftRight", "standardVerticalRoman", "noBreak", "digits", @@ -415,6 +417,8 @@ fn set_format(s: &mut Session, p: &Value) -> Result { let language = super::textedit::language_param(p, C)?; let font_axes = super::textedit::font_axes_param(p, C)?; let font_height_reference = super::textedit::font_height_reference_param(p, C)?; + let tcy_up_down = super::textedit::tcy_offset_param(p, "tcyUpDown", C)?; + let tcy_left_right = super::textedit::tcy_offset_param(p, "tcyLeftRight", C)?; let mojikumi = match p.get("mojikumi") { None => None, Some(v) => Some(match v.as_str() { @@ -501,8 +505,9 @@ fn set_format(s: &mut Session, p: &Value) -> Result { // A paragraph whose Minimum, Desired and Maximum would no longer be in order. let mut out_of_order: Option<&str> = None; let range = super::typecmd::TextRange::parse(p, C)?; - // The character attributes, given the baseline shift and horizontal scale in the type's own units. - let chars = |st: &mut vectorcraft_doc::CharStyle, baseline_shift: Option, h_scale: Option| { + // The character attributes, given the baseline shift, horizontal scale and Tate-chu-yoko + // offsets in the type's own units. + let chars = |st: &mut vectorcraft_doc::CharStyle, baseline_shift: Option, h_scale: Option, tcy: (Option, Option)| { if let Some(k) = kerning { st.kerning = k; } @@ -554,6 +559,12 @@ fn set_format(s: &mut Session, p: &Value) -> Result { if let Some(v) = flag("tateChuYoko") { st.tate_chu_yoko = v; } + if let Some(v) = tcy.0 { + st.tcy_up_down = v; + } + if let Some(v) = tcy.1 { + st.tcy_left_right = v; + } if let Some(v) = flag("standardVerticalRoman") { st.standard_vertical_roman = v; } @@ -572,7 +583,9 @@ fn set_format(s: &mut Session, p: &Value) -> Result { let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else { continue }; let baseline_shift = num("baselineShift").map(|v| super::typecmd::local_type_value(t, v, (-1296.0, 1296.0), false, C)).transpose()?; let h_scale = num("hScale").map(|v| super::typecmd::local_type_value(t, v, (1.0, 10000.0), true, C)).transpose()?; - super::typecmd::style_chars(t, range, |st| chars(st, baseline_shift, h_scale)); + let local = |v: Option| v.map(|v| super::typecmd::local_type_value(t, v, (-1296.0, 1296.0), false, C)).transpose(); + let tcy = (local(tcy_up_down)?, local(tcy_left_right)?); + super::typecmd::style_chars(t, range, |st| chars(st, baseline_shift, h_scale, tcy)); let span = super::typecmd::para_span(range, t); t.edit_paras(span, |para| { if let Some(v) = num("leftIndent") { @@ -684,7 +697,7 @@ fn set_format(s: &mut Session, p: &Value) -> Result { if range.is_none() { let shift = num("baselineShift").map(|v| v.clamp(-1296.0, 1296.0)); let scale = num("hScale").map(|v| v.clamp(1.0, 10000.0)); - super::graph::capture_label_styles(d, &ids, &|st| chars(st, shift, scale)); + super::graph::capture_label_styles(d, &ids, &|st| chars(st, shift, scale, (tcy_up_down, tcy_left_right))); } Ok(()) })?; diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index 9903ca149..d60f08c9a 100644 --- a/crates/engine/src/cmd/textedit.rs +++ b/crates/engine/src/cmd/textedit.rs @@ -39,7 +39,7 @@ pub fn specs() -> Vec { "Character", [], None, - "{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", keepModel?: bool (fill and stroke, as paint.setFill), strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, align?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], featureChanges?: [\"tnum\", \"-onum\", …] (OpenType features turned on, or with - off, on top of each character's own; its other features are kept), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", 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), standardVerticalRoman?: bool (Character panel menu › Standard Vertical Roman Alignment, with Show East Asian Options: in vertical type those half-width characters, such as Latin letters and digits, stand upright one below the other instead of being rotated; spaces and tate-chu-yoko blocks are not changed; kept on the characters and ignored in horizontal type; off by default), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), 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: the height of the font that size measures, the em box by default; with a font that has no such height, or one outside 0.1 to 2 em, the em box), language?: name|tag (Character panel › Language: one of Document Setup's languages by name or BCP 47 tag, case-insensitive, such as \"Turkish\" or \"tr\"; the characters are shaped in it, so the font uses that language's forms (OpenType `locl`); saved as the tag, English: USA, the default, as none; new type takes the document's language), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font: the setting of each variation axis named, in the axis's units and clamped to its range; null: back to the style's own setting; other axes keep theirs; tags the font has no axis for are left out; a font or style change clears them; text.fontList {family, style} lists the axes)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs, warning?: text (the font or style set isn't available: its type is drawn in the fallback font or the family's closest style)}", + "{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", keepModel?: bool (fill and stroke, as paint.setFill), strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, align?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], featureChanges?: [\"tnum\", \"-onum\", …] (OpenType features turned on, or with - off, on top of each character's own; its other features are kept), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", 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: in vertical type a block of tate-chu-yoko characters moves up by a positive Up/Down and down by a negative one, right by a positive Left/Right and left by a negative one, as one piece with the values of its first character; 0 by default; automatic digit blocks and horizontal type do not move; neither turns tateChuYoko on), standardVerticalRoman?: bool (Character panel menu › Standard Vertical Roman Alignment, with Show East Asian Options: in vertical type those half-width characters, such as Latin letters and digits, stand upright one below the other instead of being rotated; spaces and tate-chu-yoko blocks are not changed; kept on the characters and ignored in horizontal type; off by default), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), 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: the height of the font that size measures, the em box by default; with a font that has no such height, or one outside 0.1 to 2 em, the em box), language?: name|tag (Character panel › Language: one of Document Setup's languages by name or BCP 47 tag, case-insensitive, such as \"Turkish\" or \"tr\"; the characters are shaped in it, so the font uses that language's forms (OpenType `locl`); saved as the tag, English: USA, the default, as none; new type takes the document's language), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font: the setting of each variation axis named, in the axis's units and clamped to its range; null: back to the style's own setting; other axes keep theirs; tags the font has no axis for are left out; a font or style change clears them; text.fontList {family, style} lists the axes)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs, warning?: text (the font or style set isn't available: its type is drawn in the fallback font or the family's closest style)}", has_doc, set_range_style ), @@ -363,6 +363,9 @@ pub(crate) struct CharChange { char_direction: Option>, proportional_metrics: Option, tate_chu_yoko: Option, + /// Tate-chu-yoko Settings › Up/Down and Left/Right, in points. + tcy_up_down: Option, + tcy_left_right: Option, standard_vertical_roman: Option, no_break: Option, digits: Option, @@ -385,6 +388,15 @@ pub(crate) fn char_direction_param(p: &Value, cmd: &str) -> Result Result> { + let Some(v) = p.get(key).filter(|v| !v.is_null()) else { return Ok(None) }; + let v = v.as_f64().filter(|v| v.is_finite()).ok_or_else(|| bad(cmd, format!("`{key}` must be a number")))?; + let (lo, hi) = (-1296.0, 1296.0); + if (lo..=hi).contains(&v) { Ok(Some(v)) } else { Err(bad(cmd, format!("`{key}` must be from {lo} to {hi}"))) } +} + /// `digits: "default"|"arabic"|"hindi"|"farsi"|"devanagari"|…` ([`Digits::id`]) → the numeral /// system the digits are drawn in. /// @@ -635,6 +647,8 @@ impl CharChange { char_direction: char_direction_param(p, cmd)?, proportional_metrics: flag("proportionalMetrics"), tate_chu_yoko: flag("tateChuYoko"), + tcy_up_down: tcy_offset_param(p, "tcyUpDown", cmd)?, + tcy_left_right: tcy_offset_param(p, "tcyLeftRight", cmd)?, standard_vertical_roman: flag("standardVerticalRoman"), no_break: flag("noBreak"), digits: digits_param(p, cmd)?, @@ -684,6 +698,8 @@ impl CharChange { && self.char_direction.is_none() && self.proportional_metrics.is_none() && self.tate_chu_yoko.is_none() + && self.tcy_up_down.is_none() + && self.tcy_left_right.is_none() && self.standard_vertical_roman.is_none() && self.no_break.is_none() && self.digits.is_none() @@ -697,6 +713,8 @@ impl CharChange { self.size = self.size.map(|v| local_type_value(t, v, (0.1, 1296.0), false, cmd)).transpose()?; self.leading = self.leading.map(|v| v.map(|v| local_type_value(t, v, (0.1, 5000.0), false, cmd)).transpose()).transpose()?; self.baseline_shift = self.baseline_shift.map(|v| local_type_value(t, v, (-1296.0, 1296.0), false, cmd)).transpose()?; + self.tcy_up_down = self.tcy_up_down.map(|v| local_type_value(t, v, (-1296.0, 1296.0), false, cmd)).transpose()?; + self.tcy_left_right = self.tcy_left_right.map(|v| local_type_value(t, v, (-1296.0, 1296.0), false, cmd)).transpose()?; self.h_scale = self.h_scale.map(|v| local_type_value(t, v, (1.0, 10000.0), true, cmd)).transpose()?; Ok(()) } @@ -794,6 +812,12 @@ impl CharChange { if let Some(v) = self.tate_chu_yoko { st.tate_chu_yoko = v; } + if let Some(v) = self.tcy_up_down { + st.tcy_up_down = v; + } + if let Some(v) = self.tcy_left_right { + st.tcy_left_right = v; + } if let Some(v) = self.standard_vertical_roman { st.standard_vertical_roman = v; } diff --git a/crates/engine/src/cmd/textstyles.rs b/crates/engine/src/cmd/textstyles.rs index a44dd1773..cb6b1d0b5 100644 --- a/crates/engine/src/cmd/textstyles.rs +++ b/crates/engine/src/cmd/textstyles.rs @@ -175,6 +175,8 @@ pub fn char_attrs(v: &CharStyle) -> Map { ("charDirection", json!(v.char_direction)), ("proportionalMetrics", json!(v.proportional_metrics)), ("tateChuYoko", json!(v.tate_chu_yoko)), + ("tcyUpDown", json!(v.tcy_up_down)), + ("tcyLeftRight", json!(v.tcy_left_right)), ("standardVerticalRoman", json!(v.standard_vertical_roman)), ("noBreak", json!(v.no_break)), ("digits", json!(v.digits)), diff --git a/crates/engine/src/cmd/typecmd/area_tests.rs b/crates/engine/src/cmd/typecmd/area_tests.rs index 115a3bd13..8c2d76648 100644 --- a/crates/engine/src/cmd/typecmd/area_tests.rs +++ b/crates/engine/src/cmd/typecmd/area_tests.rs @@ -1206,6 +1206,61 @@ fn tate_chu_yoko_is_a_character_attribute_saved_only_when_on() { assert!(runs(&s).iter().all(|r| !r.style.tate_chu_yoko)); } +/// Tate-chu-yoko Settings (Character panel menu): Up/Down and Left/Right are character attributes +/// in points, 0 by default and saved only when not 0. Both commands set them without changing the +/// Tate-chu-yoko mark, refuse a value that is not a number or lies outside -1296 to 1296, and a +/// character style captured from the characters carries them. +#[test] +fn tate_chu_yoko_settings_are_character_attributes_saved_only_when_set() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap(); + let id = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "2026年"})).unwrap()["id"].as_u64().unwrap(); + let runs = |s: &Session| match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind { + NodeKind::Text(t) => t.runs.clone(), + _ => panic!("text"), + }; + let offsets = |s: &Session| runs(s).iter().map(|r| (r.text.clone(), r.style.tcy_up_down, r.style.tcy_left_right)).collect::>(); + let saved = serde_json::to_value(&runs(&s)[0].style).unwrap(); + assert!(saved.get("tcyUpDown").is_none() && saved.get("tcyLeftRight").is_none()); + s.execute("select.set", &json!({"ids": [id]})).unwrap(); + s.execute("text.setFormat", &json!({"tcyUpDown": 3.5, "tcyLeftRight": -2})).unwrap(); + assert_eq!(offsets(&s), [("2026年".to_string(), 3.5, -2.0)]); + assert!(!runs(&s)[0].style.tate_chu_yoko, "the mark stays off"); + let saved = serde_json::to_value(&runs(&s)[0].style).unwrap(); + assert_eq!((saved["tcyUpDown"].clone(), saved["tcyLeftRight"].clone()), (json!(3.5), json!(-2.0))); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(offsets(&s), [("2026年".to_string(), 0.0, 0.0)], "one undo step"); + s.execute("edit.redo", &json!({})).unwrap(); + assert_eq!(offsets(&s), [("2026年".to_string(), 3.5, -2.0)]); + s.execute("edit.undo", &json!({})).unwrap(); + + s.execute("text.setRangeStyle", &json!({"id": id, "start": 0, "end": 4, "tateChuYoko": true, "tcyUpDown": -4})).unwrap(); + assert_eq!(offsets(&s), [("2026".to_string(), -4.0, 0.0), ("年".to_string(), 0.0, 0.0)]); + s.execute("text.setRangeStyle", &json!({"id": id, "start": 0, "end": 4, "tcyLeftRight": 1296})).unwrap(); + assert_eq!(offsets(&s)[0], ("2026".to_string(), -4.0, 1296.0)); + assert!(runs(&s)[0].style.tate_chu_yoko); + // Saved and reopened as they were. + let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap(); + let NodeKind::Text(t) = &back.node(NodeId(id)).unwrap().kind else { panic!("text") }; + let reopened: Vec<_> = t.runs.iter().map(|r| (r.text.clone(), r.style.tcy_up_down, r.style.tcy_left_right)).collect(); + assert_eq!(reopened, offsets(&s)); + let attrs = super::super::textstyles::char_attrs(&runs(&s)[0].style); + assert_eq!((attrs["tcyUpDown"].clone(), attrs["tcyLeftRight"].clone()), (json!(-4.0), json!(1296.0))); + assert_eq!(super::super::textstyles::char_attrs(&runs(&s)[1].style)["tcyUpDown"], json!(0.0)); + + let before = offsets(&s); + for (cmd, p, want) in [ + ("text.setFormat", json!({"tcyUpDown": "3"}), "`tcyUpDown` must be a number"), + ("text.setFormat", json!({"tcyLeftRight": 1296.5}), "`tcyLeftRight` must be from -1296 to 1296"), + ("text.setRangeStyle", json!({"id": id, "tcyLeftRight": true}), "`tcyLeftRight` must be a number"), + ("text.setRangeStyle", json!({"id": id, "tcyUpDown": -2000}), "`tcyUpDown` must be from -1296 to 1296"), + ] { + let e = s.execute(cmd, &p).unwrap_err(); + assert!(matches!(&e, EngineError::BadParams { msg, .. } if msg == want), "{cmd} {p}: {e}"); + } + assert_eq!(offsets(&s), before); +} + /// Standard Vertical Roman Alignment (Character panel menu, East Asian options): a character /// attribute, off by default and saved only when on. Documents written before it read as off; a /// character style captured from the characters carries it. diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index cc1080bdb..a7665612c 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -37,7 +37,7 @@ use breaking::tab_advance; #[cfg(test)] use breaking::{break_line, candidates}; pub use driver::{decorations, fit_first_line, layout, layout_with, scaled}; -use vertical::{stands_upright, tate_chu_yoko, upright_cell, upright_centre, whole_character_widths}; +use vertical::{TcyStyle, stands_upright, tate_chu_yoko, upright_cell, upright_centre, whole_character_widths}; const EPS: f64 = 1e-6; @@ -114,7 +114,7 @@ impl Ctx<'_> { if self.vertical { tate_chu_yoko(v, |g| { let st = style(g); - (st.size, st.tate_chu_yoko) + TcyStyle { size: st.size, marked: st.tate_chu_yoko, up: st.tcy_up_down, right: st.tcy_left_right } }); // An upright glyph advances down the column by its vertical advance (the font's vertical // metrics; without them at least one em, as CJK fonts' is), centred across it: a narrow @@ -254,13 +254,16 @@ impl Ctx<'_> { // Vertical path type keeps the baseline path and turns each glyph across it. m = Affine::rotate_about(std::f64::consts::FRAC_PI_2, origin) * m; } else if let (true, Some(t)) = (self.vertical, g.tcy) { - // Tate-chu-yoko: the block set across the column, centred on its em of it. + // Tate-chu-yoko: the block set across the column, centred on its em of it, then moved up + // by its Up/Down and right by its Left/Right (line x runs down the column and line y + // from right to left across it). let em = self.style_at(g.byte).size; let start = origin.x - t.pen; let centre = Point::new(start + em * 0.5, origin.y - g.face.ideographic_centre() * em); let across = Affine::translate((centre.x - t.width * 0.5 + t.ink - origin.x, 0.0)); let squeeze = Affine::translate((centre.x, 0.0)) * Affine::scale_non_uniform(t.squeeze, 1.0) * Affine::translate((-centre.x, 0.0)); - m = Affine::rotate_about(-std::f64::consts::FRAC_PI_2, centre) * squeeze * across * m; + let offset = Affine::translate((-t.up, -t.right)); + m = offset * Affine::rotate_about(-std::f64::consts::FRAC_PI_2, centre) * squeeze * across * m; } else if self.vertical && stands_upright(g) { let next = self.out.glyphs.len(); let after_base = self.out.glyphs.last().is_some_and(|p| p.byte == g.byte && p.line == line && p.len > 0); diff --git a/crates/text/src/layout/driver.rs b/crates/text/src/layout/driver.rs index 63168025b..0b76f2c5a 100644 --- a/crates/text/src/layout/driver.rs +++ b/crates/text/src/layout/driver.rs @@ -182,7 +182,7 @@ fn shrink_to_fit(db: &FontDb, t: &TextObject, opts: &LayoutOptions, min: f64) -> Some(best.unwrap_or_else(|| at(lo))) } -/// `t` with every run's size, explicit leading and baseline shift scaled by `f`, inline art's own +/// `t` with every run's size, explicit leading, baseline shift and tate-chu-yoko offsets scaled by `f`, inline art's own /// shift included (auto leading and the art's size follow the size; paragraph spacing stays). /// Shrink Text to Fit lays `t` out as `scaled(t, f)`, where `f` is the layout's /// [`TextLayout::fit_scale`], so the result holds the styles the text is drawn in. @@ -192,6 +192,8 @@ pub fn scaled(t: &TextObject, f: f64) -> TextObject { r.style.size *= f; r.style.leading = r.style.leading.map(|l| l * f); r.style.baseline_shift *= f; + r.style.tcy_up_down *= f; + r.style.tcy_left_right *= f; if let Some(a) = &mut r.inline { a.baseline_shift *= f; } diff --git a/crates/text/src/layout/vertical.rs b/crates/text/src/layout/vertical.rs index 61251d76e..e571953d8 100644 --- a/crates/text/src/layout/vertical.rs +++ b/crates/text/src/layout/vertical.rs @@ -20,10 +20,11 @@ pub(super) fn whole_character_widths(glyphs: &mut [SGlyph]) { /// /// Manual (Character panel menu › Tate-chu-yoko): any characters marked `tateChuYoko`, including /// one character, letters and numbers longer than three digits. Marked characters are taken -/// before the automatic digit rule. +/// before the automatic digit rule. A marked block moves by the Tate-chu-yoko Settings Up/Down +/// and Left/Right of its first character; an automatic block does not move. /// -pub(super) fn tate_chu_yoko(g: &mut [SGlyph], style: impl Fn(&SGlyph) -> (f64, bool)) { - let manual: Vec = g.iter().map(|gl| gl.ch != '\u{2028}' && style(gl).1).collect(); +pub(super) fn tate_chu_yoko(g: &mut [SGlyph], style: impl Fn(&SGlyph) -> TcyStyle) { + let manual: Vec = g.iter().map(|gl| gl.ch != '\u{2028}' && style(gl).marked).collect(); let digits: Vec = g.iter().map(|gl| gl.inline.is_none() && gl.ch.is_ascii_digit()).collect(); let mut i = 0; while i < g.len() { @@ -32,8 +33,8 @@ pub(super) fn tate_chu_yoko(g: &mut [SGlyph], style: impl Fn(&SGlyph) -> (f64, b while manual.get(end).copied().unwrap_or(false) { end += 1; } - let em = style(&g[i]).0; - place_tcy_block(&mut g[i..end], em); + let first = style(&g[i]); + place_tcy_block(&mut g[i..end], first.size, (first.up, first.right)); i = end; continue; } @@ -46,21 +47,31 @@ pub(super) fn tate_chu_yoko(g: &mut [SGlyph], style: impl Fn(&SGlyph) -> (f64, b end += 1; } if (2..=3).contains(&(end - i)) { - let em = style(&g[i]).0; - place_tcy_block(&mut g[i..end], em); + let em = style(&g[i]).size; + place_tcy_block(&mut g[i..end], em, (0.0, 0.0)); } i = end; } } -/// Lay `g` across one em of the column. The first glyph takes the em; the rest share its cell. -fn place_tcy_block(g: &mut [SGlyph], em: f64) { +/// What [`tate_chu_yoko`] reads from a glyph's character style: its size, whether it is marked +/// Tate-chu-yoko, and its Tate-chu-yoko Settings Up/Down and Left/Right in points. +pub(super) struct TcyStyle { + pub size: f64, + pub marked: bool, + pub up: f64, + pub right: f64, +} + +/// Lay `g` across one em of the column, moved by `(up, right)` points. The first glyph takes the +/// em; the rest share its cell. +fn place_tcy_block(g: &mut [SGlyph], em: f64, (up, right): (f64, f64)) { let width: f64 = g.iter().map(|gl| gl.adv).sum(); let squeeze = if width > em && width > 0.0 { em / width } else { 1.0 }; let mut ink = 0.0; for (k, gl) in g.iter_mut().enumerate() { let adv = gl.adv; - gl.tcy = Some(Tcy { pen: if k == 0 { 0.0 } else { em }, ink, width, squeeze }); + gl.tcy = Some(Tcy { pen: if k == 0 { 0.0 } else { em }, ink, width, squeeze, up, right }); gl.adv = if k == 0 { em } else { 0.0 }; ink += adv; } diff --git a/crates/text/src/shape.rs b/crates/text/src/shape.rs index 273c2e95e..e2ba0a8a9 100644 --- a/crates/text/src/shape.rs +++ b/crates/text/src/shape.rs @@ -134,6 +134,10 @@ pub(crate) struct Tcy { pub width: f64, /// Horizontal scale that fits the block into one em (1 when it fits as it is). pub squeeze: f64, + /// Tate-chu-yoko Settings › Up/Down of the block, in points (positive moves it up). + pub up: f64, + /// Tate-chu-yoko Settings › Left/Right of the block, in points (positive moves it right). + pub right: f64, } impl SGlyph { @@ -724,6 +728,8 @@ fn shapes_alike(a: &CharStyle, b: &CharStyle) -> bool { char_direction: _, proportional_metrics, tate_chu_yoko, + tcy_up_down: _, + tcy_left_right: _, standard_vertical_roman: _, digits, font_height_reference, diff --git a/crates/text/src/tests/combos.rs b/crates/text/src/tests/combos.rs index 02fdd6d79..4145c8937 100644 --- a/crates/text/src/tests/combos.rs +++ b/crates/text/src/tests/combos.rs @@ -53,7 +53,7 @@ fn area(runs: Vec, frame: Rect) -> TextObject { } /// `t` as Shrink Text to Fit lays it out at factor `f`, without fitting: sizes, explicit leading -/// and baseline shifts scaled (inline art's own shift too), paragraph spacing kept. +/// and baseline shifts scaled (inline art's own shift and tate-chu-yoko offsets too), paragraph spacing kept. fn scaled_by(t: &TextObject, f: f64) -> TextObject { let mut s = t.clone(); s.area.fit = AreaFit::None; @@ -61,6 +61,8 @@ fn scaled_by(t: &TextObject, f: f64) -> TextObject { r.style.size *= f; r.style.leading = r.style.leading.map(|l| l * f); r.style.baseline_shift *= f; + r.style.tcy_up_down *= f; + r.style.tcy_left_right *= f; if let Some(a) = &mut r.inline { a.baseline_shift *= f; } diff --git a/crates/text/src/tests/vertical.rs b/crates/text/src/tests/vertical.rs index d2b7f0b4e..9c9d61681 100644 --- a/crates/text/src/tests/vertical.rs +++ b/crates/text/src/tests/vertical.rs @@ -2,7 +2,7 @@ //! Japanese text only with craft-fonts' faces, skipped without them). use crate::*; use kurbo::Shape; -use vectorcraft_doc::{CharStyle, Justify, TextKind, TextRun}; +use vectorcraft_doc::{AreaFit, CharStyle, Justify, TextKind, TextRun}; use vectorcraft_geom::PathData; fn vertical(text: &str) -> TextObject { @@ -381,6 +381,71 @@ fn manual_tate_chu_yoko_sets_marked_characters_across_one_em() { assert!(ink(&row, 1).center().x > ink(&row, 0).center().x + 5.0, "horizontal type stays a row"); } +/// Character panel menu › Tate-chu-yoko Settings: a marked block moves up by a positive Up/Down +/// and right by a positive Left/Right, in points, as one piece (the offsets of its first +/// character), and keeps its one em down the column. An automatic two-digit block and horizontal +/// type do not move. +#[test] +fn tate_chu_yoko_settings_move_a_marked_block_up_down_left_and_right() { + let marked = CharStyle { size: 20.0, tate_chu_yoko: true, ..CharStyle::default() }; + let union = |l: &TextLayout, r: std::ops::Range| r.map(|i| ink(l, i)).reduce(|a, b| a.union(b)).unwrap(); + let at = |up: f64, right: f64| { + let mut t = vertical_styled("", marked.clone()); + t.runs = vec![ + TextRun::new("AB", CharStyle { tcy_up_down: up, tcy_left_right: right, ..marked.clone() }), + TextRun::new("C", CharStyle { tcy_up_down: -up, tcy_left_right: -right, ..marked.clone() }), + TextRun::new("§", CharStyle { size: 20.0, ..CharStyle::default() }), + ]; + layout(FontDb::global(), &t) + }; + let still = at(0.0, 0.0); + for (up, right) in [(5.0, 0.0), (-3.5, 0.0), (0.0, 4.0), (0.0, -2.0), (6.0, -7.0)] { + let moved = at(up, right); + let (a, b) = (union(&still, 0..3), union(&moved, 0..3)); + assert!((b.y0 - (a.y0 - up)).abs() < 1e-6 && (b.y1 - (a.y1 - up)).abs() < 1e-6, "up {up}: {a:?} {b:?}"); + assert!((b.x0 - (a.x0 + right)).abs() < 1e-6 && (b.x1 - (a.x1 + right)).abs() < 1e-6, "right {right}: {a:?} {b:?}"); + assert!((moved.glyphs[0].advance - 20.0).abs() < 1e-6 && moved.glyphs[1..3].iter().all(|g| g.advance == 0.0), "one em"); + assert_eq!(ink(&moved, 3), ink(&still, 3), "the mark after the block stays"); + assert_eq!(moved.glyphs[0].origin, still.glyphs[0].origin, "the caret and hit region stay"); + } + + // An automatic block of two digits takes no offsets, and horizontal type does not move. + let shifted = CharStyle { size: 20.0, tcy_up_down: 5.0, tcy_left_right: 5.0, ..CharStyle::default() }; + let digits = layout(FontDb::global(), &vertical_styled("10", shifted.clone())); + let plain = layout(FontDb::global(), &vertical("10")); + assert_eq!(union(&digits, 0..2), union(&plain, 0..2)); + let row = |st: CharStyle| { + let mut t = TextObject::point(Point::ZERO, "AB", st); + t.xf = Affine::IDENTITY; + layout(FontDb::global(), &t) + }; + assert_eq!(union(&row(CharStyle { tate_chu_yoko: true, ..shifted }), 0..2), union(&row(marked.clone()), 0..2)); +} + +/// Shrink Text to Fit scales a marked block's Up/Down and Left/Right offsets with the size, as it +/// scales the baseline shift. +#[test] +fn shrink_text_to_fit_scales_tate_chu_yoko_offsets() { + let marked = CharStyle { size: 20.0, tate_chu_yoko: true, ..CharStyle::default() }; + let union = |l: &TextLayout, r: std::ops::Range| r.map(|i| ink(l, i)).reduce(|a, b| a.union(b)).unwrap(); + let (up, right) = (6.0, -4.0); + let at = |up: f64, right: f64| { + let mut t = vertical_styled("", marked.clone()); + t.kind = TextKind::Area { frame: PathData::from_bezpath(&Rect::new(0.0, 0.0, 60.0, 60.0).to_path(0.1)) }; + t.area.fit = AreaFit::ShrinkText { min_percent: 10.0 }; + t.runs = vec![ + TextRun::new("AB", CharStyle { tcy_up_down: up, tcy_left_right: right, ..marked.clone() }), + TextRun::new("§§§§§§§§§§§§", CharStyle { size: 20.0, ..CharStyle::default() }), + ]; + layout(FontDb::global(), &t) + }; + let (still, moved) = (at(0.0, 0.0), at(up, right)); + let f = moved.fit_scale; + assert!(f < 1.0 && f == still.fit_scale, "{f} {}", still.fit_scale); + let (a, b) = (union(&still, 0..2), union(&moved, 0..2)); + assert!((b.y0 - (a.y0 - up * f)).abs() < 1e-6 && (b.x0 - (a.x0 + right * f)).abs() < 1e-6, "{f}: {a:?} {b:?}"); +} + /// A marked run is one block; the unmarked digits beside it still follow the automatic rule. /// A column shorter than two ems holds the marked number on one line. #[test] diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 82cd2b8b0..8f365df45 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -113,6 +113,7 @@ pub mod swatch_options; pub mod symbol_delete; pub mod symbol_options; pub mod symbolism_options; +pub mod tate_chu_yoko; mod text_export; pub mod text_import; mod tiff_bmp_tga; @@ -331,6 +332,7 @@ registry! { Corners: [corners::KIND] => corners::SPEC, Hyphenation: [hyphenation::KIND] => hyphenation::SPEC, Justification: [justification::KIND] => justification::SPEC, + TateChuYokoSettings: [tate_chu_yoko::KIND] => tate_chu_yoko::SPEC, SavePdf: [save_pdf::KIND] => save_pdf::SPEC, SvgOptions: [svg_options::KIND] => svg_options::SPEC, JavaScriptFiles: [javascript_files::KIND] => javascript_files::SPEC, diff --git a/crates/ui-egui/src/dialogs/tate_chu_yoko.rs b/crates/ui-egui/src/dialogs/tate_chu_yoko.rs new file mode 100644 index 000000000..d9665c2dc --- /dev/null +++ b/crates/ui-egui/src/dialogs/tate_chu_yoko.rs @@ -0,0 +1,192 @@ +//! Character panel menu › Tate-chu-yoko Settings… (or `ui.tateChuYokoSettings`): the Up/Down and +//! Left/Right of the characters the Character panel acts on, in points, which move a block of +//! tate-chu-yoko characters in vertical type up or down and right or left. The dialog opens on +//! the values the selected characters share (a blank field where they differ). OK runs +//! `text.setFormat` (`text.setRangeStyle` while the Type tool edits text) with the values that +//! changed, as one undo step; characters that differ keep a value left blank. +//! +//! Fields: `tcyUpDown` and `tcyLeftRight` (pt, -1296..1296), as `text.setFormat` takes them, and +//! `__initial` (the values it opened with). +//! +//! Behavior reference (public documentation): +//! . + +use serde_json::{Map, Value, json}; + +use super::swatch_options::{grid, label}; +use super::{DialogSpec, form}; +use crate::VectorcraftApp; +use crate::state::Dialog; + +/// The dialog kind of Tate-chu-yoko Settings. +pub const KIND: &str = "tateChuYokoSettings"; + +/// The field holding the values the dialog opened with. +const INITIAL: &str = "__initial"; + +/// The fields, as `text.setFormat` names them. +const KEYS: [&str; 2] = ["tcyUpDown", "tcyLeftRight"]; + +/// The range of both fields, in points. +const RANGE: std::ops::RangeInclusive = -1296.0..=1296.0; + +pub(super) const SPEC: DialogSpec = DialogSpec { + heading: |_| tl!("Tate-chu-yoko Settings").into(), + body, + confirm, + min_width: 280.0, + // A value the characters don't share opens blank, without its field. + inputs: |_| &KEYS, + ..DialogSpec::FORM +}; + +/// Open Tate-chu-yoko Settings on the characters the Character panel acts on. +pub fn open(app: &mut VectorcraftApp) -> Result { + crate::panels::character::text_style(app).ok_or("select some type first")?; + let (up, right) = crate::panels::character::shared_tcy_offsets(app); + let mut values = Map::new(); + for (key, v) in KEYS.into_iter().zip([up, right]) { + if let Some(v) = v { + values.insert(key.into(), json!(v)); + } + } + let mut fields = Value::Object(values.clone()); + fields[INITIAL] = Value::Object(values); + app.ui.dialog = Some(Dialog::new(KIND, fields)); + Ok(Value::Null) +} + +fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { + let unit = app.session.type_unit(); + grid(ui, |ui| { + for (key, text) in KEYS.into_iter().zip([tl!("Up/Down:"), tl!("Left/Right:")]) { + label(ui, text); + form::length_within(ui, d, key, unit, 90.0, Some(RANGE)); + ui.end_row(); + } + }); + false +} + +/// `text.setFormat` parameters: the fields that differ from the values the dialog opened with, +/// in points (a length typed with its unit, "1 mm", is converted) and kept within -1296..1296. +fn params(d: &Dialog) -> Map { + let initial = d.fields.get(INITIAL).and_then(Value::as_object); + let mut p = Map::new(); + for key in KEYS { + if !d.fields.contains_key(key) { + continue; + } + let v = d.f64(key, 0.0).clamp(*RANGE.start(), *RANGE.end()); + if initial.and_then(|m| m.get(key)).and_then(Value::as_f64) != Some(v) { + p.insert(key.into(), json!(v)); + } + } + p +} + +fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + let p = params(d); + app.ui.dialog = None; + if p.is_empty() { + return Ok(Value::Null); + } + crate::panels::character::run_format(app, Value::Object(p)) +} + +#[cfg(test)] +mod tests { + use super::*; + use vectorcraft_engine::Session; + use vectorcraft_engine::doc::{NodeId, NodeKind}; + + /// A selected point type object with three characters. + fn app() -> (VectorcraftApp, u64) { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 400})).unwrap(); + let id = app.run("text.create", json!({"x": 10, "y": 50, "text": "2026"})).unwrap()["id"].as_u64().unwrap(); + app.run("select.set", json!({"ids": [id]})).unwrap(); + (app, id) + } + + /// (Up/Down, Left/Right) of each run. + fn offsets(app: &VectorcraftApp, id: u64) -> Vec<(f64, f64)> { + match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind { + NodeKind::Text(t) => t.runs.iter().map(|r| (r.style.tcy_up_down, r.style.tcy_left_right)).collect(), + _ => panic!("not text"), + } + } + + /// `ui.tateChuYokoSettings` opens on the characters' values; OK sets both in one undo step, + /// and a length typed with its unit is converted to points. + #[test] + fn tate_chu_yoko_settings_dialog_sets_up_down_and_left_right() { + let (mut app, id) = app(); + app.run("ui.tateChuYokoSettings", json!({})).unwrap(); + let d = app.ui.dialog.clone().expect("Tate-chu-yoko Settings opened"); + assert_eq!(d.kind, KIND); + assert_eq!((d.f64("tcyUpDown", -1.0), d.f64("tcyLeftRight", -1.0)), (0.0, 0.0)); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("tcyUpDown".into(), json!(3)); + d.fields.insert("tcyLeftRight".into(), json!("-1 in")); + let undo = app.session.active().unwrap().history.undo.len(); + super::super::confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none()); + assert_eq!(offsets(&app, id), [(3.0, -72.0)]); + assert_eq!(app.session.active().unwrap().history.undo.len(), undo + 1); + // It opens on the values set, and OK with nothing changed adds no undo step. + app.run("ui.tateChuYokoSettings", json!({})).unwrap(); + let d = app.ui.dialog.clone().unwrap(); + assert_eq!((d.f64("tcyUpDown", 0.0), d.f64("tcyLeftRight", 0.0)), (3.0, -72.0)); + super::super::confirm(&mut app).unwrap(); + assert_eq!(app.session.active().unwrap().history.undo.len(), undo + 1); + // Cancel changes nothing. + app.run("ui.tateChuYokoSettings", json!({})).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("tcyUpDown".into(), json!(9)); + app.ui.dialog = None; + assert_eq!(offsets(&app, id), [(3.0, -72.0)]); + // With no type selected the dialog does not open. + app.run("select.set", json!({"ids": []})).unwrap(); + assert!(app.run("ui.tateChuYokoSettings", json!({})).is_err()); + assert!(app.ui.dialog.is_none()); + } + + /// Characters that differ open a blank field; OK leaves that value alone and sets the other. + #[test] + fn tate_chu_yoko_settings_dialog_leaves_mixed_values_blank() { + let (mut app, id) = app(); + app.run("text.setRangeStyle", json!({"id": id, "start": 0, "end": 2, "tcyUpDown": 4})).unwrap(); + app.run("ui.tateChuYokoSettings", json!({})).unwrap(); + let d = app.ui.dialog.clone().unwrap(); + assert!(!d.fields.contains_key("tcyUpDown")); + assert_eq!(d.f64("tcyLeftRight", -1.0), 0.0); + app.ui.dialog.as_mut().unwrap().fields.insert("tcyLeftRight".into(), json!(2)); + super::super::confirm(&mut app).unwrap(); + assert_eq!(offsets(&app, id), [(4.0, 2.0), (0.0, 2.0)]); + } + + /// OK returns the error of a command that is refused, and the characters keep their values. + #[test] + fn tate_chu_yoko_settings_dialog_returns_a_refused_command_error() { + let (mut app, id) = app(); + app.run("ui.tateChuYokoSettings", json!({})).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("tcyUpDown".into(), json!(5)); + app.run("select.set", json!({"ids": []})).unwrap(); + assert!(super::super::confirm(&mut app).is_err()); + assert!(app.ui.dialog.is_none()); + assert_eq!(offsets(&app, id), [(0.0, 0.0)]); + } + + /// While the Type tool edits text, OK sets the selected characters and returns the result. + #[test] + fn tate_chu_yoko_settings_dialog_sets_the_range_the_type_tool_selects() { + let (mut app, id) = app(); + app.session.select_tool("type", Default::default()).unwrap(); + app.session.set_tool_option("textSelection", &json!({"editing": id, "anchor": 0, "caret": 2})); + assert_eq!(crate::panels::character::text_editing(&app), Some((NodeId(id), 0, 2))); + app.run("ui.tateChuYokoSettings", json!({})).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("tcyLeftRight".into(), json!(6)); + assert!(super::super::confirm(&mut app).is_ok()); + assert_eq!(offsets(&app, id), [(0.0, 6.0), (0.0, 0.0)]); + } +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index cbbf3e550..7a038d041 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -3279,6 +3279,10 @@ digits Malayalam Малаялам Target Contents Целево съдържание Target: Цел: Tate-chu-yoko Тате-чу-йоко + Tate-chu-yoko Settings… Настройки на Тате-чу-йоко… + Tate-chu-yoko Settings Настройки на Тате-чу-йоко + Up/Down: Нагоре/надолу: + Left/Right: Наляво/надясно: Temperature Температура Temperature/Tint… Температура/Нюанс… Template Шаблон diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index c841a7756..f6fa7f7bb 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -1451,6 +1451,10 @@ digits Malayalam Malajalam Superscript (on the roadmap) Eksponent (u planu) System Layout Sistemski raspored Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Postavke za Tate-chu-yoko… + Tate-chu-yoko Settings Postavke za Tate-chu-yoko + Up/Down: Gore/dolje: + Left/Right: Lijevo/desno: Underline Podvučeno Vertical Scale % Okomita skala % Axis Osa diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 403e9c8bc..e35e94afa 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -4194,6 +4194,10 @@ digits Malayalam Malaiàlam Target Contents Contingut del destí Target: Destí: Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Configuració de Tate-chu-yoko… + Tate-chu-yoko Settings Configuració de Tate-chu-yoko + Up/Down: Amunt/avall: + Left/Right: Esquerra/dreta: Technical Drawing Dibuix tècnic Temperature Temperatura Temperature/Tint… Temperatura/To… diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index f634e9fec..0e1bf1e9a 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -1475,6 +1475,10 @@ digits Malayalam Malajálamské Superscript (on the roadmap) Horní index (v plánu) System Layout Systémové rozvržení Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Nastavení Tate-chu-yoko… + Tate-chu-yoko Settings Nastavení Tate-chu-yoko + Up/Down: Nahoru/dolů: + Left/Right: Vlevo/vpravo: Underline Podtržení Vertical Scale % Svislá změna velikosti % Axis Osa diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index ea6c96d80..043623e46 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -1385,6 +1385,10 @@ digits Malayalam Malayalam Superscript (on the roadmap) Hævet skrift (på vejkortet) System Layout Systemlayout Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Indstillinger for Tate-chu-yoko… + Tate-chu-yoko Settings Indstillinger for Tate-chu-yoko + Up/Down: Op/ned: + Left/Right: Venstre/højre: Underline Understreget Vertical Scale % Lodret skalering % Axis Akse diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index ce6f53f62..5366b7929 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -3300,6 +3300,10 @@ effect Points Punkte Target Contents Inhalt als Ziel auswählen Target: Ziel: Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Tate-chu-yoko-Einstellungen… + Tate-chu-yoko Settings Tate-chu-yoko-Einstellungen + Up/Down: Oben/Unten: + Left/Right: Links/Rechts: Temperature Temperatur Temperature/Tint… Temperatur/Farbstich… Template Vorlage diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 40bf0907b..339ac5ee4 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3697,6 +3697,10 @@ digits Malayalam Μαλαγιάλαμ Target Στόχος Target Contents Περιεχόμενα στόχου Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Ρυθμίσεις Tate-chu-yoko… + Tate-chu-yoko Settings Ρυθμίσεις Tate-chu-yoko + Up/Down: Πάνω/Κάτω: + Left/Right: Αριστερά/Δεξιά: Temperature/Tint… Θερμοκρασία/Απόχρωση… Templates Folder Φάκελος προτύπων Text Displayed in Cell: Κείμενο που εμφανίζεται στο κελί: diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index ad849fc00..fbf96dc4c 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3357,6 +3357,10 @@ digits Malayalam Malayálam Target Contents Contenido de destino Target: Destino: Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Ajustes de Tate-chu-yoko… + Tate-chu-yoko Settings Ajustes de Tate-chu-yoko + Up/Down: Arriba/abajo: + Left/Right: Izquierda/derecha: Temperature Temperatura Temperature/Tint… Temperatura/tinte… Template Plantilla diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 9c8b9e051..aad23fcdf 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -1448,6 +1448,10 @@ digits Malayalam Malajalam Superscript (on the roadmap) Yläindeksi (suunnitteilla) System Layout Järjestelmän asettelu Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Tate-chu-yoko-asetukset… + Tate-chu-yoko Settings Tate-chu-yoko-asetukset + Up/Down: Ylös/alas: + Left/Right: Vasen/oikea: Underline Alleviivaus Vertical Scale % Pystyskaalaus % Axis Akseli diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 5179c001a..3f11674f1 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3349,6 +3349,10 @@ digits Malayalam Malayalam Target Contents Contenu de la cible Target: Cible : Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Paramètres Tate-chu-yoko… + Tate-chu-yoko Settings Paramètres Tate-chu-yoko + Up/Down: Haut/bas : + Left/Right: Gauche/droite : Temperature Température Temperature/Tint… Température/teinte… Template Modèle diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 9811e5cd1..f856894dc 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4004,6 +4004,10 @@ mark-style Japanese Japán Target Contents Tartalom célzása Target: Cél: Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Tate-chu-yoko beállításai… + Tate-chu-yoko Settings Tate-chu-yoko beállításai + Up/Down: Fel/le: + Left/Right: Balra/jobbra: Technical Drawing Műszaki rajz Temperature Színhőmérséklet Temperature/Tint… Színhőmérséklet/tónus… diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 510aa4469..42b881cb8 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -1385,6 +1385,10 @@ digits Malayalam Malayalam Superscript (on the roadmap) Superskrip (masih dalam roadmap) System Layout Tata letak sistem Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Pengaturan Tate-chu-yoko… + Tate-chu-yoko Settings Pengaturan Tate-chu-yoko + Up/Down: Atas/bawah: + Left/Right: Kiri/kanan: Underline Garis bawah Vertical Scale % Skala vertikal % Axis Sumbu diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 29566d7d1..b4ed1dd1b 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3392,6 +3392,10 @@ digits Malayalam Malayalam Target Contents Contenuto destinazione Target: Destinazione: Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Impostazioni Tate-chu-yoko… + Tate-chu-yoko Settings Impostazioni Tate-chu-yoko + Up/Down: Su/giù: + Left/Right: Sinistra/destra: Temperature Temperatura Temperature/Tint… Temperatura/tinta… Template Modello diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 6d79901f8..7f7d212a7 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -3707,6 +3707,10 @@ file-filter Templates テンプレート Tabular Oldstyle 等幅オールドスタイル Titling Alternates タイトル代替字 Tate-chu-yoko 縦中横 + Tate-chu-yoko Settings… 縦中横の設定… + Tate-chu-yoko Settings 縦中横の設定 + Up/Down: 上下: + Left/Right: 左右: Temperature 色温度 Template テンプレート Templates テンプレート diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 4bff917ee..1cbcec98d 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -1302,6 +1302,10 @@ digits Malayalam მალაიალამური Superscript (on the roadmap) ზედა ინდექსი (სამომავლო გეგმაშია) System Layout სისტემის განლაგება Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Tate-chu-yoko პარამეტრები… + Tate-chu-yoko Settings Tate-chu-yoko პარამეტრები + Up/Down: ზემოთ/ქვემოთ: + Left/Right: მარცხნივ/მარჯვნივ: Underline ხაზგასმული Vertical Scale % ვერტიკალური მასშტაბი % Axis ღერძი diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index e9ed52abb..1e98b5bde 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -3695,6 +3695,10 @@ file-filter Templates 템플릿 Tabular Oldstyle 고정폭 올드스타일 Titling Alternates 제목 대체 글자 Tate-chu-yoko 다테추요코 + Tate-chu-yoko Settings… 다테추요코 설정… + Tate-chu-yoko Settings 다테추요코 설정 + Up/Down: 위/아래: + Left/Right: 왼쪽/오른쪽: Temperature 색온도 Template 템플릿 Templates 템플릿 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 730a5f03b..e5e558c55 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -1399,6 +1399,10 @@ digits Malayalam Malayalam Superscript (on the roadmap) Superscript (op de roadmap) System Layout Systeemlayout Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Tate-chu-yoko-instellingen… + Tate-chu-yoko Settings Tate-chu-yoko-instellingen + Up/Down: Omhoog/omlaag: + Left/Right: Links/rechts: Underline Onderstrepen Vertical Scale % Verticale schaal % Axis As diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 146c29c71..984c727cd 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -1388,6 +1388,10 @@ digits Malayalam Malajalam Superscript (on the roadmap) Indeks górny (w planach) System Layout Układ systemowy Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Ustawienia Tate-chu-yoko… + Tate-chu-yoko Settings Ustawienia Tate-chu-yoko + Up/Down: W górę/w dół: + Left/Right: W lewo/w prawo: Underline Podkreślenie Vertical Scale % Skala pionowa % Axis Oś diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index eacef0f4c..eacf83722 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -4770,6 +4770,10 @@ axis Both Ambos Tabular Oldstyle Antigo Tabular Titling Alternates Variantes de Titulação Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Configurações de Tate-chu-yoko… + Tate-chu-yoko Settings Configurações de Tate-chu-yoko + Up/Down: Para Cima/Para Baixo: + Left/Right: Esquerda/Direita: Temperature Temperatura Templates Modelos Text Wrap Options Opções de Quebra de Texto diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index e7b9fd659..1369da148 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3387,6 +3387,10 @@ digits Malayalam Малаялам Target Contents Содержимое объекта Target: Объект: Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Параметры Tate-chu-yoko… + Tate-chu-yoko Settings Параметры Tate-chu-yoko + Up/Down: Вверх/вниз: + Left/Right: Влево/вправо: Temperature Температура Temperature/Tint… Температура/оттенок… Template Шаблон diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 11398b46a..57c214cd7 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -3279,6 +3279,10 @@ digits Malayalam Malayalam Target Contents Målets innehåll Target: Mål: Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Inställningar för Tate-chu-yoko… + Tate-chu-yoko Settings Inställningar för Tate-chu-yoko + Up/Down: Upp/ned: + Left/Right: Vänster/höger: Temperature Temperatur Temperature/Tint… Temperatur/färgton… Template Mall diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index da1a8cb96..c6b19a13c 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -3172,6 +3172,10 @@ digits Malayalam Malayalam Target Contents Hedef İçerikler Target: Hedef: Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Tate-chu-yoko Ayarları… + Tate-chu-yoko Settings Tate-chu-yoko Ayarları + Up/Down: Yukarı/Aşağı: + Left/Right: Sol/Sağ: Temperature Sıcaklık Temperature/Tint… Sıcaklık/Renk Tonu… Template Şablon diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index d9f29cd3e..53fbc4335 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3385,6 +3385,10 @@ effect Points Точки Target Contents Вміст об’єкта Target: Об’єкт: Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Параметри Tate-chu-yoko… + Tate-chu-yoko Settings Параметри Tate-chu-yoko + Up/Down: Вгору/вниз: + Left/Right: Ліворуч/праворуч: Temperature Температура Temperature/Tint… Температура/відтінок… Template Шаблон diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 529e20dac..7f3cca98e 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -3714,6 +3714,10 @@ mark-style Japanese Kiểu Nhật Target Contents Nội dung mục tiêu Target: Mục tiêu: Tate-chu-yoko Tate-chu-yoko + Tate-chu-yoko Settings… Cài đặt Tate-chu-yoko… + Tate-chu-yoko Settings Cài đặt Tate-chu-yoko + Up/Down: Lên/Xuống: + Left/Right: Trái/Phải: Temperature Nhiệt độ màu Temperature/Tint… Nhiệt độ màu/Tông… Template Mẫu diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 104e44209..d9ea32e27 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3224,6 +3224,10 @@ digits Malayalam 马拉雅拉姆数字 Target Contents 目标内容 Target: 目标: Tate-chu-yoko 纵中横 + Tate-chu-yoko Settings… 纵中横设置… + Tate-chu-yoko Settings 纵中横设置 + Up/Down: 上/下: + Left/Right: 左/右: Temperature 色温 Temperature/Tint… 色温/色调… Template 模板 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index b10c5ab5e..37204b81a 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -4234,6 +4234,10 @@ digits Malayalam 馬來亞拉姆數字 Target Contents 目標內容 Target: 目標: Tate-chu-yoko 縱中橫 + Tate-chu-yoko Settings… 縱中橫設定… + Tate-chu-yoko Settings 縱中橫設定 + Up/Down: 上/下: + Left/Right: 左/右: Temperature 色溫 Temperature/Tint… 色溫/色調… Template 範本 diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs index 92a625174..e526e13ce 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -743,6 +743,7 @@ fn run_ui_part10(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option crate::dialogs::corners::open(app, p), "ui.hyphenation" => crate::dialogs::hyphenation::open(app), "ui.justification" => crate::dialogs::justification::open(app), + "ui.tateChuYokoSettings" => crate::dialogs::tate_chu_yoko::open(app), "ui.colorGuideLimit" => crate::panels::color_guide::set_limit(app, p), "ui.colorPanel" => crate::panels::color::command(app, p), "ui.savePdfDialog" => crate::dialogs::open_save_pdf(app, p), diff --git a/crates/ui-egui/src/menus/state.rs b/crates/ui-egui/src/menus/state.rs index 762b98c4b..8f8bd53f4 100644 --- a/crates/ui-egui/src/menus/state.rs +++ b/crates/ui-egui/src/menus/state.rs @@ -235,7 +235,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool { "ui.sampleScreenColor" => crate::screen_color::available(app), "ui.newBrush" => app.session.active().is_some(), "ui.brushOptions" => crate::dialogs::brush_options::available(app), - "ui.hyphenation" | "ui.justification" => crate::panels::character::text_style(app).is_some(), + "ui.hyphenation" | "ui.justification" | "ui.tateChuYokoSettings" => crate::panels::character::text_style(app).is_some(), "ui.colorBalanceDialog" | "ui.saturateDialog" => app.session.active().is_some_and(|d| !d.selection.is_empty()), "ui.saveSwatchLibrary" => app.session.active().is_some(), id if id.starts_with(crate::panels::swatches::USER_SLOT) => crate::panels::swatches::user_library(app, id).is_some(), diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index f642d0ee1..133952d36 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -990,6 +990,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "", "{} open Hyphenation (the Paragraph panel menu's Hyphenation…) on the paragraphs the Paragraph panel acts on, filled in with the values it shows (dialog `hyphenation`: hyphenate, wordsLongerThan 1..25, afterFirst 1..15, beforeLast 1..15, hyphenLimit 0..25 (0: no limit), hyphenationZone (pt), hyphenateCapitalized): OK runs text.setFormat with the values that changed, as one undo step; an error with no type selected", ), + ( + "ui.tateChuYokoSettings", + "Tate-chu-yoko Settings…", + "", + "{} open Tate-chu-yoko Settings (the Character panel menu's Tate-chu-yoko Settings…, with Show East Asian Options) on the characters the Character panel acts on, filled in with the values they share (dialog `tateChuYokoSettings`: tcyUpDown, tcyLeftRight (pt, -1296..1296)): OK runs text.setFormat (text.setRangeStyle while the Type tool edits text) with the values that changed, as one undo step; an error with no type selected", + ), ( "ui.colorGuideLimit", "Limit Color Guide to Library", diff --git a/crates/ui-egui/src/panels/character.rs b/crates/ui-egui/src/panels/character.rs index db01f2e38..f1f0707f6 100644 --- a/crates/ui-egui/src/panels/character.rs +++ b/crates/ui-egui/src/panels/character.rs @@ -218,10 +218,30 @@ impl Shared { fn style(app: &mut VectorcraftApp, p: Value) { char_cmd(app, "text.setStyle", p); } -fn format(app: &mut VectorcraftApp, p: Value) { +pub(crate) fn format(app: &mut VectorcraftApp, p: Value) { char_cmd(app, "text.setFormat", p); } +/// [`format`] that returns the command's result: `text.setRangeStyle` on the range the Type tool +/// has selected (the whole text when nothing is selected) while it edits text, else +/// `text.setFormat` on the selected text objects. +pub(crate) fn run_format(app: &mut VectorcraftApp, mut p: Value) -> Result { + let Some((id, a, b)) = text_editing(app) else { return app.run("text.setFormat", p) }; + end_typing(app); + p["id"] = json!(id.0); + if b > a { + p["start"] = json!(a); + p["end"] = json!(b); + } + app.run("text.setRangeStyle", p) +} + +/// The Tate-chu-yoko Settings Up/Down and Left/Right the characters the panels act on share, in +/// document points; `None` where they differ, or with no type. +pub(crate) fn shared_tcy_offsets(app: &VectorcraftApp) -> (Option, Option) { + (shared_effective(app, |s| s.tcy_up_down), shared_effective(app, |s| s.tcy_left_right)) +} + /// Apply character attributes: to the selected range while the Type tool edits text (the whole /// object when nothing is selected), else to the selected text objects. fn char_cmd(app: &mut VectorcraftApp, cmd: &str, p: Value) { @@ -1168,6 +1188,12 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { if menu_item(ui, tl!("Tate-chu-yoko"), has, tcy) { format(app, json!({"tateChuYoko": !tcy})); } + // Tate-chu-yoko Settings…: the Up/Down and Left/Right that move a tate-chu-yoko block. + if menu_item(ui, tl!("Tate-chu-yoko Settings…"), has, false) + && let Err(e) = app.run("ui.tateChuYokoSettings", json!({})) + { + app.status(e); + } // Standard Vertical Roman Alignment: the selected half-width characters stand upright in // vertical type instead of being rotated. Checked when every selected character has it; a // mixed selection turns it on. @@ -1239,9 +1265,9 @@ fn fractional_widths_on(app: &VectorcraftApp) -> bool { /// kerning, no baseline shift, 100% scales, no rotation, no underline or strikethrough, All Caps /// and Small Caps off, the normal position (neither superscript nor subscript), Roman Baseline /// character alignment, Proportional Metrics, Tate-chu-yoko, Standard Vertical Roman Alignment and -/// No Break off, the Default character direction, the Default digits and the Em Box font height -/// reference, whether or not Show East Asian Options, Show Indic Options and Show Font Height -/// Options are on. +/// No Break off, Tate-chu-yoko Settings Up/Down and Left/Right 0, the Default character direction, +/// the Default digits and the Em Box font height reference, whether or not Show East Asian +/// Options, Show Indic Options and Show Font Height Options are on. fn reset_panel(app: &mut VectorcraftApp) { let d = CharStyle::default(); style(app, json!({"tracking": 0, "leading": "auto"})); @@ -1251,6 +1277,7 @@ fn reset_panel(app: &mut VectorcraftApp) { "kerning": "auto", "baselineShift": 0, "hScale": 100, "vScale": 100, "rotation": 0, "underline": false, "strikethrough": false, "allCaps": false, "smallCaps": d.small_caps.is_some(), "position": d.position.id(), "charAlign": d.char_align, "proportionalMetrics": d.proportional_metrics, "tateChuYoko": d.tate_chu_yoko, + "tcyUpDown": d.tcy_up_down, "tcyLeftRight": d.tcy_left_right, "standardVerticalRoman": d.standard_vertical_roman, "noBreak": d.no_break, "charDirection": "default", "digits": d.digits, "fontHeightReference": d.font_height_reference }), diff --git a/crates/ui-egui/src/panels/character/tests.rs b/crates/ui-egui/src/panels/character/tests.rs index f4774dd01..d9c419f12 100644 --- a/crates/ui-egui/src/panels/character/tests.rs +++ b/crates/ui-egui/src/panels/character/tests.rs @@ -673,6 +673,23 @@ fn tate_chu_yoko_menu_item_shows_with_east_asian_options_and_toggles() { assert!(!on(&app)); } +/// Tate-chu-yoko Settings… is in the panel menu with the East Asian options, and choosing it opens +/// the dialog on the selected characters. +#[test] +fn tate_chu_yoko_settings_menu_item_shows_with_east_asian_options_and_opens_the_dialog() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 200})).unwrap(); + let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "2026"})).unwrap()["id"].as_u64().unwrap(); + app.session.execute("select.set", &json!({"ids": [id]})).unwrap(); + let ctx = egui::Context::default(); + assert!(!crate::tests_labels::painted_text(&mut app, menu).contains("Tate-chu-yoko Settings…")); + app.session.prefs.show_east_asian_options = true; + let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], menu); + let at = texts.iter().find(|(t, _)| t.trim() == "Tate-chu-yoko Settings…").unwrap_or_else(|| panic!("{texts:?}")).1; + super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu); + assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(crate::dialogs::tate_chu_yoko::KIND)); +} + /// Standard Vertical Roman Alignment is in the panel menu with the East Asian options. Choosing it /// marks the selected characters, one undo step each way; its check shows only while every /// selected character has it, and a mixed selection turns it on. @@ -995,7 +1012,7 @@ fn no_break_menu_item_toggles_the_selected_characters() { /// Reset Panel sets every character attribute the panel shows, except the font family, style /// and size, back to its default (Small Caps, the position, Character Alignment, Proportional -/// Metrics, Tate-chu-yoko, No Break, Character Direction, Digits and the font height reference +/// Metrics, Tate-chu-yoko and its settings, No Break, Character Direction, Digits and the font height reference /// included), in the selected type and in the text the Type tool has selected while it edits. #[test] fn reset_panel_resets_every_attribute_but_the_font() { @@ -1023,6 +1040,8 @@ fn reset_panel_resets_every_attribute_but_the_font() { s.char_align != d.char_align, s.proportional_metrics != d.proportional_metrics, s.tate_chu_yoko != d.tate_chu_yoko, + s.tcy_up_down != d.tcy_up_down, + s.tcy_left_right != d.tcy_left_right, s.standard_vertical_roman != d.standard_vertical_roman, s.no_break != d.no_break, s.char_direction != d.char_direction, @@ -1036,17 +1055,17 @@ fn reset_panel_resets_every_attribute_but_the_font() { json!({ "id": id, "tracking": 50, "leading": 30, "kerning": 20, "baselineShift": 3, "hScale": 120, "vScale": 90, "rotation": 15, "underline": true, "strikethrough": true, "allCaps": true, "smallCaps": true, "position": "superscript", - "charAlign": "emBoxCenter", "proportionalMetrics": true, "tateChuYoko": true, "standardVerticalRoman": true, - "noBreak": true, "charDirection": "rightToLeft", "digits": "hindi", "fontHeightReference": "capHeight" + "charAlign": "emBoxCenter", "proportionalMetrics": true, "tateChuYoko": true, "tcyUpDown": 2, "tcyLeftRight": -3, + "standardVerticalRoman": true, "noBreak": true, "charDirection": "rightToLeft", "digits": "hindi", "fontHeightReference": "capHeight" }), ) .unwrap(); }; let plain = runs(&app)[0].1.clone(); - assert_eq!(differs(&plain, &CharStyle::default()), [false; 20]); + assert_eq!(differs(&plain, &CharStyle::default()), [false; 22]); custom(&mut app); let styled = runs(&app)[0].1.clone(); - assert_eq!(differs(&styled, &CharStyle::default()), [true; 20]); + assert_eq!(differs(&styled, &CharStyle::default()), [true; 22]); app.run("select.set", json!({"ids": [id]})).unwrap(); reset_panel(&mut app); assert_eq!(runs(&app), [("One 12".to_string(), plain.clone())]); diff --git a/docs/mcp.md b/docs/mcp.md index 40fe307d3..7b7a60d4c 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4963,6 +4963,24 @@ undo step. {"name":"run_command","arguments":{"command":"text.setFormat","params":{"burasagari":"forced"}}} ``` +## Tate-chu-yoko Settings + +`text.setFormat` and `text.setRangeStyle {tcyUpDown?, tcyLeftRight?}` set the Character panel menu's Tate-chu-yoko +Settings of the characters, in points from -1296 to 1296 (document points, as `baselineShift`). In vertical type a +block of tate-chu-yoko characters (`tateChuYoko`) moves up by a positive Up/Down and down by a negative one, and right +by a positive Left/Right and left by a negative one. The block moves as one piece with the values of its first +character, and keeps its one em down the column, its caret positions and its hit region. Two or three digits set +across the column without the mark do not move, and horizontal type keeps the values without moving the characters. +Neither value turns `tateChuYoko` on. Both are 0 by default and saved only when not 0, and character styles carry +them. A value that is not a number is refused with `` `tcyUpDown` must be a number ``, and one outside the range with +`` `tcyUpDown` must be from -1296 to 1296 ``, naming the key. One undo step. `ui.tateChuYokoSettings` opens the +Tate-chu-yoko Settings dialog (the menu item shows with Show East Asian Options) on the values the selected characters +share, a blank field where they differ; OK sends the values that changed. + +```json +{"name":"run_command","arguments":{"command":"text.setRangeStyle","params":{"id":12,"start":0,"end":4,"tateChuYoko":true,"tcyUpDown":2,"tcyLeftRight":-1}}} +``` + ## Optical Margin Alignment `type.opticalMarginAlignment {on?: bool}` (Type › Optical Margin Alignment) turns Optical Margin Alignment on or off diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index c6e2172dd..f1ed76b9d 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -908,6 +908,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The Tabs panel menu's Clear All Tabs (`text.tabs.clear`), Delete Tab and Repeat Tab (`text.tabs.set`) can be chosen while the panel's ruler shows tab stops. Delete Tab removes the stop selected on the ruler. Repeat Tab replaces the stops after the selected stop with up to 20 copies of it. The copies are spaced by the selected stop's distance from the stop before it, or from the paragraph's left indent when it is the first stop, and no copy is placed at 2,000 pt or beyond. Clear All Tabs removes every stop, so the default stops every ½ inch apply again. - **Interface languages, vertical type:** Preferences ▸ User Interface (or VectorCraft ▸ Language on macOS) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional and Simplified Chinese, Japanese, German (every `de-*` locale), Spanish (neutral international Spanish, for every `es-*` locale), French (every `fr-*` locale), Italian (every `it-*` locale), Russian (every `ru-*` locale) and Ukrainian (every `uk-*` locale), Catalan (every `ca-*` locale) and Polish (every `pl-*` locale; AI-drafted, native review pending) cover the whole interface, Czech every menu label; Czech, German, Greek, Spanish, French, Hungarian, Italian, Japanese, Korean, Vietnamese, Polish, Brazilian Portuguese, Russian, Simplified Chinese, Traditional Chinese, Swedish, Ukrainian and Finnish also translate status-bar and error messages as they are shown (`@msg` templates, every message literal tested; the messages themselves stay English for agents); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech, German, Spanish, French, Italian, Russian and Ukrainian ones from the bundled UI fonts. The web app follows the browser's languages (or `?lang=`). The Basic toolbar's Type button holds Vertical Type in its flyout. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku applies to horizontal and vertical Japanese (Paragraph panel › Kinsoku Set, `text.setFormat {kinsoku}`: Hard, the default, keeps closing marks, iteration marks, the prolonged sound mark and small kana off the line start and opening brackets off its end; Soft lets 々, ー and small kana start a line as JLREQ's level 3 rules do; None turns it off), and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph panel › Mojikumi Set (shown with Preferences › Type › Show East Asian Options, `text.setFormat {mojikumi}`): None or Line-end Punctuation Half Width (new type's default, as in Illustrator; older documents and imported text keep None) shares one half-em space between consecutive punctuation, sets a closing mark ending a line half width and an opening bracket starting a line flush with its start (a paragraph's first line at its first-line indent, JIS X 4051's principle in JLREQ 3.1.5), and puts a quarter em between Japanese and Latin letters or digits; Paragraph panel menu › Top-to-Top / Bottom-to-Bottom Leading measures leading from em box top to top (area type's first line touches the frame) or from baseline to baseline (new type in a Japanese interface starts top to top, with em box centre character alignment, #432); Paragraph panel menu › Burasagari None / Regular / Force (`text.setFormat {burasagari: "none"|"standard"|"forced"}`) hangs an East Asian comma or full stop (、。,.、。) ending an area type line outside it, Regular (new type's default) when it doesn't fit and Force always (the rest of the line then fills the measure; closing brackets and Latin punctuation don't hang); Character panel menu › Character Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {charAlign}`) lines characters smaller than the largest on their line up on the Roman baseline, the em box top/right, centre or bottom/left, or the ICF (ideographic character face) top/right or bottom/left, from the font's BASE table or, without one, the average ink box of some ideographs and kana; Proportional Metrics (the same menu; `{proportionalMetrics}`) sets full-width glyphs on the font's proportional widths (OpenType `palt`) in horizontal type and upright glyphs on its proportional heights (`vpal`, shaped top to bottom; fonts without vertical metrics keep their cells) in vertical type, and Line-end Punctuation Half Width takes nothing more off the punctuation they re-spaced; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines. The interface is available in Polish (`pl`): menus, panels, dialogs and messages, with one/few/many plurals; AI-drafted, native review pending (#1498). Character panel menu › Standard Vertical Roman Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {standardVerticalRoman}`, off by default) stands the selected Latin letters, digits and other half-width characters upright one below the other in vertical type instead of rotating them ([formatting Asian characters](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)); spaces keep their width, tate-chu-yoko blocks stay across the column, and Character Style Options gives it its own checkbox beside Character Alignment. + Character panel menu › Tate-chu-yoko Settings… (with the East Asian options; `ui.tateChuYokoSettings`, `text.setFormat` or `text.setRangeStyle {tcyUpDown, tcyLeftRight}`, in points, 0 by default) moves a marked tate-chu-yoko block in vertical type up by a positive Up/Down and right by a positive Left/Right ([format East Asian characters in vertical text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/format-east-asian-characters-in-vertical-text.html), [formatting Asian characters](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)); the block moves as one piece with its first character's values and keeps its one em down the column, automatic digit blocks do not move, Reset Panel sets both back to 0, and character styles and the native file keep them. The Character panel's All Caps, Underline and Strikethrough buttons and its menu's All Caps, Underline, Strikethrough, Proportional Metrics, Tate-chu-yoko and No Break show off while only some of the selected characters have the attribute, and choosing one then turns it on for all of them, as `type.underline` (Cmd+Shift+U) does for underlining; the Character Alignment and Digits submenus check no item while the selected characters differ. `text.setFormat` refuses a value that is not true or false for its on/off attributes, as `text.setRangeStyle` does. - **Transparency:** opacity masks (clip/invert/disable/link), exported as SVG `` and PDF soft masks. The constant alpha soft mask that PDF and `.ai` export draw a CMYK page with transparency through is white, so applications that read every soft mask by its luminosity show the page too (#1359, part of #1286). Isolation mode and opacity mask editing exclude each other ([transparency and blending modes](https://web.archive.org/web/20230102045151/https://helpx.adobe.com/illustrator/using/transparency-blending-modes.html)): in isolation mode a click or Alt-click on the mask thumbnail does not edit the mask, and Make Mask is off for one object; while a mask is edited, Enter Isolation Mode and Object › Perspective › Edit Text are off, the Contextual Task Bar leaves out Isolate, and Quick Actions leave out Isolate Group. diff --git a/docs/type-parity.md b/docs/type-parity.md index 4520315b6..5542a37f1 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -58,7 +58,7 @@ against Illustrator's public documentation; **estimated** unless noted. | CJK: input methods (marked text, candidate window) | done | | | CJK: kinsoku (Hard, Soft, None), Line-end Punctuation Half Width, tate-chu-yoko (auto) | done / partial | custom kinsoku sets, the Mojikumi Settings dialog and the other mojikumi sets missing | | CJK: Proportional Metrics (`palt` in horizontal type, `vpal` in vertical type) | done | #966 | -| CJK: manual tate-chu-yoko | done | Character panel menu › Tate-chu-yoko (`text.setFormat` / `text.setRangeStyle {tateChuYoko}`), with Show East Asian Options: marked characters stand side by side across one em of a vertical column, squeezed to the em when wider; two or three half-width digits still do so without the mark; horizontal type keeps the mark and does not move them. Up/Down and Left/Right settings remain. Evidence: `tests_vertical::manual_tate_chu_yoko_sets_marked_characters_across_one_em`, `typecmd::tate_chu_yoko_is_a_character_attribute_saved_only_when_on` | +| CJK: manual tate-chu-yoko | done | Character panel menu › Tate-chu-yoko (`text.setFormat` / `text.setRangeStyle {tateChuYoko}`), with Show East Asian Options: marked characters stand side by side across one em of a vertical column, squeezed to the em when wider; two or three half-width digits still do so without the mark; horizontal type keeps the mark and does not move them. Tate-chu-yoko Settings… (`ui.tateChuYokoSettings`, `{tcyUpDown, tcyLeftRight}` in points) moves a marked block up or down and right or left as one piece. Evidence: `tests_vertical::manual_tate_chu_yoko_sets_marked_characters_across_one_em`, `tests_vertical::tate_chu_yoko_settings_move_a_marked_block_up_down_left_and_right`, `typecmd::tate_chu_yoko_is_a_character_attribute_saved_only_when_on`, `typecmd::tate_chu_yoko_settings_are_character_attributes_saved_only_when_set`, `dialogs::tate_chu_yoko::tests` | | CJK: Standard Vertical Roman Alignment | done | Character panel menu › Standard Vertical Roman Alignment (`text.setFormat` / `text.setRangeStyle {standardVerticalRoman}`), with Show East Asian Options, off by default: in vertical type the marked half-width characters (Latin letters, digits and other characters that would lie on their side) stand upright one below the other, each in its upright cell; spaces keep their width and tate-chu-yoko blocks stay across the column; horizontal type keeps the mark and does not change. Character Style Options has its own checkbox for it. Evidence: `tests_vertical::standard_vertical_roman_alignment_stands_half_width_characters_upright`, `typecmd::standard_vertical_roman_is_a_character_attribute_saved_only_when_on` | | CJK: ruby, manual tate-chu-yoko | partial | ruby (ふりがな) for live text (`type.ruby`): a reading over a byte range, above (right of the column in vertical type) or below, start/centre/end alignment, size, overhang or squeeze; saved natively, laid out with its line growing, drawn on the canvas, in SVG and PDF (as outlines where ruby is present), in EPS, EMF/WMF and legacy `.ai` (legacy `.ai` writes type with ruby as outlines), and kept by Create Outlines, Expand Appearance and flattening, each glyph where the layout puts it (`typecmd::ruby_tests::create_outlines_keeps_the_ruby_where_it_is_laid_out`); manual tate-chu-yoko missing | #633 | | Language per character (Character panel › Language, character styles), text shaped in it | done | `text.setFormat` / `text.setRangeStyle {language}` by name or BCP 47 tag, one of Document Setup's languages (Hindi, Marathi, Gujarati, Bengali, Punjabi, Tamil, Telugu, Kannada, Malayalam, Odia and Urdu added, #4943; Serbian: Cyrillic, Serbian: Latin and Croatian); the shaper gets the tag, so the font's language forms apply (OpenType `locl`: Turkish keeps `fi` apart and the dot on its i, #4940); new type takes the document's language; saved as the tag only when set, carried by character styles (#4946). Not yet: hyphenation and spelling by it, `.ai`/PDF import of a text's language, paragraph styles. Evidence: `features::tests::language_picks_the_fonts_forms`, `features::tests::serbian_gets_the_fonts_serbian_forms`, `typecmd::area_tests::language_is_a_character_attribute_saved_only_when_set` |