Skip to content

LT-22638: Show OpenType feature names and multi-valued variants - #1029

Merged
jasonleenaylor merged 3 commits into
mainfrom
LT-22638
Sep 9, 2026
Merged

jasonleenaylor merged 3 commits into
mainfrom
LT-22638

Conversation

@jasonleenaylor

@jasonleenaylor jasonleenaylor commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes LT-22638. Makes OpenType font
features usable in Font Options: features show human-readable names from the
font (or a registered-feature catalog), and character variants with multiple
named alternates are selectable instead of a single On/Off toggle — parity with
the existing Graphite experience, with no change to the dropdown menu, so both
the Writing System and Styles font dialogs inherit it.

What changed

  • New OpenTypeFontFeatureInfoReader (FwUtils) — parses GSUB/GPOS feature
    lists, featureParams, and the name table via a table-source delegate (GDI
    GetFontData in the app, font-file bytes in tests). Bounds-checked; malformed
    fonts degrade to tag-only records. Adapted from Paratext's OpenTypeFeatures.Ttf.
  • New OpenTypeFeatureCatalog (FwUtils) — classifies registered features
    (hidden / default-on) and supplies English names, seeded from Paratext and
    audited against the OpenType registry (dlig visible, aalt hidden, kern
    default-on).
  • Rewrote the OpenType provider in FontFeaturesButton — character variants
    with named options become "None + option" submenus stored as cvNN=k; labels
    resolve font-supplied → resx → catalog → numbered fallback; default-on features
    initialize enabled; hidden features filtered.
  • resx — added None / numbered-set fallbacks; removed dead labels (aalt,
    ccmp) and entries orphaned by the rewrite (ss01–ss05, ValueOff/ValueOn).
  • Docs — Docs/opentype-font-features.md updated.

Storage, rendering, and CSS/Word export are untouched — the renderer-neutral
tag=value string already carried multi-values.

Testing

  • Reader verified against real Charis SIL 6.200 (cv43 "Capital Eng" + 3
    options, ss01 "Single-story a and g", GPOS mark/mkmk) and Scheherazade
    New
    (cv70 "Damma").
  • Synthetic malformed-table robustness tests.
  • Provider tests: multi-value cv, binary fallback, default-on init, label
    priority, hidden filtering, cv43=2 round-trip, resx↔catalog consistency.
  • Green: FwUtilsTests 33, FwCoreDlgControlsTests 17, FwCoreDlgsTests 19. Coverage:
    catalog/info 100%, reader 86%.
  • Manual acceptance passed (WS setup → Font tab → Charis → Font Features).

🤖 Generated with Claude Code


This change is Reviewable

@github-actions

github-actions Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

NUnit Tests

    1 files  ± 0      1 suites  ±0   12m 56s ⏱️ -11s
6 084 tests +35  5 999 ✅ +35  85 💤 ±0  0 ❌ ±0 
6 093 runs  +35  6 008 ✅ +35  85 💤 ±0  0 ❌ ±0 

Results for commit 4ae3482. ± Comparison against base commit 96b164c.

This pull request removes 2 and adds 37 tests. Note that renamed tests count towards both.
SIL.FieldWorks.FwCoreDlgControlsTests.TestFontFeaturesButton ‑ OpenTypeFontFeatureReader_CachesFeatureTagsForSameFontKey
SIL.FieldWorks.FwCoreDlgControlsTests.TestFontFeaturesButton ‑ OpenTypeFontFeatureReader_FiltersRequiredShapingFeatures
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ AccessAllAlternates_IsHidden
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ CommonlyDefaultOnFeatures_AreDefaultOn("calt")
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ CommonlyDefaultOnFeatures_AreDefaultOn("clig")
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ CommonlyDefaultOnFeatures_AreDefaultOn("kern")
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ CommonlyDefaultOnFeatures_AreDefaultOn("liga")
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ DefaultOnFeatures_AreOnlyTheDocumentedFour
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ DiscretionaryLigatures_AreUserVisible
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ LegacyBlocklistTags_AreAllHidden
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ UnknownTag_IsNeitherKnownNorHidden
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ VisibleFeatures_HaveEnglishNames
…

