mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Merge pull request #3399 from nrq6993-a11y/stream/vc-featD-manual-tate-chu-yoko
Character panel Tate-chu-yoko marks vertical type as a horizontal block
This commit is contained in:
@@ -109,6 +109,12 @@ pub struct CharStyle {
|
||||
/// its proportional heights, `vpal`.
|
||||
#[serde(default, rename = "proportionalMetrics", skip_serializing_if = "crate::skip::is_default")]
|
||||
pub proportional_metrics: bool,
|
||||
/// Tate-chu-yoko (Character panel menu, East Asian options): in vertical type these characters
|
||||
/// stand side by side across one em of the column. Horizontal type keeps the mark and does not
|
||||
/// move them. Two or three half-width digits do this without the mark.
|
||||
/// <https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html>
|
||||
#[serde(default, rename = "tateChuYoko", skip_serializing_if = "crate::skip::is_default")]
|
||||
pub tate_chu_yoko: bool,
|
||||
/// Digits (Character panel menu, Middle Eastern options): the numeral system the digits of
|
||||
/// these characters are drawn in, whatever digits were typed.
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
@@ -292,6 +298,7 @@ impl Default for CharStyle {
|
||||
position: CharPosition::Normal,
|
||||
char_align: CharAlign::RomanBaseline,
|
||||
proportional_metrics: false,
|
||||
tate_chu_yoko: false,
|
||||
small_caps: None,
|
||||
digits: Digits::Default,
|
||||
no_break: false,
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Character / Paragraph",
|
||||
[],
|
||||
None,
|
||||
"{ids?|id?, kerning?: 1/1000 em|\"auto\", 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), 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), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), 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\", 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), 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), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), 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
|
||||
),
|
||||
@@ -328,6 +328,7 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"leadingModel",
|
||||
"charAlign",
|
||||
"proportionalMetrics",
|
||||
"tateChuYoko",
|
||||
"noBreak",
|
||||
"digits",
|
||||
"fontAxes",
|
||||
@@ -465,6 +466,9 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(v) = flag("proportionalMetrics") {
|
||||
st.proportional_metrics = v;
|
||||
}
|
||||
if let Some(v) = flag("tateChuYoko") {
|
||||
st.tate_chu_yoko = v;
|
||||
}
|
||||
if let Some(v) = flag("noBreak") {
|
||||
st.no_break = v;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"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\", 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\", …], 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), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), 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}",
|
||||
"{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\", 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\", …], 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), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), 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}",
|
||||
has_doc,
|
||||
set_range_style
|
||||
),
|
||||
@@ -257,6 +257,7 @@ pub(crate) struct CharChange {
|
||||
small_caps: Option<Option<f64>>,
|
||||
char_align: Option<vectorcraft_doc::CharAlign>,
|
||||
proportional_metrics: Option<bool>,
|
||||
tate_chu_yoko: Option<bool>,
|
||||
no_break: Option<bool>,
|
||||
digits: Option<vectorcraft_doc::Digits>,
|
||||
font_axes: Option<AxisChange>,
|
||||
@@ -416,6 +417,7 @@ impl CharChange {
|
||||
small_caps,
|
||||
char_align: char_align_param(p, cmd)?,
|
||||
proportional_metrics: flag("proportionalMetrics"),
|
||||
tate_chu_yoko: flag("tateChuYoko"),
|
||||
no_break: flag("noBreak"),
|
||||
digits: digits_param(p, cmd)?,
|
||||
font_axes: font_axes_param(p, cmd)?,
|
||||
@@ -458,6 +460,7 @@ impl CharChange {
|
||||
&& self.small_caps.is_none()
|
||||
&& self.char_align.is_none()
|
||||
&& self.proportional_metrics.is_none()
|
||||
&& self.tate_chu_yoko.is_none()
|
||||
&& self.no_break.is_none()
|
||||
&& self.digits.is_none()
|
||||
&& self.font_axes.is_none()
|
||||
@@ -554,6 +557,9 @@ impl CharChange {
|
||||
if let Some(v) = self.proportional_metrics {
|
||||
st.proportional_metrics = v;
|
||||
}
|
||||
if let Some(v) = self.tate_chu_yoko {
|
||||
st.tate_chu_yoko = v;
|
||||
}
|
||||
if let Some(v) = self.no_break {
|
||||
st.no_break = v;
|
||||
}
|
||||
|
||||
@@ -156,6 +156,7 @@ pub fn char_attrs(v: &CharStyle) -> Map<String, Value> {
|
||||
("small_caps", json!(v.small_caps)),
|
||||
("charAlign", json!(v.char_align)),
|
||||
("proportionalMetrics", json!(v.proportional_metrics)),
|
||||
("tateChuYoko", json!(v.tate_chu_yoko)),
|
||||
("noBreak", json!(v.no_break)),
|
||||
("digits", json!(v.digits)),
|
||||
] {
|
||||
|
||||
@@ -1627,6 +1627,36 @@ mod area_tests {
|
||||
assert!(!serde_json::from_value::<vectorcraft_doc::CharStyle>(old).unwrap().proportional_metrics);
|
||||
}
|
||||
|
||||
/// Tate-chu-yoko (Character panel menu, East Asian options): a character attribute, saved only
|
||||
/// when on. Documents written before it read as off.
|
||||
#[test]
|
||||
fn tate_chu_yoko_is_a_character_attribute_saved_only_when_on() {
|
||||
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"),
|
||||
};
|
||||
assert!(!runs(&s)[0].style.tate_chu_yoko);
|
||||
assert!(serde_json::to_value(&runs(&s)[0].style).unwrap().get("tateChuYoko").is_none());
|
||||
s.execute("select.set", &json!({"ids": [id]})).unwrap();
|
||||
s.execute("text.setFormat", &json!({"tateChuYoko": true})).unwrap();
|
||||
assert!(runs(&s).iter().all(|r| r.style.tate_chu_yoko));
|
||||
assert_eq!(serde_json::to_value(&runs(&s)[0].style).unwrap()["tateChuYoko"], true);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(!runs(&s)[0].style.tate_chu_yoko);
|
||||
// The year only (2026 is 4 bytes); 年 stays unmarked.
|
||||
s.execute("text.setRangeStyle", &json!({"id": id, "start": 0, "end": 4, "tateChuYoko": true})).unwrap();
|
||||
let on: Vec<_> = runs(&s).iter().map(|r| (r.text.clone(), r.style.tate_chu_yoko)).collect();
|
||||
assert_eq!(on, [("2026".to_string(), true), ("年".to_string(), false)]);
|
||||
let old = serde_json::to_value(&runs(&s)[1].style).unwrap();
|
||||
assert!(old.get("tateChuYoko").is_none());
|
||||
assert!(!serde_json::from_value::<vectorcraft_doc::CharStyle>(old).unwrap().tate_chu_yoko);
|
||||
s.execute("text.setFormat", &json!({"tateChuYoko": false})).unwrap();
|
||||
assert!(runs(&s).iter().all(|r| !r.style.tate_chu_yoko));
|
||||
}
|
||||
|
||||
/// No Break is a character attribute, saved only when on.
|
||||
#[test]
|
||||
fn no_break_is_a_character_attribute_saved_only_when_on() {
|
||||
|
||||
+44
-20
@@ -56,7 +56,10 @@ impl Ctx<'_> {
|
||||
let levels = bidi.map_or(&[][..], |b| &b.levels);
|
||||
shape_range(self.db, self.text, r, &self.runs, &self.inlines, &self.opts.features, levels, &mut v);
|
||||
if self.vertical {
|
||||
tate_chu_yoko(&mut v, |g| self.style_at(g.byte).size);
|
||||
tate_chu_yoko(&mut v, |g| {
|
||||
let st = self.style_at(g.byte);
|
||||
(st.size, st.tate_chu_yoko)
|
||||
});
|
||||
// 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
|
||||
// mark like § must not overlap its neighbours.
|
||||
@@ -468,38 +471,59 @@ fn whole_character_widths(glyphs: &mut [SGlyph]) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tate-chu-yoko: a run of two or three half-width digits (`10`月, `100`年) stands upright across
|
||||
/// the column as one block one em long, squeezed to the em when wider; longer numbers (`2026`)
|
||||
/// stay on their side.
|
||||
fn tate_chu_yoko(g: &mut [SGlyph], size: impl Fn(&SGlyph) -> f64) {
|
||||
let digits: Vec<bool> = g.iter().map(|g| g.ch.is_ascii_digit()).collect();
|
||||
let digit = |i: usize| digits.get(i).copied().unwrap_or(false);
|
||||
/// Tate-chu-yoko: a horizontal block one em of the column long, squeezed to the em when wider.
|
||||
///
|
||||
/// Automatic: a run of two or three half-width digits (`10`月, `100`年). A single digit and a
|
||||
/// longer number (`2026`) stay on their side.
|
||||
///
|
||||
/// 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.
|
||||
/// <https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html>
|
||||
fn tate_chu_yoko(g: &mut [SGlyph], style: impl Fn(&SGlyph) -> (f64, bool)) {
|
||||
let manual: Vec<bool> = g.iter().map(|gl| style(gl).1).collect();
|
||||
let digits: Vec<bool> = g.iter().map(|gl| gl.inline.is_none() && gl.ch.is_ascii_digit()).collect();
|
||||
let mut i = 0;
|
||||
while i < g.len() {
|
||||
if !digit(i) {
|
||||
if manual.get(i).copied().unwrap_or(false) {
|
||||
let mut end = i + 1;
|
||||
while manual.get(end).copied().unwrap_or(false) {
|
||||
end += 1;
|
||||
}
|
||||
let em = style(&g[i]).0;
|
||||
place_tcy_block(&mut g[i..end], em);
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
if !digits.get(i).copied().unwrap_or(false) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let mut end = i;
|
||||
while digit(end) {
|
||||
let mut end = i + 1;
|
||||
while digits.get(end).copied().unwrap_or(false) && !manual.get(end).copied().unwrap_or(false) {
|
||||
end += 1;
|
||||
}
|
||||
if (2..=3).contains(&(end - i)) {
|
||||
let em = size(&g[i]);
|
||||
let width: f64 = g[i..end].iter().map(|g| g.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[i..end].iter_mut().enumerate() {
|
||||
let adv = gl.adv;
|
||||
gl.tcy = Some(Tcy { pen: if k == 0 { 0.0 } else { em }, ink, width, squeeze });
|
||||
gl.adv = if k == 0 { em } else { 0.0 };
|
||||
ink += adv;
|
||||
}
|
||||
let em = style(&g[i]).0;
|
||||
place_tcy_block(&mut g[i..end], em);
|
||||
}
|
||||
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) {
|
||||
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.adv = if k == 0 { em } else { 0.0 };
|
||||
ink += adv;
|
||||
}
|
||||
}
|
||||
|
||||
/// Half an em of `g`'s size when it is full-width Japanese punctuation of kind `kind` (its
|
||||
/// advance an em, give or take a tenth), the space that mojikumi can take off.
|
||||
fn punct_half(g: &SGlyph, kind: Punct) -> Option<f64> {
|
||||
|
||||
@@ -470,6 +470,7 @@ fn shapes_alike(a: &CharStyle, b: &CharStyle) -> bool {
|
||||
small_caps,
|
||||
char_align: _,
|
||||
proportional_metrics,
|
||||
tate_chu_yoko,
|
||||
digits,
|
||||
no_break,
|
||||
} = a;
|
||||
@@ -485,6 +486,7 @@ fn shapes_alike(a: &CharStyle, b: &CharStyle) -> bool {
|
||||
&& *position == b.position
|
||||
&& *small_caps == b.small_caps
|
||||
&& *proportional_metrics == b.proportional_metrics
|
||||
&& *tate_chu_yoko == b.tate_chu_yoko
|
||||
&& *digits == b.digits
|
||||
&& *no_break == b.no_break
|
||||
&& ligatures_suppressed_by(*tracking) == ligatures_suppressed_by(b.tracking)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! Japanese text only with craft-fonts' faces, skipped without them).
|
||||
use super::*;
|
||||
use kurbo::Shape;
|
||||
use vectorcraft_doc::{CharStyle, Justify, TextKind};
|
||||
use vectorcraft_doc::{CharStyle, Justify, TextKind, TextRun};
|
||||
use vectorcraft_geom::PathData;
|
||||
|
||||
fn vertical(text: &str) -> TextObject {
|
||||
@@ -318,6 +318,67 @@ fn tracking_spaces_upright_marks_without_moving_them_off_the_centre_line() {
|
||||
assert!((step - 24.0).abs() < 1e-6, "one em plus 200/1000 em of tracking: {step}");
|
||||
}
|
||||
|
||||
/// Character panel menu › Tate-chu-yoko: a marked run stands across one em of the column, including
|
||||
/// a number longer than three digits and Latin letters. Unmarked text keeps the automatic rule
|
||||
/// (two or three digits only). Horizontal type stores the mark and does not move the letters.
|
||||
#[test]
|
||||
fn manual_tate_chu_yoko_sets_marked_characters_across_one_em() {
|
||||
let marked = CharStyle { size: 20.0, tate_chu_yoko: true, ..CharStyle::default() };
|
||||
let year = layout(FontDb::global(), &vertical_styled("2026", marked.clone()));
|
||||
assert_eq!(year.glyphs.len(), 4);
|
||||
assert!((year.glyphs[0].advance - 20.0).abs() < 1e-6, "{:?}", year.glyphs[0].advance);
|
||||
assert!(year.glyphs[1..].iter().all(|g| g.advance == 0.0));
|
||||
let inks: Vec<Rect> = (0..4).map(|i| ink(&year, i)).collect();
|
||||
for pair in inks.windows(2) {
|
||||
assert!((pair[0].center().y - pair[1].center().y).abs() < 1.0, "one row: {inks:?}");
|
||||
assert!(pair[1].center().x > pair[0].center().x, "left to right: {inks:?}");
|
||||
}
|
||||
let union = inks.iter().copied().reduce(|a, b| a.union(b)).unwrap();
|
||||
assert!(union.width() <= 21.0, "squeezed into the em: {union:?}");
|
||||
assert!(union.center().x.abs() < 1.5, "centred on the column: {union:?}");
|
||||
|
||||
let plain = layout(FontDb::global(), &vertical("2026"));
|
||||
let down: f64 = plain.glyphs.iter().map(|g| g.advance).sum();
|
||||
assert!(down > 30.0 && plain.glyphs.iter().all(|g| g.advance > 0.0), "unmarked 2026 runs down the column: {down}");
|
||||
|
||||
let letters = layout(FontDb::global(), &vertical_styled("AB", marked.clone()));
|
||||
assert!((letters.glyphs[0].advance - 20.0).abs() < 1e-6 && letters.glyphs[1].advance == 0.0);
|
||||
let (a, b) = (ink(&letters, 0), ink(&letters, 1));
|
||||
assert!((a.center().y - b.center().y).abs() < 1.0 && b.center().x > a.center().x, "letters side by side: {a:?} {b:?}");
|
||||
let side = layout(FontDb::global(), &vertical("AB"));
|
||||
assert!(ink(&side, 1).center().y > ink(&side, 0).center().y + 5.0, "unmarked letters run down the column");
|
||||
|
||||
let mut horizontal = TextObject::point(Point::ZERO, "AB", marked);
|
||||
horizontal.xf = Affine::IDENTITY;
|
||||
let row = layout(FontDb::global(), &horizontal);
|
||||
assert!(!row.vertical);
|
||||
assert!(row.glyphs.iter().all(|g| g.advance > 0.0));
|
||||
assert!(ink(&row, 1).center().x > ink(&row, 0).center().x + 5.0, "horizontal type stays a row");
|
||||
}
|
||||
|
||||
/// 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]
|
||||
fn manual_tate_chu_yoko_leaves_unmarked_digits_and_stays_on_one_line() {
|
||||
let plain = CharStyle { size: 20.0, ..CharStyle::default() };
|
||||
let marked = CharStyle { tate_chu_yoko: true, ..plain.clone() };
|
||||
let mut t = vertical_styled("", plain.clone());
|
||||
t.runs = vec![TextRun::new("X", plain), TextRun::new("2026", marked), TextRun::new("10", CharStyle { size: 20.0, ..CharStyle::default() })];
|
||||
let l = layout(FontDb::global(), &t);
|
||||
assert_eq!(l.glyphs.len(), 7);
|
||||
assert!(l.glyphs[0].advance > 0.0 && l.glyphs[0].advance < 20.0, "X on its side: {:?}", l.glyphs[0].advance);
|
||||
assert!((l.glyphs[1].advance - 20.0).abs() < 1e-6 && l.glyphs[2].advance == 0.0 && l.glyphs[3].advance == 0.0 && l.glyphs[4].advance == 0.0);
|
||||
assert!((l.glyphs[5].advance - 20.0).abs() < 1e-6 && l.glyphs[6].advance == 0.0, "unmarked 10 stays automatic");
|
||||
|
||||
t.kind = TextKind::Area { frame: PathData::from_bezpath(&Rect::new(0.0, 0.0, 80.0, 30.0).to_path(0.1)) };
|
||||
t.runs = vec![TextRun::new("2026", CharStyle { size: 20.0, tate_chu_yoko: true, ..CharStyle::default() })];
|
||||
let one = layout(FontDb::global(), &t);
|
||||
assert_eq!(one.lines.len(), 1, "the block is one em, so it fits a 30 pt column");
|
||||
t.runs = vec![TextRun::new("2026", CharStyle { size: 20.0, ..CharStyle::default() })];
|
||||
let wrapped = layout(FontDb::global(), &t);
|
||||
assert!(wrapped.lines.len() > 1, "unmarked 2026 is longer than the column");
|
||||
}
|
||||
|
||||
/// Issue #260 as reported, with a Japanese face of craft-fonts (whose own digits are proportional).
|
||||
#[test]
|
||||
fn japanese_numbers_keep_their_own_cells_down_the_column() {
|
||||
|
||||
@@ -769,6 +769,11 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if menu_item(ui, tl!("Proportional Metrics"), has, proportional) {
|
||||
format(app, json!({"proportionalMetrics": !proportional}));
|
||||
}
|
||||
// Tate-chu-yoko: the selected characters stand side by side across one em of a vertical column.
|
||||
let tcy = s.as_ref().is_some_and(|s| s.tate_chu_yoko);
|
||||
if menu_item(ui, tl!("Tate-chu-yoko"), has, tcy) {
|
||||
format(app, json!({"tateChuYoko": !tcy}));
|
||||
}
|
||||
ui.separator();
|
||||
}
|
||||
// Digits: the numeral system the selected characters' digits are drawn in, with the Middle
|
||||
@@ -814,9 +819,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if menu_item(ui, tl!("System Layout"), open, system) {
|
||||
app.run("type.systemLayout", json!({})).ok();
|
||||
}
|
||||
for l in ["Standard Vertical Roman Alignment", "Tate-chu-yoko"] {
|
||||
menu_item(ui, tl!(l), false, false);
|
||||
}
|
||||
menu_item(ui, tl!("Standard Vertical Roman Alignment"), false, false);
|
||||
ui.separator();
|
||||
// The installed fonts are always listed; this picks up fonts installed since the app started.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -844,7 +847,7 @@ fn fractional_widths_on(app: &VectorcraftApp) -> bool {
|
||||
/// Reset Panel: the characters the panel acts on ([`char_cmd`]) take tracking 0, Auto leading and
|
||||
/// 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 and No Break off and the Default digits, whether
|
||||
/// character alignment, Proportional Metrics, Tate-chu-yoko and No Break off and the Default digits, whether
|
||||
/// or not Show East Asian Options and Show Indic Options are on.
|
||||
fn reset_panel(app: &mut VectorcraftApp) {
|
||||
let d = CharStyle::default();
|
||||
@@ -854,7 +857,7 @@ fn reset_panel(app: &mut VectorcraftApp) {
|
||||
json!({
|
||||
"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, "noBreak": d.no_break, "digits": d.digits
|
||||
"proportionalMetrics": d.proportional_metrics, "tateChuYoko": d.tate_chu_yoko, "noBreak": d.no_break, "digits": d.digits
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1247,6 +1250,34 @@ mod tests {
|
||||
assert_eq!(shown(&mut app), (true, true));
|
||||
}
|
||||
|
||||
/// Tate-chu-yoko is in the panel menu with the East Asian options, and choosing it marks the
|
||||
/// selected characters.
|
||||
#[test]
|
||||
fn tate_chu_yoko_menu_item_shows_with_east_asian_options_and_toggles() {
|
||||
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();
|
||||
let painted = |app: &mut VectorcraftApp| crate::tests_labels::painted_text(app, menu);
|
||||
assert!(!painted(&mut app).contains("Tate-chu-yoko"));
|
||||
app.session.prefs.show_east_asian_options = true;
|
||||
let on = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => t.runs.iter().all(|r| r.style.tate_chu_yoko),
|
||||
_ => panic!("text"),
|
||||
};
|
||||
assert!(!on(&app));
|
||||
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], menu);
|
||||
let at = texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == "Tate-chu-yoko").unwrap_or_else(|| panic!("{texts:?}")).1;
|
||||
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
|
||||
assert!(on(&app));
|
||||
assert!(painted(&mut app).contains("✓ Tate-chu-yoko"));
|
||||
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], menu);
|
||||
let at = texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == "Tate-chu-yoko").unwrap().1;
|
||||
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
|
||||
assert!(!on(&app));
|
||||
}
|
||||
|
||||
/// Digits is in the panel menu with the Middle Eastern (Indic) options only, as Paragraph
|
||||
/// Direction is in the Paragraph panel.
|
||||
#[test]
|
||||
@@ -1290,8 +1321,8 @@ mod tests {
|
||||
|
||||
/// 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, No Break and Digits included), in the selected type and in the text the Type tool
|
||||
/// has selected while it edits.
|
||||
/// Metrics, Tate-chu-yoko, No Break and Digits 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() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
@@ -1317,6 +1348,7 @@ mod tests {
|
||||
s.position != d.position,
|
||||
s.char_align != d.char_align,
|
||||
s.proportional_metrics != d.proportional_metrics,
|
||||
s.tate_chu_yoko != d.tate_chu_yoko,
|
||||
s.no_break != d.no_break,
|
||||
s.digits != d.digits,
|
||||
]
|
||||
@@ -1327,16 +1359,16 @@ mod tests {
|
||||
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, "noBreak": true, "digits": "hindi"
|
||||
"charAlign": "emBoxCenter", "proportionalMetrics": true, "tateChuYoko": true, "noBreak": true, "digits": "hindi"
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
let plain = runs(&app)[0].1.clone();
|
||||
assert_eq!(differs(&plain, &CharStyle::default()), [false; 16]);
|
||||
assert_eq!(differs(&plain, &CharStyle::default()), [false; 17]);
|
||||
custom(&mut app);
|
||||
let styled = runs(&app)[0].1.clone();
|
||||
assert_eq!(differs(&styled, &CharStyle::default()), [true; 16]);
|
||||
assert_eq!(differs(&styled, &CharStyle::default()), [true; 17]);
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
reset_panel(&mut app);
|
||||
assert_eq!(runs(&app), [("One 12".to_string(), plain.clone())]);
|
||||
|
||||
File diff suppressed because one or more lines are too long
+4
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user