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
64 changes: 26 additions & 38 deletions .claude/skills/fieldworks-avalonia-ui/references/style-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,12 @@ sit inside a `Border.fwFieldHost` that supplies the box.

## The tokens / values (the calibrated numbers)

**Font:** `12` px app-wide on the Avalonia views (down from Fluent's ~14). One value: `DialogFontSize`
in `DialogTheme.axaml`, `FwSurfaceStyles.SurfaceFontSize`, and `CompactDialogStyles.DialogFontSize` are all 12
and must stay equal.
**Font:** `11` px app-wide on the Avalonia views (down from Fluent's ~14). One source of truth:
`FwSurfaceFontSize` in `Src/Common/FwAvaloniaTheme/Tokens/FwColorTokens.axaml`.
`FwSurfaceStyles.SurfaceFontSize`, `CompactDialogStyles.DialogFontSize`, and
`DialogTheme.axaml`'s `{StaticResource FwSurfaceFontSize}` all resolve that one token
directly (not three independently-maintained copies) -- see
`Src/Common/FwAvalonia/FwThemeResources.cs`.

**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
Expand All @@ -47,42 +50,27 @@ from Fluent's ~32px).
**Paddings:** `TextBox 4,2` · `ComboBox 6,1` · `Button 8,2` · `TabItem 8,3` · `ListBoxItem 4,1`.

**Checkboxes (the ONE global, deterministic rule):** checkboxes are **font-proportional** and **never add row
height**. `FwAvaloniaDensity.CheckboxBoxSize = 14` (a fixed function of the 12px surface font) is the glyph-box
height**. `FwAvaloniaDensity.CheckboxBoxSize = 14` (a fixed function of the 11px surface font) is the glyph-box
size on *every* view — dialogs (chooser, options, feature manager), the chooser's flat list + tree, and the
detail view's `FwOptionChooser` field. The size is **deterministic** (a concrete px size applied to the template,
identical regardless of content) — **not** a `RenderTransform`/`ScaleTransform` (a scale shrinks the paint but
leaves the tall layout slot, which still inflates the row — the rejected hack, now removed). The single builder
`FwCheckBoxStyle.Build()` REPLACES the Fluent 11.3 `CheckBox` template outright (the same move `FwRadioButtonStyle`
makes for radios, below) with a compact `ControlTheme`: `MinHeight=0`/`MinWidth=0`/`VerticalAlignment=Center` on
the `CheckBox`, an outer `Border#FwCheckBox_Box` pinned to `14×14`, and `Path#FwCheckBox_CheckGlyph`/
`FwCheckBox_IndeterminateGlyph` riding a `Viewbox` inside it that auto-scales to the box — so the layout
footprint, not just the paint, is the box. Net: a row with a checkbox is no taller than a text row
(`BrowseRowMinHeight = 18`). This is **global — applied in both render paths: the runtime host and the headless
test renderer**: `FwSurfaceStyles.Build()` (region/detail) calls `FwCheckBoxStyle.Build()` directly; the dialog
path gets it once via `DialogThemeBootstrap.Apply` (deliberately NOT `CompactDialogStyles`, which skips it to
avoid a double-add — see the note in `CompactDialogStyles.cs`), and `DialogTheme.axaml` mirrors the SAME `14` as
an XAML token for the headless dialog tests — the `14` there must stay equal to `CheckboxBoxSize`. The Fluent
11.3 template being replaced hardcoded the box as a 20×20 `Border` (`NormalRectangle`) inside an unnamed inner
`Grid` pinned to `Height=32` — both LOCAL values a style selector cannot override, which is why a full template
replace (not a selector tweak) was required (`Avalonia.Themes.Fluent 11.3.6`, `Controls/CheckBox.xaml`).
detail view's `FwOptionChooser` field. The size is **deterministic** (a concrete px size), not a
`RenderTransform`/`ScaleTransform` (a scale shrinks the paint but leaves the tall layout slot, which still
inflates the row — a rejected hack). Unlike Fluent 11.3 (which hardcodes the checkbox box as LOCAL template
values — a 20×20 `Border` inside a `Height=32` `Grid` — that a style selector cannot override, so FieldWorks
used to replace the whole `ControlTheme` for it), Semi's `CheckBox` template reads the box size from overridable
`DynamicResource`s, so retargeting the resources is enough: `FwSemiDensity.ApplyTo` sets `CheckBoxBoxWidth`,
`CheckBoxBoxHeight`, `CheckBoxBoxGlyphWidth`, and `CheckBoxBoxGlyphHeight` to `14` on the `Application`'s
resources — called once from `FwAvaloniaApp`'s (and `PreviewHostApp`'s) constructor, so no per-view or
per-dialog style is needed. Net: a row with a checkbox is no taller than a text row (`BrowseRowMinHeight = 18`).