♻️ This comment has been updated with latest results.

@codecov-commenter

codecov-commenter commented Jul 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.80786% with 65 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.68%. Comparing base (96b164c) to head (4ae3482).

Files with missing lines Patch % Lines
...rc/Common/FwUtils/OpenTypeFontFeatureInfoReader.cs 80.92% 19 Missing and 18 partials ⚠️
...FwCoreDlgs/FwCoreDlgControls/FontFeaturesButton.cs 74.28% 13 Missing and 14 partials ⚠️
Src/Common/FwUtils/OpenTypeFeatureCatalog.cs 99.37% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1029      +/-   ##
==========================================
+ Coverage   38.62%   38.68%   +0.06%     
==========================================
  Files        1514     1516       +2     
  Lines      351047   351405     +358     
  Branches    40360    40421      +61     
==========================================
+ Hits       135583   135936     +353     
+ Misses     186264   186248      -16     
- Partials    29200    29221      +21     
Files with missing lines Coverage Δ
Src/Common/FwUtils/OpenTypeFeatureCatalog.cs 99.37% <99.37%> (ø)
...FwCoreDlgs/FwCoreDlgControls/FontFeaturesButton.cs 61.84% <74.28%> (+1.16%) ⬆️
...rc/Common/FwUtils/OpenTypeFontFeatureInfoReader.cs 80.92% <80.92%> (ø)

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@johnml1135 johnml1135 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Four merge issues; details are inline:

  1. Preserve cvNN option positions when labels cannot decode.
  2. Align default-on features with verified renderer behavior, spec, and docs; hide size.
  3. Move user-visible catalog names to .resx.
  4. Add default-on persistence tests: unset stays absent; unchecked writes liga=0.

Next: address or explicitly defer each inline finding.

Comment thread Src/Common/FwUtils/OpenTypeFontFeatureInfoReader.cs Outdated
Comment thread Src/Common/FwUtils/OpenTypeFeatureCatalog.cs Outdated
Comment thread Src/Common/FwUtils/OpenTypeFeatureCatalog.cs Outdated
Comment thread Src/Common/FwUtils/OpenTypeFeatureCatalog.cs
Comment thread Src/Common/FwUtils/OpenTypeFontFeatureInfoReader.cs
Comment thread Src/FwCoreDlgs/FwCoreDlgControls/FontFeaturesButton.cs
@johnml1135

johnml1135 commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Three follow-ups:

  1. Align visible defaults with verified Uniscribe behavior; confirm kern and document the policy.
  2. Move FieldWorks-authored names to .resx; track UI-language wiring separately. Not known: whether bundled fonts contain reusable translations.
  3. File the planned UI follow-up for a selection summary and grouped live preview. LibreOffice is a license-compatible reference, subject to normal review.

Next: decide which items belong in this PR versus Jira.

@johnml1135

johnml1135 commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Consolidated into the follow-up summary.

1 similar comment
@johnml1135

johnml1135 commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Consolidated into the follow-up summary.

@johnml1135 johnml1135 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three follow-up findings; details are inline:

  1. OpenType character variants after option 31 are unreachable.
  2. Same-tag records can expose one script's labels and values in another writing system.
  3. s_tableReader is read outside the lock guarding test-time replacement.

Next: fix or explicitly accept each limitation.

Comment thread Src/FwCoreDlgs/FwCoreDlgControls/FontFeaturesButton.cs
Comment thread Src/Common/FwUtils/OpenTypeFontFeatureInfoReader.cs
Comment thread Src/FwCoreDlgs/FwCoreDlgControls/FontFeaturesButton.cs Outdated
@johnml1135

johnml1135 commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Correction incorporated into the follow-up summary.

2 similar comments
@johnml1135

johnml1135 commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Correction incorporated into the follow-up summary.

@johnml1135

johnml1135 commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Correction incorporated into the follow-up summary.

