Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ toward the Fluent theme per the migration hub's `architecture-patterns.md` §12.

## Why a global token system (and why it renders headlessly)

`FwAvaloniaApp` adds `new FluentTheme()`, which ships ~14px fonts and ~32px control floors app-wide.
`FwAvaloniaApp` adds Semi core plus Ursa Semi (no Fluent), which ships ~14px fonts and ~32px control floors app-wide.
Density/borders must be layered ON TOP of Fluent. The reliable mechanism — proven by a prior agent —
is **per-control-tree `.Styles`**, added to each view's own `Styles` collection by its view ctor.
App-level `Application.Styles` did **not** apply in the headless test app, so that path is rejected.
Expand Down Expand Up @@ -42,6 +42,12 @@ sit inside a `Border.fwFieldHost` that supplies the box.
directly (not three independently-maintained copies) -- see
`Src/Common/FwAvalonia/FwThemeResources.cs`.

**Font family:** the bundled Andika 7.000, token `FwUiFontFamily` (`fonts:FieldWorks#Andika`) in the same
file. `FwUiFont.ApplyTo` (called from both `Application.Initialize` overrides) registers the collection
and points Semi's `DefaultFontFamily`, `ContentControlThemeFontFamily` and `SemiFontFamilyRegular` at it, so
chrome inherits it. The stack continues with fallback fonts for UI languages whose script Andika lacks (`FwUiFont.FallbackFamiliesFor`). Data text keeps its writing-system font. Measure UI text with `FwUiFont.UiFontFamily`,
never `FontFamily.Default`. See ADR 0004.

