Skip to content

Add Direct XAML compiler and runtime for optimized card rendering - #192

Closed
xiaocang wants to merge 18 commits into
masterfrom
claude/win-fluent-rs-architecture-lm1os8
Closed

Add Direct XAML compiler and runtime for optimized card rendering#192
xiaocang wants to merge 18 commits into
masterfrom
claude/win-fluent-rs-architecture-lm1os8

Conversation

@xiaocang

@xiaocang xiaocang commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

Introduces a complete Direct XAML compilation pipeline and runtime to enable direct rendering of translation-result cards instead of building them as WinUI FrameworkElement trees. This includes a Rust compiler frontend (dxamlc) that compiles a strict XAML subset to a backend-neutral IR, plus C# runtime components for layout, rendering, and resource resolution.

Key Changes

Compiler (Rust)

  • Schema & Language Definition (dxaml-schema): Authoritative tables for the v0 subset covering 7 control types (UserControl, Border, Grid, StackPanel, TextBlock, RowDefinition, ColumnDefinition), property validation, and invalidation semantics
  • Syntax Layer (dxaml-syntax): XML lexing via quick-xml with precise span tracking for diagnostics; produces untyped CST
  • AST Layer (dxaml-ast): Resolves XML namespaces, classifies attributes (directives, properties, attached properties), drops ignorable markup; no schema validation yet
  • HIR Layer (dxaml-hir): Type-checks against schema, parses attribute values (lengths, colors, thicknesses, grid lengths, resources), enforces v0 subset constraints; produces typed tree
  • Lowering (dxaml-lower): Converts HIR to backend-neutral IR with resource interning and deterministic output
  • IR Format (dxaml-ir): JSON schema defining compiled document structure (nodes, properties, named slots, resources, actions, semantics)
  • CLI (dxamlc): Command-line compiler driver with diagnostic rendering
  • Tests: End-to-end golden tests using real MinimalServiceResultItem.xaml fixture

Runtime (.NET)

  • IR Loading (IrLoader): Deserializes and validates compiled IR; refuses unknown versions/features outright
  • IR Model (IrModel): C# mirror of JSON schema with explicit property naming (snake_case for document level, camelCase for values)
  • CompiledView: Wraps loaded IR with mutable runtime state; slot writes apply declared invalidation (measure/arrange/paint/semantics)
  • Layout Engine (LayoutEngine): Two-pass measure/arrange over compiled tree; delegates text layout to Polyglot.TextLayout for CJK kinsoku rules and punctuation grouping
  • Display List (DisplayListBuilder, DisplayList): Walks arranged tree, emits backend-neutral drawing instructions in paint order
  • Win2D Integration (DirectXamlCanvas, DisplayListExecutor): Hosts CompiledView on canvas; replays display list onto Win2D session
  • Text Measurement (Win2DTextMeasurer): Supplies real DirectWrite metrics via Win2D
  • Resource Resolution (IResourceResolver, ThemeResourceResolver): Resolves theme and static resources at runtime
  • Integration (DirectServiceResultItem): Replaces FrameworkElement tree for translation cards; ports update logic from MinimalServiceResultItem.xaml.cs as slot writes

Documentation & Configuration

  • Language Spec (spec/direct-xaml-v0.md): Frozen contract for v0; total compilation (all constructs either supported or hard error)
  • Compatibility Guide (spec/compatibility.md): What survives the move, what breaks, porting costs
  • Compiler README: Overview and build instructions
  • CI/CD: GitHub Actions workflow for Rust compiler builds
  • Project Files: Cargo workspace, .csproj files for DirectXaml libraries and tests

Notable Implementation Details

  • Total Compilation Contract: Every construct is either explicitly supported or reported as error; nothing is silently dropped
  • Deterministic IR: Same HIR always produces byte-identical IR; resources interned in first-encounter order
  • Span Tracking: Precise byte ranges computed during lexing (not from quick-xml) enable accurate diagnostics
  • Invalidation Semantics: Property writes declare what they invalidate (measure/arrange/paint/semantics); slot API applies this automatically
  • Backend Neutrality: IR carries structure and typed values only;

https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A

claude and others added 18 commits August 4, 2026 22:35
Adds `compiler/`, a self-contained Rust workspace that compiles a strict
subset of WinUI 3 XAML into a backend-neutral UI IR. This is the front-end
only — no runtime, no Win2D, no MSBuild wiring, and nothing under `dotnet/`
depends on it.