@github-actions

github-actions Bot commented Sep 2, 2026 •

Copy link
Copy Markdown

Comment hygiene (advisory)

No comment-style violations in the lines this branch adds since origin/main.

@jasonleenaylor

Copy link
Copy Markdown
Contributor Author

Round two. All nine inline comments answered, seven with code changes and two documented as accepted limitations. Details are in the threads; this is the method and the parts that don't belong in any one of them.

Fixed: cvNN option positions, the default-on set plus a test pinning it, size hidden, 57 feature names moved to resx, parse failures traced, the feature cache no longer handing out its own array, and the table reader read under its lock with a generation guard. Deferred with reasons in the threads: the >31-option limit and script-aware deduplication.

Font survey. Several of these came down to what fonts actually do, so I measured rather than reasoned. 192 font files: Windows fonts, the per-user font store, DistFiles and fwroot/Downloads. I applied the reader's own decode rules, platforms 0, 1 and 3 with a non-empty string, so the numbers describe what our code sees and not what fontTools can read.

cvNN features declaring named options   477, across 15 fonts
option counts                          1 option x420, 2 x40, 3 x16, 4 x1
maximum named options                  4  (ScheherazadeNew cv82)
above 31, where the menu truncates     0
above 1024, where the reader drops all 0
slots the reader cannot resolve        0
fonts declaring 'size'                 0
cv/ss tags appearing more than once    176
  of those with differing metadata     0

script coverage   CharisSIL, DoulosSIL   DFLT cyrl latn
                  GentiumPlus            DFLT cyrl grek latn
                  ScheherazadeNew        arab latn

Verification. build.ps1 clean, 0 warnings and 0 errors. Full test.ps1: 4156 passed, 75 not executed, 1 failed. The failure is DataTreeOpt_VisibilitySequenceHasNoGaps, which exceeded its 70000ms timeout and then passed in 12 seconds on its own. DetailControls and its tests contain no reference to FontFeaturesButton, OpenTypeFeatureCatalog or OpenTypeFontFeatureInfoReader, so nothing here can reach it; the machine was carrying 15 worktrees at the time. The 75 not-executed are pre-existing skips with their own messages, including the shaping test discussed below.

LT-22774 covers what's deferred: confirming the default-on set against measured renderer behavior, and a test that every visible feature resolves a resx label. Fix version FW 9.3, related to LT-22638. The blocker there is that ShapeText_OpenTypeFeatureToggleChangesShapingData skips itself for want of a font with a deterministic liga delta, so the harness needs work before it can answer anything.

Docs/opentype-font-features.md now carries the design rationale for the default-on set, including why this provider isn't equivalent to the Graphite one on that point, so the next person to widen the set reads the reason first.

On your three follow-ups: defaults are aligned with the docs and pinned, with the measurement in LT-22774. Names are in resx, and I don't know whether the bundled fonts carry reusable translations, so treat that as still open. The UI follow-up for a selection summary and grouped live preview isn't filed. Say the word and I'll file it, or take it yourself if you have the shape in mind.

Nothing is resolved from my side. Items 7 and 8 are deferrals and 2 is partly one, so they're yours to close or push back on.

@jasonleenaylor
jasonleenaylor marked this pull request as ready for review September 2, 2026 17:31
@johnml1135

Copy link
Copy Markdown
Contributor

Round three. All nine threads are answered and resolved: seven fixes verified in a worktree build (build.ps1 -SkipNative clean; FwUtilsTests 402, FwCoreDlgControlsTests 55, FwCoreDlgsTests 353 pass, 5 pre-existing skips), the two deferrals accepted, the 1024 reader ceiling accepted with them, and the resx location withdrawn: the repo's rule is that strings live in the resx of the project that displays them, so FwCoreDlgControls.resx is correct.

The follow-up is filed: LT-22792 "Font Features: show the set features together and preview them live", linked Related to LT-22638 and LT-22774. It also carries the note that an Avalonia font dialog will want the same 71 names.

