Merge pull request #5098 from dotCooCoo/feat/opentype-panel-position

OpenType: the panel's Position menu sets superscript, subscript, numerator or denominator glyphs
This commit is contained in:
BFlat
2026-11-02 12:30:46 +07:00
committed by GitHub
34 changed files with 525 additions and 23 deletions
+66 -2
View File
@@ -39,7 +39,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Character",
[],
None,
"{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", keepModel?: bool (fill and stroke, as paint.setFill), strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, align?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), tateChuYoko?: bool (Character panel menu › Tate-chu-yoko, with Show East Asian Options: in vertical type those characters stand side by side across one em of the column; kept on the characters and ignored in horizontal type), standardVerticalRoman?: bool (Character panel menu › Standard Vertical Roman Alignment, with Show East Asian Options: in vertical type those half-width characters, such as Latin letters and digits, stand upright one below the other instead of being rotated; spaces and tate-chu-yoko blocks are not changed; kept on the characters and ignored in horizontal type; off by default), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), fontHeightReference?: \"emBox\"|\"capHeight\"|\"xHeight\"|\"icfBox\" (Character panel › Set Font Height Reference: the height of the font that size measures, the em box by default; with a font that has no such height, or one outside 0.1 to 2 em, the em box), language?: name|tag (Character panel › Language: one of Document Setup's languages by name or BCP 47 tag, case-insensitive, such as \"Turkish\" or \"tr\"; the characters are shaped in it, so the font uses that language's forms (OpenType `locl`); saved as the tag, English: USA, the default, as none; new type takes the document's language), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font: the setting of each variation axis named, in the axis's units and clamped to its range; null: back to the style's own setting; other axes keep theirs; tags the font has no axis for are left out; a font or style change clears them; text.fontList {family, style} lists the axes)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs, warning?: text (the font or style set isn't available: its type is drawn in the fallback font or the family's closest style)}",
"{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", keepModel?: bool (fill and stroke, as paint.setFill), strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, align?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], featureChanges?: [\"tnum\", \"-onum\", …] (OpenType features turned on, or with - off, on top of each character's own; its other features are kept), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), tateChuYoko?: bool (Character panel menu › Tate-chu-yoko, with Show East Asian Options: in vertical type those characters stand side by side across one em of the column; kept on the characters and ignored in horizontal type), standardVerticalRoman?: bool (Character panel menu › Standard Vertical Roman Alignment, with Show East Asian Options: in vertical type those half-width characters, such as Latin letters and digits, stand upright one below the other instead of being rotated; spaces and tate-chu-yoko blocks are not changed; kept on the characters and ignored in horizontal type; off by default), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), fontHeightReference?: \"emBox\"|\"capHeight\"|\"xHeight\"|\"icfBox\" (Character panel › Set Font Height Reference: the height of the font that size measures, the em box by default; with a font that has no such height, or one outside 0.1 to 2 em, the em box), language?: name|tag (Character panel › Language: one of Document Setup's languages by name or BCP 47 tag, case-insensitive, such as \"Turkish\" or \"tr\"; the characters are shaped in it, so the font uses that language's forms (OpenType `locl`); saved as the tag, English: USA, the default, as none; new type takes the document's language), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font: the setting of each variation axis named, in the axis's units and clamped to its range; null: back to the style's own setting; other axes keep theirs; tags the font has no axis for are left out; a font or style change clears them; text.fontList {family, style} lists the axes)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs, warning?: text (the font or style set isn't available: its type is drawn in the fallback font or the family's closest style)}",
has_doc,
set_range_style
),
@@ -355,6 +355,8 @@ pub(crate) struct CharChange {
strikethrough: Option<bool>,
all_caps: Option<bool>,
features: Option<Vec<String>>,
/// Features turned on or off on top of each character's own (`featureChanges`).
feature_changes: Option<Vec<String>>,
position: Option<vectorcraft_doc::CharPosition>,
small_caps: Option<Option<f64>>,
char_align: Option<vectorcraft_doc::CharAlign>,
@@ -468,13 +470,70 @@ pub(crate) fn features_param(p: &Value, cmd: &str) -> Result<Option<Vec<String>>
return Err(bad(
cmd,
format!(
"unknown OpenType feature `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs; prefix - to turn off)"
"unknown OpenType feature `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom; prefix - to turn off)"
),
));
}
Ok(Some(vectorcraft_text::OtFeatures::default().with_tags(tags).to_tags()))
}
/// The most entries a `featureChanges` list may have.
const MAX_FEATURE_CHANGES: usize = 256;
/// A feature tag without its `-` or `+` prefix.
fn feature_name(tag: &str) -> &str {
tag.strip_prefix(['-', '+']).unwrap_or(tag)
}
/// `featureChanges: ["tnum", "-onum", …]` → the features to turn on (or, with `-`, off) on top of
/// each character's own, one change for each feature (the last one listed).
pub(crate) fn feature_changes_param(p: &Value, cmd: &str) -> Result<Option<Vec<String>>> {
let Some(v) = p.get("featureChanges").filter(|v| !v.is_null()) else { return Ok(None) };
let not_tags = || bad(cmd, "featureChanges must be a list of tags");
let list = v.as_array().ok_or_else(not_tags)?;
if list.len() > MAX_FEATURE_CHANGES {
return Err(bad(cmd, format!("featureChanges lists at most {MAX_FEATURE_CHANGES} tags")));
}
let mut tags: Vec<String> = Vec::new();
for t in list {
let t = t.as_str().ok_or_else(not_tags)?;
if !vectorcraft_text::OtFeatures::known_tag(t) || feature_name(t).starts_with(['-', '+']) {
return Err(bad(cmd, format!("unknown OpenType feature `{t}` in featureChanges (the tags `features` takes)")));
}
tags.retain(|o| feature_name(o) != feature_name(t));
tags.push(t.to_string());
}
Ok(Some(tags))
}
/// `features` with `changes` (a `featureChanges` list) made: the tags of the features `changes`
/// names are replaced by the ones that differ from the defaults, as `features` writes them, and
/// every other tag stays as it was. Standard ligatures turned on are written as `liga`, which keeps
/// them on in tracked type. The new tags go after the others. When the list would run past the
/// [`vectorcraft_text::MAX_STYLE_FEATURES`] entries that shaping reads, the other tags are first cut
/// to the ones shaping uses: the entries it reads, without tags it does not know and with only the
/// last tag of each feature.
pub(crate) fn change_features(features: &[String], changes: &[String]) -> Vec<String> {
let named = |t: &String| changes.iter().any(|c| feature_name(c) == feature_name(t));
let mut made = vectorcraft_text::OtFeatures::from_tags(changes.iter().map(String::as_str)).to_tags();
if changes.iter().any(|c| c == "liga" || c == "+liga") {
made.push("liga".into());
}
let mut kept: Vec<String> = features.iter().filter(|t| !named(t)).cloned().collect();
if kept.len() + made.len() > vectorcraft_text::MAX_STYLE_FEATURES {
let read = features.get(..vectorcraft_text::MAX_STYLE_FEATURES).unwrap_or(features);
let shaped = |t: &String| vectorcraft_text::OtFeatures::known_tag(t) || feature_name(t) == "clig";
kept = read
.iter()
.enumerate()
.filter(|(i, t)| !named(t) && shaped(t) && !read.iter().skip(i + 1).any(|l| feature_name(l) == feature_name(t)))
.map(|(_, t)| t.clone())
.collect();
}
kept.extend(made);
kept
}
/// `charAlign: "romanBaseline"|"emBoxTop"|"emBoxCenter"|"emBoxBottom"|"icfTop"|"icfBottom"` (Character Alignment).
pub(crate) fn char_align_param(p: &Value, cmd: &str) -> Result<Option<vectorcraft_doc::CharAlign>> {
use vectorcraft_doc::CharAlign;
@@ -569,6 +628,7 @@ impl CharChange {
strikethrough: flag("strikethrough"),
all_caps: flag("allCaps"),
features: features_param(p, cmd)?,
feature_changes: feature_changes_param(p, cmd)?,
position,
small_caps,
char_align: char_align_param(p, cmd)?,
@@ -617,6 +677,7 @@ impl CharChange {
&& self.strikethrough.is_none()
&& self.all_caps.is_none()
&& self.features.is_none()
&& self.feature_changes.is_none()
&& self.position.is_none()
&& self.small_caps.is_none()
&& self.char_align.is_none()
@@ -712,6 +773,9 @@ impl CharChange {
if let Some(v) = &self.features {
st.features = v.clone();
}
if let Some(v) = &self.feature_changes {
st.features = change_features(&st.features, v);
}
if let Some(v) = self.position {
st.position = v;
}
+9 -1
View File
@@ -123,7 +123,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Character",
[],
None,
"{ids?|id?, font?, style? (font alone: the style is kept when the new family has it, else its closest), size?: pt, leading?: pt|\"auto\", tracking?: 1/1000 em, justify?: \"auto\" (the start of each paragraph's direction)|\"left\"|\"center\"|\"right\"|\"justifyAll\", fill?: colour, keepModel?: bool (as paint.setFill), features?: [\"dlig\", \"-liga\", …] OpenType, start?: byte, end?: byte} (size/leading are document points after object scaling; with a range: the character attributes style that range and justify the paragraphs it touches; without: all the text) → {ids, overflow?: [id…] (area type and type on a path whose text no longer fits; left out when all fits), warning?: text (the font or style set isn't available: its type is drawn in the fallback font or the family's closest style)}",
"{ids?|id?, font?, style? (font alone: the style is kept when the new family has it, else its closest), size?: pt, leading?: pt|\"auto\", tracking?: 1/1000 em, justify?: \"auto\" (the start of each paragraph's direction)|\"left\"|\"center\"|\"right\"|\"justifyAll\", fill?: colour, keepModel?: bool (as paint.setFill), features?: [\"dlig\", \"-liga\", …] OpenType, featureChanges?: [\"tnum\", \"-onum\", …] (OpenType features turned on, or with - off, on top of each character's own; its other features are kept), start?: byte, end?: byte} (size/leading are document points after object scaling; with a range: the character attributes style that range and justify the paragraphs it touches; without: all the text) → {ids, overflow?: [id…] (area type and type on a path whose text no longer fits; left out when all fits), 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_style
),
@@ -791,6 +791,7 @@ fn set_style(s: &mut Session, p: &Value) -> Result<Value> {
Some(v) => Some(vectorcraft_color::Paint::solid(super::paint::new_color(s, p, color_value(v).ok_or_else(|| bad(C, "bad fill colour"))?))),
};
let features = super::textedit::features_param(p, C)?;
let feature_changes = super::textedit::feature_changes_param(p, C)?;
if font.is_none()
&& style.is_none()
&& size.is_none()
@@ -799,6 +800,7 @@ fn set_style(s: &mut Session, p: &Value) -> Result<Value> {
&& justify.is_none()
&& fill.is_none()
&& features.is_none()
&& feature_changes.is_none()
{
return Err(bad(C, "nothing to change"));
}
@@ -844,6 +846,9 @@ fn set_style(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(f) = &features {
st.features = f.clone();
}
if let Some(c) = &feature_changes {
st.features = super::textedit::change_features(&st.features, c);
}
});
if let Some(before) = before {
super::textedit::protect_missing_glyphs(&before, &mut t.runs);
@@ -897,6 +902,9 @@ fn set_style(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(f) = &features {
st.features = f.clone();
}
if let Some(c) = &feature_changes {
st.features = super::textedit::change_features(&st.features, c);
}
});
}
Ok(())
+52
View File
@@ -87,6 +87,58 @@ fn set_range_style_splits_runs() {
assert!(s.execute("text.setRangeStyle", &json!({"id": id.0, "size": -1})).is_err());
}
/// `featureChanges` turns features on and off on top of each run's own and keeps the rest, in
/// `text.setStyle` and `text.setRangeStyle`, in one undo step; an unknown tag is an error.
#[test]
fn feature_changes_keep_each_runs_other_features() {
let mut s = session();
let id = text(&mut s, "1/23");
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 0, "end": 1, "features": ["numr", "smcp"]})).unwrap();
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 1, "end": 2, "features": ["swsh"]})).unwrap();
let features = |s: &Session| obj(s, id).runs.iter().map(|r| r.style.features.clone()).collect::<Vec<_>>();
let undo = s.doc().unwrap().history.undo.len();
s.execute("text.setStyle", &json!({"featureChanges": ["dnom", "-numr"]})).unwrap();
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
assert_eq!(features(&s), [vec!["smcp", "dnom"], vec!["swsh", "dnom"], vec!["dnom"]]);
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 0, "end": 2, "featureChanges": ["dnom", "-liga", "-dnom"]})).unwrap();
assert_eq!(features(&s), [vec!["smcp", "-liga"], vec!["swsh", "-liga"], vec!["dnom"]], "the last change of a feature counts");
for cmd in ["text.setStyle", "text.setRangeStyle"] {
for tag in ["zzzz", "--liga", "+-dnom"] {
let e = s.execute(cmd, &json!({"id": id.0, "featureChanges": [tag]})).unwrap_err().to_string();
assert!(e.contains(&format!("`{tag}`")), "{cmd}: {e}");
}
for bad in [json!(["numr", 12]), json!([12]), json!("numr")] {
let e = s.execute(cmd, &json!({"id": id.0, "featureChanges": bad})).unwrap_err().to_string();
assert!(e.contains("featureChanges must be a list of tags"), "{cmd} {bad}: {e}");
}
let e = s.execute(cmd, &json!({"id": id.0, "featureChanges": vec!["numr"; 257]})).unwrap_err().to_string();
assert!(e.contains("at most 256"), "{cmd}: {e}");
}
assert_eq!(features(&s), [vec!["smcp", "-liga"], vec!["swsh", "-liga"], vec!["dnom"]], "the refused commands change nothing");
// Tags of features the change does not name stay as they were: one the list does not know
// (kept from an opened file) and an explicit `liga`, which keeps tracked type ligated.
if let Some(NodeKind::Text(t)) = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).node_mut(id).map(|n| &mut n.kind) {
t.runs[2].style.features = vec!["zzzz".into(), "liga".into(), "dnom".into()];
}
s.execute("text.setStyle", &json!({"featureChanges": ["numr", "-dnom"]})).unwrap();
assert_eq!(features(&s), [vec!["smcp", "-liga", "numr"], vec!["swsh", "-liga", "numr"], vec!["zzzz", "liga", "numr"]]);
// Ligatures turned on are written as `liga`, so tracked type keeps them.
s.execute("text.setStyle", &json!({"featureChanges": ["+liga"]})).unwrap();
assert_eq!(features(&s), [vec!["smcp", "numr", "liga"], vec!["swsh", "numr", "liga"], vec!["zzzz", "numr", "liga"]]);
// A list longer than shaping reads keeps the tags shaping uses, and the new tag is read too:
// `-onum` overrides `onum`, unknown tags do nothing, and `ordn` lies past the entries read.
let max = vectorcraft_text::MAX_STYLE_FEATURES;
let mut long = vec!["onum".to_string(), "-onum".into()];
long.extend(std::iter::repeat_n("zzzz".to_string(), max - 3));
long.extend(["swsh".to_string(), "ordn".into()]);
if let Some(NodeKind::Text(t)) = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).node_mut(id).map(|n| &mut n.kind) {
t.runs[2].style.features = long;
}
s.execute("text.setStyle", &json!({"featureChanges": ["dnom"]})).unwrap();
assert_eq!(features(&s), [vec!["smcp", "numr", "liga", "dnom"], vec!["swsh", "numr", "liga", "dnom"], vec!["-onum", "swsh", "dnom"]]);
}
#[test]
fn optical_command_manual_pair_undo_redo_and_native_reopen() {
let mut s = session();
+79 -5
View File
@@ -42,19 +42,21 @@ pub struct OtFeatures {
/// Swashes (`swsh`).
pub swash: bool,
/// The features of [`EXTRA_FEATURES`] turned on, one bit each (off by default): stylistic
/// sets, alternates, titling, proportional and lining figures, superscript and subscript.
/// sets, alternates, titling, proportional and lining figures, superscript, subscript,
/// numerators and denominators.
pub extra: u32,
}
/// The most entries of a style's feature list read: every segment of its text reads the list again.
const MAX_STYLE_FEATURES: usize = 256;
pub const MAX_STYLE_FEATURES: usize = 256;
/// Features beyond the named fields, off unless asked for, each bit `i` of [`OtFeatures::extra`]:
/// stylistic sets `ss01`–`ss20`, stylistic alternates `salt`, titling `titl`, proportional `pnum`
/// and lining `lnum` figures, superscript `sups` and subscript `subs`.
pub const EXTRA_FEATURES: [&str; 26] = [
/// and lining `lnum` figures, superscript `sups`, subscript `subs`, numerators `numr` and
/// denominators `dnom`.
pub const EXTRA_FEATURES: [&str; 28] = [
"ss01", "ss02", "ss03", "ss04", "ss05", "ss06", "ss07", "ss08", "ss09", "ss10", "ss11", "ss12", "ss13", "ss14", "ss15", "ss16", "ss17", "ss18",
"ss19", "ss20", "salt", "titl", "pnum", "lnum", "sups", "subs",
"ss19", "ss20", "salt", "titl", "pnum", "lnum", "sups", "subs", "numr", "dnom",
];
impl Default for OtFeatures {
@@ -85,6 +87,40 @@ impl OtFeatures {
Self::default().with_tags(tags)
}
/// The tags that make `change` on any set, for a `change` that turns each feature on, turns it
/// off or keeps it, whatever the other features are: `tag` for each feature it turns on, `-tag`
/// for each it turns off, and none for one it keeps. [`Self::with_tags`] applies them.
pub fn changes(change: impl Fn(&mut Self)) -> Vec<String> {
let mut off = Self { ligatures: false, contextual: false, ..Self::default() };
let mut on = Self {
vertical: false,
ligatures: true,
contextual: true,
discretionary_ligatures: true,
small_caps: true,
fractions: true,
oldstyle_figures: true,
tabular_figures: true,
ordinals: true,
swash: true,
extra: u32::MAX >> (32 - EXTRA_FEATURES.len()),
};
change(&mut off);
change(&mut on);
let named = ["liga", "calt", "dlig", "smcp", "frac", "onum", "tnum", "ordn", "swsh"];
let mut v = vec![];
for tag in named.into_iter().chain(EXTRA_FEATURES) {
let turn_off = format!("-{tag}");
let has = |f: &Self| f.with_tags([turn_off.as_str()]) != *f;
match (has(&off), has(&on)) {
(true, true) => v.push(tag.to_string()),
(false, false) => v.push(turn_off.clone()),
_ => {}
}
}
v
}
/// The tags this set differs from the defaults by (what `CharStyle::features` stores).
pub fn to_tags(&self) -> Vec<String> {
let d = Self::default();
@@ -277,6 +313,44 @@ mod tests {
let tags: Vec<String> = OtFeatures::default().resolve(&st).iter().filter(|f| f.value == 1).map(|f| f.tag.to_string()).collect();
assert!(tags.contains(&"ss05".to_string()) && tags.contains(&"titl".to_string()), "{tags:?}");
}
/// Numerators and denominators (the OpenType panel's Position menu) are known, round-trip
/// through the stored tags and reach the shaper.
#[test]
fn numerators_and_denominators_are_kept_and_shaped() {
for t in ["numr", "dnom", "-numr", "+dnom"] {
assert!(OtFeatures::known_tag(t), "{t}");
}
let o = OtFeatures::from_tags(["dnom", "sups", "numr"]);
assert_eq!(o.to_tags(), vec!["sups", "numr", "dnom"]);
assert_eq!(OtFeatures::from_tags(["numr", "dnom", "-numr"]).to_tags(), ["dnom"]);
for tag in ["numr", "dnom"] {
let st = CharStyle { features: vec![tag.into()], ..CharStyle::default() };
let on: Vec<String> = OtFeatures::default().resolve(&st).iter().filter(|f| f.value == 1).map(|f| f.tag.to_string()).collect();
assert_eq!(on, [tag], "{tag}");
}
}
/// `changes` lists the features a change turns on and off and leaves out the ones it keeps,
/// and applying them to any set makes the same change.
#[test]
fn changes_list_what_turns_on_and_off() {
let figure = |f: &mut OtFeatures| {
f.tabular_figures = true;
f.oldstyle_figures = false;
f.extra &= !(1 << 22);
};
assert_eq!(OtFeatures::changes(figure), ["-onum", "tnum", "-pnum"]);
assert_eq!(OtFeatures::changes(|f| f.ligatures = false), ["-liga"]);
assert_eq!(OtFeatures::changes(|f| f.extra |= 1 << 27), ["dnom"]);
assert!(OtFeatures::changes(|_| {}).is_empty());
for start in [&["lnum", "smcp"][..], &["pnum", "onum", "-liga"][..], &[][..]] {
let mut want = OtFeatures::from_tags(start.iter().copied());
figure(&mut want);
let tags = OtFeatures::changes(figure);
assert_eq!(OtFeatures::from_tags(start.iter().copied()).with_tags(tags.iter().map(String::as_str)), want, "{start:?}");
}
}
use crate::{FontDb, layout};
use vectorcraft_doc::TextObject;
use vectorcraft_geom::Point;
+1 -1
View File
@@ -33,7 +33,7 @@ pub mod thread;
pub use bitmap::GlyphImage;
pub use craft_fonts::{CRAFT_FONTS, CraftFont, WEB_FONTS, WebFont};
pub use features::{EXTRA_FEATURES, LIGATURE_TRACKING_LIMITS, OtFeatures, explicit_ligatures, ligatures_suppressed_by};
pub use features::{EXTRA_FEATURES, LIGATURE_TRACKING_LIMITS, MAX_STYLE_FEATURES, OtFeatures, explicit_ligatures, ligatures_suppressed_by};
pub use fontdb::{
FALLBACK_FAMILY, FontClass, FontDb, FontFace, FontMatch, FontTraits, IcfMargins, PlatformFontFiles, WantedFont, WantedFonts, app_font_dir,
is_font_file, is_suitcase, set_app_font_dir, set_platform_font_files, set_user_font_dirs, set_user_font_dirs_confined, style_weight,
+4
View File
@@ -2416,6 +2416,10 @@ digits Telugu Телугу
digits Kannada Каннада
digits Malayalam Малаялам
Proportional Lining Пропорционални подравнени цифри
Position: Позиция:
Default Position Стандартна позиция
Numerator Числител
Denominator Знаменател
Proportional Oldstyle Пропорционални стари цифри
Pucker & Bloat… Свиване и раздуване…
Pucker Tool Инструмент „Свиване“
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1783,6 +1783,9 @@ digits Malayalam Malajalam
Ligatures are off while the text is tracked. Ligature su isključene dok tekst ima razmak slova.
Ordinals Redni brojevi
Proportional Lining Proporcionalne brojke
Default Position Zadani položaj
Numerator Brojnik
Denominator Nazivnik
Proportional Oldstyle Proporcionalne stare brojke
Reset OpenType Features Vrati OpenType mogućnosti na početno
Select text to set its OpenType features. Odaberite tekst da postavite njegove OpenType mogućnosti.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -3099,6 +3099,10 @@ digits Malayalam Malaiàlam
Properties Propietats
Proportional Metrics Mètriques proporcionals
Proportional Lining Xifres proporcionals alineades
Position: Posició:
Default Position Posició predeterminada
Numerator Numerador
Denominator Denominador
Proportional Oldstyle Oldstyle proporcional
PRs PRs
Public Domain Domini públic
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1809,6 +1809,10 @@ digits Malayalam Malajálamské
Ligatures are off while the text is tracked. Ligatury jsou vypnuté, když má text prostrkání.
Ordinals Řadové číslovky
Proportional Lining Proporcionální verzálkové
Position: Poloha:
Default Position Výchozí poloha
Numerator Čitatel
Denominator Jmenovatel
Proportional Oldstyle Proporcionální minuskové
Reset OpenType Features Obnovit funkce OpenType
Select text to set its OpenType features. Vyberte text, abyste mohli nastavit jeho funkce OpenType.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1622,6 +1622,10 @@ digits Malayalam Malayalam
Ligatures are off while the text is tracked. Ligaturer er slået fra, mens der er sporing på teksten.
Ordinals Ordenstal
Proportional Lining Proportionale versalcifre
Position: Placering:
Default Position Standardplacering
Numerator Tæller
Denominator Nævner
Proportional Oldstyle Proportionale mediævalcifre
Reset OpenType Features Nulstil OpenType-funktioner
Select text to set its OpenType features. Markér tekst for at angive dens OpenType-funktioner.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2435,6 +2435,10 @@ digits Telugu Telugu
digits Kannada Kannada
digits Malayalam Malayalam
Proportional Lining Proportionale Versalziffern
Position: Position:
Default Position Standardposition
Numerator Zähler
Denominator Nenner
Proportional Oldstyle Proportionale Mediävalziffern
Pucker & Bloat… Zusammenziehen und aufblasen…
Pucker Tool Zusammenziehen-Werkzeug
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -3071,6 +3071,10 @@ digits Telugu Τελούγκου
digits Kannada Κανάντα
digits Malayalam Μαλαγιάλαμ
Proportional Lining Ισοϋψείς αναλογικοί αριθμοί
Position: Θέση:
Default Position Προεπιλεγμένη θέση
Numerator Αριθμητής
Denominator Παρονομαστής
Proportional Oldstyle Αναλογικοί παλαιοτυπικοί αριθμοί
Pucker & Bloat… Συρρίκνωση και διόγκωση…
Pucker Tool Συρρίκνωση (Pucker Tool)
Can't render this file because it contains an unexpected character in line 164 and column 5.
+4
View File
@@ -2479,6 +2479,10 @@ digits Telugu Telugu
digits Kannada Canarés
digits Malayalam Malayálam
Proportional Lining Proporcionales de caja alta
Position: Posición:
Default Position Posición predeterminada
Numerator Numerador
Denominator Denominador
Proportional Oldstyle Estilo antiguo proporcional
Pucker & Bloat… Fruncir y engordar…
Pucker Tool Herramienta Fruncir
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1770,6 +1770,10 @@ digits Malayalam Malajalam
Ligatures are off while the text is tracked. Ligatuurit eivät ole käytössä, kun tekstin merkkiväliä on muutettu.
Ordinals Järjestysluvut
Proportional Lining Suhteelliset numerot
Position: Sijainti:
Default Position Oletussijainti
Numerator Osoittaja
Denominator Nimittäjä
Proportional Oldstyle Suhteelliset vanhanaikaiset numerot
Reset OpenType Features Palauta OpenType-ominaisuudet
Select text to set its OpenType features. Valitse teksti, jonka OpenType-ominaisuudet haluat asettaa.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2471,6 +2471,10 @@ digits Telugu Télougou
digits Kannada Kannada
digits Malayalam Malayalam
Proportional Lining Chiffres proportionnels alignés
Position: Position :
Default Position Position par défaut
Numerator Numérateur
Denominator Dénominateur
Proportional Oldstyle Elzévirien proportionnel
Pucker & Bloat… Contraction et dilatation…
Pucker Tool Outil Contraction
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -3007,6 +3007,10 @@ mark-style Japanese Japán
Properties Tulajdonságok
Proportional Metrics Arányos metrika
Proportional Lining Arányos modern
Position: Pozíció:
Default Position Alapértelmezett pozíció
Numerator Számláló
Denominator Nevező
Proportional Oldstyle Arányos régies
Public Domain Közkincs
Pucker Összehúzás
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1622,6 +1622,10 @@ digits Malayalam Malayalam
Ligatures are off while the text is tracked. Ligatur nonaktif saat teks diberi tracking.
Ordinals Ordinal
Proportional Lining Lining proporsional
Position: Posisi:
Default Position Posisi default
Numerator Pembilang
Denominator Penyebut
Proportional Oldstyle Oldstyle proporsional
Reset OpenType Features Setel ulang fitur OpenType
Select text to set its OpenType features. Pilih teks untuk mengatur fitur OpenType-nya.
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
+4
View File
@@ -2511,6 +2511,10 @@ digits Telugu Telugu
digits Kannada Kannada
digits Malayalam Malayalam
Proportional Lining Proporzionali allineati
Position: Posizione:
Default Position Posizione predefinita
Numerator Numeratore
Denominator Denominatore
Proportional Oldstyle Proporzionali stile antico
Pucker & Bloat… Contrai e gonfia…
Pucker Tool Strumento Contrai
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -3623,6 +3623,10 @@ file-filter Templates テンプレート
Preview in Browser needs the desktop app (the web downloads files instead) ブラウザーでプレビューにはデスクトップアプリが必要です (Web アプリではファイルがダウンロードされます)
Print presets プリントプリセット
Proportional Lining プロポーショナルライニング
Position: 位置:
Default Position デフォルトの位置
Numerator 分子
Denominator 分母
Proportional Oldstyle プロポーショナルオールドスタイル
Punctuation 句読点
Radius 1 半径 1
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1525,6 +1525,9 @@ digits Malayalam მალაიალამური
Ligatures are off while the text is tracked. ტექსტზე ტრეკინგის დროს ლიგატურები გამორთულია.
Ordinals რიგობითი რიცხვები
Proportional Lining პროპორციული ხაზოვანი
Default Position ნაგულისხმევი პოზიცია
Numerator მრიცხველი
Denominator მნიშვნელი
Proportional Oldstyle პროპორციული ძველებური
Reset OpenType Features OpenType შესაძლებლობების ჩამოყრა
Select text to set its OpenType features. OpenType შესაძლებლობების დასაყენებლად მონიშნეთ ტექსტი.
Can't render this file because it is too large.
+4
View File
@@ -3611,6 +3611,10 @@ file-filter Templates 템플릿
Preview in Browser needs the desktop app (the web downloads files instead) 브라우저에서 미리 보기는 데스크톱 앱이 필요합니다(웹에서는 파일이 다운로드됩니다)
Print presets 인쇄 프리셋
Proportional Lining 비례 라이닝
Position: 위치:
Default Position 기본 위치
Numerator 분자
Denominator 분모
Proportional Oldstyle 비례 올드스타일
Punctuation 문장 부호
Radius 1 반지름 1
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1636,6 +1636,10 @@ digits Malayalam Malayalam
Ligatures are off while the text is tracked. Ligaturen staan uit terwijl de tekst wordt gespatieerd.
Ordinals Rangtelwoorden
Proportional Lining Proportionele lining
Position: Positie:
Default Position Standaardpositie
Numerator Teller
Denominator Noemer
Proportional Oldstyle Proportionele oldstyle
Reset OpenType Features OpenType-functies herstellen
Select text to set its OpenType features. Selecteer tekst om de OpenType-functies in te stellen.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1728,6 +1728,10 @@ digits Malayalam Malajalam
Ligatures are off while the text is tracked. Ligatury są wyłączone, gdy tekst ma zmienione światło.
Ordinals Liczebniki porządkowe
Proportional Lining Proporcjonalne nowoczesne
Position: Pozycja:
Default Position Pozycja domyślna
Numerator Licznik
Denominator Mianownik
Proportional Oldstyle Proporcjonalne nautyczne
Reset OpenType Features Resetuj funkcje OpenType
Select text to set its OpenType features. Zaznacz tekst, aby ustawić jego funkcje OpenType.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -4641,6 +4641,9 @@ axis Both Ambos
Preview in Browser needs the desktop app (the web downloads files instead) A visualização no navegador exige o aplicativo para desktop (na web, os arquivos são baixados)
Print presets Predefinidos de impressão
Proportional Lining Alinhado Proporcional
Default Position Posição Padrão
Numerator Numerador
Denominator Denominador
Proportional Oldstyle Antigo Proporcional
Punctuation Pontuação
Radius 1 Raio 1
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -2506,6 +2506,10 @@ digits Telugu Телугу
digits Kannada Каннада
digits Malayalam Малаялам
Proportional Lining Пропорциональные цифры
Position: Положение:
Default Position Положение по умолчанию
Numerator Числитель
Denominator Знаменатель
Proportional Oldstyle Пропорциональные старого стиля
Pucker & Bloat… Сморщивание и раздувание…
Pucker Tool Инструмент «Сморщивание»
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2416,6 +2416,10 @@ digits Telugu Telugu
digits Kannada Kannada
digits Malayalam Malayalam
Proportional Lining Proportionella versalsiffror
Position: Position:
Default Position Standardposition
Numerator Täljare
Denominator Nämnare
Proportional Oldstyle Proportionella medievalsiffror
Pucker & Bloat… Dra ihop och svullna…
Pucker Tool Verktyget Dra ihop
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4
View File
@@ -2290,6 +2290,10 @@ digits Telugu Telugu
digits Kannada Kannada
digits Malayalam Malayalam
Proportional Lining Orantılı Astar
Position: Pozisyon:
Default Position Varsayılan Pozisyon
Numerator Pay
Denominator Payda
Proportional Oldstyle Orantılı Eski Stil
Pucker & Bloat… Büzüşme ve Şişkinlik…
Pucker Tool Büzme Aracı
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4
View File
@@ -2503,6 +2503,10 @@ digits Telugu Телугу
digits Kannada Каннада
digits Malayalam Малаялам
Proportional Lining Пропорційні цифри
Position: Положення:
Default Position Положення за замовчуванням
Numerator Чисельник
Denominator Знаменник
Proportional Oldstyle Пропорційні цифри старого стилю
Pucker & Bloat… Зморщування та роздування …
Pucker Tool Інструмент «Зморщування»
Can't render this file because it contains an unexpected character in line 178 and column 5.
+4
View File
@@ -2790,6 +2790,10 @@ mark-style Japanese Kiểu Nhật
Properties Thuộc tính
Proportional Metrics Số liệu theo tỷ lệ
Proportional Lining Số tỷ lệ đứng
Position: Vị trí:
Default Position Vị trí mặc định
Numerator Tử số
Denominator Mẫu số
Proportional Oldstyle Kiểu cũ theo tỷ lệ
Pucker & Bloat… Nhíu & Phồng…
Pucker Tool Công cụ Nhíu
Can't render this file because it contains an unexpected character in line 200 and column 5.
+4
View File
@@ -2383,6 +2383,10 @@ digits Telugu 泰卢固数字
digits Kannada 卡纳达数字
digits Malayalam 马拉雅拉姆数字
Proportional Lining 比例齐线数字
Position: 位置:
Default Position 默认位置
Numerator 分子
Denominator 分母
Proportional Oldstyle 比例旧式数字
Pucker & Bloat… 缩拢与膨胀…
Pucker Tool 收缩工具
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -3336,6 +3336,10 @@ digits Telugu 泰盧固數字
digits Kannada 坎那達數字
digits Malayalam 馬來亞拉姆數字
Proportional Lining 比例齊線數字
Position: 位置:
Default Position 預設位置
Numerator 分子
Denominator 分母
Proportional Oldstyle 比例舊式數字
Pucker & Bloat… 縮攏與膨脹…
Pucker Tool 縮攏工具
Can't render this file because it contains an unexpected character in line 6 and column 47.
+215 -13
View File
@@ -1,11 +1,12 @@
//! OpenType panel: per-character OpenType features (ligatures, alternates, swashes, small caps,
//! ordinals, fractions, figure style) for the selected text or the Type tool's selected characters.
//! ordinals, fractions, figure style, position) for the selected text or the Type tool's selected
//! characters.
use egui::Ui;
use serde_json::json;
use vectorcraft_text::{EXTRA_FEATURES, OtFeatures};
use super::character::{text_editing, text_style};
use super::character::{shared, text_editing, text_style};
use crate::VectorcraftApp;
use crate::widgets::{self, menu_item};
@@ -21,6 +22,11 @@ type Toggle = (&'static str, Flag);
const FIGURES: [&str; 5] = ["Default Figure", "Tabular Lining", "Proportional Lining", "Proportional Oldstyle", "Tabular Oldstyle"];
const POSITIONS: [&str; 5] = ["Default Position", "Superscript", "Subscript", "Numerator", "Denominator"];
/// The features the Position menu's entries after Default Position turn on, in menu order.
const POSITION_TAGS: [&str; 4] = ["sups", "subs", "numr", "dnom"];
/// The bit of [`OtFeatures::extra`] that turns `tag` on (0 for a tag outside [`EXTRA_FEATURES`]).
fn extra_bit(tag: &str) -> u32 {
EXTRA_FEATURES.iter().position(|t| *t == tag).map_or(0, |i| 1 << i)
@@ -72,15 +78,91 @@ fn set_figure(f: &mut OtFeatures, i: usize) {
}
}
/// The Position menu entry `f` shows: Superscript (`sups`), Subscript (`subs`), Numerator
/// (`numr`) or Denominator (`dnom`) when `f` turns that one on, Default Position when it turns
/// none of them on, and `None` when it turns on more than one.
fn position(f: &OtFeatures) -> Option<usize> {
let mut on = POSITION_TAGS.iter().enumerate().filter(|(_, t)| f.extra & extra_bit(t) != 0).map(|(i, _)| i + 1);
match (on.next(), on.next()) {
(None, _) => Some(0),
(Some(i), None) => Some(i),
(Some(_), Some(_)) => None,
}
}
/// `f` with Position menu entry `i` chosen: the entry's feature on and the other three off.
/// Default Position turns all four off.
fn set_position(f: &mut OtFeatures, i: usize) {
for (k, tag) in POSITION_TAGS.iter().enumerate() {
if k + 1 == i {
f.extra |= extra_bit(tag);
} else {
f.extra &= !extra_bit(tag);
}
}
}
/// The Position menu entry for the characters the panel acts on, or `None` when they differ or
/// one of them turns on more than one position.
fn shown_position(app: &VectorcraftApp) -> Option<usize> {
shared(app, |s| position(&OtFeatures::from_tags(s.features.iter().map(String::as_str)))).flatten()
}
/// The Position menu's text for entry `shown`: Mixed when the characters differ.
fn position_label(shown: Option<usize>) -> &'static str {
shown.and_then(|i| POSITIONS.get(i).copied()).unwrap_or("Mixed")
}
/// A change one of the panel's controls makes to the features of each character it acts on.
#[derive(Clone, Copy)]
enum Change {
/// The Figure menu entry chosen.
Figure(usize),
/// The Position menu entry chosen.
Position(usize),
/// A checkbox row turned on or off.
Toggle(Flag, bool),
}
impl Change {
fn apply(self, f: &mut OtFeatures) {
match self {
Change::Figure(i) => set_figure(f, i),
Change::Position(i) => set_position(f, i),
Change::Toggle(flag, on) => flag.put(f, on),
}
}
}
/// Make `change` to the features of each character the panel acts on, from the character's own
/// features (`featureChanges`), unless it changes none of them.
fn edit(app: &mut VectorcraftApp, change: Change) {
let unchanged = shared(app, |s| {
let before = OtFeatures::from_tags(s.features.iter().map(String::as_str));
let mut after = before;
change.apply(&mut after);
after == before
});
if unchanged != Some(true) {
run_features(app, "featureChanges", OtFeatures::changes(|f| change.apply(f)));
}
}
/// Apply `f` to the selection's features (the Type tool range, or whole objects).
fn set(app: &mut VectorcraftApp, f: OtFeatures) {
let tags = f.to_tags();
let r = match text_editing(app) {
Some((id, a, b)) if b > a => app.run("text.setRangeStyle", json!({ "id": id.0, "start": a, "end": b, "features": tags })),
Some((id, _, _)) => app.run("text.setRangeStyle", json!({ "id": id.0, "features": tags })),
None => app.run("text.setStyle", json!({ "features": tags })),
run_features(app, "features", f.to_tags());
}
/// Send `tags` as the `key` param (`features` or `featureChanges`) for the Type tool's range or
/// caret, or else for the selected type.
fn run_features(app: &mut VectorcraftApp, key: &str, tags: Vec<String>) {
let (cmd, mut p) = match text_editing(app) {
Some((id, a, b)) if b > a => ("text.setRangeStyle", json!({ "id": id.0, "start": a, "end": b })),
Some((id, _, _)) => ("text.setRangeStyle", json!({ "id": id.0 })),
None => ("text.setStyle", json!({})),
};
if let Err(e) = r {
p[key] = json!(tags);
if let Err(e) = app.run(cmd, p) {
app.ui.status = e;
}
}
@@ -91,26 +173,37 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
return;
};
let cur = OtFeatures::from_tags(st.features.iter().map(String::as_str));
let mut next = cur;
let mut change = None;
ui.horizontal(|ui| {
widgets::dim_label(ui, tl!("Figure:"));
let fig = figure(&cur);
if let Some(i) = widgets::dropdown(ui, "ot-figure", FIGURES[fig], &FIGURES, 170.0) {
set_figure(&mut next, i);
change = Some(Change::Figure(i));
}
});
let shown = shown_position(app);
ui.horizontal(|ui| {
widgets::dim_label(ui, tl!("Position:"));
if let Some(i) = widgets::dropdown(ui, "ot-position", position_label(shown), &POSITIONS, 170.0) {
change = Some(Change::Position(i));
}
});
widgets::divider(ui);
let mut next = cur;
if let Some(c) = change {
c.apply(&mut next);
}
for (label, flag) in rows() {
let on = flag.get(&next);
if widgets::check(ui, label, on, true) {
flag.put(&mut next, !on);
change = Some(Change::Toggle(flag, !on));
}
}
if cur.ligatures && vectorcraft_text::ligatures_suppressed_by(st.tracking) && !vectorcraft_text::explicit_ligatures(&st.features) {
widgets::dim_label(ui, tl!("Ligatures are off while the text is tracked."));
}
if next != cur {
set(app, next);
if let Some(c) = change {
edit(app, c);
}
}
@@ -137,6 +230,8 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
#[cfg(test)]
mod tests {
use serde_json::Value;
use super::*;
#[test]
@@ -245,4 +340,111 @@ mod tests {
assert_eq!(figure(&features(&app)), 1);
}
}
/// The Position menu lists its five entries in order; Superscript, Subscript, Numerator and
/// Denominator each set their feature and clear the other three, and Default Position clears
/// all four, on the selected type.
#[test]
fn position_menu_sets_one_position() {
assert_eq!(POSITIONS, ["Default Position", "Superscript", "Subscript", "Numerator", "Denominator"]);
let mut app = selected_text();
assert_eq!(shown_position(&app), Some(0));
set(&mut app, OtFeatures::from_tags(["sups", "subs", "numr", "dnom", "smcp"]));
for (i, tag) in [(1, "sups"), (2, "subs"), (3, "numr"), (4, "dnom")] {
edit(&mut app, Change::Position(i));
assert_eq!(text_style(&app).unwrap().0.features, ["smcp", tag], "{}", POSITIONS[i]);
assert_eq!(position_label(shown_position(&app)), POSITIONS[i]);
}
edit(&mut app, Change::Position(0));
assert_eq!(text_style(&app).unwrap().0.features, ["smcp"]);
assert_eq!(shown_position(&app), Some(0));
}
fn undo_steps(app: &VectorcraftApp) -> usize {
app.session.active().unwrap().history.undo.len()
}
/// Selected objects whose positions differ show the Position menu as mixed; an entry chosen
/// then applies to every one, the one the panel reads its features from included, in one undo
/// step, and each keeps its other features.
#[test]
fn mixed_positions_show_mixed_and_take_the_choice() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
let a = app.session.execute("text.create", &json!({"x": 10, "y": 40, "text": "x2"})).unwrap()["id"].clone();
let b = app.session.execute("text.create", &json!({"x": 10, "y": 80, "text": "y2"})).unwrap()["id"].clone();
app.session.execute("select.set", &json!({"ids": [a]})).unwrap();
set(&mut app, OtFeatures::from_tags(["sups"]));
app.session.execute("select.set", &json!({"ids": [b]})).unwrap();
set(&mut app, OtFeatures::from_tags(["smcp"]));
app.session.execute("select.set", &json!({"ids": [a, b]})).unwrap();
assert_eq!(shown_position(&app), None);
assert_eq!(position_label(shown_position(&app)), "Mixed");
let ctx = egui::Context::default();
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(&mut app, ui));
out.textures_delta.clear();
assert_eq!(position(&features(&app)), Some(1), "the panel reads the first object");
let undo = undo_steps(&app);
edit(&mut app, Change::Position(1));
assert_eq!(undo_steps(&app), undo + 1);
assert_eq!(shown_position(&app), Some(1));
for (id, want) in [(&a, vec!["sups"]), (&b, vec!["smcp", "sups"])] {
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
assert_eq!(text_style(&app).unwrap().0.features, want);
}
}
/// Type with two positions turned on shows the Position menu as mixed, and the entry of one
/// of them turns the other off.
#[test]
fn two_positions_show_mixed_and_one_is_chosen() {
let mut app = selected_text();
set(&mut app, OtFeatures::from_tags(["sups", "numr"]));
assert_eq!(shown_position(&app), None);
edit(&mut app, Change::Position(1));
assert_eq!(text_style(&app).unwrap().0.features, ["sups"]);
assert_eq!(shown_position(&app), Some(1));
}
/// The runs of `id`'s characters `0..end`, each as its text and its features.
fn run_features(app: &mut VectorcraftApp, id: &Value, end: usize) -> Vec<(String, Value)> {
let runs = app.session.execute("text.getRange", &json!({"id": id, "start": 0, "end": end})).unwrap()["runs"].clone();
runs.as_array().unwrap().iter().map(|r| (r["text"].as_str().unwrap_or("").to_string(), r["style"]["features"].clone())).collect()
}
/// The Type tool's selected characters show mixed when only some are numerators; Denominator
/// then applies to those characters alone, each run keeping its other features, and the
/// Figure menu and the checkboxes also keep each run's other features.
#[test]
fn mixed_range_takes_the_choice() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
app.session.select_tool("type", Default::default()).unwrap();
let id = app.session.execute("text.create", &json!({"x": 10, "y": 40, "text": "1/23"})).unwrap()["id"].clone();
app.session.execute("text.setRangeStyle", &json!({"id": id, "start": 0, "end": 1, "features": ["numr"]})).unwrap();
app.session.execute("text.setRangeStyle", &json!({"id": id, "start": 1, "end": 3, "features": ["smcp"]})).unwrap();
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
app.session.set_tool_option("select", &json!({"start": 0, "end": 3}));
assert_eq!(text_editing(&app).map(|(_, a, b)| (a, b)), Some((0, 3)));
assert_eq!(shown_position(&app), None);
let undo = undo_steps(&app);
edit(&mut app, Change::Position(4));
assert_eq!(undo_steps(&app), undo + 1);
assert_eq!(shown_position(&app), Some(4));
let none = Value::Null;
assert_eq!(
run_features(&mut app, &id, 4),
[("1".to_string(), json!(["dnom"])), ("/2".to_string(), json!(["smcp", "dnom"])), ("3".to_string(), none.clone())]
);
edit(&mut app, Change::Figure(4));
edit(&mut app, Change::Toggle(Flag::Field(|f| &mut f.swash), true));
assert_eq!(
run_features(&mut app, &id, 4),
[
("1".to_string(), json!(["dnom", "onum", "tnum", "swsh"])),
("/2".to_string(), json!(["smcp", "dnom", "onum", "tnum", "swsh"])),
("3".to_string(), none)
]
);
}
}
+1
View File
@@ -865,6 +865,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
The Every-line Composer composes a paragraph whose characters differ in line metrics (type size, font, leading or vertical scale, or an inline graphic taller than the text) in rectangular area type, rows and columns included. In area type of another shape, or with text wrap, such a paragraph is broken with the Single-line Composer.
The Character panel's Small Caps, Superscript and Subscript buttons and the panel menu's items of the same names turn small caps and the superscript or subscript position on and off for the selected type, or for the text the Type tool has selected (`text.setFormat` or `text.setRangeStyle` `{smallCaps, position}`), in Document Setup's proportions. Turning Superscript on turns Subscript off, and turning Subscript on turns Superscript off. A button or item shows as on when the first selected character (at a caret, the character before it) has its attribute.
The OpenType panel's Stylistic Alternates and Titling Alternates checkboxes turn `salt` and `titl` on and off for the selected type, and its Figure menu lists Default Figure, Tabular Lining, Proportional Lining, Proportional Oldstyle and Tabular Oldstyle; Proportional Lining sets `pnum` and `lnum` and turns `tnum` and `onum` off, Tabular Lining turns `pnum` off and keeps `lnum`, Proportional Oldstyle turns `lnum` off and keeps `pnum`, and Default Figure and Tabular Oldstyle turn `pnum` and `lnum` off. Behavior reference: [Format text with OpenType features](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/format-text-with-opentype-features.html) and [Use swashes, titling alternates, or stylistic alternates](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/use-swashes-titling-alternates-or-stylistic-alternates.html) (public documentation). Tests: `panels::opentype::tests`.
The OpenType panel's Position menu lists Default Position, Superscript, Subscript, Numerator and Denominator, as [Format text with OpenType features](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/format-text-with-opentype-features.html) describes; Superscript, Subscript, Numerator and Denominator set `sups`, `subs`, `numr` and `dnom` for the selected type and turn the other three off, and Default Position turns all four off. The menu shows Mixed when the characters it acts on differ, and an entry chosen then applies to all of them. These two rules are VectorCraft's own, as is setting the feature on type whose font lacks the glyphs, which then draws unchanged. The panel's menus and checkboxes change only their own features on each selected character, so characters keep their other OpenType features (`text.setStyle` and `text.setRangeStyle` `{featureChanges}`). Tests: `panels::opentype::tests`, `features::tests::numerators_and_denominators_are_kept_and_shaped`.
- **Paragraph alignment:** the shared Paragraph/Properties alignment row leaves every button unselected when the paragraphs it acts on differ. Auto resolves per paragraph; choosing an alignment uses the existing command. Headless paragraph_mixed_alignment_* regressions exercise mixed → Center → Undo, caret/uniform states, Unicode range boundaries, Auto/direction, split runs and grouped text. These regressions and full integration CI pass. Original captures confirm mixed → Center → Undo in the existing alignment-button layout. The [public Paragraph panel guide](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/paragraph-panel-overview.html) lists the seven controls, but does not specify mixed-state rendering. Other paragraph fields remain outside this change; scores and hours unchanged.
For Auto Size area type in columns, the bisection for the frame's height runs on the lines of the one-column layout, and its result is checked with at most two layouts of the columns. When the text wraps around objects or does not fit the one-column layout, or when the check fails, the columns are laid out at every step of the bisection, as before. Typing at the start of a story of 50,000 characters in 2 columns took 1,670 ms a keystroke and now takes 292 ms (release build, `auto_height_in_columns_typing_timing`).
Exports to PDF, EPS, SVG, EMF, WMF and the raster formats warn about the area type and type on a path that they draw without the text that doesn't fit, and give their ids, as Print and Print Preview do.
+1 -1
View File
@@ -28,7 +28,7 @@ against Illustrator's public documentation; **estimated** unless noted.
| Character and Paragraph panels, styles with overrides | done | Character-style Auto-leading resets, partial definitions, local leading overrides, override markers, Undo/Redo and native serialization have regression coverage; differing paragraph alignments leave the shared alignment buttons unselected (headless regressions, full integration CI and visual review pass); mixed-value display for other paragraph fields remains missing |
| Character size and horizontal scale after object/group scaling | done | #1034: fields and setters compensate the stored affine; point size follows the transformed em-height axis, not the glyph ink bounds. Range edits, keyboard increments, leading and baseline shift use document points; rotation, reflection and shear remain editable. |
| Preserve font sizes while scaling a group | missing | Separate enhancement; scaling still scales text with the other artwork. |
| OpenType panel, Glyphs panel, alternates | done | Highlight Alternate Glyphs preference (#394, off by default) paints pink behind OpenType substitutions on screen when on (required shaping such as Arabic joining forms and the lam-alef ligature, including a lam-alef older fonts file under `liga`, is not highlighted; an optional feature that changes the ligature is); stylistic sets ss01–ss20, stylistic and titling alternates, proportional and lining figures, superscript and subscript are character-style features (`text.setStyle {features}`, #1607). The Glyphs panel highlights the character selected with the Type tool (a range's first character, else the one before the caret) and scrolls it into view as the selection moves (#4908; `panels::glyphs` tests); its Show menu lists Unicode ranges, and Alternates for Current Selection is not there yet |
| OpenType panel, Glyphs panel, alternates | done | Highlight Alternate Glyphs preference (#394, off by default) paints pink behind OpenType substitutions on screen when on (required shaping such as Arabic joining forms and the lam-alef ligature, including a lam-alef older fonts file under `liga`, is not highlighted; an optional feature that changes the ligature is); stylistic sets ss01–ss20, stylistic and titling alternates, proportional and lining figures, superscript and subscript are character-style features (`text.setStyle {features}`, #1607); the OpenType panel's Position menu sets superscript, subscript, numerator or denominator glyphs (`sups`, `subs`, `numr`, `dnom`). The Glyphs panel highlights the character selected with the Type tool (a range's first character, else the one before the caret) and scrolls it into view as the selection moves (#4908; `panels::glyphs` tests); its Show menu lists Unicode ranges, and Alternates for Current Selection is not there yet |
| Font menu: samples, live preview, filters, favourites, recent families; font matching by any name; Find Font | done | #3515: recently applied families appear first, once each; missing families are omitted and the existing Number of Recent Fonts setting applies; #339; script filter: Japanese, Latin, Thai; the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux) |
| Missing glyphs set in an installed font made for their script (Thai, Lao, Khmer, Myanmar first; then any font that has them) | done | Thai: Leelawadee UI, Tahoma, Thonburi, Sukhumvit Set, Noto Sans Thai, TLWG |
| Full font-name matching (OpenType name ID 4) | done | #1258: loaded/cataloged faces and Missing Fonts searches; localized names and variable default faces; Windows font-service QA pending |