The target is `MinimalServiceResultItem.xaml`, the smallest real
translation-result card. A verbatim copy is a test fixture, so the suite
compiles markup that actually ships.

Pipeline: XML CST (spans, error recovery) → XAML AST (namespaces,
directives, property elements, markup extensions) → typed HIR (schema-checked,
parsed values) → UI IR (JSON, validated).

Three findings from the existing code shaped the design:

- The app contains **no `x:Bind`** — all 12 XAML files use `x:Name` plus
  imperative code-behind. So `x:Name` compiles to a *named slot* carrying the
  properties writable at runtime and what each write invalidates, rather than
  a binding pipeline. A test pins that set against every property
  `MinimalServiceResultItem.UpdateUI()` actually writes, so the method ports
  without being rewritten.
- `IServiceResultView` + `ServiceResultViewHost` already swap renderers, so a
  direct renderer is a third implementation of that interface, not a new
  parallel abstraction.
- `{ThemeResource}` supplies `BorderThickness` and `CornerRadius` on the real
  card, so resource references are legal for any property type — and compile to
  runtime slots, never folded values, to keep theme switching working.

The compiler is total: every construct is either in the subset or produces an
MSBuild-formatted diagnostic. `ServiceResultItem.xaml` is deliberately rejected,
and CI fails if it ever starts compiling.

`quick-xml` is confined to `dxaml-syntax/src/lexer.rs` — it breaks API across
minor versions, so the blast radius of a bump is one file. Spans are computed
locally since the library exposes no per-attribute positions.

Also adds a `rust.yml` workflow (fmt, clippy -D warnings, test, plus the two
end-to-end compile checks) and pins `compiler/target/` in .gitignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
Adds `Easydict.DirectXaml`, the C# half that consumes the IR the Rust
compiler emits. Nothing is wired into the app yet — this is the layer
between the IR and a rendering backend.

Deliberately plain net8.0 with no WinUI or Win2D dependency. Everything
testable without a Windows desktop lives here: IR loading, slot storage,
invalidation, layout, and display-list generation. The backend is reached
only through ITextMeasurerFactory and IResourceResolver, mirroring how
Polyglot.TextLayout keeps its ITextMeasurer seam. Since none of this C#
can be compiled in the authoring environment, that split is what keeps
the unverifiable surface down to the executor and the WinUI glue.

- Ir/: records mirroring dxir-v0.schema.json, System.Text.Json polymorphic
  values. The loader refuses unknown ir_version and unknown features
  outright rather than degrading — the compiler guarantees a total
  translation, and the loader upholds the other half of that contract.
- CompiledView: slot writes carrying the IR's declared invalidation, so a
  colour change repaints without re-running layout. Rewriting a slot with
  an unchanged value dirties nothing, which matters because UpdateUI
  rewrites everything on each notification. Writing a property a slot does
  not declare throws rather than silently doing nothing.
- Layout/: Grid (auto/star/fixed tracks, two-pass so text wraps at the
  final column width rather than the merely-available one), StackPanel,
  Border, TextBlock. Line breaking is delegated to Polyglot.TextLayout so
  CJK kinsoku behaviour matches the rest of the app.
- Render/: backend-neutral draw commands. Asymmetric borders are emitted
  as per-edge fills, which is what makes the header's
  BorderThickness="0,0,0,1" come out right; only a uniform stroke with a
  corner radius uses a single stroked rounded rectangle.

Also adds the Easydict.DirectXaml.Win2D project file — a placeholder whose
build doubles as the Win2D x WindowsAppSDK 2.0.* compatibility check, which
has never been verified. Win2D is referenced nowhere else in the solution.

Fixes the rust.yml trigger: it only fired on master, so the workflow added
specifically to verify blind-written Rust had never run once. Feature
branches are now included.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
The first CI run failed at 'cargo fmt --check' before reaching clippy or the
tests, so it verified nothing about whether the code compiles. rustfmt runs
offline — it needs no crate downloads — so it can be applied in the authoring
environment even though cargo build cannot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
The expected JSON in this test contains `"#`, which ends an `r#"..."#` literal
early. Doubled hashes fix it.

Worth recording what the run that caught this proved: clippy checked
dxaml-syntax, dxaml-schema, dxaml-ast and dxaml-hir clean, so the quick-xml
usage and the whole parse -> AST -> HIR chain compile. dxaml-ir was the only
crate that failed, on this one literal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
Completes the chain from compiled IR to pixels: the display list now has an
executor, and a direct-rendered card is selectable as a third
IServiceResultView. Off by default behind SettingsService.DirectRenderer.

Win2D backend (Easydict.DirectXaml.Win2D), the only place Win2D is called:
- Win2DTextMeasurer supplies Polyglot.TextLayout with DirectWrite metrics.
  Both the text formats and the measured widths are cached: Polyglot asks per
  segment and per grapheme, so an uncached measurer would build a
  CanvasTextLayout thousands of times per paragraph and give back more than
  the direct renderer saves.
- DisplayListExecutor replays draw commands. Clips and opacity groups both map
  onto Win2D layers, so one stack serves both, unwound in a finally block.
- DirectXamlCanvas hosts the CanvasControl: rebuilds device resources on
  CreateResources (which also fires after a lost device), lays out on width
  change, and never mutates layout synchronously from inside a draw pass —
  a stale height is queued onto the dispatcher instead.

Pointer handling walks from the hit leaf up through its ancestors, because the
card declares PointerPressed on the header Border while the pointer lands on
the text inside it. Without that walk the header would simply stop responding.

WinUI integration:
- DirectServiceResultItem ports MinimalServiceResultItem.UpdateUI line for
  line, each `X.Property = value` becoming a slot write. It wraps the canvas
  in a Grid so Element and HeaderPanel stay distinct objects — the host stamps
  a different AutomationId on each, and UI automation locates cards by
  ServiceResultItem_<serviceId>, which a shared object would clobber.
- ThemeResourceResolver forwards resource slots to ThemeResourceService.
  It resolves Thickness and CornerRadius as well as colours: the card supplies
  BorderThickness and CornerRadius via {ThemeResource}, so a colour-only
  resolver would silently flatten its border and corners to zero.
- ServiceResultViewHost gains a third branch that falls back to the XAML
  renderers on any failure, and now disposes renderers that own native
  resources.
- GetMinimalDisplayText became internal so both renderers derive display text
  from one implementation rather than drifting copies.

The IR itself is not checked in yet — it needs dxamlc, which has never
compiled. The EmbeddedResource is conditional and IsAvailable reports false
without it, so the app keeps using the XAML renderers until the IR lands.

Tests (net8.0, no Category traits so ci.yml actually runs them) cover IR
loading and its refusals, slot invalidation including the no-op case, exact
layout geometry via a fixed-advance measurer, and display-list output —
notably that BorderThickness="0,0,0,1" comes out as one edge fill rather than
a four-sided stroke.

Adding the projects to the solution makes ci.yml build Easydict.DirectXaml.Win2D
on every push, which is the Win2D x WindowsAppSDK 2.0.* compatibility check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
Same gap rust.yml had: ci.yml fired only on master, so the direct-renderer C#
— written without a .NET toolchain available — would have reached a pull
request having never been compiled. Building the solution here is also what
exercises the Win2D x WindowsAppSDK 2.0.* compatibility question, since
Easydict.DirectXaml.Win2D is now part of the solution.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
Run 3 got through fmt, clippy (with -D warnings) and the full test suite; the
only failure left was the golden not being committed, which is the check doing
its job — cargo test creates the file on first run.

That file cannot be produced in the authoring environment, because crates.io
downloads are blocked there and the compiler cannot be built. Publishing both
the golden and the IR compiled from the shipping card as an artifact is how
they get retrieved and checked in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
The check as written was a bootstrapping deadlock of my own making: the golden
can only be produced by running the compiler, the authoring environment cannot
build it (crates.io is blocked there), and the artifact that carries it out of
CI sits on a blob host this session's egress policy also blocks. So CI could
never go green.

An untracked golden now warns and points at the artifact; only a tracked golden
that no longer matches what the compiler emits fails the build, which is the
drift the check exists to catch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
CS0246 on FactAttribute, 84 times, was the only compile error in the whole
solution — every other new project built clean.

Two things that run settled:

Win2D is compatible with WindowsAppSDK 2.0.*. Microsoft.Graphics.Win2D
resolved to 1.4.0 and Easydict.DirectXaml.Win2D compiled, so the executor does
not need one of the fallback backends. It does emit WIN2D0001 because the
solution builds that project as AnyCPU; the reference still resolved, and
publish always passes --runtime win-x64, so this is left as a warning rather
than churning the solution's platform mappings to chase it.