Four things before merge, all yours, one cleanup commit:

  1. Rebase onto main. The branch predates the comment-hygiene tooling, so build.ps1 -CommentHygiene does not exist here yet. After the rebase it does, and CI already checks the merge result: 57 advisory violations. 46 are mechanical rewraps the script does itself (read the result, it splits without reflowing). 11 need a hand: five comments over the 200-character budget (OpenTypeFontFeatureInfoReader.cs:197, FontFeaturesButton.cs:32, :1170, :1183, the 877-character one at OpenTypeFeatureCatalogTests.cs:61), two absence narrations (FontFeaturesButton.cs:34, OpenTypeFeatureCatalog.cs:43), two doc pointers (OpenTypeFeatureCatalogTests.cs:63, :71).
  2. Drop the survey numbers from everything that ships. They already disagree (192/176 in the threads, 178/173 in Docs/opentype-font-features.md and the Read remark). Keep in the doc: the label order, the multi-value rule, the hidden and default-on lists, and one short paragraph on why default-on is a policy assertion. Move the registry argument and the counts to LT-22774 and the archived research.md, which is dated and allowed to age. For the script-blind paragraph, cite the registry's "Script/language sensitivity: None" for cvNN/ssNN instead of the count. This also clears most of the 11 above.
  3. Sync the delta spec. openspec/changes/archive/2026-07-24-improve-opentype-feature-ui/specs/font-feature-settings/spec.md adds four requirements that openspec/specs/font-feature-settings/spec.md does not have. The previous OpenType change was synced; this one was archived without the sync.
  4. Move LT-22638 to In Progress so the ticket matches the PR. Resolve with Fix Version FW 9.3 after the merge.

Next: Jason rebases and pushes the cleanup commit; I re-review the diff of that commit only.

jasonleenaylor and others added 3 commits September 9, 2026 10:21
- Read the font-supplied names and named options from GSUB featureParams
  and the name table so features are comprehensible and character
  variants are selectable, as with Graphite.
- Add OpenTypeFontFeatureInfoReader adapted from Paratext's
  OpenTypeFeatures.Ttf.
- Add OpenTypeFeatureCatalog registered-feature hidden and default-on
  classification plus English names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An undecodable character-variant option name was dropped from the option
list, which renumbered every option after it. That index is the value
persisted to the writing system, so choosing one variant could apply a
different glyph. Slots now keep their position and the UI numbers any the
font did not name; a font that names none of them still falls back to a
binary toggle.

The default-on set held nine tags where the proposal, design and docs name
four. Dropped rand, chws, cpsp and halt, and hid size, which references no
lookups at all, so size=1 could never have done anything. A test now pins
the exact set instead of asserting one tag at a time, which is why five
extra tags survived.

IsDefaultOn is the one place this provider is not equivalent to the Graphite
one. GraphiteFontFeatureProvider reads defaultValue out of the font, and the
same font table drives the Graphite shaper, so the checkbox and the renderer
cannot disagree. OpenType records no per-feature default anywhere, so this
provider asserts one from a static table. Marking a tag on only sets the
initial checkbox for a feature the writing system does not mention: wrongly
on, it displays as enabled while the text is unaffected and the user's first
click writes tag=0, so the gesture meant to enable it disables it; wrongly
off, the first click writes tag=1, a no-op if the renderer applies the
feature anyway. The OpenType registry does suggest cpsp, rand, chws and halt
be on by default, but that instructs an application to apply a feature,
which marking a tag default-on prevents by leaving the string silent.
Docs/opentype-font-features.md records the reasoning; LT-22774 measures what
the renderer actually applies.

Moved the 57 remaining visible feature names into FwCoreDlgControls.resx,
taking it from 14 keys to 71, so no user-visible name is English-only in
C#. AGENTS.md requires this and crowdin.json already globs Src/**/*.resx.

Swallowed parse failures now report through the font-feature trace
switches, so a malformed font is distinguishable from one with no
features. Option counts above the menu ceiling report too rather than
truncating in silence; no font we have measured declares more than four.