**Radio buttons (the checkbox's counterpart — same global, deterministic rule):** radios are
**font-proportional** and **never add row height**, exactly like checkboxes. `FwAvaloniaDensity.RadioBoxSize`
(= `CheckboxBoxSize` = 14) is the outer-circle size on *every* view (dialogs, detail, bulk-edit bar). The
single builder `FwRadioButtonStyle.Build()` REPLACES the Fluent 11.3 `RadioButton` template (whose ~20px ellipse
on a tall ~32px slot are LOCAL values a style selector cannot override — same precedence trap as the checkbox)
with a compact `ControlTheme`: an outer `Ellipse#FwRadio_Box` pinned to `14×14` + an inner filled
`Ellipse#FwRadio_Dot` (~45% of the box) revealed on `:checked`, the label after a `CheckboxLabelGap` (6px)
`StackPanel.Spacing`, `MinHeight=0`/`MinWidth=0`, `VerticalAlignment=Center`. Concrete brushes (white fill, gray
`#7A7A7A` stroke, blue `#005FB8` accent stroke + dot when checked, gray when disabled) — NOT Fluent
`DynamicResource`s (hard rule 1). **Global in both render paths**, wired in the SAME two places as the checkbox:
`FwSurfaceStyles.Build()` (region/browse/bulk-bar) and `DialogThemeBootstrap.Apply` (dialogs — runtime host AND
headless tests). It is NOT in `DialogTheme.axaml` (the template replace must be a C# `ControlTheme`) and NOT in
`CompactDialogStyles` (the bootstrap already covers both dialog paths). The dedicated headless no-inflation test
for this (`RadioButton_OnStyledSurface_IsFontProportional_AndDoesNotExceedTheTextRowHeight`, asserting the ring is
exactly `RadioBoxSize`, the control is ≤ `BrowseRowMinHeight`, and the dot opacity goes 0 → 1 on `:checked`) lived
in `LexicalBrowseDensityTests.cs`, deleted along with the rest of the browse table (commit `bd7d3a5e5`); no test
currently covers this invariant for radios — add one before a new view ships them.
(= `CheckboxBoxSize` = 14) is the outer-circle size on *every* view (dialogs, detail, bulk-edit bar). Same
Semi-resource mechanism as the checkbox: `FwSemiDensity.ApplyTo` sets `RadioButtonIconRadius` (the outer ring)
to `14` and `RadioButtonGlyphRadius` (the inner checked dot) to `14 * 0.45` — deliberately NOT equal to
`IconRadius`, or a checked radio would render as a solid disc, since Semi's own default ratio is ~0.375 —
on the `Application`'s resources, from the same single call site as the checkbox (`FwSemiDensity.cs`). No
dedicated headless no-inflation test currently covers this invariant for radios; add one before a new view
leans on it.

**Group separation:** adjacent logical control GROUPS (e.g. a radio group followed by a checkbox group)
get a little visual distance so they read as distinct rather than butting together:
Expand Down Expand Up @@ -159,9 +147,9 @@ values already in `DialogTheme.axaml`.
`AvaloniaDialogHost.ShowModal` additionally calls `CompactDialogStyles.Apply` — a belt-and-suspenders C#
duplicate of the same values (both idempotent; keep the two numerically identical).
- **Region / browse** — `FwSurfaceStyles.Apply(this)` in the `DataTree`
ctor adds the **font-only** baseline (TextBlock/TextBox → 12px). The flat-with-separators (region)
structure comes from `FwAvaloniaDensity` literals, which are concrete and already render
headlessly; `FwSurfaceStyles` exists only to drop the Fluent default font those literals don't touch.
ctor adds the **font-only** baseline (TextBlock/TextBox → 11px). The flat-with-separators (region)
structure comes from `FwAvaloniaDensity`'s token-resolved values, which are concrete and already
render headlessly; `FwSurfaceStyles` exists only to drop the Fluent default font those values don't touch.

## Changing the density

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,30 @@ not the content-overlap defect.
no second copy of the logic). `FwAvaloniaTests` (which owns `DialogSnapshot`) links `DialogLayoutAssert.cs`;
`FwAvaloniaDialogsTests` (which owns `DialogLayoutAssert`) links `DialogSnapshot.cs` — symmetric, so both
test projects get both the PNG harness and the geometry tripwire from a single copy of each.
- Snapshots are ephemeral. Don't assert on pixels/bytes beyond "non-empty"; the PNG is for human/agent
eyes, the geometry tripwire is the deterministic gate.
- Most snapshots stay ephemeral: don't assert on pixels/bytes beyond "non-empty", the PNG is for
human/agent eyes, and the geometry tripwire is the deterministic gate. A small curated subset is
committed instead — see the next section.

## Committed baseline screenshots

`Output/Snapshots/` is gitignored, so every capture above vanishes at the end of the run — no reviewer,
human or AI, can ever check a past "I looked at this and it's fine" claim against a specific PNG. To keep
that possible for the surfaces that matter most, one representative screenshot per dialog is committed to
`Docs/migration/baseline-screenshots/`, tracked in git.

- **Small and curated, not exhaustive.** One screenshot per dialog — whichever captured stage best answers
"does this dialog look right" (usually its normal populated state, not an empty or error stage) — not
every interaction stage of every test. Everything else stays ephemeral in `Output/Snapshots/` as
described above.
- **Reuse the existing capture, don't invent a new one.** Pick from the stage names the dialog's own test
suite already captures (e.g. `Options-01-initial.png`); do not add a capture point solely to produce a
baseline image.
- **Refresh by copying, not by hand-editing.** After running the dialog tests, copy the chosen file(s) from
`Output/Snapshots/` over their committed counterpart, e.g.:
```powershell
Copy-Item Output/Snapshots/Options-01-initial.png Docs/migration/baseline-screenshots/ -Force
```
- **A baseline diff gets the same review scrutiny as a code change.** When a PR changes a committed PNG's
bytes, that is a real, reviewable claim that the dialog's look has changed on purpose — a reviewer must
actually open the image and judge it (the same six questions from the review step above), never
rubber-stamp it as "just an image diff."
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ migration burden.
| ListBox (17) / CheckedListBox (11) | `ListBox`; multi-select with per-node checkboxes | `Src/Common/FwAvaloniaDialogs/ChooserDialogView.axaml` (flat + multi-select modes) |
| TreeView (4) + chooser dialogs | virtualizing `TreeView` + `TreeDataTemplate` | `ChooserDialogView.axaml` / `ChooserDialogViewModel.cs` (hierarchy, expand/collapse, filter-swaps-to-flat) |
| TabControl (6) | `TabControl`, two-way `SelectedTabIndex` | `Src/Common/FwAvaloniaDialogs/LexOptionsDlgView.axaml` |
| GroupBox (37) | headered composite control | `Src/Common/FwAvaloniaDialogs/MSAGroupBox.cs` |
| GroupBox (37), plain visual grouping (a titled border around otherwise-independent controls, no shared logic of its own) | `Border.fwGroupBox` + `TextBlock.fwGroupHeader` (`DialogTheme.axaml`) | `Src/Common/FwAvaloniaDialogs/LexOptionsDlgView.axaml` (General/Updates tabs) — the default for a plain GroupBox; do NOT reach for a bespoke composite control unless the box also owns real adaptive logic (see next row) |
| GroupBox (37), adaptive composite sub-editor (the box's own logic decides which of several related widgets are visible, e.g. driven by a type/kind field) | LCModel-free composite control | `Src/Common/FwAvaloniaDialogs/MSAGroupBox.cs` (grammatical-info editor: widget visibility driven by MsaType). **Known gap:** predates `fwGroupBox` and sets its own `BorderBrush`/`BorderThickness` in C# rather than `Classes="fwGroupBox"` -- a future touch of this file should switch it over rather than copying its current hand-set chrome. |
| TableLayoutPanel (33) / FlowLayoutPanel (20) / Panel (40) | `Grid` / `StackPanel` / `WrapPanel` — translate layout *semantics*, not widget-for-widget | any converted dialog view; spacing rules in dialog-conversion.md §2a-bis |
| ToolTip (12) | `ToolTip.Tip` attached property | converted dialog views |
| ContextMenuStrip built in code (22 files) | `MenuFlyout` populated from data | `Src/Common/FwAvalonia/Detail/DetailMenuFlyout.cs` |
Expand Down
21 changes: 20 additions & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,30 @@ jobs:
Build\Agent\Test-BuildCommentHygieneComment.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

# Same dual-engine rationale as the comment-hygiene suite above: the
# token-hygiene gate runs under whichever engine invoked build.ps1.
- name: Token hygiene fixture tests (PowerShell 7)
id: token-hygiene-tests-pwsh
shell: pwsh
run: |
Build/Agent/TokenHygiene.Tests.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

- name: Token hygiene fixture tests (Windows PowerShell 5.1)
id: token-hygiene-tests-winps
shell: powershell
run: |
Build\Agent\TokenHygiene.Tests.ps1
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

# -TokenHygiene fails CI on any hardcoded color/spacing literal in the Avalonia
# surface (full-tree, no grandfathering -- unlike -CommentHygiene, which stays
# advisory-only for humans here and only blocks agents locally).
- name: Build with tests
id: build
shell: powershell
run: |
.\build.ps1 -Configuration Debug -BuildTests
.\build.ps1 -Configuration Debug -BuildTests -TokenHygiene
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

# Native tests run under OpenCppCoverage inside the test step below; without the tool they
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ Output/RenderBenchmarks/
Output/RenderBenchmarks/**
Output_i686/
Output_x86_64/
# GenerateTokenKeys (Build/Src/FwBuildTasks) regenerates this at every build.
Src/Common/FwAvalonia/GeneratedTokenKeys.g.cs
__pycache__/
.venv/
venv/
Expand Down
12 changes: 10 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@ Minimal, high-signal guidance for coding agents in this repository.
## Non-negotiable defaults

- Platform is Windows/x64.
- Build with `.\build.ps1 -CommentHygiene`.
- Test with `.\test.ps1 -CommentHygiene`.
- Build with `.\build.ps1 -CommentHygiene -TokenHygiene`.
- Test with `.\test.ps1 -CommentHygiene -TokenHygiene`.
- `-CommentHygiene` is required of agents and not of humans: it fails the run on
any comment-hygiene violation in the lines your branch adds, so you fix your
own comments before they reach review. Do not drop the flag to get a build
through.
- `-TokenHygiene` is required of agents and not of humans locally, and also
fails CI outright (unlike comment-hygiene, which stays advisory-only in
CI): it fails the run on any hardcoded color or spacing/sizing literal
anywhere in the Avalonia surface (Src/Common/FwAvalonia,
FwAvaloniaDialogs, FwAvaloniaTheme, FwAvaloniaPreviewHost,
Src/LexText/LexTextControls/Avalonia, Src/xWorks/Avalonia) -- not
diff-scoped like comment-hygiene, the whole scoped tree must be clean on
every run. Do not drop the flag to get a build through.
- Do not bypass repository scripts for normal build/test work.
- Commit messages must pass `gitlint` (CI: `.github/workflows/CommitMessage.yml`):
title <=72 characters, body lines <=80 characters, blank line between
Expand Down
Loading
Loading