From 0384726c7aa8170ab2120d3cfc23058080693900 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 22:54:10 -0800 Subject: [PATCH] Type: Adjust Horizontal and Vertical Diacritic Position move marks off their letters The Character panel shows Adjust Horizontal and Adjust Vertical Diacritic Position with the Middle Eastern options. text.setFormat and text.setRangeStyle take diacriticHorizontal and diacriticVertical, in thousandths of an em from -1000 to 1000. Positive values move the characters' mark glyphs (GDEF class 3) right and up, and the letters keep their places. An Arabic harakat or a Hebrew point styled apart from its letter takes its own values without splitting the shaping cluster. Character styles and Character Style Options carry both values. They are saved only when set, and SVG export writes such type as outlines. Docs: docs/mcp.md, docs/type-parity.md, docs/target-app-parity.md. --- crates/doc/src/text.rs | 4 +- crates/doc/src/text/char_style.rs | 24 +++- crates/doc/src/text/object.rs | 4 + crates/doc/src/text/tests.rs | 36 ++++++ crates/engine/src/cmd/panelcmds.rs | 8 +- crates/engine/src/cmd/textedit.rs | 28 ++++- crates/engine/src/cmd/textstyles.rs | 15 ++- crates/engine/src/cmd/textstyles/tests.rs | 5 +- crates/engine/src/cmd/typecmd/area_tests.rs | 60 ++++++++++ crates/svg/src/export/text.rs | 6 +- crates/svg/tests/it/text_layout.rs | 13 +++ crates/text/src/harakat.rs | 41 ++++++- crates/text/src/shape.rs | 47 ++++++-- crates/text/src/tests/harakat.rs | 72 ++++++++++++ .../ui-egui/src/dialogs/char_style_options.rs | 108 +++++++++++++++++- crates/ui-egui/src/i18n/bg.tsv | 2 + crates/ui-egui/src/i18n/bs.tsv | 2 + crates/ui-egui/src/i18n/ca.tsv | 2 + crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/da.tsv | 2 + crates/ui-egui/src/i18n/de.tsv | 2 + crates/ui-egui/src/i18n/el.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fi.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/gu.tsv | 2 + crates/ui-egui/src/i18n/hi.tsv | 2 + crates/ui-egui/src/i18n/hu.tsv | 2 + crates/ui-egui/src/i18n/id.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/ka.tsv | 2 + crates/ui-egui/src/i18n/ko.tsv | 2 + crates/ui-egui/src/i18n/nl.tsv | 2 + crates/ui-egui/src/i18n/pl.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/sv.tsv | 2 + crates/ui-egui/src/i18n/tr.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/vi.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + crates/ui-egui/src/panels/character.rs | 33 +++++- crates/ui-egui/src/panels/character/tests.rs | 60 +++++++++- docs/mcp.md | 30 ++++- docs/target-app-parity.md | 1 + docs/type-parity.md | 1 + 48 files changed, 612 insertions(+), 40 deletions(-) diff --git a/crates/doc/src/text.rs b/crates/doc/src/text.rs index 06b71f8fc8..f149cf6b30 100644 --- a/crates/doc/src/text.rs +++ b/crates/doc/src/text.rs @@ -18,8 +18,8 @@ mod wrap; pub use area::{AreaFit, AreaOptions, PathAlign, PathEffect, TextFlow, TextWrap, VerticalAlign, WrapShape}; use char_style::yes; pub use char_style::{ - CharAlign, CharPosition, CharStyle, Digits, FontHeightReference, GlyphChoice, GlyphForm, MAX_AKI, MAX_TSUME, MAX_WARICHU_GAP, ScriptMetrics, - WARICHU_LINES, WARICHU_SCALE, WarichuAlign, + CharAlign, CharPosition, CharStyle, Digits, FontHeightReference, GlyphChoice, GlyphForm, MAX_AKI, MAX_DIACRITIC_POSITION, MAX_TSUME, + MAX_WARICHU_GAP, ScriptMetrics, WARICHU_LINES, WARICHU_SCALE, WarichuAlign, }; pub use composite::{ COMPOSITE_BASELINE, COMPOSITE_FONTS_MAX, COMPOSITE_NAME_MAX, COMPOSITE_SCALE, COMPOSITE_SIZE, CharClass, CompositeFont, CompositePart, diff --git a/crates/doc/src/text/char_style.rs b/crates/doc/src/text/char_style.rs index 97f744dba8..30883ac95b 100644 --- a/crates/doc/src/text/char_style.rs +++ b/crates/doc/src/text/char_style.rs @@ -180,6 +180,18 @@ pub struct CharStyle { /// these characters are drawn in, whatever digits were typed. #[serde(default, skip_serializing_if = "crate::skip::is_default")] pub digits: Digits, + /// Adjust Horizontal Diacritic Position (Character panel, Middle Eastern options): the font's + /// mark glyphs among these characters (GDEF class 3, such as Arabic harakat and Hebrew points) + /// move right by a positive value and left by a negative one, relative to their letters, in + /// 1/1000 em of the size, from -[`MAX_DIACRITIC_POSITION`] to [`MAX_DIACRITIC_POSITION`]. + /// Saved only when not 0. + /// + #[serde(default, rename = "diacriticHorizontal", skip_serializing_if = "crate::skip::is_default")] + pub diacritic_horizontal: f64, + /// Adjust Vertical Diacritic Position: the mark glyphs move up by a positive value and down by + /// a negative one, as [`Self::diacritic_horizontal`] moves them across. + #[serde(default, rename = "diacriticVertical", skip_serializing_if = "crate::skip::is_default")] + pub diacritic_vertical: f64, /// Font height reference (Character panel menu › Show Font Height Options): the height of the /// font that [`Self::size`] measures, the em box by default. #[serde(default, rename = "fontHeightReference", skip_serializing_if = "crate::skip::is_default")] @@ -544,6 +556,10 @@ pub const MAX_AKI: f64 = 1000.0; /// The largest Tsume ([`CharStyle::tsume`]), in percent: all the blank space beside the ink. pub const MAX_TSUME: f64 = 100.0; +/// The largest Adjust Horizontal or Vertical Diacritic Position either way +/// ([`CharStyle::diacritic_horizontal`]), in 1/1000 em: one em. +pub const MAX_DIACRITIC_POSITION: f64 = 1000.0; + /// The number of lines a warichu block may be stacked on ([`CharStyle::warichu_lines`]). pub const WARICHU_LINES: std::ops::RangeInclusive = 1..=6; @@ -675,6 +691,8 @@ impl Default for CharStyle { warichu_align: WarichuAlign::Auto, small_caps: None, digits: Digits::Default, + diacritic_horizontal: 0.0, + diacritic_vertical: 0.0, font_height_reference: FontHeightReference::EmBox, no_break: false, language: None, @@ -698,7 +716,8 @@ impl CharStyle { /// same way. The Warichu Settings Lines and Scale /// are kept to [`WARICHU_LINES`] and [`WARICHU_SCALE`]; a gap or scale that is not a number becomes its default. The gap is kept to 0 to /// [`crate::MAX_COORD`], as the baseline shift is, since a scaled type object holds it in its - /// own units. + /// own units. The Adjust Horizontal and Vertical Diacritic Positions are kept within + /// [`MAX_DIACRITIC_POSITION`] either way, and one that is not a number becomes 0. 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 }; @@ -714,6 +733,9 @@ impl CharStyle { self.aki_left = aki(self.aki_left); self.aki_right = aki(self.aki_right); self.tsume = if self.tsume.is_nan() { 0.0 } else { self.tsume.clamp(0.0, MAX_TSUME) }; + let diacritic = |v: f64| if v.is_nan() { 0.0 } else { v.clamp(-MAX_DIACRITIC_POSITION, MAX_DIACRITIC_POSITION) }; + self.diacritic_horizontal = diacritic(self.diacritic_horizontal); + self.diacritic_vertical = diacritic(self.diacritic_vertical); self.warichu_lines = self.warichu_lines.clamp(*WARICHU_LINES.start(), *WARICHU_LINES.end()); self.warichu_gap = if self.warichu_gap.is_nan() { default.warichu_gap } else { self.warichu_gap.clamp(0.0, crate::MAX_COORD) }; self.warichu_scale = diff --git a/crates/doc/src/text/object.rs b/crates/doc/src/text/object.rs index b787cebfe0..46f86b68ca 100644 --- a/crates/doc/src/text/object.rs +++ b/crates/doc/src/text/object.rs @@ -511,6 +511,8 @@ impl crate::Document { st.aki_left = num("akiLeft").unwrap_or(st.aki_left); st.aki_right = num("akiRight").unwrap_or(st.aki_right); st.tsume = num("tsume").unwrap_or(st.tsume); + st.diacritic_horizontal = num("diacriticHorizontal").unwrap_or(st.diacritic_horizontal); + st.diacritic_vertical = num("diacriticVertical").unwrap_or(st.diacritic_vertical); st.warichu_gap = num("warichuGap").unwrap_or(st.warichu_gap); st.warichu_scale = num("warichuScale").unwrap_or(st.warichu_scale); st.keep_dimensions_in_range(); @@ -537,6 +539,8 @@ impl crate::Document { ("akiLeft", Some(st.aki_left)), ("akiRight", Some(st.aki_right)), ("tsume", Some(st.tsume)), + ("diacriticHorizontal", Some(st.diacritic_horizontal)), + ("diacriticVertical", Some(st.diacritic_vertical)), ("leading", st.leading), ]; for (k, v) in kept { diff --git a/crates/doc/src/text/tests.rs b/crates/doc/src/text/tests.rs index 0976edbb93..a5230d5d86 100644 --- a/crates/doc/src/text/tests.rs +++ b/crates/doc/src/text/tests.rs @@ -129,6 +129,42 @@ fn insert_aki_is_saved_only_when_set() { assert_eq!((d.char_styles[0].attrs["akiLeft"].as_f64(), d.char_styles[0].attrs["akiRight"].as_f64()), (Some(MAX_AKI), Some(0.0))); } +/// Adjust Horizontal and Vertical Diacritic Position are 0 by default and saved only when not 0, +/// each on its own; styles written before them read as 0, and a value out of range from a file is +/// kept within one em either way. +#[test] +fn diacritic_positions_are_saved_only_when_set() { + let st = CharStyle::default(); + assert_eq!((st.diacritic_horizontal, st.diacritic_vertical), (0.0, 0.0)); + let saved = serde_json::to_value(&st).unwrap(); + assert!(saved.get("diacriticHorizontal").is_none() && saved.get("diacriticVertical").is_none()); + for set in [ + CharStyle { diacritic_horizontal: -250.0, ..CharStyle::default() }, + CharStyle { diacritic_vertical: 500.0, ..CharStyle::default() }, + CharStyle { diacritic_horizontal: 12.5, diacritic_vertical: -1000.0, ..CharStyle::default() }, + ] { + let saved = serde_json::to_value(&set).unwrap(); + assert_eq!(saved.get("diacriticHorizontal").and_then(serde_json::Value::as_f64).unwrap_or(0.0), set.diacritic_horizontal); + assert_eq!(saved.get("diacriticVertical").and_then(serde_json::Value::as_f64).unwrap_or(0.0), set.diacritic_vertical); + assert_eq!(saved.get("diacriticHorizontal").is_some(), set.diacritic_horizontal != 0.0); + assert_eq!(saved.get("diacriticVertical").is_some(), set.diacritic_vertical != 0.0); + assert_eq!(serde_json::from_value::(saved).unwrap(), set); + } + let mut damaged = CharStyle { diacritic_horizontal: -4000.0, diacritic_vertical: 4000.0, ..CharStyle::default() }; + damaged.keep_dimensions_in_range(); + assert_eq!((damaged.diacritic_horizontal, damaged.diacritic_vertical), (-MAX_DIACRITIC_POSITION, MAX_DIACRITIC_POSITION)); + let mut damaged = CharStyle { diacritic_horizontal: f64::NAN, diacritic_vertical: f64::NEG_INFINITY, ..CharStyle::default() }; + damaged.keep_dimensions_in_range(); + assert_eq!((damaged.diacritic_horizontal, damaged.diacritic_vertical), (0.0, -MAX_DIACRITIC_POSITION)); + // A character style from a file is kept in range the same way. + let mut d = crate::Document::new(100.0, 100.0); + let attrs = serde_json::json!({"diacriticHorizontal": 5000, "diacriticVertical": -5000}); + d.char_styles.push(TextStyleDef { name: "Moved".into(), attrs: attrs.as_object().cloned().unwrap_or_default() }); + d.keep_type_values_in_range(); + let kept = |key: &str| d.char_styles[0].attrs[key].as_f64(); + assert_eq!((kept("diacriticHorizontal"), kept("diacriticVertical")), (Some(MAX_DIACRITIC_POSITION), Some(-MAX_DIACRITIC_POSITION))); +} + /// Tsume is 0 by default and saved only when not 0; styles written before it read as 0, and a /// value out of range from a file, in a run or a character style, is kept from 0 to 100. #[test] diff --git a/crates/engine/src/cmd/panelcmds.rs b/crates/engine/src/cmd/panelcmds.rs index fa0492c2db..d0a1a43863 100644 --- a/crates/engine/src/cmd/panelcmds.rs +++ b/crates/engine/src/cmd/panelcmds.rs @@ -67,7 +67,7 @@ pub fn specs() -> Vec { "Character / Paragraph", [], None, - "{ids?|id?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg counter-clockwise (0: upright), underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), antiAlias?: \"none\"|\"sharp\"|\"crisp\"|\"strong\" (Character panel › Anti-aliasing Method, how the type's edges are drawn as pixels; it sets each targeted type object, with or without a range; each frame of a thread keeps its own method; graph labels keep it for every label of their kind when the graph is drawn again; new type: \"sharp\"), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt (±1296), list?: \"none\"|\"bullet\"|\"number\", bullet?: \"none\"|\"dot\"|\"dash\"|\"circle\"|\"square\", number?: \"none\"|\"arabic\"|\"upperLetters\"|\"lowerLetters\"|\"upperRoman\"|\"lowerRoman\"|\"twoDigits\"|\"threeDigits\", listAlign?: \"left\"|\"right\", listLevel?: 1..9 (Paragraph panel › Bullets and Numbered List: each paragraph of a list starts with its marker, drawn in the font, size and color of the paragraph's first character, and its text starts at the next tab stop after the marker, the left indent counting as a stop; a bulleted list's marker is its bullet, and a numbered list's is the paragraph's number and a period: 1, 2, 3; A to Z, then AA, BB; a to z, then aa, bb; I, II, III; i, ii, iii; 01 to 99, then 100; 001 to 999, then 1000; letters and roman numerals past 3999 in Arabic digits; the numbers count the story's numbered paragraphs, its threaded frames included, from 1 after a paragraph in no list, and an empty or bulleted paragraph is not counted; list \"bullet\" keeps a list's bullet, else a dot, and list \"number\" keeps a numbered list's style, else arabic; without list, a number style makes a numbered list and a bullet makes a bulleted one; \"none\" in any of the three turns the list off; a paragraph that joins a list with left and first-line indents of 0 gets 18 and -18 unless the call sets an indent; a horizontal right-to-left list paragraph has its indents mirrored, measured from the right; listAlign \"right\" (Bullets and Numbering…'s Alignment) puts the marker against the text, and \"left\" (the default) puts it at the first line's start, the text starting at the same place with either; it sets the paragraphs that are in a list or that the call puts in one, and a list keeps it when it changes between bullets and numbers; listLevel (Tab and Shift+Tab at the start of a list paragraph, Bullets and Numbering…'s Level) is a whole number from 1 to 9 that sets the paragraphs in a list or that the call puts in one, a list keeping it when it changes between bullets and numbers, and a list turned on without listLevel starting at level 1; each level past the first sets the paragraph 18 pt further in than its left indent, and each level counts its own numbers: a list paragraph with text sets the counts of the levels deeper than its own back to 0, so a deeper numbered paragraph after it counts from 1, and a paragraph back at a shallower level goes on with that level's count), hyphenate?: bool, wordsLongerThan?: letters 1..25 (5), afterFirst?, beforeLast?: letters 1..15 (3), hyphenLimit?: lines 0..25 (0: no limit), hyphenationZone?: pt 0..1296 (0; Single-line Composer only), hyphenateCapitalized?: bool (true) (Paragraph panel menu › Hyphenation…: only words longer than wordsLongerThan break, keeping afterFirst letters before the hyphen and beforeLast after it; at most hyphenLimit lines in a row end with a hyphen; a line whose text already ends within hyphenationZone of its right edge breaks before the next word instead of hyphenating it; the counts are rounded, and every value is clamped to its range), wordSpacing?: % 0..1000 (80, 100, 133), letterSpacing?: % of a word space -100..500 (0, 0, 0), glyphScaling?: % 50..200 (100, 100, 100): each [min, desired, max] or {min?, desired?, max?} (Paragraph panel menu › Justification…: Desired applies to every line, Minimum and Maximum bound what justified lines may use; values are clamped to their range, and Minimum ≤ Desired ≤ Maximum must hold afterwards or nothing changes), autoLeading?: % of the size 0..500 (120; the leading of characters whose leading is Auto), singleWordJustification?: \"fullJustify\"|\"alignLeft\"|\"alignCenter\"|\"alignRight\" (a justified line holding one word), mojikumi?: \"none\"|\"yakumonoHankaku\"|\"gyomatsuYakumonoHankaku\"|\"gyomatsuYakumonoZenkaku\"|\"yakumonoZenkaku\"|\"lineEndHalf\" (Mojikumi Set: Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), kinsokuType?: \"pushInFirst\"|\"pushOutFirst\"|\"pushOutOnly\" (Paragraph panel menu › Kinsoku Shori Type: how area type keeps those characters off a line's ends; pushInFirst sets them on the line when its gaps next to CJK characters can close up enough, by at most a tenth of an em each, pushOutFirst does so only when moving the character before them to the next line would leave the line more than an em short, and pushOutOnly, the default, moves it unless Burasagari hangs an overflowing comma or full stop; no effect with kinsoku \"none\"), bunriKinshi?: bool (Paragraph panel menu › Bunri-Kinshi: a line never breaks inside an inseparable pair, two em dashes, two horizontal ellipses, two two-dot leaders or the two halves of a vertical kana repeat mark, and justification adds no space inside one; kept but without effect with kinsoku \"none\"; off by default), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (where characters smaller than the largest on their line line up with it), proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), tateChuYoko?: bool (Character panel menu › Tate-chu-yoko, with Show East Asian Options: in vertical type those characters stand side by side across one em of the column; kept on the characters and ignored in horizontal type), tcyUpDown?, tcyLeftRight?: pt -1296..1296 (Character panel menu › Tate-chu-yoko Settings, as text.setRangeStyle: a tate-chu-yoko block moves up or down, right or left, and on a vertical path toward or away from the path's start, to its left or right), warichu?: bool, warichuLines?: 1..6, warichuGap?: pt 0..100, warichuScale?: % 10..100, warichuAlign?: \"auto\"|\"left\"|\"center\"|\"right\"|\"justifyLeft\"|\"justifyCenter\"|\"justifyRight\"|\"justifyAll\" (Character panel menu › Warichu and Warichu Settings, as text.setRangeStyle: the characters set smaller and stacked on lines inside their line), akiLeft?, akiRight?: 1/1000 em 0..1000 (Character panel › Insert Aki Left and Right, with Show East Asian Options, as text.setRangeStyle: space before and after each character in reading order), tsume?: % 0..100 (Character panel › Tsume, with Show East Asian Options, as text.setRangeStyle: the share of the blank space beside each character's ink taken away 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), kashidas?: bool (Character panel menu › Kashidas: selected characters allow insertion only when both joined neighbors have it on; on by default), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), glyphForm?: \"traditional\"|\"expert\"|\"jis04\"|\"jis90\"|\"jis78\"|\"jis83\"|\"fullWidth\"|\"halfWidth\"|\"thirdWidth\"|\"quarterWidth\"|\"default\" (Glyphs panel menu, with Show East Asian Options, as text.setRangeStyle: the characters drawn in one OpenType form of the font, or its default glyphs), fontHeightReference?: \"emBox\"|\"capHeight\"|\"xHeight\"|\"icfBox\" (Character panel › Set Font Height Reference, as text.setRangeStyle: the height of the font that the size measures), language?: name|tag (Character panel › Language, as text.setRangeStyle), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font, as text.setRangeStyle: each axis named set in its units and clamped to its range, null back to the style's own setting), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\"), romanHangingPunctuation?: bool (Paragraph panel menu › Roman Hanging Punctuation: quotation marks, hyphens, full stops and commas starting or ending a line hang outside the margin, asterisks, tildes, ellipses, dashes, colons and semicolons by half; off by default), koreanLineBreakAnywhere?: bool (Paragraph panel menu › Korean Line Break Anywhere: a line may break between any two Hangul syllables; off, a word with Hangul in it breaks only at its spaces and punctuation, or where it is wider than the line; new type: false, paragraphs saved before the option: true), kashida?: \"none\"|\"short\"|\"medium\"|\"long\"|\"stylistic\" (Paragraph panel › Insert Kashidas: justified lines of horizontal area type lengthen each Arabic word with a kashida between its last pair of joined letters, at most a quarter, half or whole em long, or whole tatweels up to an em for stylistic, before they widen their word spaces; none by default), composer?: \"singleLine\"|\"everyLine\" (line breaking, Every-line by default), start?: byte, end?: byte} (baselineShift is in document points and hScale includes the object transform; groups include their text descendants; with a range: the character attributes style that range and the paragraph attributes apply to the paragraphs it touches; without: all the text, and graph labels keep the character and paragraph attributes for every label of their kind when the graph is drawn again, the graph still setting each label's alignment; a number or switch given as another type fails the call, and a null value leaves its attribute as it is, so a call with only null values fails with nothing to change; a call that gives the values the text already has records no undo step). Or {paragraphIndents: [{id, start: byte where the paragraph starts, leftIndent?, firstLineIndent?: pt -1296..1296}…]} alone (Tabs panel indent markers over paragraphs with indents of their own): each entry sets one paragraph of a type object, at least one indent each, at most 100000 entries; every entry is checked before anything changes, and an unknown key, a value out of range, an object that is not type, a byte where no paragraph starts or two entries for one paragraph with other values fail the call; all of it is one edit, a threaded story flowing again once, and when no paragraph's indents change nothing is recorded → {ids}", + "{ids?|id?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg counter-clockwise (0: upright), underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), antiAlias?: \"none\"|\"sharp\"|\"crisp\"|\"strong\" (Character panel › Anti-aliasing Method, how the type's edges are drawn as pixels; it sets each targeted type object, with or without a range; each frame of a thread keeps its own method; graph labels keep it for every label of their kind when the graph is drawn again; new type: \"sharp\"), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt (±1296), list?: \"none\"|\"bullet\"|\"number\", bullet?: \"none\"|\"dot\"|\"dash\"|\"circle\"|\"square\", number?: \"none\"|\"arabic\"|\"upperLetters\"|\"lowerLetters\"|\"upperRoman\"|\"lowerRoman\"|\"twoDigits\"|\"threeDigits\", listAlign?: \"left\"|\"right\", listLevel?: 1..9 (Paragraph panel › Bullets and Numbered List: each paragraph of a list starts with its marker, drawn in the font, size and color of the paragraph's first character, and its text starts at the next tab stop after the marker, the left indent counting as a stop; a bulleted list's marker is its bullet, and a numbered list's is the paragraph's number and a period: 1, 2, 3; A to Z, then AA, BB; a to z, then aa, bb; I, II, III; i, ii, iii; 01 to 99, then 100; 001 to 999, then 1000; letters and roman numerals past 3999 in Arabic digits; the numbers count the story's numbered paragraphs, its threaded frames included, from 1 after a paragraph in no list, and an empty or bulleted paragraph is not counted; list \"bullet\" keeps a list's bullet, else a dot, and list \"number\" keeps a numbered list's style, else arabic; without list, a number style makes a numbered list and a bullet makes a bulleted one; \"none\" in any of the three turns the list off; a paragraph that joins a list with left and first-line indents of 0 gets 18 and -18 unless the call sets an indent; a horizontal right-to-left list paragraph has its indents mirrored, measured from the right; listAlign \"right\" (Bullets and Numbering…'s Alignment) puts the marker against the text, and \"left\" (the default) puts it at the first line's start, the text starting at the same place with either; it sets the paragraphs that are in a list or that the call puts in one, and a list keeps it when it changes between bullets and numbers; listLevel (Tab and Shift+Tab at the start of a list paragraph, Bullets and Numbering…'s Level) is a whole number from 1 to 9 that sets the paragraphs in a list or that the call puts in one, a list keeping it when it changes between bullets and numbers, and a list turned on without listLevel starting at level 1; each level past the first sets the paragraph 18 pt further in than its left indent, and each level counts its own numbers: a list paragraph with text sets the counts of the levels deeper than its own back to 0, so a deeper numbered paragraph after it counts from 1, and a paragraph back at a shallower level goes on with that level's count), hyphenate?: bool, wordsLongerThan?: letters 1..25 (5), afterFirst?, beforeLast?: letters 1..15 (3), hyphenLimit?: lines 0..25 (0: no limit), hyphenationZone?: pt 0..1296 (0; Single-line Composer only), hyphenateCapitalized?: bool (true) (Paragraph panel menu › Hyphenation…: only words longer than wordsLongerThan break, keeping afterFirst letters before the hyphen and beforeLast after it; at most hyphenLimit lines in a row end with a hyphen; a line whose text already ends within hyphenationZone of its right edge breaks before the next word instead of hyphenating it; the counts are rounded, and every value is clamped to its range), wordSpacing?: % 0..1000 (80, 100, 133), letterSpacing?: % of a word space -100..500 (0, 0, 0), glyphScaling?: % 50..200 (100, 100, 100): each [min, desired, max] or {min?, desired?, max?} (Paragraph panel menu › Justification…: Desired applies to every line, Minimum and Maximum bound what justified lines may use; values are clamped to their range, and Minimum ≤ Desired ≤ Maximum must hold afterwards or nothing changes), autoLeading?: % of the size 0..500 (120; the leading of characters whose leading is Auto), singleWordJustification?: \"fullJustify\"|\"alignLeft\"|\"alignCenter\"|\"alignRight\" (a justified line holding one word), mojikumi?: \"none\"|\"yakumonoHankaku\"|\"gyomatsuYakumonoHankaku\"|\"gyomatsuYakumonoZenkaku\"|\"yakumonoZenkaku\"|\"lineEndHalf\" (Mojikumi Set: Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), kinsokuType?: \"pushInFirst\"|\"pushOutFirst\"|\"pushOutOnly\" (Paragraph panel menu › Kinsoku Shori Type: how area type keeps those characters off a line's ends; pushInFirst sets them on the line when its gaps next to CJK characters can close up enough, by at most a tenth of an em each, pushOutFirst does so only when moving the character before them to the next line would leave the line more than an em short, and pushOutOnly, the default, moves it unless Burasagari hangs an overflowing comma or full stop; no effect with kinsoku \"none\"), bunriKinshi?: bool (Paragraph panel menu › Bunri-Kinshi: a line never breaks inside an inseparable pair, two em dashes, two horizontal ellipses, two two-dot leaders or the two halves of a vertical kana repeat mark, and justification adds no space inside one; kept but without effect with kinsoku \"none\"; off by default), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (where characters smaller than the largest on their line line up with it), proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), tateChuYoko?: bool (Character panel menu › Tate-chu-yoko, with Show East Asian Options: in vertical type those characters stand side by side across one em of the column; kept on the characters and ignored in horizontal type), tcyUpDown?, tcyLeftRight?: pt -1296..1296 (Character panel menu › Tate-chu-yoko Settings, as text.setRangeStyle: a tate-chu-yoko block moves up or down, right or left, and on a vertical path toward or away from the path's start, to its left or right), warichu?: bool, warichuLines?: 1..6, warichuGap?: pt 0..100, warichuScale?: % 10..100, warichuAlign?: \"auto\"|\"left\"|\"center\"|\"right\"|\"justifyLeft\"|\"justifyCenter\"|\"justifyRight\"|\"justifyAll\" (Character panel menu › Warichu and Warichu Settings, as text.setRangeStyle: the characters set smaller and stacked on lines inside their line), akiLeft?, akiRight?: 1/1000 em 0..1000 (Character panel › Insert Aki Left and Right, with Show East Asian Options, as text.setRangeStyle: space before and after each character in reading order), tsume?: % 0..100 (Character panel › Tsume, with Show East Asian Options, as text.setRangeStyle: the share of the blank space beside each character's ink taken away in horizontal type), diacriticHorizontal?, diacriticVertical?: 1/1000 em -1000..1000 (Character panel › Adjust Horizontal and Vertical Diacritic Position, with Show Indic Options, as text.setRangeStyle: the mark glyphs among the characters move right or up relative to their letters), standardVerticalRoman?: bool (Character panel menu › Standard Vertical Roman Alignment, as text.setRangeStyle), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), kashidas?: bool (Character panel menu › Kashidas: selected characters allow insertion only when both joined neighbors have it on; on by default), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), glyphForm?: \"traditional\"|\"expert\"|\"jis04\"|\"jis90\"|\"jis78\"|\"jis83\"|\"fullWidth\"|\"halfWidth\"|\"thirdWidth\"|\"quarterWidth\"|\"default\" (Glyphs panel menu, with Show East Asian Options, as text.setRangeStyle: the characters drawn in one OpenType form of the font, or its default glyphs), fontHeightReference?: \"emBox\"|\"capHeight\"|\"xHeight\"|\"icfBox\" (Character panel › Set Font Height Reference, as text.setRangeStyle: the height of the font that the size measures), language?: name|tag (Character panel › Language, as text.setRangeStyle), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font, as text.setRangeStyle: each axis named set in its units and clamped to its range, null back to the style's own setting), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\"), romanHangingPunctuation?: bool (Paragraph panel menu › Roman Hanging Punctuation: quotation marks, hyphens, full stops and commas starting or ending a line hang outside the margin, asterisks, tildes, ellipses, dashes, colons and semicolons by half; off by default), koreanLineBreakAnywhere?: bool (Paragraph panel menu › Korean Line Break Anywhere: a line may break between any two Hangul syllables; off, a word with Hangul in it breaks only at its spaces and punctuation, or where it is wider than the line; new type: false, paragraphs saved before the option: true), kashida?: \"none\"|\"short\"|\"medium\"|\"long\"|\"stylistic\" (Paragraph panel › Insert Kashidas: justified lines of horizontal area type lengthen each Arabic word with a kashida between its last pair of joined letters, at most a quarter, half or whole em long, or whole tatweels up to an em for stylistic, before they widen their word spaces; none by default), composer?: \"singleLine\"|\"everyLine\" (line breaking, Every-line by default), start?: byte, end?: byte} (baselineShift is in document points and hScale includes the object transform; groups include their text descendants; with a range: the character attributes style that range and the paragraph attributes apply to the paragraphs it touches; without: all the text, and graph labels keep the character and paragraph attributes for every label of their kind when the graph is drawn again, the graph still setting each label's alignment; a number or switch given as another type fails the call, and a null value leaves its attribute as it is, so a call with only null values fails with nothing to change; a call that gives the values the text already has records no undo step). Or {paragraphIndents: [{id, start: byte where the paragraph starts, leftIndent?, firstLineIndent?: pt -1296..1296}…]} alone (Tabs panel indent markers over paragraphs with indents of their own): each entry sets one paragraph of a type object, at least one indent each, at most 100000 entries; every entry is checked before anything changes, and an unknown key, a value out of range, an object that is not type, a byte where no paragraph starts or two entries for one paragraph with other values fail the call; all of it is one edit, a threaded story flowing again once, and when no paragraph's indents change nothing is recorded → {ids}", has_doc, set_format ), @@ -614,6 +614,8 @@ fn set_format(s: &mut Session, p: &Value) -> Result { "akiLeft", "akiRight", "tsume", + "diacriticHorizontal", + "diacriticVertical", "standardVerticalRoman", "noBreak", "digits", @@ -661,6 +663,8 @@ fn set_format(s: &mut Session, p: &Value) -> Result { let aki_left = super::textedit::aki_param(p, "akiLeft", C)?; let aki_right = super::textedit::aki_param(p, "akiRight", C)?; let tsume = super::textedit::tsume_param(p, C)?; + let diacritic_horizontal = super::textedit::diacritic_param(p, "diacriticHorizontal", C)?; + let diacritic_vertical = super::textedit::diacritic_param(p, "diacriticVertical", C)?; let warichu_lines = super::textedit::warichu_lines_param(p, C)?; let warichu_gap = super::textedit::warichu_number_param(p, "warichuGap", C)?; let warichu_scale = super::textedit::warichu_number_param(p, "warichuScale", C)?; @@ -851,6 +855,8 @@ fn set_format(s: &mut Session, p: &Value) -> Result { if let Some(v) = tsume { st.tsume = v; } + st.diacritic_horizontal = diacritic_horizontal.unwrap_or(st.diacritic_horizontal); + st.diacritic_vertical = diacritic_vertical.unwrap_or(st.diacritic_vertical); if let Some(v) = flag("standardVerticalRoman") { st.standard_vertical_roman = v; } diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index 10f1d57342..fa6e556c6c 100644 --- a/crates/engine/src/cmd/textedit.rs +++ b/crates/engine/src/cmd/textedit.rs @@ -40,7 +40,7 @@ pub fn specs() -> Vec { "Character", [], None, - "{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), story?: bool = false (all the text of every frame of the threaded story `id` is a frame of, in one undo step; no `start` or `end`), font?, style?, size?: pt 0.1..1296 (outside it: an error), leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\"|\"optical\" (the space after each character), kerningBefore?: 1/1000 em -1000..10000 (the space before a character that starts a line by itself: the first character of the text, of a paragraph or after a forced line break; no effect elsewhere; 0 by default), baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg counter-clockwise (0: upright), fill?: colour|\"none\", stroke?: colour|\"none\" (#rrggbbaa and #rgba are refused in both), 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\", \"salt=2\", …] (swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom and aalt take the font's alternate number as `tag=N`, N 0–65535: \"salt=2\" draws the second alternate of each glyph that has one, \"salt\" is \"salt=1\", \"-salt\" and \"salt=0\" are off; \"hkna\" is H or V Style, kana in the font's forms for the writing direction, shaped as `vkna` in vertical type, and \"ital\" is Roman Italics, half-width alphanumerics in the font's italic forms, both on or off only; the range is shaped with its word, so a letter's alternate keeps it joined to its neighbours), featureChanges?: [\"tnum\", \"-onum\", \"salt=2\", …] (OpenType features turned on, or with - off, on top of each character's own, a value replacing the feature's last one; 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; on a vertical path a positive Up/Down moves it toward the path's start and a positive Left/Right to the path's left; 0 by default; automatic digit blocks and horizontal type do not move; neither turns tateChuYoko on), warichu?: bool (Character panel menu › Warichu, with Show East Asian Options: the characters are set at warichuScale percent of their size and stacked on warichuLines lines inside their line, one above the other in horizontal type and side by side in vertical type, the first line above or to the right; a stretch of them is one block with the settings of its first character that stays whole on one line and may run past its end; type on a path sets them at their own size), warichuLines?: 1..6 (default 2), warichuGap?: pt 0..100 (the space between the lines, default 0), warichuScale?: % 10..100 (default 50), warichuAlign?: \"auto\"|\"left\"|\"center\"|\"right\"|\"justifyLeft\"|\"justifyCenter\"|\"justifyRight\"|\"justifyAll\" (Warichu Settings: how the lines line up along the block, which is as long as its longest line; the justify values spread every line but the last, which is set left, centered or right, or spread with justifyAll; auto, the default, sets them as justifyLeft; left and right are top and bottom in vertical type; none of them turns warichu on), akiLeft?, akiRight?: 1/1000 em 0..1000 (Character panel › Insert Aki Left and Right, with Show East Asian Options: space before and after each character in reading order, on its left and right in left-to-right type, on its right and left in right-to-left type, above and below it in vertical type; a ligature or a letter with its marks takes the values of its first character; line wrapping counts it, alignment keeps it at a line's ends and justification does not stretch or narrow it; 0 by default; a value that is not a number or is outside the range is refused, naming the key), tsume?: % 0..100 (Character panel › Tsume, with Show East Asian Options: the share of the blank space on each side of each character's ink, inside its own width, that is taken away in horizontal type, the glyph keeping its shape; 100 sets the ink edge to edge; a character without ink, such as a space, keeps its width; a ligature or a letter with its marks takes the value of its first character; tracking, kerning and Insert Aki are added after it, and justification keeps it; vertical type keeps the characters' spacing; 0 by default; a value that is not a number or is outside the range is refused), 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), kashidas?: bool (Character panel menu › Kashidas: selected characters allow insertion only when both joined neighbors have it on; on by default), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), glyphForm?: \"traditional\"|\"expert\"|\"jis04\"|\"jis90\"|\"jis78\"|\"jis83\"|\"fullWidth\"|\"halfWidth\"|\"thirdWidth\"|\"quarterWidth\"|\"default\" (Glyphs panel menu, with Show East Asian Options: the characters are drawn in that form of the font, the OpenType feature trad, expt, jp04, jp90, jp78, jp83, fwid, hwid, twid or qwid, one form at a time; a character the font has no such glyph for keeps its own; \"default\", Revert To Default Forms, draws the font's default glyphs; another value is refused), 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), glyphAlternate?: null (takes away the glyph choice text.setGlyphAlternate made on the characters, in the same undo step as the other attributes; any other value is refused)} 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), story?: bool = false (all the text of every frame of the threaded story `id` is a frame of, in one undo step; no `start` or `end`), font?, style?, size?: pt 0.1..1296 (outside it: an error), leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\"|\"optical\" (the space after each character), kerningBefore?: 1/1000 em -1000..10000 (the space before a character that starts a line by itself: the first character of the text, of a paragraph or after a forced line break; no effect elsewhere; 0 by default), baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg counter-clockwise (0: upright), fill?: colour|\"none\", stroke?: colour|\"none\" (#rrggbbaa and #rgba are refused in both), 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\", \"salt=2\", …] (swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom and aalt take the font's alternate number as `tag=N`, N 0–65535: \"salt=2\" draws the second alternate of each glyph that has one, \"salt\" is \"salt=1\", \"-salt\" and \"salt=0\" are off; \"hkna\" is H or V Style, kana in the font's forms for the writing direction, shaped as `vkna` in vertical type, and \"ital\" is Roman Italics, half-width alphanumerics in the font's italic forms, both on or off only; the range is shaped with its word, so a letter's alternate keeps it joined to its neighbours), featureChanges?: [\"tnum\", \"-onum\", \"salt=2\", …] (OpenType features turned on, or with - off, on top of each character's own, a value replacing the feature's last one; 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; on a vertical path a positive Up/Down moves it toward the path's start and a positive Left/Right to the path's left; 0 by default; automatic digit blocks and horizontal type do not move; neither turns tateChuYoko on), warichu?: bool (Character panel menu › Warichu, with Show East Asian Options: the characters are set at warichuScale percent of their size and stacked on warichuLines lines inside their line, one above the other in horizontal type and side by side in vertical type, the first line above or to the right; a stretch of them is one block with the settings of its first character that stays whole on one line and may run past its end; type on a path sets them at their own size), warichuLines?: 1..6 (default 2), warichuGap?: pt 0..100 (the space between the lines, default 0), warichuScale?: % 10..100 (default 50), warichuAlign?: \"auto\"|\"left\"|\"center\"|\"right\"|\"justifyLeft\"|\"justifyCenter\"|\"justifyRight\"|\"justifyAll\" (Warichu Settings: how the lines line up along the block, which is as long as its longest line; the justify values spread every line but the last, which is set left, centered or right, or spread with justifyAll; auto, the default, sets them as justifyLeft; left and right are top and bottom in vertical type; none of them turns warichu on), akiLeft?, akiRight?: 1/1000 em 0..1000 (Character panel › Insert Aki Left and Right, with Show East Asian Options: space before and after each character in reading order, on its left and right in left-to-right type, on its right and left in right-to-left type, above and below it in vertical type; a ligature or a letter with its marks takes the values of its first character; line wrapping counts it, alignment keeps it at a line's ends and justification does not stretch or narrow it; 0 by default; a value that is not a number or is outside the range is refused, naming the key), tsume?: % 0..100 (Character panel › Tsume, with Show East Asian Options: the share of the blank space on each side of each character's ink, inside its own width, that is taken away in horizontal type, the glyph keeping its shape; 100 sets the ink edge to edge; a character without ink, such as a space, keeps its width; a ligature or a letter with its marks takes the value of its first character; tracking, kerning and Insert Aki are added after it, and justification keeps it; vertical type keeps the characters' spacing; 0 by default; a value that is not a number or is outside the range is refused), diacriticHorizontal?, diacriticVertical?: 1/1000 em -1000..1000 (Character panel › Adjust Horizontal and Vertical Diacritic Position, with Show Indic Options: the font's mark glyphs among the characters, those its GDEF table classes as marks such as Arabic harakat and Hebrew points, move right by a positive horizontal value and up by a positive vertical one, relative to their letters, in thousandths of the size; a harakat or Hebrew point styled apart from its letter takes its own values and stays attached to it, and another mark takes its letter's; letters and fonts without mark classes do not move; 0 by default; a value that is not a number or is outside the range is refused, naming the key), 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), kashidas?: bool (Character panel menu › Kashidas: selected characters allow insertion only when both joined neighbors have it on; on by default), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), glyphForm?: \"traditional\"|\"expert\"|\"jis04\"|\"jis90\"|\"jis78\"|\"jis83\"|\"fullWidth\"|\"halfWidth\"|\"thirdWidth\"|\"quarterWidth\"|\"default\" (Glyphs panel menu, with Show East Asian Options: the characters are drawn in that form of the font, the OpenType feature trad, expt, jp04, jp90, jp78, jp83, fwid, hwid, twid or qwid, one form at a time; a character the font has no such glyph for keeps its own; \"default\", Revert To Default Forms, draws the font's default glyphs; another value is refused), 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), glyphAlternate?: null (takes away the glyph choice text.setGlyphAlternate made on the characters, in the same undo step as the other attributes; any other value is refused)} 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 ), @@ -470,6 +470,9 @@ pub(crate) struct CharChange { aki_right: Option, /// Tsume, in percent. tsume: Option, + /// Adjust Horizontal and Vertical Diacritic Position, in 1/1000 em. + diacritic_horizontal: Option, + diacritic_vertical: Option, standard_vertical_roman: Option, no_break: Option, digits: Option, @@ -548,6 +551,16 @@ pub(crate) fn aki_param(p: &Value, key: &str, cmd: &str) -> Result> if (lo..=hi).contains(&v) { Ok(Some(v)) } else { Err(bad(cmd, format!("`{key}` must be from {lo} to {hi}"))) } } +/// `diacriticHorizontal` or `diacriticVertical` (Adjust Horizontal and Vertical Diacritic +/// Position): a number of thousandths of an em within +/// [`vectorcraft_doc::text::MAX_DIACRITIC_POSITION`] either way. Any other value is an error. +pub(crate) fn diacritic_param(p: &Value, key: &str, cmd: &str) -> 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) = (-vectorcraft_doc::text::MAX_DIACRITIC_POSITION, vectorcraft_doc::text::MAX_DIACRITIC_POSITION); + if (lo..=hi).contains(&v) { Ok(Some(v)) } else { Err(bad(cmd, format!("`{key}` must be from {lo} to {hi}"))) } +} + /// `tsume` (Tsume): a percentage from 0 to [`vectorcraft_doc::text::MAX_TSUME`]. Any other value is /// an error. pub(crate) fn tsume_param(p: &Value, cmd: &str) -> Result> { @@ -852,6 +865,8 @@ impl CharChange { aki_left: aki_param(p, "akiLeft", cmd)?, aki_right: aki_param(p, "akiRight", cmd)?, tsume: tsume_param(p, cmd)?, + diacritic_horizontal: diacritic_param(p, "diacriticHorizontal", cmd)?, + diacritic_vertical: diacritic_param(p, "diacriticVertical", cmd)?, standard_vertical_roman: flag("standardVerticalRoman"), no_break: flag("noBreak"), digits: digits_param(p, cmd)?, @@ -918,6 +933,8 @@ impl CharChange { && self.aki_left.is_none() && self.aki_right.is_none() && self.tsume.is_none() + && self.diacritic_horizontal.is_none() + && self.diacritic_vertical.is_none() && self.standard_vertical_roman.is_none() && self.no_break.is_none() && self.digits.is_none() @@ -963,7 +980,8 @@ impl CharChange { } } - /// The character alignment, direction and East Asian settings of [`Self::apply`]. + /// The character alignment, direction, diacritic positions and East Asian settings of + /// [`Self::apply`]. fn apply_cjk(&self, st: &mut CharStyle) { if let Some(v) = self.char_align { st.char_align = v; @@ -1010,6 +1028,12 @@ impl CharChange { if let Some(v) = self.tsume { st.tsume = v; } + if let Some(v) = self.diacritic_horizontal { + st.diacritic_horizontal = v; + } + if let Some(v) = self.diacritic_vertical { + st.diacritic_vertical = 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 18f87d44f7..efcc078e65 100644 --- a/crates/engine/src/cmd/textstyles.rs +++ b/crates/engine/src/cmd/textstyles.rs @@ -223,6 +223,8 @@ pub fn char_attrs(v: &CharStyle) -> Map { ("akiLeft", json!(v.aki_left)), ("akiRight", json!(v.aki_right)), ("tsume", json!(v.tsume)), + ("diacriticHorizontal", json!(v.diacritic_horizontal)), + ("diacriticVertical", json!(v.diacritic_vertical)), ("standardVerticalRoman", json!(v.standard_vertical_roman)), ("noBreak", json!(v.no_break)), ("digits", json!(v.digits)), @@ -584,6 +586,13 @@ fn attrs_param(d: &Document, p: &Value, kind: Kind, verb: &str, kept: Option<&Ma if changed(key) && !(lo..=hi).contains(&st.tsume) { return Err(bad(&c, format!("`{key}` must be from {lo} to {hi}"))); } + let hi = vectorcraft_doc::text::MAX_DIACRITIC_POSITION; + let lo = -hi; + for (key, value) in [("diacriticHorizontal", st.diacritic_horizontal), ("diacriticVertical", st.diacritic_vertical)] { + if changed(key) && !(lo..=hi).contains(&value) { + return Err(bad(&c, format!("`{key}` must be from {lo} to {hi}"))); + } + } let lines = vectorcraft_doc::text::WARICHU_LINES; if changed("warichuLines") && !lines.contains(&st.warichu_lines) { return Err(bad(&c, format!("`warichuLines` must be a whole number from {} to {}", lines.start(), lines.end()))); @@ -1081,12 +1090,12 @@ fn unused(s: &mut Session, kind: Kind) -> Result { /// The styles of `kind` in `src` that load, with their source names so that art in the file keeps /// referring to them: not the Normal style, and not one whose attributes don't read, whose sizes, -/// scales, leading, shifts, Insert Aki, Tsume, Warichu Settings or stroke width are out of the range `charStyle.new` takes, whose +/// scales, leading, shifts, Insert Aki, Tsume, diacritic positions, Warichu Settings or stroke width are out of the range `charStyle.new` takes, whose /// indents, spacing, Hyphenation or Justification options or tab stops are out of the range /// `paraStyle.new` takes, or whose name is empty, which are skipped. → (the styles, the names in /// `src` of those skipped) fn loadable(src: &Document, kind: Kind) -> (Vec, Vec) { - const RANGED: [&str; 22] = [ + const RANGED: [&str; 24] = [ "size", "h_scale", "v_scale", @@ -1095,6 +1104,8 @@ fn loadable(src: &Document, kind: Kind) -> (Vec, Vec) { "akiLeft", "akiRight", "tsume", + "diacriticHorizontal", + "diacriticVertical", "tcyUpDown", "tcyLeftRight", "warichuLines", diff --git a/crates/engine/src/cmd/textstyles/tests.rs b/crates/engine/src/cmd/textstyles/tests.rs index c8da4b578c..5b2c19ed51 100644 --- a/crates/engine/src/cmd/textstyles/tests.rs +++ b/crates/engine/src/cmd/textstyles/tests.rs @@ -399,6 +399,8 @@ fn character_style_dimensions_are_checked_before_a_definition_changes() { json!({"akiRight": 1000.1}), json!({"tsume": -0.1}), json!({"tsume": 100.1}), + json!({"diacriticHorizontal": -1000.1}), + json!({"diacriticVertical": 1000.1}), ] { let key = attrs.as_object().unwrap().keys().next().unwrap(); for cmd in ["charStyle.edit", "charStyle.setAttrs", "charStyle.new"] { @@ -422,7 +424,8 @@ fn character_style_dimensions_are_checked_before_a_definition_changes() { "baseline_shift": -1296.0, "tcyUpDown": 1296.0, "tcyLeftRight": -1296.0, "akiLeft": 0.0, "akiRight": 1000.0, - "tsume": 100.0 + "tsume": 100.0, + "diacriticHorizontal": -1000.0, "diacriticVertical": 1000.0 }}), ) .unwrap(); diff --git a/crates/engine/src/cmd/typecmd/area_tests.rs b/crates/engine/src/cmd/typecmd/area_tests.rs index e0e5d8a926..17ac2c647e 100644 --- a/crates/engine/src/cmd/typecmd/area_tests.rs +++ b/crates/engine/src/cmd/typecmd/area_tests.rs @@ -1921,6 +1921,66 @@ fn tsume_is_a_character_attribute() { assert_eq!(s.doc().unwrap().history.undo.len(), undo, "no undo step for a refused call"); } +/// Adjust Horizontal and Vertical Diacritic Position (Character panel, Middle Eastern options): +/// character attributes in thousandths of an em, 0 by default and saved only when not 0. Both +/// commands set them in one undo step, setting one leaves the other, a call that gives the values +/// the text already has records none, and a value that is not a number or lies outside -1000 to +/// 1000 is refused, naming the key, before anything changes; the native file and a character style +/// captured from the characters carry them. +#[test] +fn diacritic_positions_are_character_attributes() { + 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": "بَب"})).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 moved = |s: &Session| runs(s).iter().map(|r| (r.text.clone(), r.style.diacritic_horizontal, r.style.diacritic_vertical)).collect::>(); + let saved = serde_json::to_value(&runs(&s)[0].style).unwrap(); + assert!(saved.get("diacriticHorizontal").is_none() && saved.get("diacriticVertical").is_none()); + s.execute("select.set", &json!({"ids": [id]})).unwrap(); + s.execute("text.setFormat", &json!({"diacriticHorizontal": -150})).unwrap(); + s.execute("text.setFormat", &json!({"diacriticVertical": 250})).unwrap(); + assert_eq!(moved(&s), [("بَب".to_string(), -150.0, 250.0)], "setting one leaves the other"); + let undo = s.doc().unwrap().history.undo.len(); + s.execute("text.setFormat", &json!({"diacriticHorizontal": -150, "diacriticVertical": 250})).unwrap(); + assert_eq!(s.doc().unwrap().history.undo.len(), undo, "no undo step without a change"); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(moved(&s), [("بَب".to_string(), -150.0, 0.0)], "one undo step"); + s.execute("edit.redo", &json!({})).unwrap(); + + let mark = "بَ".len(); + s.execute("text.setRangeStyle", &json!({"id": id, "start": "ب".len(), "end": mark, "diacriticVertical": -1000})).unwrap(); + assert_eq!(moved(&s), [("ب".to_string(), -150.0, 250.0), ("\u{064E}".to_string(), -150.0, -1000.0), ("ب".to_string(), -150.0, 250.0)]); + let saved = serde_json::to_value(&runs(&s)[1].style).unwrap(); + assert_eq!((saved["diacriticHorizontal"].clone(), saved["diacriticVertical"].clone()), (json!(-150.0), json!(-1000.0))); + 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.diacritic_horizontal, r.style.diacritic_vertical)).collect(); + assert_eq!(reopened, moved(&s)); + let attrs = super::super::textstyles::char_attrs(&runs(&s)[1].style); + assert_eq!((attrs["diacriticHorizontal"].clone(), attrs["diacriticVertical"].clone()), (json!(-150.0), json!(-1000.0))); + + let before = moved(&s); + let undo = s.doc().unwrap().history.undo.len(); + for (cmd, p, want) in [ + ("text.setFormat", json!({"diacriticHorizontal": "50"}), "`diacriticHorizontal` must be a number"), + ("text.setFormat", json!({"diacriticVertical": 1000.5}), "`diacriticVertical` must be from -1000 to 1000"), + ("text.setRangeStyle", json!({"id": id, "diacriticVertical": true}), "`diacriticVertical` must be a number"), + ( + "text.setRangeStyle", + json!({"id": id, "start": 0, "end": mark, "diacriticHorizontal": -1001}), + "`diacriticHorizontal` must be from -1000 to 1000", + ), + ] { + let e = s.execute(cmd, &p).unwrap_err(); + assert!(matches!(&e, EngineError::BadParams { msg, .. } if msg == want), "{cmd} {p}: {e}"); + } + assert_eq!(moved(&s), before); + assert_eq!(s.doc().unwrap().history.undo.len(), undo, "no undo step for a refused call"); +} + /// 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/svg/src/export/text.rs b/crates/svg/src/export/text.rs index 2f8e8b8a1b..f325387033 100644 --- a/crates/svg/src/export/text.rs +++ b/crates/svg/src/export/text.rs @@ -86,11 +86,15 @@ impl Writer<'_> { // type with warichu, whose stacked lines a reader's can't place either. So are // small capitals, superscript and subscript and tab leaders, which SVG text doesn't draw (#3869), // and a glyph chosen for a character, which a reader's draws with the default glyph. So - // is type in one of the document's composite fonts, which a reader can't find by its name. + // is type in one of the document's composite fonts, which a reader can't find by its name, + // and type with an Adjust Diacritic Position, whose marks a reader's draws where the + // font puts them. let unwritable = t.runs.iter().any(|r| { r.style.small_caps.is_some() || r.style.position != vectorcraft_doc::CharPosition::Normal || r.style.glyph_alternate.is_some() + || r.style.diacritic_horizontal != 0.0 + || r.style.diacritic_vertical != 0.0 || vectorcraft_doc::CompositeFont::named(&doc.composite_fonts, &r.style.font_family).is_some() }) || std::iter::once(&t.para).chain(&t.paras).any(|p| p.tabs.iter().any(|tab| !tab.leader.is_empty())); let outlined = self.opts.outline_text diff --git a/crates/svg/tests/it/text_layout.rs b/crates/svg/tests/it/text_layout.rs index c53318c358..8c61087e3e 100644 --- a/crates/svg/tests/it/text_layout.rs +++ b/crates/svg/tests/it/text_layout.rs @@ -357,6 +357,19 @@ fn a_chosen_glyph_is_written_as_outlines() { assert!(!svg.contains("` would draw +/// the marks where the font puts them. +#[test] +fn moved_diacritics_are_written_as_outlines() { + let svg = export(&area_doc(vec![run("re\u{301}sume\u{301}", style(12.0))], 280.0, Justify::Left), &ExportOptions::default()); + assert!(svg.contains("` element's 12. diff --git a/crates/text/src/harakat.rs b/crates/text/src/harakat.rs index ab5aa593bd..9ea14d0f08 100644 --- a/crates/text/src/harakat.rs +++ b/crates/text/src/harakat.rs @@ -13,6 +13,12 @@ //! a mark on a ligature (for example Geeza Pro's Allah ligature) painted as its letter. Renderers //! and exporters read the glyph's run as they always did, so they need no change. The text itself //! is never changed. +//! +//! The Adjust Horizontal and Vertical Diacritic Positions ([`CharStyle::diacritic_horizontal`]) +//! move the face's mark glyphs (GDEF class 3) by the values of the glyph's run. Runs that differ +//! only in paint and those positions stay one shaping cluster at an Arabic harakat or a Hebrew +//! point ([`diacritic_mark_boundary`]), so a mark set apart from its letter takes its own values +//! the same way it takes its own paint. use skrifa::raw::TableProvider; use skrifa::raw::tables::layout::ClassDef; @@ -36,11 +42,24 @@ pub(crate) fn is_harakat(c: char) -> bool { ) } +/// Is `c` a Hebrew point or cantillation mark (U+0591–U+05BD, U+05BF, U+05C1–U+05C2, +/// U+05C4–U+05C5, U+05C7)? +fn is_hebrew_mark(c: char) -> bool { + matches!(c as u32, 0x0591..=0x05BD | 0x05BF | 0x05C1..=0x05C2 | 0x05C4..=0x05C5 | 0x05C7) +} + +/// Is `c` a diacritical mark the Adjust Diacritic Position fields set apart from its letter: an +/// Arabic harakat ([`is_harakat`]) or a Hebrew point? +pub(crate) fn is_diacritic(c: char) -> bool { + is_harakat(c) || is_hebrew_mark(c) +} + /// Do character styles `a` and `b` differ, if at all, only in paint: fill, stroke and how the /// stroke is drawn, overprint, and the character style name? OpenType feature differences are /// ignored here: they already apply to their own character ranges during shaping. Nor is the /// space before a line's first character read inside a cluster, which takes it from its first -/// character (#5111). Stricter than [`crate::shape`]'s `shapes_alike` (which also ignores baseline +/// character (#5111), nor the Adjust Diacritic Positions, which move a mark glyph by its own run's +/// values. Stricter than [`crate::shape`]'s `shapes_alike` (which also ignores baseline /// shift, tracking, underline, …). pub(crate) fn paint_only(a: &CharStyle, b: &CharStyle) -> bool { // Exhaustive: a new attribute must explicitly keep or relax this boundary rule. @@ -82,6 +101,8 @@ pub(crate) fn paint_only(a: &CharStyle, b: &CharStyle) -> bool { warichu_scale, warichu_align, digits, + diacritic_horizontal: _, + diacritic_vertical: _, font_height_reference, no_break, language, @@ -150,6 +171,16 @@ pub(crate) fn paint_only_mark_boundary(text: &str, at: usize, before: &CharStyle (next.is_some_and(is_harakat) || previous.is_some_and(is_harakat)) && paint_only(before, after) } +/// Is the run boundary at byte `at` of `text`, between styles `before` and `after`, one where the +/// Adjust Diacritic Positions differ, nothing but paint differs besides, and an Arabic harakat or a +/// Hebrew point is on either side? It stays inside a shaping cluster as a paint-only one does. +pub(crate) fn diacritic_mark_boundary(text: &str, at: usize, before: &CharStyle, after: &CharStyle) -> bool { + let next = text.get(at..).and_then(|s| s.chars().next()); + let previous = text.get(..at).and_then(|s| s.chars().next_back()); + let moved = before.diacritic_horizontal != after.diacritic_horizontal || before.diacritic_vertical != after.diacritic_vertical; + moved && (next.is_some_and(is_diacritic) || previous.is_some_and(is_diacritic)) && paint_only(before, after) +} + /// The byte ranges of the harakat in `text[range]`, each run of consecutive harakat as one range. /// The range is normalised as [`crate::edit::style_range`] takes one: its ends clamped to the /// text, each moved back to the start of the character it falls in, and put in order. @@ -194,7 +225,8 @@ pub(crate) fn mark_classes(face: &FontFace) -> Option> { /// by glyph): repeated glyph ids alone could hide a changed correspondence. A glyph whose character /// lies outside its own shaping cluster of `raw` (from its cluster value up to the next larger one, /// else `end`) gets its cluster's first character instead. A later in-cluster character is kept -/// only when the glyph is a GDEF mark and that character is a harakat; otherwise the cluster start +/// only when the glyph is a GDEF mark and that character is a harakat or a Hebrew point +/// ([`is_diacritic`]); otherwise the cluster start /// is kept (conservative: a mark may miss its colour; a base must never take a mark's). `raw`'s /// clusters are never changed. These are HarfBuzz shaping clusters, which can span more than one /// grapheme (a ligature). @@ -224,8 +256,9 @@ pub(crate) fn own_chars( if c.1 == r.1 { return c.1; } - // Later in-cluster character: only a positively identified mark glyph on a harakat. - let mark_char = text.get(c.1 as usize..).and_then(|s| s.chars().next()).is_some_and(is_harakat); + // Later in-cluster character: only a positively identified mark glyph on a harakat or + // a Hebrew point. + let mark_char = text.get(c.1 as usize..).and_then(|s| s.chars().next()).is_some_and(is_diacritic); if is_gdef_mark(r.0) && mark_char { c.1 } else { r.1 } }) .collect() diff --git a/crates/text/src/shape.rs b/crates/text/src/shape.rs index 9d36eed28b..d89561117c 100644 --- a/crates/text/src/shape.rs +++ b/crates/text/src/shape.rs @@ -1032,6 +1032,10 @@ fn drawn_alike(a: &CharStyle, b: &CharStyle, vertical: bool) -> bool { warichu_scale: _, warichu_align: _, digits, + // A mark glyph moves by its own run's values ([`shape_segment`]), so a mark keeps its + // letter whatever they are. + diacritic_horizontal: _, + diacritic_vertical: _, font_height_reference, no_break, language, @@ -1106,7 +1110,8 @@ pub(crate) fn warichu_alike(a: &CharStyle, b: &CharStyle) -> bool { /// character style's name, leading, baseline shift, rotation, Character Alignment, Character /// Direction (text is shaped apart where its bidi level changes), Kashidas, the kerning (each glyph takes its /// run's, see [`shape_segment`]), the space before a line's first character ([`space_before`]), -/// Insert Aki ([`aki`]), Tsume ([`tighten`]), the OpenType features and the East Asian glyph form (each run's apply to its own characters, as features +/// Insert Aki ([`aki`]), Tsume ([`tighten`]), the Adjust Diacritic Positions (each mark glyph takes +/// its run's), the OpenType features and the East Asian glyph form (each run's apply to its own characters, as features /// over a range of the one buffer) and the tracking, as long as the tracking keeps or drops /// standard ligatures alike ([`crate::features::ligatures_suppressed_by`]). fn shapes_alike(a: &CharStyle, b: &CharStyle) -> bool { @@ -1165,6 +1170,8 @@ fn shapes_alike(a: &CharStyle, b: &CharStyle) -> bool { warichu_scale: _, warichu_align: _, digits, + diacritic_horizontal: _, + diacritic_vertical: _, font_height_reference, no_break, language, @@ -1437,11 +1444,21 @@ fn shape_segment(text: &str, context: &Range, seg: &Segment, feats: &OtFe if !manual_ends.is_empty() { return shape_apart(manual_ends, out); } - // Harakat styled apart from their letters (paint only): shaped with them, each glyph then - // takes its own character's run ([`crate::harakat`]). + // Harakat styled apart from their letters (paint only), and harakat and Hebrew points with + // diacritic positions of their own: shaped with them, each glyph then takes its own + // character's run ([`crate::harakat`]). let mark_boundaries: Vec = boundaries() - .filter(|&b| run_at(b - 1).1 != run_at(b).1 && crate::harakat::paint_only_mark_boundary(text, b, run_at(b - 1).1, run_at(b).1)) + .filter(|&b| { + let (before, after) = (run_at(b - 1).1, run_at(b).1); + before != after + && (crate::harakat::paint_only_mark_boundary(text, b, before, after) + || crate::harakat::diacritic_mark_boundary(text, b, before, after)) + }) .collect(); + // The face's mark classes, read once when any of the segment's runs moves its marks. + let moves_marks = + runs.get(source_runs.clone()).unwrap_or_default().iter().any(|(_, s)| s.diacritic_horizontal != 0.0 || s.diacritic_vertical != 0.0); + let diacritic_classes = if moves_marks { crate::harakat::mark_classes(face) } else { None }; // Source run starts are sorted. Reuse their classification when checking cluster splits. let styled_marks = !mark_boundaries.is_empty(); let char_level = std::cell::Cell::new(false); @@ -1734,11 +1751,21 @@ fn shape_segment(text: &str, context: &Range, seg: &Segment, feats: &OtFe for (gi, &(gid, cl, xa, xo, yo)) in raw.iter().enumerate() { let cl = cl as usize; let (run, gst) = run_at(cl); - // A glyph of a harakat styled apart takes its own character's run (paint only). - let run = match chars.get(gi).map(|&c| run_at(c as usize)) { - Some((own, own_st)) if crate::harakat::paint_only(gst, own_st) => own, - _ => run, + // A glyph of a harakat styled apart takes its own character's run (paint and diacritic + // positions only). + let (run, run_st) = match chars.get(gi).map(|&c| run_at(c as usize)) { + Some((own, own_st)) if crate::harakat::paint_only(gst, own_st) => (own, own_st), + _ => (run, gst), }; + // A mark glyph (GDEF class 3) moves by its run's Adjust Diacritic Positions, thousandths + // of the em it is set in: right and up for positive values. + let (mut dx, mut dy) = (xo as f64 * k * hs, -(yo as f64) * k * vs); + if let Some(classes) = &diacritic_classes + && classes.get(skrifa::GlyphId::new(gid)) == 3 + { + dx += run_st.diacritic_horizontal / 1000.0 * size * hs; + dy -= run_st.diacritic_vertical / 1000.0 * size * vs; + } let end = cluster_end(cl); let last_in_cluster = gi + 1 == n || raw[gi + 1].1 as usize != cl; let ch = first_char(cl); @@ -1793,8 +1820,8 @@ fn shape_segment(text: &str, context: &Range, seg: &Segment, feats: &OtFe run, adv, tracking: tracked, - dx: xo as f64 * k * hs, - dy: -(yo as f64) * k * vs, + dx, + dy, kern_shift, aki_shift, aki: aki_in_adv, diff --git a/crates/text/src/tests/harakat.rs b/crates/text/src/tests/harakat.rs index 0b401a5cd3..4e11921a6f 100644 --- a/crates/text/src/tests/harakat.rs +++ b/crates/text/src/tests/harakat.rs @@ -173,6 +173,8 @@ fn paint_only_keeps_every_drawing_attribute_and_partial_equality() { b.style_name = a.style_name.clone(); b.kerning_before = a.kerning_before; b.features = a.features.clone(); + b.diacritic_horizontal = a.diacritic_horizontal; + b.diacritic_vertical = a.diacritic_vertical; *a == b } @@ -226,6 +228,8 @@ fn paint_only_keeps_every_drawing_attribute_and_partial_equality() { field!(tcy_left_right, 1.0, false); field!(standard_vertical_roman, true, false); field!(digits, Digits::Hindi, false); + field!(diacritic_horizontal, 100.0, true); + field!(diacritic_vertical, -100.0, true); field!(font_height_reference, FontHeightReference::CapHeight, false); field!(no_break, true, false); field!(language, Some("ar".into()), false); @@ -331,6 +335,74 @@ fn feature_only_harakat_boundaries_keep_attachment_and_ownership() { } } +/// Adjust Horizontal and Vertical Diacritic Position move the mark glyphs (GDEF class 3) by +/// thousandths of an em of the glyph size, right and up for positive values, and leave the letters +/// where they were. A mark styled apart from its letter takes its own values. +#[test] +fn diacritic_positions_move_marks_and_keep_letters() { + let db = FontDb::with_font_dirs(vec![]); + assert_eq!(db.add_font(crate::test_fonts::lam_alef_font(b"rlig").expect("the test font builds")), 1); + let family = crate::test_fonts::lam_alef_family(b"rlig"); + let face = db.face(family, "Regular").expect("the test face"); + const WORD: &str = "بَبَبَ"; + let plain = TextObject::point(Point::ZERO, WORD, style(family)); + let before = layout(&db, &plain); + assert_eq!(before.glyphs.len(), 6, "three bases with their attached marks"); + let (h, v) = (200.0, 100.0); + let moved = (h / 1000.0 * 48.0, -v / 1000.0 * 48.0); + // (letters set, marks set): which glyphs move. + for (letters, marks) in [(true, true), (false, true), (true, false), (false, false)] { + let mut text = plain.clone(); + for (byte, ch) in WORD.char_indices() { + if (is_harakat(ch) && marks) || (!is_harakat(ch) && letters) { + crate::edit::style_range(&mut text.runs, byte, byte + ch.len_utf8(), |st| { + st.diacritic_horizontal = h; + st.diacritic_vertical = v; + }); + } + } + let after = layout(&db, &text); + assert_eq!(after.glyphs.len(), before.glyphs.len(), "letters {letters}, marks {marks}"); + let mut marks_seen = 0; + for (a, b) in before.glyphs.iter().zip(&after.glyphs) { + assert_eq!((a.gid, a.byte, a.origin, a.advance), (b.gid, b.byte, b.origin, b.advance), "letters {letters}, marks {marks}"); + let (ta, tb) = (a.xf.translation(), b.xf.translation()); + let delta = (tb.x - ta.x, tb.y - ta.y); + let mark = is_gdef_mark(&face, a.gid); + marks_seen += usize::from(mark); + let want = if mark && marks { moved } else { (0.0, 0.0) }; + assert!( + (delta.0 - want.0).abs() < 1e-9 && (delta.1 - want.1).abs() < 1e-9, + "letters {letters}, marks {marks}: glyph {} moved {delta:?}, want {want:?}", + a.gid + ); + } + assert_eq!(marks_seen, 3, "the three fathas are marks"); + } +} + +/// Hebrew points count as diacritics as well as Arabic harakat; Latin accents and letters do not. +/// A boundary where only the diacritic positions differ asks for the character-level pass only +/// at such a mark. +#[test] +fn hebrew_points_and_harakat_are_diacritics() { + use crate::harakat::{diacritic_mark_boundary, is_diacritic}; + for c in ['\u{05B8}', '\u{05C1}', '\u{05BC}', '\u{064E}', '\u{0651}'] { + assert!(is_diacritic(c), "{c:?}"); + } + for c in ['\u{05D0}', '\u{0628}', '\u{0301}', 'a', '\u{05BE}'] { + assert!(!is_diacritic(c), "{c:?}"); + } + let a = style(FAMILY); + let b = CharStyle { diacritic_vertical: 150.0, ..a.clone() }; + assert!(diacritic_mark_boundary("שָׁ", 2, &a, &b), "a Hebrew point styled apart"); + assert!(diacritic_mark_boundary("بَ", 2, &a, &b), "a fatha styled apart"); + assert!(!diacritic_mark_boundary("e\u{301}", 1, &a, &b), "a Latin accent keeps its cluster's values"); + assert!(!diacritic_mark_boundary("بَ", 2, &a, &a), "the same values"); + let c = CharStyle { size: 20.0, ..b.clone() }; + assert!(!diacritic_mark_boundary("بَ", 2, &a, &c), "a size change splits the run anyway"); +} + /// A range is normalised as `edit::style_range` takes one: clamped, each end moved back to the /// start of its character, put in order. Never a silent empty result for a usable range. #[test] diff --git a/crates/ui-egui/src/dialogs/char_style_options.rs b/crates/ui-egui/src/dialogs/char_style_options.rs index 8915c48216..02515a3b89 100644 --- a/crates/ui-egui/src/dialogs/char_style_options.rs +++ b/crates/ui-egui/src/dialogs/char_style_options.rs @@ -26,13 +26,40 @@ const NUMBERS: &[&str] = &["size", "leading", "kerning", "tracking"]; /// The on, off or unset checkboxes. const SWITCHES: &[&str] = &["underline", "strikethrough", "standardVerticalRoman"]; /// The attributes of Advanced Character Formats, in the order the section shows them. -const ADVANCED: &[&str] = - &["h_scale", "v_scale", "baseline_shift", "tcyUpDown", "tcyLeftRight", "rotation", "akiLeft", "akiRight", "tsume", "language"]; +const ADVANCED: &[&str] = &[ + "h_scale", + "v_scale", + "baseline_shift", + "tcyUpDown", + "tcyLeftRight", + "rotation", + "akiLeft", + "akiRight", + "tsume", + "diacriticHorizontal", + "diacriticVertical", + "language", +]; /// The number fields of Advanced Character Formats. -const ADVANCED_NUMBERS: &[&str] = &["h_scale", "v_scale", "baseline_shift", "tcyUpDown", "tcyLeftRight", "rotation", "akiLeft", "akiRight", "tsume"]; +const ADVANCED_NUMBERS: &[&str] = &[ + "h_scale", + "v_scale", + "baseline_shift", + "tcyUpDown", + "tcyLeftRight", + "rotation", + "akiLeft", + "akiRight", + "tsume", + "diacriticHorizontal", + "diacriticVertical", +]; /// The Insert Aki Left and Right fields, which take the Character panel's presets and stop at /// [`vectorcraft_doc::text::MAX_AKI`]. const AKI: [&str; 2] = ["akiLeft", "akiRight"]; +/// The Adjust Horizontal and Vertical Diacritic Position fields, which take the Character panel's +/// presets and stop at [`vectorcraft_doc::text::MAX_DIACRITIC_POSITION`] either way. +const DIACRITIC: [&str; 2] = ["diacriticHorizontal", "diacriticVertical"]; /// The attributes of Character Color. const COLOR: &[&str] = &["fill", "stroke", "stroke_width", "overprint_fill", "overprint_stroke"]; /// The fields of Character Color: the fill and stroke paints, their tints in percent, the stroke @@ -103,6 +130,8 @@ pub(super) const SPEC: DialogSpec = DialogSpec { "akiLeft", "akiRight", "tsume", + "diacriticHorizontal", + "diacriticVertical", "language", "fill", "fill_tint", @@ -295,6 +324,8 @@ fn optional_number(d: &Dialog, key: &str) -> Result, String> { "akiLeft" => tl!("Insert Aki Left"), "akiRight" => tl!("Insert Aki Right"), "tsume" => tl!("Tsume"), + "diacriticHorizontal" => tl!("Adjust Horizontal Diacritic Position"), + "diacriticVertical" => tl!("Adjust Vertical Diacritic Position"), "fill_tint" => tl!("Fill Tint"), "stroke_tint" => tl!("Stroke Tint"), "stroke_width" => tl!("Weight"), @@ -344,6 +375,9 @@ fn optional_number(d: &Dialog, key: &str) -> Result, String> { let n = if AKI.contains(&key) { n.clamp(0.0, vectorcraft_doc::text::MAX_AKI) } else { n }; // Tsume stops at 0% and 100% the same way. let n = if key == "tsume" { n.clamp(0.0, vectorcraft_doc::text::MAX_TSUME) } else { n }; + // The diacritic positions stop at the largest value the style takes either way. + let max = vectorcraft_doc::text::MAX_DIACRITIC_POSITION; + let n = if DIACRITIC.contains(&key) { n.clamp(-max, max) } else { n }; Ok(Some(json!(n))) } } @@ -622,7 +656,7 @@ pub(crate) fn reset_basic(d: &mut Dialog) { } } -/// Reset Panel in Advanced Character Formats: its ten attributes become unset. +/// Reset Panel in Advanced Character Formats: its twelve attributes become unset. pub(crate) fn reset_advanced(d: &mut Dialog) { d.fields.insert("__resetAdvanced".into(), json!(true)); for key in ADVANCED_NUMBERS { @@ -777,8 +811,9 @@ pub(crate) fn basic_body(ui: &mut egui::Ui, d: &mut Dialog) { } /// Advanced Character Formats: Horizontal Scale, Vertical Scale, Baseline Shift, the Tate-chu-yoko -/// Up/Down and Left/Right, Character Rotation, Insert Aki Left and Right and Tsume (with the -/// Character panel's presets) and Language, each empty (unset) or set. +/// Up/Down and Left/Right, Character Rotation, Insert Aki Left and Right, Tsume and the Adjust +/// Horizontal and Vertical Diacritic Positions (with the Character panel's presets) and Language, +/// each empty (unset) or set. pub(crate) fn advanced_body(ui: &mut egui::Ui, d: &mut Dialog) { egui::Grid::new("character-style-advanced").num_columns(2).spacing([18.0, 8.0]).show(ui, |ui| { for (key, caption, unit) in [ @@ -820,6 +855,18 @@ pub(crate) fn advanced_body(ui: &mut egui::Ui, d: &mut Dialog) { ui.label("%"); }); ui.end_row(); + let presets: Vec = crate::panels::character::DIACRITIC_PRESETS.iter().map(f64::to_string).collect(); + let presets: Vec<&str> = presets.iter().map(String::as_str).collect(); + for (key, caption) in + [("diacriticHorizontal", tl!("Adjust Horizontal Diacritic Position")), ("diacriticVertical", tl!("Adjust Vertical Diacritic Position"))] + { + let row = ui.label(caption); + let value = if d.fields.get(key).is_none_or(Value::is_null) { String::new() } else { d.str(key) }; + if let Some(v) = ui.label_inputs_by(row.id, |ui| crate::widgets::text_presets(ui, (KIND, key), &value, &presets, 160.0)) { + d.fields.insert(key.into(), json!(v)); + } + ui.end_row(); + } let row = ui.label(tl!("Language:")); let names: Vec<&str> = std::iter::once(" ").chain(vectorcraft_doc::setup::LANGUAGES.iter().map(|(name, _, _)| *name)).collect(); let current = d.fields.get("language").and_then(Value::as_str).unwrap_or("").to_string(); @@ -1882,6 +1929,55 @@ mod tests { assert!(t.runs.iter().all(|r| (r.style.aki_left, r.style.aki_right) == (250.0, 1000.0))); } + /// The Adjust Diacritic Positions in Advanced Character Formats: empty is unset, a typed value + /// stops at -1000 and 1000, Reset Panel unsets them, and the fields are drawn and commit to the + /// style and the text set in it. + #[test] + fn diacritic_position_fields_show_set_reset_and_commit() { + let d = dialog_for(json!({"diacriticHorizontal": -250.0, "size": 12.0})); + assert_eq!((d.fields["diacriticHorizontal"].clone(), d.fields["diacriticVertical"].clone()), (json!(-250.0), json!(""))); + assert_eq!(edited_attrs(&d).unwrap(), attrs(json!({"diacriticHorizontal": -250.0, "size": 12.0}))); + let mut d = dialog_for(json!({"diacriticHorizontal": -250.0})); + d.fields.insert("diacriticHorizontal".into(), json!("")); + for (typed, kept) in [("0", 0.0), ("150", 150.0), ("5000", 1000.0), ("-5000", -1000.0)] { + d.fields.insert("diacriticVertical".into(), json!(typed)); + assert_eq!(edited_attrs(&d).unwrap(), attrs(json!({"diacriticVertical": kept})), "{typed}"); + } + d.fields.insert("diacriticHorizontal".into(), json!("left")); + assert_eq!(edited_attrs(&d).unwrap_err(), "Adjust Horizontal Diacritic Position must be a number"); + let mut d = dialog_for(json!({"diacriticHorizontal": 100.0, "diacriticVertical": 200.0, "size": 12.0})); + d.fields.insert("section".into(), json!("advanced")); + reset_section(&mut d); + assert_eq!(edited_attrs(&d).unwrap(), attrs(json!({"size": 12.0}))); + + let mut app = app(); + app.run("charStyle.new", json!({"name": "Body", "attrs": {"size": 24.0}})).unwrap(); + let id = app.run("text.create", json!({"x": 10, "y": 40, "text": "بَب"})).unwrap()["id"].as_u64().unwrap(); + app.run("charStyle.apply", json!({"name": "Body", "id": id})).unwrap(); + open(&mut app, "Body").unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(900.0, 900.0)); + let mut d = app.ui.dialog.clone().unwrap(); + let input = egui::RawInput { screen_rect: Some(screen), ..Default::default() }; + let mut out = ctx.run_ui(input, |ui| advanced_body(ui, &mut d)); + let texts = texts_of(&out); + out.textures_delta.clear(); + for label in ["Adjust Horizontal Diacritic Position", "Adjust Vertical Diacritic Position"] { + assert!(texts.iter().any(|(text, _)| text == label), "{label} is drawn: {texts:?}"); + } + { + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("diacriticHorizontal".into(), json!("-120")); + d.fields.insert("diacriticVertical".into(), json!("3000")); + } + super::super::confirm(&mut app).unwrap(); + let state = app.session.doc().unwrap(); + assert_eq!(state.doc.char_styles[0].attrs, attrs(json!({"size": 24.0, "diacriticHorizontal": -120.0, "diacriticVertical": 1000.0}))); + let vectorcraft_doc::NodeKind::Text(t) = &state.doc.node(vectorcraft_doc::NodeId(id)).unwrap().kind else { panic!("text") }; + assert!(t.runs.iter().all(|r| (r.style.diacritic_horizontal, r.style.diacritic_vertical) == (-120.0, 1000.0))); + } + /// Tsume in Advanced Character Formats: empty is unset, an explicit 0 stays set, a typed value /// stops at 0 and 100, Reset Panel unsets it, and the field is drawn and commits to the style /// and the text set in it. diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 948b295e17..632ba4cb06 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2598,6 +2598,8 @@ font-technology Variable Променливи Insert Aki Left Вмъкване на аки отляво Insert Aki Right Вмъкване на аки отдясно Tsume Цуме + Adjust Horizontal Diacritic Position Коригиране на хоризонталната позиция на диакритиците + Adjust Vertical Diacritic Position Коригиране на вертикалната позиция на диакритиците Character Direction Посока на знаците Digits Цифри Arabic Арабски diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index f8d6fa263b..11e6ad182c 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -1589,6 +1589,8 @@ lighting Light Svjetlo Insert Aki Left Umetni aki lijevo Insert Aki Right Umetni aki desno Tsume Tsume + Adjust Horizontal Diacritic Position Podesi vodoravni položaj dijakritika + Adjust Vertical Diacritic Position Podesi okomiti položaj dijakritika Character Direction Smjer znakova Digits Cifre Arabic Arapski diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 3ed3ca8331..3622512c2d 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -3367,6 +3367,8 @@ digits Malayalam Malaiàlam Insert Aki Left Insereix aki a l'esquerra Insert Aki Right Insereix aki a la dreta Tsume Tsume + Adjust Horizontal Diacritic Position Ajusta la posició horitzontal dels diacrítics + Adjust Vertical Diacritic Position Ajusta la posició vertical dels diacrítics Proportional Lining Xifres proporcionals alineades Position: Posició: Default Position Posició predeterminada diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 161e235c56..0b6aceda0a 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -1617,6 +1617,8 @@ lighting Light Světlo Insert Aki Left Vložit aki vlevo Insert Aki Right Vložit aki vpravo Tsume Tsume + Adjust Horizontal Diacritic Position Upravit vodorovnou polohu diakritiky + Adjust Vertical Diacritic Position Upravit svislou polohu diakritiky Character Direction Směr znaků Digits Číslice Arabic Arabské diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index fd0b7c042e..aac2f9dd7a 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -2134,6 +2134,8 @@ lighting Light Lys Insert Aki Left Indsæt aki til venstre Insert Aki Right Indsæt aki til højre Tsume Tsume + Adjust Horizontal Diacritic Position Juster vandret placering af diakritiske tegn + Adjust Vertical Diacritic Position Juster lodret placering af diakritiske tegn Character Direction Tegnretning Digits Cifre Arabic Arabisk diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 2a5f32a5d9..2be671ca22 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2642,6 +2642,8 @@ mark-style Japanese Japanisch Insert Aki Left Aki links einfügen Insert Aki Right Aki rechts einfügen Tsume Tsume + Adjust Horizontal Diacritic Position Horizontale Position diakritischer Zeichen anpassen + Adjust Vertical Diacritic Position Vertikale Position diakritischer Zeichen anpassen Character Direction Zeichenrichtung Digits Ziffern Arabic Arabisch diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index b1b7b4d039..1ca28c8ff6 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3439,6 +3439,8 @@ theme Appearance Εμφάνιση Insert Aki Left Εισαγωγή aki αριστερά Insert Aki Right Εισαγωγή aki δεξιά Tsume Tsume + Adjust Horizontal Diacritic Position Προσαρμογή οριζόντιας θέσης διακριτικών + Adjust Vertical Diacritic Position Προσαρμογή κατακόρυφης θέσης διακριτικών Arabic Αραβικά Hindi Χίντι Farsi Περσικά diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 5b3c675f7b..cff46bf68e 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2676,6 +2676,8 @@ font-technology Variable Variables Insert Aki Left Insertar aki a la izquierda Insert Aki Right Insertar aki a la derecha Tsume Tsume + Adjust Horizontal Diacritic Position Ajustar posición horizontal de los diacríticos + Adjust Vertical Diacritic Position Ajustar posición vertical de los diacríticos Character Direction Dirección de caracteres Digits Dígitos Arabic Árabes diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index e77a617c52..49f96667b0 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -1592,6 +1592,8 @@ lighting Light Valo Insert Aki Left Lisää aki vasemmalle Insert Aki Right Lisää aki oikealle Tsume Tsume + Adjust Horizontal Diacritic Position Säädä tarkkeiden vaakasijaintia + Adjust Vertical Diacritic Position Säädä tarkkeiden pystysijaintia Character Direction Merkkien suunta Digits Numerot Arabic Arabia diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 2762d2a954..6f1b854373 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2668,6 +2668,8 @@ font-technology Variable Variables Insert Aki Left Insérer un aki à gauche Insert Aki Right Insérer un aki à droite Tsume Tsume + Adjust Horizontal Diacritic Position Ajuster la position horizontale des signes diacritiques + Adjust Vertical Diacritic Position Ajuster la position verticale des signes diacritiques Character Direction Direction des caractères Digits Chiffres Arabic Arabes diff --git a/crates/ui-egui/src/i18n/gu.tsv b/crates/ui-egui/src/i18n/gu.tsv index 1c96900b2b..6eee1d7d0f 100644 --- a/crates/ui-egui/src/i18n/gu.tsv +++ b/crates/ui-egui/src/i18n/gu.tsv @@ -4381,6 +4381,8 @@ antiAlias Sharp શાર્પ Preview: પ્રીવ્યૂ: Insert Aki Left ડાબે અકી ઉમેરો Insert Aki Right જમણે અકી ઉમેરો + Adjust Horizontal Diacritic Position ડાયાક્રિટિકની આડી સ્થિતિ ગોઠવો + Adjust Vertical Diacritic Position ડાયાક્રિટિકની ઊભી સ્થિતિ ગોઠવો Conic Gradient કોનિક ગ્રેડિયન્ટ Diamond Gradient ડાયમંડ ગ્રેડિયન્ટ Include Placed Images પ્લેસ કરેલી ઇમેજો સામેલ કરો diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 548f961f51..fa3a5a4cf8 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -3714,6 +3714,8 @@ antiAlias Sharp शार्प Insert Aki Left बाईं ओर आकी जोड़ें Insert Aki Right दाईं ओर आकी जोड़ें Tsume त्सुमे + Adjust Horizontal Diacritic Position डायक्रिटिक की क्षैतिज स्थिति समायोजित करें + Adjust Vertical Diacritic Position डायक्रिटिक की लंबवत स्थिति समायोजित करें Proportional Oldstyle आनुपातिक ओल्डस्टाइल Protect Corners कॉर्नर को सुरक्षित रखें Proximity Guides निकटता गाइड diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 200b45d1b1..17fb9430a1 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -3233,6 +3233,8 @@ mark-style Japanese Japán Insert Aki Left Aki beszúrása balra Insert Aki Right Aki beszúrása jobbra Tsume Tsume + Adjust Horizontal Diacritic Position Mellékjelek vízszintes helyzetének igazítása + Adjust Vertical Diacritic Position Mellékjelek függőleges helyzetének igazítása Proportional Lining Arányos modern Position: Pozíció: Default Position Alapértelmezett pozíció diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index f3dcca3874..613d45b370 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -1525,6 +1525,8 @@ lighting Light Cahaya Insert Aki Left Sisipkan aki kiri Insert Aki Right Sisipkan aki kanan Tsume Tsume + Adjust Horizontal Diacritic Position Sesuaikan posisi horizontal diakritik + Adjust Vertical Diacritic Position Sesuaikan posisi vertikal diakritik Character Direction Arah karakter Digits Digit Arabic Arab diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 90ec79404c..27e8fd40c6 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2718,6 +2718,8 @@ mark-style Japanese Giapponese Insert Aki Left Inserisci aki a sinistra Insert Aki Right Inserisci aki a destra Tsume Tsume + Adjust Horizontal Diacritic Position Regola posizione orizzontale dei segni diacritici + Adjust Vertical Diacritic Position Regola posizione verticale dei segni diacritici Character Direction Direzione caratteri Digits Cifre Arabic Arabe diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index d98acfa22d..3ae17e5abb 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -55,6 +55,8 @@ font-technology Variable バリアブル Insert Aki Left 前にアキを挿入 Insert Aki Right 後ろにアキを挿入 Tsume 文字ツメ + Adjust Horizontal Diacritic Position 発音区別符号の水平位置を調整 + Adjust Vertical Diacritic Position 発音区別符号の垂直位置を調整 Character Direction 文字の方向 Digits 数字 Arabic アラビア数字 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 794e5454a9..8228dfebbb 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -1462,6 +1462,8 @@ lighting Light სინათლე Insert Aki Left აკის ჩასმა მარცხნივ Insert Aki Right აკის ჩასმა მარჯვნივ Tsume ცუმე + Adjust Horizontal Diacritic Position დიაკრიტიკის ჰორიზონტალური პოზიციის მორგება + Adjust Vertical Diacritic Position დიაკრიტიკის ვერტიკალური პოზიციის მორგება Character Direction სიმბოლოების მიმართულება Digits ციფრები Arabic არაბული diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index bb8676df71..0b8fcf490a 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -49,6 +49,8 @@ font-technology Variable 가변 Insert Aki Left 앞 아키 삽입 Insert Aki Right 뒤 아키 삽입 Tsume 츠메 + Adjust Horizontal Diacritic Position 발음 구별 기호 가로 위치 조정 + Adjust Vertical Diacritic Position 발음 구별 기호 세로 위치 조정 Character Direction 문자 방향 Digits 숫자 Arabic 아라비아어 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index a597a03fd0..4369f2e89b 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -2043,6 +2043,8 @@ lighting Light Licht Insert Aki Left Aki links invoegen Insert Aki Right Aki rechts invoegen Tsume Tsume + Adjust Horizontal Diacritic Position Horizontale positie van diakritische tekens aanpassen + Adjust Vertical Diacritic Position Verticale positie van diakritische tekens aanpassen Character Direction Tekenrichting Digits Cijfers Arabic Arabisch diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 99ee2080b8..cd19504fea 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -1530,6 +1530,8 @@ lighting Light Światło Insert Aki Left Wstaw aki z lewej Insert Aki Right Wstaw aki z prawej Tsume Tsume + Adjust Horizontal Diacritic Position Dostosuj poziome położenie znaków diakrytycznych + Adjust Vertical Diacritic Position Dostosuj pionowe położenie znaków diakrytycznych Character Direction Kierunek znaków Digits Cyfry Arabic Arabskie diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 677c8e460f..e4b68bfa9a 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2240,6 +2240,8 @@ antiAlias Sharp Nítido Insert Aki Left Inserir Aki à Esquerda Insert Aki Right Inserir Aki à Direita Tsume Tsume + Adjust Horizontal Diacritic Position Ajustar Posição Horizontal dos Diacríticos + Adjust Vertical Diacritic Position Ajustar Posição Vertical dos Diacríticos Character Direction Direção de Caracteres Digits Dígitos Arabic Arábicos diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 106343cddd..36dee93d9f 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2705,6 +2705,8 @@ mark-style Japanese Японский Insert Aki Left Вставить аки слева Insert Aki Right Вставить аки справа Tsume Цумэ + Adjust Horizontal Diacritic Position Настроить горизонтальное положение диакритических знаков + Adjust Vertical Diacritic Position Настроить вертикальное положение диакритических знаков Character Direction Направление символов Digits Цифры Arabic Арабские diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index cc854aa362..ca48397e4e 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -2613,6 +2613,8 @@ font-technology Variable Variabla Insert Aki Left Infoga aki till vänster Insert Aki Right Infoga aki till höger Tsume Tsume + Adjust Horizontal Diacritic Position Justera vågrät position för diakritiska tecken + Adjust Vertical Diacritic Position Justera lodrät position för diakritiska tecken Character Direction Teckenriktning Digits Siffror Arabic Arabiska diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index f2888a30f0..45cf7540ca 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2460,6 +2460,8 @@ shape Open Açık Insert Aki Left Sola Aki Ekle Insert Aki Right Sağa Aki Ekle Tsume Tsume + Adjust Horizontal Diacritic Position Yatay Aksan İşareti Konumunu Ayarla + Adjust Vertical Diacritic Position Dikey Aksan İşareti Konumunu Ayarla Digits Rakamlar Arabic Arapça Hindi Hintçe diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 411b2c3baf..0c9e86ac0c 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2710,6 +2710,8 @@ mark-style Japanese Японський Insert Aki Left Вставити акі ліворуч Insert Aki Right Вставити акі праворуч Tsume Цуме + Adjust Horizontal Diacritic Position Налаштувати горизонтальне положення діакритичних знаків + Adjust Vertical Diacritic Position Налаштувати вертикальне положення діакритичних знаків Character Direction Напрямок символів Digits Цифри Arabic Арабські diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 1460910197..0e81bd1a2f 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -3022,6 +3022,8 @@ mark-style Japanese Kiểu Nhật Insert Aki Left Chèn aki bên trái Insert Aki Right Chèn aki bên phải Tsume Tsume + Adjust Horizontal Diacritic Position Điều chỉnh vị trí ngang của dấu phụ + Adjust Vertical Diacritic Position Điều chỉnh vị trí dọc của dấu phụ Proportional Lining Số tỷ lệ đứng Position: Vị trí: Default Position Vị trí mặc định diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 3fe2f9e009..e32526d75b 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2572,6 +2572,8 @@ font-script Japanese 汉字 Insert Aki Left 在前方插入空白 Insert Aki Right 在后方插入空白 Tsume 比例间距 + Adjust Horizontal Diacritic Position 调整变音符号水平位置 + Adjust Vertical Diacritic Position 调整变音符号垂直位置 Character Direction 字符方向 Digits 数字 Arabic 阿拉伯数字 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index ab7f9d9dd2..8cea64775b 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3792,6 +3792,8 @@ font-script Japanese 漢字 Insert Aki Left 在前方插入空白 Insert Aki Right 在後方插入空白 Tsume 比例間距 + Adjust Horizontal Diacritic Position 調整變音符號水平位置 + Adjust Vertical Diacritic Position 調整變音符號垂直位置 Character Direction 字元方向 Digits 數字 Arabic 阿拉伯數字 diff --git a/crates/ui-egui/src/panels/character.rs b/crates/ui-egui/src/panels/character.rs index 32de20ec95..746a8fbdf4 100644 --- a/crates/ui-egui/src/panels/character.rs +++ b/crates/ui-egui/src/panels/character.rs @@ -28,6 +28,8 @@ pub const SCALE_PRESETS: [f64; 8] = [25.0, 50.0, 75.0, 90.0, 100.0, 110.0, 125.0 pub const AKI_PRESETS: [f64; 4] = [0.0, 250.0, 500.0, 1000.0]; /// Tsume presets, in percent. pub const TSUME_PRESETS: [f64; 5] = [0.0, 25.0, 50.0, 75.0, 100.0]; +/// Adjust Horizontal and Vertical Diacritic Position presets, in thousandths of an em. +pub const DIACRITIC_PRESETS: [f64; 7] = [-200.0, -100.0, -50.0, 0.0, 50.0, 100.0, 200.0]; /// The stored character style: the selected range's (or the caret's) while the Type /// tool edits text, else the first selected text object's first run (including groups). With it, the paragraph @@ -881,9 +883,10 @@ fn cell(ui: &mut Ui, label: &str, tip: &str, add: impl FnOnce(&mut Ui)) { /// The Character panel's grid of fields, `w` wide: Font Size, Leading, Kerning and Tracking, /// then with `more` the scales, Baseline Shift and Character Rotation, and a row with the /// Anti-aliasing Method dropdown under Character Rotation (`s`: the style shown), and with Show East -/// Asian Options on a row with Insert Aki Left and Right and a row with Tsume. With Show Font -/// Height Options on, a row under Font Size holds the Set Font Height Reference dropdown. The -/// Properties panel shows its first rows. +/// Asian Options on a row with Insert Aki Left and Right and a row with Tsume, and with Show Indic +/// Options (the Middle Eastern options) on a row with Adjust Horizontal and Vertical Diacritic +/// Position. With Show Font Height Options on, a row under Font Size holds the Set Font Height +/// Reference dropdown. The Properties panel shows its first rows. pub(crate) fn metrics_grid(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, s: &CharStyle, w: f32, more: bool) { let fw = ((w - 66.0) / 2.0).clamp(60.0, 100.0); let unit = app.session.type_unit(); @@ -986,6 +989,24 @@ pub(crate) fn metrics_grid(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, s: & }); ui.end_row(); } + // Adjust Horizontal and Vertical Diacritic Position, with the Middle Eastern options: the + // marks of the characters move across and up, in thousandths of an em. + if app.session.prefs.show_indic_options { + let max = vectorcraft_doc::text::MAX_DIACRITIC_POSITION; + for (field, label, key, icon) in [ + ("ch-diacritic-h", tl!("Adjust Horizontal Diacritic Position"), "diacriticHorizontal", "´↔"), + ("ch-diacritic-v", tl!("Adjust Vertical Diacritic Position"), "diacriticVertical", "´↕"), + ] { + cell(ui, icon, label, |ui| { + let value = shared(app, |s| if key == "diacriticHorizontal" { s.diacritic_horizontal } else { s.diacritic_vertical }); + if let Some(v) = widgets::spin_plain(ui, field, value, "", 0, fw, 10.0, -max, &DIACRITIC_PRESETS) { + // The field stops at the largest value the command takes. + format(app, json!({ key: v.min(max) })); + } + }); + } + ui.end_row(); + } }); } @@ -1730,7 +1751,8 @@ fn fractional_widths_on(app: &VectorcraftApp) -> bool { /// character alignment, Proportional Metrics, Tate-chu-yoko, Warichu, Standard Vertical Roman Alignment and /// No Break off, Tate-chu-yoko Settings Up/Down and Left/Right 0, the default Warichu Settings (2 lines, no /// line gap, 50%, Auto alignment), Insert Aki Left and Right 0, Tsume 0, the Default character direction, -/// Kashidas on, the Default digits and the Em Box font height reference, whether or not Show East Asian +/// Kashidas on, the Default digits, Adjust Horizontal and Vertical Diacritic Position 0 and the Em Box +/// font height reference, whether or not Show East Asian /// Options, Show Indic Options and Show Font Height Options are on. Inside an undo group that is /// already open, the reset is part of that group's step. fn reset_panel(app: &mut VectorcraftApp) { @@ -1752,7 +1774,8 @@ fn reset_panel(app: &mut VectorcraftApp) { "tsume": d.tsume, "warichu": d.warichu, "warichuLines": d.warichu_lines, "warichuGap": d.warichu_gap, "warichuScale": d.warichu_scale, "warichuAlign": d.warichu_align, "standardVerticalRoman": d.standard_vertical_roman, "noBreak": d.no_break, "charDirection": "default", "kashidas": d.kashidas, - "digits": d.digits, "fontHeightReference": d.font_height_reference + "digits": d.digits, "diacriticHorizontal": d.diacritic_horizontal, "diacriticVertical": d.diacritic_vertical, + "fontHeightReference": d.font_height_reference }), ); set_anti_alias(app, TypeAntiAlias::Sharp); diff --git a/crates/ui-egui/src/panels/character/tests.rs b/crates/ui-egui/src/panels/character/tests.rs index adaf523e53..1e3e6acec4 100644 --- a/crates/ui-egui/src/panels/character/tests.rs +++ b/crates/ui-egui/src/panels/character/tests.rs @@ -782,6 +782,57 @@ fn insert_aki_fields_show_with_the_east_asian_options() { assert!(left(&app).iter().all(|v| *v == 1000.0), "{:?}", left(&app)); } +/// Adjust Horizontal and Vertical Diacritic Position are fields of the panel with the Middle +/// Eastern options only, and show the selected characters' values, blank where they differ. A value +/// typed past -1000 sets -1000. Bad values sent to the commands are errors that change nothing. +#[test] +fn diacritic_position_fields_show_with_the_middle_eastern_options() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 200})).unwrap(); + let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "بَبَ"})).unwrap()["id"].as_u64().unwrap(); + app.run("select.set", json!({"ids": [id]})).unwrap(); + app.run("text.setFormat", json!({"diacriticHorizontal": 125, "diacriticVertical": -375})).unwrap(); + let text = crate::tests_labels::painted_text(&mut app, show); + assert!(!text.contains("´↔") && !text.contains("-375"), "{text}"); + app.session.prefs.show_indic_options = true; + let text = crate::tests_labels::painted_text(&mut app, show); + assert!(text.contains("´↔") && text.contains("´↕") && text.contains("125") && text.contains("-375"), "{text}"); + let undo = app.session.active().unwrap().history.undo.len(); + let doc = app.session.active().unwrap().doc.clone(); + for (cmd, params) in [ + ("text.setFormat", json!({"diacriticHorizontal": 1000.5})), + ("text.setFormat", json!({"diacriticVertical": "up"})), + ("text.setRangeStyle", json!({"id": id, "start": 0, "end": 2, "diacriticVertical": -1001})), + ("text.setRangeStyle", json!({"id": id, "start": 0, "end": 2, "diacriticHorizontal": true})), + ] { + let error = app.run(cmd, params.clone()).unwrap_err().to_string(); + assert!(error.contains("`diacritic"), "{cmd} {params}: {error}"); + } + assert_eq!(app.session.active().unwrap().history.undo.len(), undo); + assert!(app.session.active().unwrap().doc == doc); + // Setting the value the characters already have records no undo step. + app.run("text.setFormat", json!({"diacriticVertical": -375})).unwrap(); + assert_eq!(app.session.active().unwrap().history.undo.len(), undo); + // Only the first letter and its fatha have -375: the Vertical field is blank. + app.run("text.setRangeStyle", json!({"id": id, "start": 4, "end": 8, "diacriticVertical": 0})).unwrap(); + let text = crate::tests_labels::painted_text(&mut app, show); + assert!(text.contains("125") && !text.contains("-375"), "{text}"); + use super::super::tests_appearance::{click, frame_events, text_rect}; + let ctx = egui::Context::default(); + let texts = frame_events(&ctx, &mut app, vec![], show); + click(&ctx, &mut app, text_rect(&texts, "125").center(), show); + let key = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }; + frame_events(&ctx, &mut app, vec![key(egui::Key::A, egui::Modifiers::COMMAND)], show); + frame_events(&ctx, &mut app, vec![egui::Event::Text("-1500".into())], show); + frame_events(&ctx, &mut app, vec![key(egui::Key::Enter, egui::Modifiers::NONE)], show); + frame_events(&ctx, &mut app, vec![], show); + let horizontal = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind { + NodeKind::Text(t) => t.runs.iter().map(|r| r.style.diacritic_horizontal).collect::>(), + _ => panic!("text"), + }; + assert!(horizontal(&app).iter().all(|v| *v == -1000.0), "{:?}", horizontal(&app)); +} + /// Tsume is a field of the panel with the East Asian options only, and shows the selected /// characters' value, blank where they differ. A value typed past 100 sets 100. #[test] @@ -1487,6 +1538,8 @@ fn reset_panel_resets_every_attribute_but_the_font() { s.char_direction != d.char_direction, s.digits != d.digits, s.font_height_reference != d.font_height_reference, + s.diacritic_horizontal != d.diacritic_horizontal, + s.diacritic_vertical != d.diacritic_vertical, ] }; let custom = |app: &mut VectorcraftApp| { @@ -1496,16 +1549,17 @@ fn reset_panel_resets_every_attribute_but_the_font() { "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, "tcyUpDown": 2, "tcyLeftRight": -3, - "akiLeft": 250, "akiRight": 500, "tsume": 50, "standardVerticalRoman": true, "noBreak": true, "charDirection": "rightToLeft", "digits": "hindi", "fontHeightReference": "capHeight" + "akiLeft": 250, "akiRight": 500, "tsume": 50, "standardVerticalRoman": true, "noBreak": true, "charDirection": "rightToLeft", "digits": "hindi", "fontHeightReference": "capHeight", + "diacriticHorizontal": 120, "diacriticVertical": -80 }), ) .unwrap(); }; let plain = runs(&app)[0].1.clone(); - assert_eq!(differs(&plain, &CharStyle::default()), [false; 25]); + assert_eq!(differs(&plain, &CharStyle::default()), [false; 27]); custom(&mut app); let styled = runs(&app)[0].1.clone(); - assert_eq!(differs(&styled, &CharStyle::default()), [true; 25]); + assert_eq!(differs(&styled, &CharStyle::default()), [true; 27]); 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 8ca4121032..d00c10decc 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -9696,6 +9696,30 @@ A digit the font lacks comes from a fallback font. Default and saved only when s {"name":"run_command","arguments":{"command":"text.setFormat","params":{"digits":"hindi"}}} ``` +## Adjust Diacritic Position + +`text.setFormat {diacriticHorizontal?, diacriticVertical?}` (the selected type, or `ids`) and +`text.setRangeStyle {id, start, end, diacriticHorizontal?, diacriticVertical?}` (a range) move the characters' diacritical +marks relative to their letters, as the Character panel's Adjust Horizontal and Adjust Vertical Diacritic Position do +([the reference, page 549](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf#page=549)). +The units, range and direction are VectorCraft's own rules: each value is in thousandths of an em of the characters' size, +from -1000 to 1000; a positive horizontal value moves the marks right and a positive vertical value moves them up. Only +glyphs the font classes as marks (GDEF class 3) move; the letters keep their places and advances, and a font without +mark classes moves nothing. An Arabic harakat or a Hebrew point styled apart from its letter takes its own values; +other marks take the values of their letter. Setting one value leaves the other as it is. Both are 0 by default and +saved only when not 0, and character styles carry them (`charStyle.new`, `charStyle.setAttrs` and `charStyle.edit` +refuse a value outside the range). A value that is not a number is refused with `` `diacriticHorizontal` must be a +number ``, and one outside the range with `` `diacriticHorizontal` must be from -1000 to 1000 ``, naming the key. One +undo step, and a `text.setFormat` call that gives the values the characters already have records none. With Show Middle Eastern +Options on, the Character panel shows both fields, blank where the selected characters differ, with -200, -100, -50, +0, 50, 100 and 200 in their menus, and a value typed past either end sets that end; Reset Panel sets both to 0. +Character Style Options shows both fields in Advanced Character Formats. The canvas, PDF export and outlines draw the +moved marks; SVG export writes type with a moved mark as outlines. + +```json +{"name":"run_command","arguments":{"command":"text.setRangeStyle","params":{"id":12,"start":2,"end":4,"diacriticVertical":150}}} +``` + ## Font height reference `text.setFormat {fontHeightReference: "emBox"|"capHeight"|"xHeight"|"icfBox"}` (the selected type, or `ids`) and @@ -10017,7 +10041,7 @@ attribute map in one undo step. `to` is trimmed and must be unique. The command while preserving local overrides; Undo/Redo restores the definition and its users. The `charStyleOptions` dialog exposes `name`, `font_family`, `font_style`, `size`, `leading`, `kerning`, `tracking`, `case`, `position`, `underline`, `strikethrough`, `charAlign`, `h_scale`, `v_scale`, `baseline_shift`, `tcyUpDown`, -`tcyLeftRight`, `rotation`, `akiLeft`, `akiRight`, `tsume`, `language`, `fill`, `fill_tint`, `stroke`, `stroke_tint`, `stroke_width`, `overprint_fill`, `overprint_stroke`, +`tcyLeftRight`, `rotation`, `akiLeft`, `akiRight`, `tsume`, `diacriticHorizontal`, `diacriticVertical`, `language`, `fill`, `fill_tint`, `stroke`, `stroke_tint`, `stroke_width`, `overprint_fill`, `overprint_stroke`, `ot_figure`, `ot_position`, `ot_liga`, `ot_calt`, `ot_dlig`, `ot_swsh`, `ot_salt`, `ot_titl`, `ot_ordn`, `ot_frac`, `ot_sets`, `section` (`general`, `basic`, `advanced`, `color` or `opentype`), and `preview`. An empty field leaves its attribute unset. `leading` and `kerning` also accept `Auto` to store an explicit automatic value. @@ -10026,7 +10050,9 @@ Advanced Character Formats holds `h_scale` and `v_scale` (percent, 1 to 10000), -1296 to 1296, as `text.setRangeStyle` takes them; an explicit 0 stays distinct from unset), `rotation` (degrees, stored from -180 to 180), Insert Aki Left and Right `akiLeft` and `akiRight` (thousandths of an em, with the Character panel's 0, 250, 500 and 1000 in their menus; a value typed below 0 or past 1000 sets 0 or 1000), Tsume `tsume` (percent, with the Character panel's 0, 25, 50, 75 and 100 in its menu; a value typed below 0 or -past 100 sets 0 or 100; an explicit 0 stays distinct from unset) and `language` (a name or tag from Document +past 100 sets 0 or 100; an explicit 0 stays distinct from unset), Adjust Horizontal and Vertical Diacritic Position +`diacriticHorizontal` and `diacriticVertical` (thousandths of an em, with the Character panel's -200, -100, -50, 0, 50, +100 and 200 in their menus; a value typed below -1000 or past 1000 sets -1000 or 1000) and `language` (a name or tag from Document Setup's languages; English: USA stores the explicit default `null`, and a tag not in the list that the style already has is kept). Language sets the language the characters are shaped in; it does not change hyphenation or spelling. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f4424700be..fa30157c24 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1437,6 +1437,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The Paragraph panel's Insert Kashidas (with the Indic options; `text.setFormat {kashida}`) lengthens each Arabic word of a justified horizontal area type line with a kashida, a tatweel between its last pair of joined letters, before the word spaces widen: Short, Medium and Long kashidas are at most a quarter, half and a whole em long, Stylistic ones are whole tatweels up to an em, and None (the default) leaves the line as before; paragraph styles and the native file keep the setting. The Character panel menu's Kashidas switch (with the Indic options; `text.setFormat` / `text.setRangeStyle {kashidas}`) allows Kashidas on selected characters, on by default for new and old text. A pair of joined letters receives one only when both characters allow it; turning one off leaves other eligible pairs in the word available. Character styles and the native file keep the switch. [Public help](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/justify-text-using-kashida-and-hyphenation.html) documents the menu control and the separate paragraph length choices; the default and pair rule are VectorCraft choices. Paint-only Arabic harakat (marks whose character styles differ only in fill, stroke and related paint) stay inside one shaping cluster so letters keep their joining forms; when a second character-level shaping pass matches the monotone glyphs, a glyph may take a later in-cluster character's run only when GDEF classifies it as a mark (class 3) and that character is a harakat; otherwise it keeps the shaping cluster's first character. Insert Kashidas tatweels take the base letter's paint rather than a trailing mark's. Fonts without GDEF and some AAT/`morx` ligatures can leave a mark painted as its letter (`crates/text/src/tests/harakat.rs`). + Character panel › Adjust Horizontal and Adjust Vertical Diacritic Position (with the Middle Eastern options; `text.setFormat` or `text.setRangeStyle {diacriticHorizontal, diacriticVertical}`, VectorCraft's own thousandths of an em from -1000 to 1000, 0 by default) move the selected characters' mark glyphs (GDEF class 3) right or left and up or down while the letters stay in place ([the reference, page 549](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf#page=549)); an Arabic harakat or Hebrew point styled apart takes its own values, character styles carry both (Character Style Options › Advanced Character Formats edits them), the canvas and PDF draw the moved marks and SVG writes such type as outlines (`tests::harakat::diacritic_positions_move_marks_and_keep_letters`). The Paragraph panel's Bullets button, Type › Bullets and Numbering › Apply Bullets and the context menu's Apply Bullets (`text.setFormat {list, bullet}`, `type.applyBullets`) make paragraphs a bulleted list with the documented presets (none, filled dot, dash, hollow circle, filled square; [Create bulleted or numbered lists](https://web.archive.org/web/20260417034638/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/create-bulleted-or-numbered-lists.html)): the bullet starts each paragraph's first line in its first character's font, size and color, the first line's text starts at the next tab stop after the bullet (the left indent counts as a stop), the lines it wraps to start at the left indent, a paragraph with no indents gets 18 pt and -18 pt, and Return continues the list; Reset Panel turns it off, and paragraph styles and the native file keep it. The Paragraph panel's Bullets button, Type › Bullets and Numbering › Apply Bullets and the context menu's Apply Bullets (`text.setFormat {list, bullet}`, `type.applyBullets`) make paragraphs a bulleted list with the documented presets (none, filled dot, dash, hollow circle, filled square; [Create bulleted or numbered lists](https://web.archive.org/web/20260417034638/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/create-bulleted-or-numbered-lists.html)): the bullet starts each paragraph's first line in its first character's font, size and color, the first line's text starts at the next tab stop after the bullet (the left indent counts as a stop), the lines it wraps to start at the left indent, a paragraph with no indents gets 18 pt and -18 pt, and Return continues the list; Reset Panel turns it off, and paragraph styles and the native file keep it. The Bullets and Numbering dialog, levels and Convert to Text are not built yet. The Paragraph panel's Numbered List button, Type › Bullets and Numbering › Apply Numbers and the context menu's Apply Numbers (`text.setFormat {list, number}`, `type.applyNumbers`) make paragraphs a numbered list with the documented presets (1, A, a, I, i, 01, 001, and None, which turns the list off; [Bullets and numbering](https://web.archive.org/web/20250807202418/https://helpx.adobe.com/illustrator/using/bullets-numbering.html), [Create bulleted or numbered lists](https://web.archive.org/web/20260417034638/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/create-bulleted-or-numbered-lists.html)): each paragraph starts with its number and a period, counted through the list paragraphs of the story and its threaded frames, with the text and indents as in a bulleted list; letters go on from Z to AA, and roman numerals go on from III to IV, an empty paragraph is not numbered, and a paragraph in no list starts the count again at 1. diff --git a/docs/type-parity.md b/docs/type-parity.md index fa76290e94..150c495d97 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -62,6 +62,7 @@ against Illustrator's public documentation; **estimated** unless noted. | Middle Eastern: bidirectional layout, Paragraph Direction | done | PDF export keeps harakat and zero-advance Noto Arabic dot glyphs in one compatible batch per layout line, with per-glyph vertical offsets (#6309). `crates/pdf/src/tests/realtext.rs` (`mark_text_*`) checks decoded cluster mappings, mixed Arabic/Latin/digits, contour placement against outline export, vertical columns, differing path rotations and PDF/X-1a/3/4. Editable-text import still needs ActualText support (#6165); separately styled or differently rotated clusters and external viewer search/copy remain outside this export-only evidence. | | Middle Eastern and Indic: digit types (Character panel menu › Digits: Default, Arabic, Hindi, Farsi; Devanagari, Bengali, Gurmukhi, Gujarati, Odia, Tamil, Telugu, Kannada, Malayalam) | done | `text.setFormat` / `text.setRangeStyle {digits}` (`"devanagari"`, `"bengali"`, `"gurmukhi"`, `"gujarati"`, `"odia"`, `"tamil"`, `"telugu"`, `"kannada"`, `"malayalam"` added, #4942); the text keeps the typed digits, the glyphs and SVG export follow the style; carried by character styles. Not yet: `.ai` export of the setting; Indic digits typed as such stay as typed under another system. Evidence: `text::tests::indic_digits_map_to_their_scripts_block`, `tests::bidi::indic_digits_draw_the_scripts_own_digits`, `typecmd::area_tests::indic_digits_round_trip_through_the_commands_and_the_file` | | Middle Eastern: Kashidas | done | Paragraph Insert Kashidas sets the length for justified Arabic area type; Character-menu Kashidas allows or excludes selected characters, on by default. A kashida is inserted only between two allowed joined characters. The character switch is retained in character styles and the native file; the length is retained in paragraph styles and the native file. Paint-only harakat (coloured or hidden marks) stay in one shaping cluster so letters keep joining; when the character-level pass matches, a glyph may take a later in-cluster character's run only if GDEF names it a mark (class 3) and that character is a harakat—otherwise the cluster start (a mark may miss its colour; a base never takes a mark's). Inserted tatweels take the base letter's paint—not a trailing mark's. Fonts without GDEF and some AAT/`morx` ligatures (e.g. Geeza Pro Allah) can leave a mark painted as its letter. No Character-panel harakat colour command yet. Evidence: `tests::harakat`. [Public help](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/justify-text-using-kashida-and-hyphenation.html). | +| Middle Eastern: Adjust Horizontal and Vertical Diacritic Position | done | `text.setFormat` / `text.setRangeStyle {diacriticHorizontal, diacriticVertical}` and the Character panel's fields (with the Middle Eastern options) move mark glyphs (GDEF class 3) by VectorCraft's own thousandths of an em, -1000 to 1000; letters keep their places, harakat and Hebrew points styled apart take their own values, character styles and the native file keep both, and SVG writes such type as outlines. Not yet: `.ai` import and export of the values. Evidence: `tests::harakat::diacritic_positions_move_marks_and_keep_letters`. [Reference, page 549](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf#page=549). | | Middle Eastern: Middle Eastern composers, split caret | missing | Character Direction is done; editable bidirectional SVG text remains incomplete. | | CJK: input methods (marked text, candidate window) | done | | | CJK: kinsoku (Hard, Soft, None), the predefined mojikumi sets (None, YakumonoHankaku, GyomatsuYakumonoHankaku, GyomatsuYakumonoZenkaku, YakumonoZenkaku, with VectorCraft's own spacing for each) and Line-end Punctuation Half Width, tate-chu-yoko (auto) | done / partial | custom kinsoku sets, custom mojikumi sets and the Mojikumi Settings dialog missing |