Skip to content

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

Draft
jasonleenaylor wants to merge 1 commit into
mainfrom
LT-22638
Draft

LT-22638: Show OpenType feature names and multi-valued variants#1029
jasonleenaylor wants to merge 1 commit 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 (ss01ss05, ValueOff/ValueOn).
  • DocsDocs/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

- 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>
@github-actions

Copy link
Copy Markdown

NUnit Tests

    1 files  ± 0      1 suites  ±0   10m 51s ⏱️ + 2m 38s
4 345 tests +32  4 272 ✅ +32  73 💤 ±0  0 ❌ ±0 
4 354 runs  +32  4 281 ✅ +32  73 💤 ±0  0 ❌ ±0 

Results for commit b8a2dd1. ± Comparison against base commit ac89f08.

This pull request removes 2 and adds 34 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 ‑ DiscretionaryLigatures_AreUserVisible
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ LegacyBlocklistTags_AreAllHidden
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ UnknownTag_IsNeitherKnownNorHidden
SIL.FieldWorks.Common.FwUtils.OpenTypeFeatureCatalogTests ‑ VisibleFeatures_HaveEnglishNames
SIL.FieldWorks.Common.FwUtils.OpenTypeFontFeatureInfoReaderRobustnessTests ‑ FeatureParamsOffsetPastTableEnd_YieldsTagWithoutLabel
…

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.02148% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 33.09%. Comparing base (321ba87) to head (b8a2dd1).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...rc/Common/FwUtils/OpenTypeFontFeatureInfoReader.cs 81.46% 16 Missing and 17 partials ⚠️
...FwCoreDlgs/FwCoreDlgControls/FontFeaturesButton.cs 85.36% 3 Missing and 9 partials ⚠️
Src/Common/FwUtils/OpenTypeFeatureCatalog.cs 99.37% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1029      +/-   ##
==========================================
+ Coverage   33.02%   33.09%   +0.07%     
==========================================
  Files        1202     1204       +2     
  Lines      278232   278559     +327     
  Branches    37169    37226      +57     
==========================================
+ Hits        91880    92196     +316     
+ Misses     158502   158493       -9     
- Partials    27850    27870      +20     
Files with missing lines Coverage Δ
Src/Common/FwUtils/OpenTypeFeatureCatalog.cs 99.37% <99.37%> (ø)
...FwCoreDlgs/FwCoreDlgControls/FontFeaturesButton.cs 62.85% <85.36%> (+2.17%) ⬆️
...rc/Common/FwUtils/OpenTypeFontFeatureInfoReader.cs 81.46% <81.46%> (ø)

... and 3 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.

Reviewed this along two axes: does it follow the repo's documented standards (AGENTS.md, .github/instructions/*), and does it faithfully implement the OpenSpec bundle the PR ships. Nice piece of work overall — the reader is carefully bounds-checked, the Graphite path is genuinely untouched, and the dlig visible / mark,mkmk,init,ccmp hidden behaviour, name-only labels and malformed-table degradation all verify correctly against the spec.

Four things I'd want addressed before merge, plus some smaller cleanups. Inline comments have the detail; summarising here:

Correctness

  1. ReadCharacterVariantParams drops undecodable option names instead of holding their position, which renumbers the options after them. That's a silent wrong-glyph, not a graceful fallback — see inline.

Spec drift

  1. The default-on set in OpenTypeFeatureCatalog is liga, calt, kern, clig, plus chws, cpsp, halt, rand, size. The proposal, design.md decision 5, tasks.md 1.2 and the Docs/opentype-font-features.md this PR ships all name only the first four, so the shipped doc is now wrong. rand and halt in particular aren't applied by default by any shaper, so they'd render pre-checked and misrepresent what the user is actually seeing — the exact defect this change sets out to fix.
  2. size shouldn't be a user toggle at all; it's optical-size metadata and size=1 is meaningless. It fails the spec's own hidden test ("otherwise not user-configurable") — the same rationale used to hide aalt.

Standards

  1. The ~120 English feature names in OpenTypeFeatureCatalog are hardcoded in C# and reach the menu via GetFeatureLabelGetEnglishName. AGENTS.md says "Keep localization in .resx; do not hardcode translatable UI strings", and FwUtilsStrings.resx already exists in the same assembly. Aggravating that the PR removes translatable resx entries (_aalt, _ccmp, _ss01_ss05) at the same time. The invariant "Off"/"On" returns are fine — callers pattern-match those, and the comment explaining why is correct.

Test gaps

The two normative persistence scenarios aren't covered: an unset default-on feature must stay absent from the stored string, and unchecking it must write liga=0. The mechanism reads correct (nDefault seeds display, GenerateFeatureString skips Int32.MaxValue, ItemClickHandler writes 0) but nothing asserts it, and tasks.md 2.4 omits them too. A test pinning the default-on set would also have caught #2.

Smaller things, take or leave

  • OnClick regenerates the whole feature string from visible ids only, so a stored aalt=1 (visible before this change, hidden after) is silently erased on the next click. Pre-existing machinery, newly reachable.
  • ssXX/cvXX tag-shape parsing now exists twice — IsCharacterVariantTag/IsStylisticSetTag in the reader, and TryGetSetNumber(tag, 's', 's', …) in the button. Worth one shared helper.
  • OpenTypeFeatureDefaultState.Unspecified vs .Off is never distinguished (only IsDefaultOn is consumed), and EnglishName on a Hidden(…) entry can never be displayed.
  • private static readonly Encoding MacRoman breaks the s_ prefix its siblings use (s_layoutTables, s_entries); .editorconfig has no naming rules so nothing will flag it.
  • CharisSilTestFontVersion 6.200 in FwUtilsTests.csproj duplicates CharisSilVersion in Build/PackageRestore.targets, kept in sync by a comment only — a mismatch makes the reader tests silently Assert.Ignore rather than fail. There's precedent in TestViews.vcxproj, so low priority.
  • A few doc comments could shrink: the /// Initializes a new instance of the <see cref="…"/> class. boilerplate on both new ctors adds nothing over the signature.

