From 63eacbd94a40e7443f43df776a119a9853487413 Mon Sep 17 00:00:00 2001 From: Taha-Mahmoodi <85902429+Taha-Mahmoodi@users.noreply.github.com> Date: Thu, 12 Nov 2026 16:04:52 +0430 Subject: [PATCH] Fix glyph browser reset, variation and insertion behavior --- crates/text/src/fontdb/alternates.rs | 1 - crates/text/src/test_fonts.rs | 70 +++++++++++++++++++++ crates/ui-egui/src/panels/glyphs.rs | 91 +++++++++++++++++++++++++++- docs/target-app-parity.md | 3 +- docs/type-parity.md | 3 +- 5 files changed, 163 insertions(+), 5 deletions(-) diff --git a/crates/text/src/fontdb/alternates.rs b/crates/text/src/fontdb/alternates.rs index c39067f0b6..4a374755ef 100644 --- a/crates/text/src/fontdb/alternates.rs +++ b/crates/text/src/fontdb/alternates.rs @@ -74,7 +74,6 @@ impl FontFace { pub fn is_unlisted_alternate(&self, c: char, gid: u32) -> bool { self.unencoded_glyphs().binary_search(&gid).is_ok() && self.feature_reaching(gid).is_none() - && self.glyph_origin(gid).is_none() && self.name_owner(gid).is_none_or(|owner| owner == c) } diff --git a/crates/text/src/test_fonts.rs b/crates/text/src/test_fonts.rs index 601a67e064..e9bb2917ab 100644 --- a/crates/text/src/test_fonts.rs +++ b/crates/text/src/test_fonts.rs @@ -128,6 +128,41 @@ pub fn variable_font() -> Option> { Some(builder.build()) } +/// The family of [`unlisted_variable_font`]. +pub const UNLISTED_VARIABLE_FAMILY: &str = "Unlisted Varitest Sans"; + +/// A variable face with no GSUB: `a.a` is an unencoded alternate named for `a`, but the Glyphs +/// panel has no listed alternates. The weight axis comes from [`variable_font`]. +pub fn unlisted_variable_font() -> Option> { + use write_fonts::tables::cmap::Cmap; + + let bytes = variable_font()?; + let font = skrifa::FontRef::new(&bytes).ok()?; + let mappings = [' ', 'a', 'l'] + .into_iter() + .map(|c| Some((c, write_fonts::types::GlyphId::new(font.charmap().map(c)?.to_u32())))) + .collect::>>()?; + let cmap = Cmap::from_mappings(mappings).ok()?; + let name = name_table(&[ + (1, UNLISTED_VARIABLE_FAMILY), + (2, "Regular"), + (4, "Unlisted Varitest Sans Regular"), + (5, "Version 1.000"), + (6, "UnlistedVaritestSans-Regular"), + (256, "Weight"), + ])?; + let mut builder = FontBuilder::new(); + builder.add_raw(Tag::new(b"name"), name); + builder.add_table(&cmap).ok()?; + for r in font.table_directory.table_records() { + let tag = Tag::new(&r.tag().to_be_bytes()); + if tag != Tag::new(b"GSUB") && !builder.contains(tag) { + builder.add_raw(tag, font.table_data(r.tag())?.as_bytes()); + } + } + Some(builder.build()) +} + /// The family of [`two_axis_font`]. pub const TWO_AXIS_FAMILY: &str = "Biaxtest Sans"; @@ -920,6 +955,41 @@ pub fn joining_alternates_font() -> Option> { Some(builder.build()) } +/// The family of [`aliased_joining_font`]. +pub const ALIASED_JOINING_FAMILY: &str = "Alias Jointest Sans"; + +/// [`joining_alternates_font`] with Latin `B` and the Arabic initial beh presentation form +/// U+FE91 both mapped to beh's initial glyph. Only the presentation form should insert beh. +pub fn aliased_joining_font() -> Option> { + use write_fonts::tables::cmap::Cmap; + + let bytes = joining_alternates_font()?; + let font = skrifa::FontRef::new(&bytes).ok()?; + let source = skrifa::FontRef::new(SOURCE_SANS).ok()?; + let initial = write_fonts::types::GlyphId::new(source.charmap().map('B')?.to_u32()); + let mut mappings = + font.charmap().mappings().filter_map(|(c, g)| Some((char::from_u32(c)?, write_fonts::types::GlyphId::new(g.to_u32())))).collect::>(); + mappings.extend([('B', initial), ('\u{FE91}', initial)]); + let cmap = Cmap::from_mappings(mappings).ok()?; + let name = name_table(&[ + (1, ALIASED_JOINING_FAMILY), + (2, "Regular"), + (4, "Alias Jointest Sans Regular"), + (5, "Version 1.000"), + (6, "AliasJointestSans-Regular"), + ])?; + let mut builder = FontBuilder::new(); + builder.add_raw(Tag::new(b"name"), name); + builder.add_table(&cmap).ok()?; + for r in font.table_directory.table_records() { + let tag = Tag::new(&r.tag().to_be_bytes()); + if !builder.contains(tag) { + builder.add_raw(tag, font.table_data(r.tag())?.as_bytes()); + } + } + Some(builder.build()) +} + /// The family of [`ligature_caret_font`]. pub const LIGATURE_CARET_FAMILY: &str = "Carettest Sans"; diff --git a/crates/ui-egui/src/panels/glyphs.rs b/crates/ui-egui/src/panels/glyphs.rs index ba1f1c0f7d..3ac6ac5800 100644 --- a/crates/ui-egui/src/panels/glyphs.rs +++ b/crates/ui-egui/src/panels/glyphs.rs @@ -241,7 +241,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { font_row(app, ui, &family, &style, w); return; } - let Some(face) = db.face(&family, &style) else { + // The same family and style can have a different face id at the selection's version or axes. + // Use that instance for both the previews and Fit's applicability check. + let face = selected_target(app) + .filter(|t| t.style.font_family.eq_ignore_ascii_case(&family) && t.style.font_style.eq_ignore_ascii_case(&style)) + .and_then(|t| db.style_face(t.style)) + .or_else(|| db.face(&family, &style)); + let Some(face) = face else { ui.label(egui::RichText::new(tl!("No fonts are available.")).color(t.text_dim)); return; }; @@ -447,7 +453,7 @@ fn alternates(app: &mut VectorcraftApp, ui: &mut Ui, w: f32) { let (features, has_choice) = (target.style.features.clone(), target.style.glyph_alternate.is_some()); let found = alternates_found(app, ui, id, byte, face.id()); let font = format!("{} {}", face.family, face.style); - let Some(found) = found.filter(|f| !f.alternates.is_empty()) else { + let Some(found) = found.filter(|f| has_choice || !f.alternates.is_empty()) else { note(ui, tl!("No alternates for the selected character.")); ui.label(egui::RichText::new(font).size(11.0).color(t.text_dim)); return; @@ -679,6 +685,9 @@ fn glyph_label(face: &vectorcraft_text::FontFace, gid: u32) -> String { /// them; the letter takes the form its place in the word asks for, so the word stays joined. /// `None` when `c` is not such a form in this font. fn base_letter(face: &vectorcraft_text::FontFace, c: char, gid: u32) -> Option { + if !matches!(c, '\u{FB50}'..='\u{FDFF}' | '\u{FE70}'..='\u{FEFF}') { + return None; + } let origin = face.glyph_origin(gid)?; origin.position()?; (origin.ch != c).then_some(origin.ch) @@ -1567,4 +1576,82 @@ mod tests { assert_eq!(glyph_ids(&app, &id), typed, "shaped as the word typed with beh"); assert!(app.ui.status.starts_with("Inserted ب (U+0628)"), "{}", app.ui.status); } + + #[test] + fn an_unlisted_choice_without_gsub_can_be_reset_from_the_default_cell() { + use vectorcraft_text::test_fonts::{UNLISTED_VARIABLE_FAMILY, unlisted_variable_font}; + let db = FontDb::global(); + db.add_font(unlisted_variable_font().expect("the test font builds")); + let face = db.face(UNLISTED_VARIABLE_FAMILY, "Regular").expect("the test face"); + let glyph = gid_named(&face, "a.a"); + let (mut app, id) = editing("a"); + app.run("text.setRangeStyle", json!({"id": id, "font": UNLISTED_VARIABLE_FAMILY})).unwrap(); + select(&mut app, 0, 1); + let plain = glyph_ids(&app, &id); + app.run("text.setGlyphAlternate", json!({"id": id, "start": 0, "gid": glyph})).unwrap(); + assert_eq!(glyph_ids(&app, &id), [glyph]); + let ctx = egui::Context::default(); + choose(&ctx, &mut app, "Alternates for Current Selection"); + let cells = squares(&shapes(&ctx, &mut app, vec![])); + assert_eq!(cells.len(), 1, "the default remains available without any GSUB alternates"); + let undo = app.session.active().unwrap().history.undo.len(); + double_click(&ctx, &mut app, cells[0].center()); + assert_eq!(glyph_ids(&app, &id), plain); + assert_eq!(app.session.active().unwrap().history.undo.len(), undo + 1); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(glyph_ids(&app, &id), [glyph]); + } + + #[test] + fn entire_font_applies_an_unlisted_glyph_at_the_selections_variable_axes() { + use vectorcraft_text::test_fonts::{UNLISTED_VARIABLE_FAMILY, unlisted_variable_font}; + let db = FontDb::global(); + db.add_font(unlisted_variable_font().expect("the test font builds")); + let base = db.face(UNLISTED_VARIABLE_FAMILY, "Regular").expect("the test face"); + let glyph = gid_named(&base, "a.a"); + let (mut app, id) = editing("a"); + app.run("text.setRangeStyle", json!({"id": id, "font": UNLISTED_VARIABLE_FAMILY, "fontAxes": {"wght": 650}})).unwrap(); + select(&mut app, 0, 1); + let target = selected_target(&app).expect("the selected character"); + let face = db.style_face(target.style).expect("the variable instance"); + assert_ne!(face.id(), base.id(), "a non-default setting has its own face id"); + let ctx = egui::Context::default(); + let chars = face.chars().into_iter().filter(|(c, _)| !c.is_control() && !c.is_whitespace()).count(); + let at = chars + face.unencoded_glyphs().iter().position(|g| *g == glyph).expect("the unencoded glyph"); + let cells = inks(&tall_shapes(&ctx, &mut app)); + assert_eq!(cells[at].1, Tokens::get(&ctx).text_strong, "the instance's alternate is enabled"); + double_click_in(&ctx, &mut app, cells[at].0.center(), TALL); + assert_eq!(glyph_ids(&app, &id), [glyph]); + assert_eq!(selected_target(&app).unwrap().style.font_axes["wght"], 650.0); + app.run("edit.undo", json!({})).unwrap(); + assert_ne!(glyph_ids(&app, &id), [glyph]); + } + + #[test] + fn encoded_characters_normalize_only_arabic_presentation_forms() { + use vectorcraft_text::test_fonts::{ALIASED_JOINING_FAMILY, aliased_joining_font}; + let db = FontDb::global(); + db.add_font(aliased_joining_font().expect("the test font builds")); + let face = db.face(ALIASED_JOINING_FAMILY, "Regular").expect("the test face"); + let chars = face.chars().into_iter().filter(|(c, _)| !c.is_control() && !c.is_whitespace()).collect::>(); + for (c, expected) in [('B', "B"), ('\u{FE91}', "\u{0628}")] { + let (mut app, id) = editing("a"); + app.run("text.setRangeStyle", json!({"id": id, "font": ALIASED_JOINING_FAMILY})).unwrap(); + select(&mut app, 0, 1); + let at = chars.iter().position(|(ch, _)| *ch == c).expect("an encoded character"); + let origin = face.glyph_origin(chars[at].1).expect("the shared positional glyph"); + assert_eq!(origin.ch, '\u{0628}'); + assert!(origin.position().is_some()); + let ctx = egui::Context::default(); + let cells = inks(&tall_shapes(&ctx, &mut app)); + double_click_in(&ctx, &mut app, cells[at].0.center(), TALL); + let state = app.session.active().unwrap(); + let NodeKind::Text(text) = &state.doc.node(NodeId(id.as_u64().unwrap())).unwrap().kind else { panic!("text") }; + assert_eq!(text.plain_text(), expected, "the encoded {c} cell inserts the intended character"); + app.run("edit.undo", json!({})).unwrap(); + let state = app.session.active().unwrap(); + let NodeKind::Text(text) = &state.doc.node(NodeId(id.as_u64().unwrap())).unwrap().kind else { panic!("text") }; + assert_eq!(text.plain_text(), "a"); + } + } } diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f5120b6e4c..6a943b83ce 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -2,7 +2,7 @@ > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-12 · **Change:** minor (Type tool: kerning at a caret at the start of a line moves the character after it, #5111; scores and estimates unchanged; previously minor: Rulers: the zero point dragged out of the rulers' corner snaps as a drawn point does; previously minor: Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: Envelopes: with Distort Appearance, type's strokes paint along the bent glyph outlines at their own weight, as users' `.ai` files draw them, which changes how VectorCraft's own envelopes of stroked type draw (#6644; scores and estimates unchanged); previously minor: G5: Energy Beams brush library; bundled content ~44% → ~46%; previously minor: G5: Skill Icons symbol library; bundled content ~42% → ~44%; previously minor: `.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art (#6570; scores and estimates unchanged); previously minor: ruler double-click adds a guide at the pointer, #5239; G5: Status Effects graphic style library; bundled content ~40% → ~42%; MCP symbol libraries list UI Frames and Map Markers; previously minor: G5: Inventory Icons built-in symbol library; bundled content ~38% → ~40%; previously minor: `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke (#3179; scores and estimates unchanged); previously minor: `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid where VectorCraft's warp doesn't draw as their drawn art (#3179; scores and estimates unchanged); G5: Game Trails brush library; bundled content ~36% → ~38%; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page (#3179; scores and estimates unchanged); G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) > -> Glyphs update (#5514): Alternates for Current Selection applies a variant's OpenType feature to that letter alone (`salt=2`) so an Arabic word stays joined; Entire Font lists unencoded glyphs. A positional variant no supported feature draws can be applied as a glyph choice (#6084). Scores and estimates unchanged. +> Glyphs update (#5514): Alternates for Current Selection applies a variant's OpenType feature to that letter alone (`salt=2`) so an Arabic word stays joined; Entire Font lists unencoded glyphs. A positional variant no supported feature draws can be applied as a glyph choice (#6084). Entire Font uses the selection's font version and variable axes, and the default cell remains available after an unlisted choice even without GSUB alternates. Only Arabic presentation-form characters insert their joining base letter. Scores and estimates unchanged. > > Live Paint Bucket update (2026-11-12): one drag fills the faces crossed in the same group (#6373). > @@ -2070,6 +2070,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | Date | Change | Summary | |---|---|---| +| 2026-11-12 | minor | Glyphs review (#6084): Entire Font uses the selection's version and variable axes for previews and alternate applicability. The default cell can reset an unlisted glyph choice when GSUB lists no alternates. Encoded Latin and other ordinary characters insert unchanged even if their glyph also has Arabic positional provenance; Arabic presentation forms retain base-letter insertion. Remove a redundant charmap scan from unlisted-glyph checks. Scores and estimates unchanged. | | 2026-11-09 | minor | Glyphs review (#6084): positional alternates with no supported feature are accepted as glyph choices in both panel views; Entire Font's unlisted-glyph and name-ownership behavior is retained. OpenType style options keep only a valid last on-value when enabling a set. Contradictory old glyph-choice evidence corrected; scores and estimates unchanged. | | 2026-11-07 | minor | Type core: the Glyphs panel's Alternates for Current Selection applies the OpenType feature that draws a variant to the selected letter alone, so an Arabic word stays joined (`salt=2`; the style commands take `tag=N`), and Show › Entire Font lists the glyphs no character maps (#5514; [Replace characters with alternate glyphs](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/replace-characters-with-alternate-glyphs.html)); evidence added; scores unchanged. | | 2026-11-12 | minor | Type: kerning at the Type tool's caret at the start of a line (the text's, a paragraph's, or after a forced line break) sets the space before the character after it (`kerningBefore`), which moves it and the rest of its line; it did nothing at the text's start and kerned the invisible line break at a paragraph's (#5111). Scores and estimates unchanged. | diff --git a/docs/type-parity.md b/docs/type-parity.md index f4750fca3f..7917c0cf3d 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -2,7 +2,7 @@ > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (kerning at the Type tool's caret at the start of a line sets the space before the character after it, #5111; previously minor: type's strokes in an envelope paint along its bent glyph outlines at their own weight, Distort Appearance on or off, as users' `.ai` files draw them, #6644; previously minor: a centred or right-aligned line of tracked type is aligned without the tracking after its last character, as the page of users' `.ai` files aligns it, #6610; previously minor: tracking and manual kerning in horizontally scaled type are thousandths of the em as the Horizontal Scale draws it, as the page of users' `.ai` files spaces such type; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: Direct Selection shows and drags the direction handles of type on a path's path, #3565; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; paint-only Arabic harakat keep joining with GDEF-mark ownership and Insert Kashidas base-letter paint; scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: right-aligned area type sets the text after a line's last tab against the right edge; a soft return in justified type no longer keeps its line from being justified; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen) · **Target:** Adobe Illustrator 2026 (30.x) -Glyphs review (#6084): a positional variant no supported feature draws is accepted as a glyph choice. Scores and estimates unchanged. +Glyphs review (#6084): a positional variant no supported feature draws is accepted as a glyph choice. Entire Font respects the selection's variable axes and font version; the default cell can reset an unlisted choice without GSUB alternates. Encoded characters insert unchanged except Arabic presentation forms with a joining base letter. Scores and estimates unchanged. 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 @@ -84,6 +84,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha | Date | Change | Summary | |---|---|---| +| 2026-11-12 | minor | Glyphs review (#6084): Entire Font resolves the selected font's version and variable axes; the default cell remains available to reset an unlisted choice with no listed alternates. Character insertion normalizes only Arabic presentation forms, preserving ordinary characters whose glyphs share Arabic substitution provenance. Add deterministic panel apply/reset/Undo regressions without OS-font dependencies. Scores and estimates unchanged. | | 2026-11-09 | minor | Glyphs review (#6084): the engine accepts a positional alternate as a glyph choice when no supported feature draws it, not only an isolated character's alternates; both panel views exercise apply and Undo. OpenType style options preserve only a valid last on-value when enabling a stylistic set. Scores and estimates unchanged. | | 2026-11-07 | minor | Glyphs panel (#5514): Alternates for Current Selection lists the alternates of the selected character's glyph in its word and a double-click applies the OpenType feature that draws one to that letter alone (`salt=2`; `features` and `featureChanges` take `tag=N`), so an Arabic word stays joined, and Entire Font lists the unencoded glyphs, applies the ones that are the selected letter's alternates, or that no feature reaches and the font's names do not give to another letter, and dims, with the reason, those that are not. Runs that differ only in their features shape as one piece of text. Scores and estimates unchanged. | | 2026-11-11 | minor | Kerning at the start of a line (#5111): at the Type tool's caret before the first character of the text, of a paragraph or after a forced line break, the Kerning field, Alt+arrows and Reset Tracking and Kerning set the space before the character after the caret (`CharStyle::kerning_before`, `kerningBefore` in `text.setRangeStyle`), which moves it and the rest of its line, in horizontal, right-to-left and vertical type and in SVG export. They did nothing at the text's start and kerned the invisible line break at a paragraph's. Scores and estimates unchanged. |