**Control height:** `TextBox`/`ComboBox`/`Button` `MinHeight = 24` (WinForms runs ~21-23px; 24 is the
pointer-accessibility floor — see the "Why `DialogMinControlHeight` is 24, not 22" note below — still far
from Fluent's ~32px).
Expand Down
5 changes: 3 additions & 2 deletions Build/Src/FwBuildTasks/GenerateTokenKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ public class GenerateTokenKeys : Task
{
private static readonly XNamespace XamlNs = "http://schemas.microsoft.com/winfx/2006/xaml";

/// <summary>Every top-level ThemeDictionary entry becomes a key constant.</summary>
/// <summary>Every top-level ThemeDictionary entry becomes a key constant. FontFamily
/// entries are included, so the interface font family has a constant too.</summary>
private static readonly HashSet<string> TokenElementNames = new HashSet<string>(
new[] { "SolidColorBrush", "Color", "Double", "Thickness", "CornerRadius", "StaticResource" });
new[] { "SolidColorBrush", "Color", "Double", "Thickness", "CornerRadius", "FontFamily", "StaticResource" });

/// <summary>.axaml files whose EVERY x:Key becomes a generated constant.</summary>
[Required]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# The Avalonia interface font is controlled by the theme; data text keeps its writing-system font

LT-22854. FieldWorks controls its interface font instead of leaving it to the platform
default. One theme token, `FwUiFontFamily` in `FwColorTokens.axaml`, names the font that every
Avalonia surface (dialogs, detail view chrome, menus, flyouts) draws its interface text in. The
font is bundled in `FwAvaloniaTheme.dll` and registered at startup as the private font
collection `fonts:FieldWorks`, with per-language fallbacks, so every machine and every test run
looks the same, and changing the font later is a one-token change plus the font files. The
proposed default is Andika 7.000 (`fonts:FieldWorks#Andika`; see "Alternatives for the
default"). `FwUiFont.ApplyTo`, called from both
`Application.Initialize` overrides, registers the collection and points Semi's
`DefaultFontFamily`, `ContentControlThemeFontFamily` and `SemiFontFamilyRegular` resources at
the token, so text inherits it from the root. Data text keeps the font of its writing system.
The family is a stack: the default font, then the fallback fonts of the UI language the application
started in (see "Fallback fonts" below).

**Why a bundled collection rather than an installed font**: the installer ships Andika 6.101
as a system font and a developer machine may carry any build or none. A bundled 7.000 gives
the same glyphs and kerning in the installed product, the Preview Host and headless tests,
which would otherwise vary by machine. A writing system that names "Andika" still resolves to
the system copy through the plain family name, because a private collection key never captures
it, so data rendering is unchanged.

**Why Semi's resource keys**: Semi sets the family on its own root and popup themes from those
three resources, and the WinForms-hosted root uses the same theme. Setting only
`FontManagerOptions.DefaultFamilyName` would leave Semi's stack (landing on Segoe UI) in
charge, and the headless test builders bypass that option anyway.

**Five faces of the default**: Regular, Italic, Bold, BoldItalic and SemiBold, the weights and styles the
interface uses, about 4.1 MB. Avalonia matches the nearest weight for anything else.

**Sizes stay at 11 px for surfaces and 13 px for labels.** Andika's x-height (0.508 em) matches
Segoe UI's (0.500 em), so legibility holds. Its line height is 1.61 em against 1.33 em, so
a line grows about 3 px at 11 px (14.6 to 17.7) and rows and dialogs get taller. This is
visible in the refreshed baseline screenshots. Line-height tokens to recover WinForms row
height are a separate decision.

**Fallback fonts**: Andika lacks the scripts of five of the sixteen UI languages shipped in
`Localizations/l10ns` (az es fa fr hi hu id ko ms pt ru rw te tr vi zh-CN). Persian (fa), Hindi
(hi), Korean (ko), Telugu (te) and Simplified Chinese (zh-CN) therefore get a named fallback
after Andika in the family stack, chosen from the UI culture by `FwUiFont.FallbackFamiliesFor`.
Each list names a Windows system font first (Segoe UI, Nirmala UI, Malgun Gothic, Microsoft
YaHei UI), so the fallback is present on every Windows machine, and then the SIL or Noto family
that an SIL per-language font table names (Scheherazade New, Annapurna SIL, Noto Sans CJK KR, Noto
Sans Telugu, Noto Sans CJK SC), which is used where installed and skipped otherwise. Avalonia
tries the families in the stack per character, so Latin text stays in Andika. The table holds
only these five languages and each has a test; a language added to Crowdin later needs a new
entry and test. No fallback fonts are bundled. The other eleven languages are drawn in Andika
alone, which a test confirms with a sample string each.

**What this deliberately does not do**:

- It does not set `FontManagerOptions.DefaultFamilyName`, which would change the fallback for
data text whose writing-system font is missing.
- It does not bump the installer's Andika 6.101 to 7.000 (tracked under LT-19288).
- It does not bundle any fallback font; the fallbacks are Windows system fonts and installed
SIL or Noto families.
- It does not follow a UI language change after start-up: the stack is chosen once, as the
UI language itself is only read at start-up.
- It does not add FontFamily setters to dialog or surface styles; inheritance carries the font.

**Revisit when**: the installer ships Andika 7.000 as a system font (the bundled copy could
then be dropped), line-height tokens are introduced, or testers report unreadable text on a
machine that has none of the named fallback families.

**Alternatives for the default**: Andika is the proposed default, not a requirement of this
decision. Swapping it means replacing the font files in `Src/Common/FwAvaloniaTheme/Assets/Fonts`
(with their licence and notice) and changing the value of `FwUiFontFamily`; no code changes. Its
line height (1.61 em) is taller than Segoe UI's (1.33 em), so a font with tighter vertical
metrics would keep rows closer to the WinForms height.

- The platform UI font (Segoe UI on Windows) is the closest to today's look (x-height 0.500,
line height 1.33). Its licence does not allow bundling, so other platforms and test machines
would differ. The token can still name it if the team prefers today's look.
- Sophia Nubian 1.0 (SIL, OFL, 2009; https://software.sil.org/sophianubian/) is a Univers-like
sans whose vertical metrics are closest to Segoe UI (x-height 0.500, line height 1.385). It has
no Cyrillic, so it cannot draw the Russian UI. It also has no SemiBold and little Greek and
IPA, and its last release was in 2009.
- Padauk 6.000 (SIL, OFL, 2025; https://software.sil.org/padauk/) is a Myanmar family with a
Latin sans. It has a SemiBold weight. Its x-height is 0.435, so it would need a size about 15%
larger, and it has no Cyrillic, IPA or Italic.

Andika 7.000 is the only SIL Latin sans with full Latin, Cyrillic, Greek and IPA and all the
weights FieldWorks uses. Its cost is the taller line (1.61 em against 1.33 em). Metrics are per
em.
1 change: 1 addition & 0 deletions Docs/architecture/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ editing the next time the code changes, it belongs in one of the other three.
| [0001](0001-alias-semi-semantic-tokens.md) | Alias Semi's semantic tokens instead of inventing a FieldWorks palette |
| [0002](0002-whole-tree-token-hygiene-check.md) | token-hygiene.ps1 enforces full conformance across the Avalonia surface, with no grandfathering |
| [0003](0003-geometric-assertions-not-pixel-diff.md) | Assert geometry, not pixel diffs, for layout parity |
| [0004](0004-interface-font-controlled-by-the-theme.md) | Control the Avalonia interface font through a theme token; data text keeps its writing-system font |
5 changes: 3 additions & 2 deletions Docs/migration/adjust-the-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,9 @@ host. Treat difficulty here as a design signal, not a tooling problem.
- **The host window is WinForms, not Avalonia.** Title bar, icon, and close
button belong to the modal host form. You cannot style them from the
`.axaml`, and neither loop above shows them.
- **Converted dialogs render on the Fluent background, not WinForms grey**, and
use the Fluent font family. That is stack-wide, not something your dialog did.
- **Converted dialogs render on the Semi theme background, not WinForms grey**,
and use the FieldWorks interface font (the bundled Andika, `FwUiFontFamily`,
ADR 0004). That is stack-wide, not something your dialog did.
Changing it means editing `DialogTheme.axaml`'s `fwDialogRoot` style, which
restyles **every** converted dialog -- treat it as its own change, with dark
mode considered.
Expand Down
Binary file modified Docs/migration/baseline-screenshots/AddNewSense-02-populated.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Docs/migration/baseline-screenshots/Chooser-01-initial.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Docs/migration/baseline-screenshots/CreateFeature-01-empty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Docs/migration/baseline-screenshots/EntryGo-04-row-selected.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Docs/migration/baseline-screenshots/InsertEntry-02-populated.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Docs/migration/baseline-screenshots/MessageBox-07-warning-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Docs/migration/baseline-screenshots/MsaCreator-01-initial.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Docs/migration/baseline-screenshots/Options-01-initial.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion Src/Common/FwAvalonia/Detail/FwFieldControls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public static double ComputeWsAbbrevColumnWidth(DetailModel model)
if (model == null)
return FwAvaloniaDensity.WsAbbrevWidth;

var typeface = new Typeface(FontFamily.Default);
var typeface = new Typeface(FwUiFont.UiFontFamily);
double widest = 0;
foreach (var abbrev in model.Fields.Where(f => f.IsMultiStringRow)
.SelectMany(f => f.Values).Select(v => v.WsAbbrev)
Expand Down
2 changes: 2 additions & 0 deletions Src/Common/FwAvalonia/FwAvaloniaApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public override void Initialize()
// Shared dialog spacing/border tokens, so the Dialog* keys this assembly
// publishes as generated constants are actually registered here too.
MergeTokens("avares://FwAvaloniaTheme/Tokens/DialogTokens.axaml");
// After the token merge: the interface font family is itself a token.
FwUiFont.ApplyTo(this);
}

private void MergeTokens(string avaresUri)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ private sealed class DisposableControl : Control, IDisposable
public void Dispose() => DisposeCount++;
}

[Test]
[AvaloniaTest]
public void DisposeDialogResources_DisposesDisposableBodyAndViewModel()
{
var body = new DisposableControl();
Expand All @@ -58,7 +58,7 @@ public void DisposeDialogResources_DisposesDisposableBodyAndViewModel()
Assert.That(vm.DisposeCount, Is.EqualTo(1), "the VM is disposed if IDisposable (it owns its resources)");
}

[Test]
[AvaloniaTest]
public void DisposeDialogResources_NonDisposableViewModel_DoesNotThrow()
{
Assert.DoesNotThrow(() => AvaloniaDialogHost.DisposeDialogResources(new Control(), new PlainVm()));
Expand Down
12 changes: 12 additions & 0 deletions Src/Common/FwAvalonia/FwAvaloniaTests/DetailPreviewTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ public void Preview_WsText_UsesConfiguredFonts()
Assert.That(Find<TextBox>(window, "LexemeFormEditor.en").FontFamily.Name, Is.EqualTo("Times New Roman"));
}

[AvaloniaTest]
public void Preview_WsText_KeepsWsFonts_WhileLabelsUseTheInterfaceFont()
{
var window = ShowPreview();
Assert.That(Find<TextBox>(window, "LexemeFormEditor.seh").FontFamily.Name, Is.EqualTo("Charis SIL"));
Assert.That(Find<TextBox>(window, "LexemeFormEditor.en").FontFamily.Name, Is.EqualTo("Times New Roman"));

var label = window.GetVisualDescendants().OfType<TextBlock>()
.First(t => !string.IsNullOrEmpty(t.Text) && t.FindAncestorOfType<TextBox>() == null);
Assert.That(label.FontFamily, Is.EqualTo(SIL.FieldWorks.Common.FwAvalonia.FwUiFont.UiFontFamily));
}

[AvaloniaTest]
public void Preview_MorphTypeChooser_UsesSharedOptionPicker_AndReturnsFocus()
{
Expand Down
Loading
Loading