{
var option = LookupName(names, firstParamNameId + i);
if (!string.IsNullOrEmpty(option))
resolved.Add(option);

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.

This drops undecodable option names instead of holding their position, which silently renumbers everything after them.

If a cvNN declares three options and option 2's name record is undecodable, resolved ends up [option1, option3], so option 3 is offered to the user as value 2 and persists cvNN=2. The shaper then applies alternate 2 — the wrong glyph, with no indication anything went wrong.

The spec is explicit that the index/value mapping is positional: "value i selects the i-th named option" and "selecting the second option SHALL persist cv25=2". Suggest keeping the slot with a numbered placeholder ("Option 2", or the existing Character Variant N style) so index ↔ value stays 1:1 and the degradation is visible rather than silent.

The robustness tests cover a missing label, but not a missing option name, which is why this path isn't caught.

["psts"] = Hidden("Post Base Substitutions"),
["pwid"] = Visible("Proportional Width"),
["qwid"] = Visible("Quarter Widths", OpenTypeFeatureDefaultState.Off),
["rand"] = Visible("Randomize", OpenTypeFeatureDefaultState.On),

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.

rand as default-on looks wrong, and it's outside what the change authorised.

The proposal, design.md decision 5 and tasks.md 1.2 all specify the default-on set as liga, calt, kern, clig. This catalog also marks chws (L82), cpsp (L86), halt (L105), rand (here) and size (L166) as On, and the Docs/opentype-font-features.md shipped in this same PR still documents the four-tag set — so the doc is now inaccurate.

rand and halt in particular aren't applied by default by any shaper, so they'd show pre-checked while doing nothing, and unchecking one writes rand=0 for a feature the user never had on. That's the same class of "UI misrepresents rendering" problem LT-22638 exists to fix.

Either trim the set back to the documented four, or amend the spec and doc deliberately — but not silently. A test asserting the exact default-on set would keep the catalog, the doc and the spec from drifting again.

["salt"] = Visible("Stylistic Alternatives", OpenTypeFeatureDefaultState.Off),
["sinf"] = Visible("Scientific Inferiors", OpenTypeFeatureDefaultState.Off),
["smcp"] = Visible("Lowercase to Small Capitals", OpenTypeFeatureDefaultState.Off),
["size"] = Visible("Optical size", OpenTypeFeatureDefaultState.On),

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.

size should be Hidden, not a visible default-on toggle. It's optical-size metadata (the size featureParams carry a design-size range for the font), not something a shaper switches on or off — size=1 doesn't mean anything downstream.

The spec's hidden criterion is a feature "required for script shaping or … otherwise not user-configurable", which is exactly the rationale used to move aalt to hidden in this change. size fits the second half of that.

["abvf"] = Hidden("Above Base Forms"),
["abvm"] = Hidden("Above Base Mark"),
["abvs"] = Hidden("Above Base Substitutions"),
["afrc"] = Visible("Vertical Fractions", OpenTypeFeatureDefaultState.Off),

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.

These ~120 English names are user-visible — they reach the Font Options menu through GetFeatureLabelOpenTypeFeatureCatalog.GetEnglishName(tag) — so they belong in .resx.

AGENTS.md: "Keep localization in .resx; do not hardcode translatable UI strings", and .github/instructions/fieldworks-ui-review.instructions.md says the same. FwUtilsStrings.resx already exists in this assembly, so there's a home for them. The class doc acknowledges the tension ("a resx entry may override it for the UI") but the fallback still ships English strings from C#.

Worth noting the PR moves in the opposite direction at the same time: kstidOpenTypeFeature_aalt, _ccmp and _ss01_ss05 come out of FwCoreDlgControls.resx, and design.md decision 3 said "the 21 existing (possibly translated) entries are preserved". Any of those with existing translations are lost.

(To be clear — the invariant "Off"/"On" returns in FontFeaturesButton are correct as-is, and the comment explaining that they're classification-only rather than display strings is a genuine improvement.)

return candidateScore > existingScore;
}

private static byte[] SafeGet(Func<string, byte[]> tableSource, string tag)

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.

SafeGet, DecodeName and CreateMacRoman all swallow everything with a bare catch, and this revision drops the file's only diagnostics (Trace.WriteLineIf(s_openTypeTraceSwitch, …)).

Degrading gracefully rather than throwing into the UI is right and the spec requires it — but with no trace output, a font that parses to nothing is indistinguishable from a font with no features, and there's nothing to go on when a user reports missing features. managed.instructions.md asks that we not swallow exceptions without logging context.

s_openTypeTraceSwitch is still there in FontFeaturesButton (L46); routing these through it (or an equivalent in FwUtils) would cost nothing at runtime and make field diagnosis possible.

return cachedTags.ToArray();
OpenTypeFontFeatureInfo[] cached;
if (s_featureCache.TryGetValue(cacheKey, out cached))
return cached;

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.

Minor: return cached; here and return discovered; at L1149 hand the caller the cached array directly, where the previous implementation returned .ToArray() copies. Nothing in the diff mutates the result today, but the cache is now shared mutable state across every caller of GetFeatureInfos. Returning a copy, or having the cache hold something genuinely immutable, keeps that from biting later.

@johnml1135

Copy link
Copy Markdown
Contributor

Follow-up 1/3: what the default-on set should be, and why

Expanding on the inline comment about chws/cpsp/halt/rand/size, because I think there's a citable answer here rather than a judgement call.

The OpenType registry is not the authority. It says things like "this feature should be on by default" in prose, inconsistently, and with no normative force. The actual authority is the shaping engine, and the engines have converged:

Source Default-on, user-controllable
CSS Fonts L4, "features enabled by default" (normative) liga, clig, calt, rlig, locl, ccmp, mark, mkmk, kern (via font-kerning: auto), vert
HarfBuzz hb-ot-shape.cc — Chrome, Firefox, LibreOffice, Android, Qt required: rvrn, ccmp, locl, mark, mkmk, rlig, abvm, blwm; horizontal on-by-default: calt, clig, curs, dist, kern, liga, rclt
DirectWrite default typographic set (Latin) ccmp, liga, clig, calt, rlig, locl, mark, mkmk, kern, rclt, dist

They agree almost exactly. Suggest we adopt the CSS Fonts L4 list as the cited authority — it's normative, published, stable, and it's what HarfBuzz implements, so it's the same answer with a URL a future reviewer can check. Better than maintaining our own opinion.

The nice part: intersect that list with the tags this PR already hides as shaping-required (ccmp, mark, mkmk, locl, rlig, and arguably curs/dist/rclt), and what remains as a visible checked toggle is exactly:

liga, clig, calt, kern

Which is precisely what the proposal, design.md decision 5, tasks.md 1.2 and Docs/opentype-font-features.md already specify. So the spec's original four are right and industry-standard — the catalog is what drifted, not the spec. The five extras match no engine's default set: rand is randomized glyph selection (nobody enables that by default), halt/chws/cpsp are CJK metrics features that are opt-in everywhere, and size isn't a toggle at all. My guess is they arrived with the Paratext catalog seed, where the flag may have meant "notable/recommended" rather than "applied by default."

Recommendation: trim to the four, add a one-line comment in OpenTypeFeatureCatalog citing CSS Fonts L4 as the source so future edits have a standard to argue against, and add a test asserting the exact default-on set (which would have caught this drift, and keeps the catalog, the doc and the spec from separating again).

One caveat worth recording in that comment. We shape through Uniscribe's OpenType path — ScriptShapeOpenType/ScriptPlaceOpenType in Src/views/lib/UniscribeSegment.cpp — not HarfBuzz. Uniscribe's older per-script engines apply calt inconsistently, so "checked by default" is a claim about rendering we can't fully honour for every script. Not a blocker, and not a reason to deviate from the standard list. Just worth not letting the catalog become a place where we assert defaults nobody has verified against the renderer we actually use.

@johnml1135

Copy link
Copy Markdown
Contributor

Follow-up 2/3: how Graphite feature names are localized (they aren't, quite), and what that means here

Context for the inline .resx comment, because the Graphite comparison turns out to be more interesting than "Graphite does it properly, copy that."

There are three tiers of feature name, and only one of them is Crowdin's:

  1. OpenType font-supplied names — the name-table strings this PR introduces (ss01 → "Single-story a and g"). Name records carry language IDs, so the font can ship translations.
  2. Graphite names — come from the font too: m_featureEngine.GetFeatureLabel(featureId, languageId, out label) (FontFeaturesButton.cs:922-925). Graphite fonts also store feature names per language ID.
  3. FieldWorks-authored names — the registered-feature catalog and the numbered fallbacks. These are ours, and Crowdin's.

Tiers 1 and 2 are both English-pinned today. The language we ask for is hardcoded:

public const int kUiCodePage = 0x00000409;  // for now the UI language is US English

FontFeaturesButton.cs:34. So Graphite feature names are capable of localization and have been for twenty years, but FLEx has always asked for US English and thrown away whatever else the font offers. Crowdin never sees them. The same will be true of the OpenType names this PR adds — and the non-goals correctly acknowledge that, noting the ranking function is isolated so a UI-language parameter can be added later without rework.

Tier 3 localizes for free, but only from .resx. From crowdin.json:

{ "source": "Src/**/*.resx",
  "ignore": ["Src/**/*Tests/**/*", "Src/**/HelpTopicPaths.resx"] }

Both Src/Common/FwUtils/FwUtilsStrings.resx and FwCoreDlgControls.resx are already in scope. Moving the catalog names there costs zero pipeline work — they show up in Crowdin on the next sync. Left in C#, they can never be translated. That's the concrete reason behind the inline comment, and it's cheap.

It also makes the _ss01_ss05 deletions worse than I first flagged: those may have translations already sitting in Crowdin that the removal orphans. Worth checking before merge.

Recommendation — two parts:

Now, in this PR: put the catalog names in a .resx so we're hooked into Crowdin from day one rather than retrofitting later. Given ~120 fairly jargon-heavy strings, mark them low priority so translators aren't flooded (there's machinery for this in Localizations/SetUpPriorityLabels/).

Separate Jira issue: wire kUiCodePage to the actual UI language. This is a small change with outsized payoff — it localizes both Graphite and OpenType font-supplied names in one stroke, for every font that ships translations. That // for now comment has been load-bearing for a very long time.

And a nice bonus @johnml1135 raised: the Graphite fonts we already ship are themselves a translation corpus. SIL Graphite fonts carry localized feature names for languages we care about, written by people who know both the typography and the language. Those are almost certainly better than what a general translator would produce for terms like "Contextual Alternates." Harvesting them as seed translations for the tier-3 catalog strings is worth scoping into that same ticket.

@johnml1135

Copy link
Copy Markdown
Contributor

Follow-up 3/3: you can't see what's selected without opening the menu

Not a defect in this PR — it's pre-existing — but this change makes it materially more noticeable, so it's worth raising while the code is warm.

Current state. The control is a plain Button with static text "Font Features" plus a menu-arrow icon, sitting in the group box labelled "Font Options" (DefaultFontsControl.resx:277-279) next to the "Enable Graphite" checkbox. Selection state exists only as checkmarks and radio dots inside a ContextMenu you have to open to see. Nothing on the button, beside it, or in a tooltip tells you whether anything is set.

That was survivable when the menu held a handful of tags. After this PR, a font like Charis SIL 6.200 produces 40+ entries with submenus — genuinely good, and much harder to scan for "what did I turn on?" The improvement in reach makes the lack of a summary more visible, not less.

Recommendation for this PR (or immediately after): summary text beside the button. Something like "3 features set", or better, the resolved list — "Capital Eng: Alt 2; Single-story a and g" — as a label next to the button and as its tooltip. It reuses the label resolution this PR already builds, needs no new dialog or menu machinery, and it's the single biggest fix for "what is currently on." Cheap enough that it doesn't need to wait for the bigger redesign.

A close second, if it's easy: mark non-default entries in the menu (bold, or a leading bullet) so a user's own choices stand out from the defaults in a long list.

Recommendation for a new Jira issue: a proper font-feature selection UI. The proposal's non-goals already anticipate this ("no new dialog, grouping, tooltips, or sample glyphs — tier 'b' presentation upgrade, candidate follow-up ticket"), so this is just putting a number on it. Scope worth considering: a feature list or grid grouped into Stylistic Sets / Character Variants / other, with a live preview of the selected sample text.

Worth noting we already have the preview machinery — FwFontDialog has a Views-backed live preview (m_preview with WritingSystemFactory/StyleSheet/Tss, refreshed by UpdatePreview(), FwFontDialog.cs:408-439). "This is how your text would render" doesn't need inventing, just reusing.

Sources of inspiration for that ticket:

  • LibreOfficeFormat ▸ Character ▸ Font ▸ Features… (cui/source/dialogs/FontFeaturesDialog.cxx). A grid of checkboxes and dropdowns built from feature enumeration, with a live preview string underneath. The closest existing thing to what we want and the most directly transferable layout. Design inspiration only, not code — LibreOffice is MPL 2.0 / LGPLv3+ and FontFeaturesButton.cs is LGPL 2.1-or-later, so it isn't the clean in-family borrow that the Paratext reader was.
  • Wakamai Fondue (wakamaifondue.com) — best-in-class "show me everything this font can do," with live sample text per feature. The reference to look at if we ever do the sample-glyph tier.
  • Adobe InDesign / Illustrator OpenType panel — proprietary, concept only. One good idea to steal: features the font lacks are shown greyed rather than hidden, so users learn what's possible.

Also worth a look before we design ours: Paratext 9.6, since it's the source of this PR's reader and the user bases overlap heavily. Consistency across SIL apps has value on its own, and it may already have solved some of this.

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