The feature cache handed out its own array behind an IReadOnlyList, which
a caller could cast back and write through, so it now stores a
ReadOnlyCollection. The table reader and a generation counter are read
together under the cache lock, so a discovery that began under one reader
cannot seed the cache after the reader changed.

Documented the script-blind deduplication as an accepted limitation with
the reason: across 192 installed fonts, 176 cv/ss tags appear more than
once and every duplicate carries identical metadata.
Cite the OpenType registry's Script/language sensitivity field instead of
survey counts in the feature doc and the reader's Read remark, and record the
survey itself in the archived research notes where a dated, machine-local
measurement belongs.

Rewrite the eleven comments the hygiene check flags: two absence narrations,
two markdown pointers, and seven over the length budget.

Sync the archived delta spec into openspec/specs/font-feature-settings: the
modified discovery requirement plus four requirements the live spec never
received.

The counts disagreed with each other because they came from different scans of
one machine's font folder, and nothing in the repository can reproduce either.
@jasonleenaylor

Copy link
Copy Markdown
Contributor Author

Rebased and pushed as 4ae348262, one cleanup commit as asked. All four items are done; three notes where what I found differs from what the list expected.

1. Rebase and hygiene. The 46 mechanical rewraps are in, and the 11 are fixed by hand. Two corrections to the breakdown: the count of 11 is right, but the enumeration named nine — OpenTypeFontFeatureInfoReaderRobustnessTests.cs:118 (231 chars) and :137 (249) are also over budget and are not on the list. And one of the two absence narrations is a false positive: the rule matches \bused to\b, and OpenTypeFeatureCatalog's summary said "features used to classify feature tags", meaning employed to. I reworded it to "that classifies" rather than argue with the checker.

2. Survey numbers. Gone from everything that ships. The doc paragraph and the Read remark now cite the registry's Script/language sensitivity field, and the survey is recorded in the archived research.md — scope, date, and the fact that it is machine-local and not reproducible from the repository, which is the real reason it does not belong in a comment.

There was a third pair, which is worse than two disagreeing: OpenTypeFontFeatureInfoReaderRobustnessTests.cs carried "248 of the 275", SIL fonts only, agreeing with neither 178/173 nor 192/176. All three are now in the archive together with the option-count distribution behind the ceiling and binary-fallback decisions.

This cleared none of the 11. The survey numbers were in the doc and the Read remark; the flagged comments were different ones. The lengths came down on their own merits instead.

Worth flagging one cut: the 872-character comment on DefaultOnFeatures_AreOnlyTheDocumentedFour is now three lines. Everything else in it — the registry divergence, the Graphite-versus-OpenType asymmetry, why a wrong assertion inverts the first click — is already in Docs/opentype-font-features.md, and stated more accurately there. What survives is the only test-local part: why the test asserts the whole set rather than membership one tag at a time.

3. Delta spec. Synced. The MODIFIED discovery requirement plus all four ADDED ones, inserted after the discovery requirement rather than appended, since the live file reads thematically.

4. Ticket moved.

Verification. Comment hygiene clean and stable across two local runs, and green in CI. check-whitespace, commit messages, stray-docs, lychee and validate-refs all pass. Every changed C# line is a comment, and every doc comment in the six changed files parses as well-formed XML.

I did not compile this locally. A targeted build of FwUtils and FwCoreDlgControls is not possible in a fresh worktree — ViewsInterfaces needs native artifacts and CommonAssemblyInfo.cs is generated — so the native build is the whole cost, and I left it to CI. "Build Debug and run tests" is still running as I write this; if it fails, that is mine.

Unrelated byproduct: Worktree-CreateFromBranch.ps1 aborts under PowerShell 5.1 when git's checkout progress reaches stderr, leaving a worktree created but never colorized. That is #1131.

@jasonleenaylor
jasonleenaylor merged commit 2b2c24a into main Sep 9, 2026
10 checks passed
@jasonleenaylor
jasonleenaylor deleted the LT-22638 branch September 9, 2026 20:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants