G4: Fractional Widths rounds a type object's advances to whole points

Squashed from #3411 onto main.
This commit is contained in:
bflatastic
2026-10-21 23:16:04 +07:00
parent 23dbf5d214
commit 398c6316d9
8 changed files with 215 additions and 8 deletions
+11
View File
@@ -255,6 +255,9 @@ fn regular() -> String {
fn hundred() -> f64 {
100.0
}
fn yes() -> bool {
true
}
impl Default for CharStyle {
fn default() -> Self {
@@ -1340,6 +1343,13 @@ pub struct TextObject {
/// Off for new type and for documents from before it, and saved only when on.
#[serde(default, rename = "opticalMarginAlignment", skip_serializing_if = "crate::skip::is_default")]
pub optical_margin: bool,
/// Character panel menu › Fractional Widths: spacing between characters uses fractions of a
/// point. On for new type and for documents from before it, and saved only when off. Off, each
/// character that takes any space advances by a whole number of points, at least one, so small
/// type does not run together. The setting is the whole type object, not a range of characters.
/// <https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/turn-fractional-character-widths-off-or-on.html>
#[serde(default = "yes", rename = "fractionalWidths", skip_serializing_if = "crate::skip::is_true")]
pub fractional_widths: bool,
/// Cached layout bounds in text space, filled in by the layout engine (not serialized).
#[serde(skip)]
pub cached_bounds: Option<Rect>,
@@ -1406,6 +1416,7 @@ impl TextObject {
path_spacing: 0.0,
wrap: Vec::new(),
optical_margin: false,
fractional_widths: true,
cached_bounds: None,
cached_baselines: Vec::new(),
}
+2
View File
@@ -856,6 +856,7 @@ fn create_in_path(s: &mut Session, p: &Value) -> Result<Value> {
path_spacing: 0.0,
wrap: Vec::new(),
optical_margin: false,
fractional_widths: true,
cached_bounds: None,
cached_baselines: Vec::new(),
};
@@ -1076,6 +1077,7 @@ fn headline_tracking(t: &TextObject, target: f64) -> Option<f64> {
path_spacing: 0.0,
wrap: Vec::new(),
optical_margin: false,
fractional_widths: true,
cached_bounds: None,
cached_baselines: Vec::new(),
};
+82
View File
@@ -43,6 +43,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_selection,
optical_margin_alignment
),
cmd!(
"type.fractionalWidths",
"Fractional Widths",
[],
None,
"{ids?|id?, on?: bool} turn Fractional Widths on or off for the selected type and every frame of its threads (on omitted: the opposite of the first object's setting). On (the default, and what a document from before it reads as), spacing between characters uses fractions of a point. Off, each character that takes any space advances by a whole number of points, at least one, so small type does not run together; a character with no width stays at none. The setting is the whole type object, not a range of characters → {on, ids}",
has_selection,
fractional_widths
),
cmd!(
"text.setText",
"Set Text",
@@ -128,6 +137,29 @@ fn optical_margin_alignment(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"on": on, "ids": ids.iter().map(|id| id.0).collect::<Vec<_>>()}))
}
/// Character panel menu › Fractional Widths: set `on` (without it, the opposite of the first
/// object's setting) on every selected text object and every frame of their threads, in one undo
/// step. On is the default.
fn fractional_widths(s: &mut Session, p: &Value) -> Result<Value> {
let ids = super::threads::with_threads(&s.doc()?.doc, &text_targets(s, p, "Fractional Widths")?);
let first = ids.first().and_then(|id| match s.doc().ok()?.doc.node(*id).map(|n| &n.kind) {
Some(NodeKind::Text(t)) => Some(t.fractional_widths),
_ => None,
});
let on = p.get("on").and_then(Value::as_bool).unwrap_or(!first.unwrap_or(true));
s.edit("Fractional Widths", |d, _| {
for id in &ids {
let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else {
return Err(EngineError::NoNode(*id));
};
t.fractional_widths = on;
refresh_bounds(t);
}
Ok(())
})?;
Ok(json!({"on": on, "ids": ids.iter().map(|id| id.0).collect::<Vec<_>>()}))
}
/// Recompute the layout caches (bounds and baselines) after a text edit. Every engine text edit ends here (typing
/// through `text.editRange` too), inside its `Session::edit`, so this is where Auto Size area type
/// ([`vectorcraft_doc::AreaFit::AutoHeight`]) fits its frame to the text: part of the same undo
@@ -1019,6 +1051,56 @@ mod area_tests {
assert!(!serde_json::from_value::<TextObject>(old).unwrap().optical_margin);
}
/// Character panel menu › Fractional Widths: `type.fractionalWidths` sets or toggles the flag
/// on the selected type in one undo step. New type and a document from before the flag are on,
/// and the flag is saved only when off. Off, each positive advance is a whole number of points.
#[test]
fn fractional_widths_is_set_per_object_and_saved_only_when_off() {
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": 40, "size": 13, "text": "Hi"})).unwrap()["id"].as_u64().unwrap();
s.execute("text.setStyle", &json!({"id": id, "tracking": 50})).unwrap();
let text = |s: &Session| match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => (**t).clone(),
_ => panic!("text"),
};
let advances =
|s: &Session| vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &text(s)).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>();
// New type: on, and not saved.
assert!(text(&s).fractional_widths);
assert!(serde_json::to_value(text(&s)).unwrap().get("fractionalWidths").is_none());
let fractional = advances(&s);
assert!(fractional.iter().any(|a| (a - a.round()).abs() > 1e-6), "{fractional:?}");
// Nothing but a rectangle selected: an error.
let rect = s.execute("shape.rectangle", &json!({"x": 0, "y": 200, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap();
s.execute("select.set", &json!({"ids": [rect]})).unwrap();
assert!(s.execute("type.fractionalWidths", &json!({})).is_err());
// Toggled off: whole-point advances, and the key is written.
s.execute("select.set", &json!({"ids": [id]})).unwrap();
assert_eq!(s.execute("type.fractionalWidths", &json!({})).unwrap()["on"], false);
assert!(!text(&s).fractional_widths);
let whole = advances(&s);
assert!(whole.iter().all(|a| *a >= 1.0 && (a - a.round()).abs() < 1e-9), "{whole:?}");
assert!(fractional.iter().zip(&whole).any(|(a, b)| (a - b).abs() > 0.05), "{fractional:?} {whole:?}");
assert_eq!(serde_json::to_value(text(&s)).unwrap()["fractionalWidths"], false);
// A boolean given as text is ignored, so the flag toggles back on.
assert_eq!(s.execute("type.fractionalWidths", &json!({"on": "no"})).unwrap()["on"], true);
assert!(text(&s).fractional_widths);
assert_eq!(s.execute("type.fractionalWidths", &json!({"on": false})).unwrap()["on"], false);
assert!(!text(&s).fractional_widths);
// One undo step.
s.execute("edit.undo", &json!({})).unwrap();
assert!(text(&s).fractional_widths);
// Saved and reopened as it was; a document from before it reads as on.
s.execute("type.fractionalWidths", &json!({"on": false})).unwrap();
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") };
assert!(!t.fractional_widths);
let mut old = serde_json::to_value(text(&s)).unwrap();
old.as_object_mut().unwrap().remove("fractionalWidths");
assert!(serde_json::from_value::<TextObject>(old).unwrap().fractional_widths);
}
/// Paragraph panel menu › Hyphenation…: `text.setFormat` sets each paragraph's hyphenation
/// options, rounded and clamped, in one undo step; they are saved only when not the defaults,
/// and paragraph styles carry them.
+30 -2
View File
@@ -37,6 +37,8 @@ struct Ctx<'a> {
/// a quarter turn back (see [`stands_upright`]); [`TextLayout::line_xf`] then stands the lines
/// up as columns.
vertical: bool,
/// Character panel menu › Fractional Widths. Off, positive advances are whole points.
fractional_widths: bool,
out: TextLayout,
}
@@ -71,6 +73,11 @@ impl Ctx<'_> {
}
}
}
// Fractional Widths off: whole-point advances, after tate-chu-yoko and the upright cell
// have set the widths the line breaker will use. A zero advance stays zero.
if !self.fractional_widths {
whole_character_widths(&mut v);
}
v
}
@@ -371,8 +378,18 @@ fn layout_once(db: &FontDb, t: &TextObject, opts: &LayoutOptions) -> TextLayout
} else {
opts
};
let mut cx =
Ctx { on_path: is_on_path, db, text: &text, runs, inlines, default: CharStyle::default(), opts, vertical, out: TextLayout::default() };
let mut cx = Ctx {
on_path: is_on_path,
db,
text: &text,
runs,
inlines,
default: CharStyle::default(),
opts,
vertical,
fractional_widths: t.fractional_widths,
out: TextLayout::default(),
};
// Vertical type: line space turned a quarter turn clockwise (lines become columns, each next
// one to the left). Point type's anchor is on the first column's centre line. Vertical path
// type stays on its path (each glyph turned across it in `emit`).
@@ -423,6 +440,17 @@ fn layout_once(db: &FontDb, t: &TextObject, opts: &LayoutOptions) -> TextLayout
cx.out
}
/// Fractional Widths off: each character that takes space advances by a whole number of points,
/// at least one, so small type does not collapse onto itself. A zero advance (a tate-chu-yoko
/// continuation, a zero-width mark) stays zero.
fn whole_character_widths(glyphs: &mut [SGlyph]) {
for g in glyphs {
if g.adv.is_finite() && g.adv > 0.0 {
g.adv = g.adv.round().max(1.0);
}
}
}
/// Tate-chu-yoko: a run of two or three half-width digits (`10`月, `100`年) stands upright across
/// the column as one block one em long, squeezed to the em when wider; longer numbers (`2026`)
/// stay on their side.
+29
View File
@@ -67,6 +67,35 @@ fn add_font_rejects_garbage_and_duplicates() {
assert_eq!(db().add_font(bytes), 0, "already bundled");
}
/// Character panel menu › Fractional Widths, off: each character that takes space advances by a
/// whole number of points, at least one. On (the default) keeps the font's fractional advances,
/// including tracking. A tate-chu-yoko continuation stays at zero.
/// <https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/turn-fractional-character-widths-off-or-on.html>
#[test]
fn fractional_widths_off_rounds_positive_advances_to_whole_points() {
let mut on = point("Hi", style(13.0));
on.runs[0].style.tracking = 50.0;
assert!(on.fractional_widths);
let fractional: Vec<f64> = layout(db(), &on).glyphs.iter().map(|g| g.advance).collect();
assert!(fractional.iter().any(|a| (a - a.round()).abs() > 1e-6), "{fractional:?}");
let mut off = on.clone();
off.fractional_widths = false;
let whole: Vec<f64> = layout(db(), &off).glyphs.iter().map(|g| g.advance).collect();
assert!(whole.iter().all(|a| *a >= 1.0 && (a - a.round()).abs() < 1e-9), "{whole:?}");
assert!(fractional.iter().zip(&whole).any(|(a, b)| (a - b).abs() > 0.05), "{fractional:?} {whole:?}");
// Smaller than half a point: rounding alone would be 0, which would run the type together.
let mut tiny = point("i", style(0.4));
tiny.fractional_widths = false;
let laid = layout(db(), &tiny);
assert!(laid.glyphs.iter().any(|g| (g.advance - 1.0).abs() < 1e-9), "{:?}", laid.glyphs.iter().map(|g| g.advance).collect::<Vec<_>>());
let mut tcy = point("10", style(20.0));
tcy.vertical = true;
tcy.fractional_widths = false;
let adv: Vec<f64> = layout(db(), &tcy).glyphs.iter().map(|g| g.advance).collect();
assert!(adv.iter().any(|a| *a == 0.0), "a tate-chu-yoko continuation stays at zero: {adv:?}");
assert!(adv.iter().all(|a| *a == 0.0 || (a - a.round()).abs() < 1e-9), "{adv:?}");
}
#[test]
fn glyph_count_and_advances() {
let l = layout(db(), &point("Hello", style(24.0)));
+51 -2
View File
@@ -797,8 +797,18 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
if menu_item(ui, tl!("No Break"), has, no_break) {
format(app, json!({"noBreak": !no_break}));
}
for l in ["Standard Vertical Roman Alignment", "Tate-chu-yoko", "Fractional Widths", "System Layout"] {
menu_item(ui, tl!(l), false, l == "Fractional Widths");
// Fractional Widths: the whole type object's spacing, on unless turned off. Not a character
// range, so a selection inside the text sets the object the Type tool is editing.
let fractional = fractional_widths_on(app);
if menu_item(ui, tl!("Fractional Widths"), has, fractional) {
let mut p = json!({});
if let Some((id, _, _)) = text_editing(app) {
p["id"] = json!(id.0);
}
app.run("type.fractionalWidths", p).ok();
}
for l in ["Standard Vertical Roman Alignment", "Tate-chu-yoko", "System Layout"] {
menu_item(ui, tl!(l), false, false);
}
ui.separator();
// The installed fonts are always listed; this picks up fonts installed since the app started.
@@ -813,6 +823,17 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
}
}
/// Whether Fractional Widths is on for the type the panel shows: the object being edited, else
/// the first selected text object. On when there is no type (the item is disabled then).
fn fractional_widths_on(app: &VectorcraftApp) -> bool {
if let Some((id, _, _)) = text_editing(app)
&& let Some(NodeKind::Text(t)) = app.session.active().and_then(|d| d.doc.node(id)).map(|n| &n.kind)
{
return t.fractional_widths;
}
first_selected_text(app).is_none_or(|t| t.fractional_widths)
}
/// Reset Panel: the characters the panel acts on ([`char_cmd`]) take tracking 0, Auto leading and
/// kerning, no baseline shift, 100% scales, no rotation, no underline or strikethrough, All Caps
/// and Small Caps off, the normal position (neither superscript nor subscript), Roman Baseline
@@ -1142,6 +1163,34 @@ mod tests {
}
}
/// Character panel menu › Fractional Widths is on for new type, and a click turns it off and
/// back on for the whole type object.
#[test]
fn fractional_widths_menu_item_toggles_the_type_object() {
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": "Hi", "size": 13})).unwrap()["id"].as_u64().unwrap();
let ctx = egui::Context::default();
let flag = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.fractional_widths,
_ => panic!("text"),
};
let row = |app: &mut VectorcraftApp| {
let texts = super::super::tests_appearance::frame_events(&ctx, app, vec![], menu);
texts.into_iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == "Fractional Widths")
};
assert!(flag(&app));
assert!(row(&mut app).is_some_and(|(t, _)| t.starts_with('✓')));
let at = row(&mut app).unwrap().1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert!(!flag(&app));
assert!(row(&mut app).is_some_and(|(t, _)| !t.starts_with('✓')));
let at = row(&mut app).unwrap().1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert!(flag(&app));
assert!(row(&mut app).is_some_and(|(t, _)| t.starts_with('✓')));
}
/// Character Alignment and Proportional Metrics are in the panel menu with the East Asian
/// options only (as Mojikumi Set and Top-to-Top Leading are in the Paragraph panel).
#[test]
File diff suppressed because one or more lines are too long
+4 -1
View File
@@ -1,6 +1,6 @@
# Type parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-21 · **Change:** minor (type on a path threads, mixed with area type in one thread; scores unchanged; No Break keeps marked characters on one line; scores unchanged; a click on the out port threads the text into a closed path, a new frame at the click or a dragged rectangle, #2572 in part; scores unchanged; native family labels fall back to localized legacy names when typographic names are English-only, #2208; scores unchanged; Cmd+Shift+C aligns centre off the Mac, #1940; tab leaders are drawn in the tab's gap, #1877; type in symbols, pattern tiles, graph designs and opacity masks gets its layout bounds back on opening a native file, #1738; score unchanged; Escape while typing ends the edit, #1686; Select › Same on type, #1737; `justify` when creating point type, #1748; scores unchanged; OpenType stylistic sets, alternates, figure styles, superscript and subscript as character features, #1607; a wide word in non-rectangular area type moves down, #1615; scores unchanged; Character Styles New/Redefine capture the first selected character at run boundaries and end prior typing before capture; boundary and typing-history input tests and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Linux font folders from fontconfig, #1529; commands and MCP `add_text` report area type that does not fit its frame, #1471; Tabs panel stop drags, #1458; Fit Headline in a frame one line tall, #1444; frames and paths beyond a billion points hold no text, #1451; scores unchanged; missing glyphs are looked for again after a font rescan, #1376; scores unchanged; Full-name font matching (#1258); Windows font-service QA pending; scores unchanged; Middle Eastern digit types: Character panel menu › Digits, `{digits}`; the font menu's script filter lists Thai fonts; Thai, Lao, Khmer and Myanmar lines break between words; the caret steps over whole clusters such as Thai consonants with their marks; Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966; Cmd+Shift+C aligns centre off the Mac, a double-click on area type's bottom handle turns on Auto Size, #1940; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged. tab leaders are drawn in the tab's gap, #1877; type in symbols, pattern tiles, graph designs and opacity masks gets its layout bounds back on opening a native file, #1738; score unchanged; Escape while typing ends the edit, #1686; Select › Same on type, #1737; `justify` when creating point type, #1748; scores unchanged; OpenType stylistic sets, alternates, figure styles, superscript and subscript as character features, #1607; a wide word in non-rectangular area type moves down, #1615; scores unchanged; Character Styles New/Redefine capture the first selected character at run boundaries and end prior typing before capture; boundary and typing-history input tests and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Linux font folders from fontconfig, #1529; commands and MCP `add_text` report area type that does not fit its frame, #1471; Tabs panel stop drags, #1458; Fit Headline in a frame one line tall, #1444; frames and paths beyond a billion points hold no text, #1451; scores unchanged; missing glyphs are looked for again after a font rescan, #1376; scores unchanged; Full-name font matching (#1258); Windows font-service QA pending; scores unchanged; Middle Eastern digit types: Character panel menu › Digits, `{digits}`; the font menu's script filter lists Thai fonts; Thai, Lao, Khmer and Myanmar lines break between words; the caret steps over whole clusters such as Thai consonants with their marks; Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-21 · **Change:** minor (Fractional Widths: Character panel menu turns whole-point advances on or off per type object, on by default; scores unchanged; type on a path threads, mixed with area type in one thread; scores unchanged; No Break keeps marked characters on one line; scores unchanged; a click on the out port threads the text into a closed path, a new frame at the click or a dragged rectangle, #2572 in part; scores unchanged; native family labels fall back to localized legacy names when typographic names are English-only, #2208; scores unchanged; Cmd+Shift+C aligns centre off the Mac, #1940; tab leaders are drawn in the tab's gap, #1877; type in symbols, pattern tiles, graph designs and opacity masks gets its layout bounds back on opening a native file, #1738; score unchanged; Escape while typing ends the edit, #1686; Select › Same on type, #1737; `justify` when creating point type, #1748; scores unchanged; OpenType stylistic sets, alternates, figure styles, superscript and subscript as character features, #1607; a wide word in non-rectangular area type moves down, #1615; scores unchanged; Character Styles New/Redefine capture the first selected character at run boundaries and end prior typing before capture; boundary and typing-history input tests and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Linux font folders from fontconfig, #1529; commands and MCP `add_text` report area type that does not fit its frame, #1471; Tabs panel stop drags, #1458; Fit Headline in a frame one line tall, #1444; frames and paths beyond a billion points hold no text, #1451; scores unchanged; missing glyphs are looked for again after a font rescan, #1376; scores unchanged; Full-name font matching (#1258); Windows font-service QA pending; scores unchanged; Middle Eastern digit types: Character panel menu › Digits, `{digits}`; the font menu's script filter lists Thai fonts; Thai, Lao, Khmer and Myanmar lines break between words; the caret steps over whole clusters such as Thai consonants with their marks; Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966; Cmd+Shift+C aligns centre off the Mac, a double-click on area type's bottom handle turns on Auto Size, #1940; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged. tab leaders are drawn in the tab's gap, #1877; type in symbols, pattern tiles, graph designs and opacity masks gets its layout bounds back on opening a native file, #1738; score unchanged; Escape while typing ends the edit, #1686; Select › Same on type, #1737; `justify` when creating point type, #1748; scores unchanged; OpenType stylistic sets, alternates, figure styles, superscript and subscript as character features, #1607; a wide word in non-rectangular area type moves down, #1615; scores unchanged; Character Styles New/Redefine capture the first selected character at run boundaries and end prior typing before capture; boundary and typing-history input tests and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Linux font folders from fontconfig, #1529; commands and MCP `add_text` report area type that does not fit its frame, #1471; Tabs panel stop drags, #1458; Fit Headline in a frame one line tall, #1444; frames and paths beyond a billion points hold no text, #1451; scores unchanged; missing glyphs are looked for again after a font rescan, #1376; scores unchanged; Full-name font matching (#1258); Windows font-service QA pending; scores unchanged; Middle Eastern digit types: Character panel menu › Digits, `{digits}`; the font menu's script filter lists Thai fonts; Thai, Lao, Khmer and Myanmar lines break between words; the caret steps over whole clusters such as Thai consonants with their marks; Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966) · **Target:** Adobe Illustrator 2026 (30.x)
Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in
[target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the
@@ -35,6 +35,8 @@ against Illustrator's public documentation; **estimated** unless noted.
| Fallback glyphs keep the text's weight and slant | done | #2491: a character from a fallback face is set in that family's face nearest the run's weight and slant; Bold Italic of a missing family is drawn Bold (Source Sans 3 has no Bold Italic) rather than Italic |
| Colour fonts (OpenType SVG / COLR) | partial | |
| Every-line and Single-line composers | done | tuned by eye, never compared side by side |
| Optical Margin Alignment | done | Type › Optical Margin Alignment, `type.opticalMarginAlignment`: per type object, with one setting for all the frames of a thread, in point and area type of left-to-right paragraphs, and at the brackets of type on a path whose first paragraph runs left to right; right-to-left paragraphs of point and area type, and type on a path whose first paragraph runs right to left, do not hang |
| Fractional Widths | done | Character panel menu › Fractional Widths, `type.fractionalWidths`: per type object, and every frame of its threads. On by default and for documents from before it; saved only when off. Off, each character that takes space advances by a whole number of points, at least one, so small type does not run together. System Layout (the document-wide whole-pixel setting) remains a panel stub. Evidence: `tests::fractional_widths_off_rounds_positive_advances_to_whole_points`, `typecmd::area_tests::fractional_widths_is_set_per_object_and_saved_only_when_off`, `panels::character::tests::fractional_widths_menu_item_toggles_the_type_object` |
| Optical Margin Alignment | done | Type › Optical Margin Alignment, `type.opticalMarginAlignment`: per type object, with one setting for all the frames of a thread, in point and area type of left-to-right and right-to-left paragraphs, and at the brackets of type on a path |
| Roman Hanging Punctuation | done | Paragraph panel menu › Roman Hanging Punctuation, `text.setFormat {romanHangingPunctuation}` (#2756): per paragraph, Optical Margin Alignment's punctuation and amounts without the letters; paragraph styles carry it. Evidence: `tests::roman_hanging_punctuation_hangs_punctuation_but_not_letters`, `typecmd::area_tests::roman_hanging_punctuation_is_set_per_paragraph_and_saved_only_when_on` |
| Hyphenation… and Justification… options | done | Hyphenation… (`ui.hyphenation`, `text.setFormat`) sets each paragraph's Words Longer Than, After First, Before Last, Hyphen Limit, Hyphenation Zone (Single-line Composer only) and Hyphenate Capitalized Words, and paragraph styles carry them (#2310). Justification… (`ui.justification`, `text.setFormat`, #2755) sets Word Spacing, Letter Spacing and Glyph Scaling (Minimum, Desired, Maximum), Auto Leading and Single Word Justification; Desired applies to every line, justified lines and the Every-line Composer use the ranges. Vertical type isn't glyph scaled; type on a path doesn't use them. Evidence: `crates/text/src/tests_justify.rs`, `dialogs::justification::tests` |
@@ -64,6 +66,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha
| Date | Change | Summary |
|---|---|---|
| 2026-10-21 | minor | Fractional Widths: Character panel menu turns whole-point advances on or off per type object (on by default); System Layout remains a stub; scores and hours unchanged |
| 2026-10-21 | minor | Type on a path threads, mixed with area type in one thread: its text that doesn't fit flows on into the next frame, and its red out port, just past its end bracket, loads the Selection tool with that text as area type's does; a new frame threaded from it at a point is type on a path of its shape; scores unchanged |
| 2026-10-21 | minor | No Break: marked characters stay on one line; scores unchanged |
| 2026-10-17 | minor | Roman Hanging Punctuation (#2756): per-paragraph hanging of Latin punctuation outside the margin; scores unchanged |