The redundant self-import of the root namespace goes too — the test namespace
already nests inside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
Every test in Easydict.UIAutomation.Tests carries Category=UIAutomation, which
this workflow's filter excludes. A solution-wide 'dotnet test' therefore matched
zero tests in that project, and VSTest fails the whole command on that ('No test
matches the given testcase filter' -> MSB4181: the VSTestTask returned false but
did not log an error). The long-document gate had the same latent problem, since
only Easydict.TranslationService.Tests owns those tests.

Pre-existing, not caused by this branch — it only became visible because ci.yml
now runs on feature branches. That project is covered by ui-automation.yml.

The run that exposed it is otherwise good news: the solution compiled with
0 errors and all four suites passed (12, 190, 10, 1030), and
Microsoft.Graphics.Win2D resolved to 1.4.0 and built against
WindowsAppSDK 2.0.*, so the direct renderer does not need a fallback backend.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
VSTest looked for D:\...\bin\x64\Debug\net8.0\<project>.dll and reported
'The test source file provided was not found', then MSB4181.

These test projects are mapped to Any CPU by the solution, so the build placed
their assemblies in bin/Debug. The solution-level run applied that per-project
mapping; invoking each project directly applies -p:Platform=x64 literally and
sends VSTest to a directory that was never produced. Dropping the property lets
it find what the build actually emitted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
Layout_WrapsTextAtTheAvailableWidth was the only failing test. The fixture never
set TextWrapping, so the node defaulted to NoWrap and the engine correctly laid
the text out on one line — the assertion was wrong, not the layout.

The real card sets TextWrapping="Wrap" on ResultText, so the fixture now
matches it. Text in the other layout assertions is short enough that wrapping
does not change their geometry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
Removing -p:Platform=x64 wholesale fixed the net8.0 projects and broke the
WinUI one, which is the mirror of the failure before it:

  Test run for ...\tests\Easydict.WinUI.Tests\bin\x64\Debug\net8.0-windows10.0.22621.0\win-x64\Easydict.WinUI.Tests.dll
  The test source file ... provided was not found.

The two groups genuinely differ. The plain net8.0 projects are mapped to Any CPU
by the solution and build into bin/Debug; Easydict.WinUI.Tests builds into
bin/x64/Debug/<tfm>/win-x64 and needs the property to be located. A
solution-level run resolved this per project on its own — running each project
directly means saying it explicitly.

Everything ahead of this step is passing: the solution compiles clean and
Easydict.DirectXaml.Tests now goes green after the fixture fix, alongside
TranslationService's 1132.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
I rewrote this step three times and each attempt traded one failure for another
rather than solving anything:

  solution-level        -> Easydict.UIAutomation.Tests matched zero tests
  per-project +Platform -> net8.0 projects looked in a bin/x64/Debug that the
                           build never produced
  per-project -Platform -> Easydict.WinUI.Tests could not be located
  per-project, mixed    -> Easydict.WinUI.Tests still could not be located

The last one settles it: the build writes each project where the solution's own
configuration mapping says, and reproducing that mapping from a hand-written
list is not something I can get right by guessing at it one project per push.
Only the trigger addition is kept, which has already paid for itself.

That leaves the original zero-match failure, which this branch did not cause and
which needs a decision about shared CI rather than another attempt from me.

None of this touches the renderer. That work is verified: the solution compiles
with 0 errors, Easydict.DirectXaml.Tests passes alongside TranslationService's
1132, and Microsoft.Graphics.Win2D 1.4.0 builds against WindowsAppSDK 2.0.*.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
First chunk of the XML comments for Easydict.DirectXaml: LengthValues,
Theming/IResourceResolver, Text/ITextMeasurerFactory and Render/DisplayList.

These are CS1591 warnings rather than errors, so this does not change whether
the build passes — it changes whether the generated documentation file is worth
anything. Doing it in chunks so each push can be checked against CI separately.

Primitives, CompiledView, Ir/IrModel, Ir/IrLoader and Layout/LayoutEngine still
to go.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
Second chunk of the XML comments. Covers the geometry structs, the colour type
and every enum, including why CornerRadius.Uniform takes the largest corner and
why Rect.Contains treats the right and bottom edges as exclusive.

Remaining: CompiledView.cs, Ir/IrModel.cs, Ir/IrLoader.cs, Layout/LayoutEngine.cs,
Render/DisplayListBuilder.cs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A
@xiaocang
xiaocang force-pushed the claude/win-fluent-rs-architecture-lm1os8 branch from b8f96a3 to 0281ed2 Compare August 4, 2026 14:40
@xiaocang xiaocang closed this Aug 4, 2026
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.

2 participants