Add Direct XAML compiler and runtime for optimized card rendering - #192
Closed
xiaocang wants to merge 18 commits into
Closed
Add Direct XAML compiler and runtime for optimized card rendering#192xiaocang wants to merge 18 commits into
xiaocang wants to merge 18 commits into
Conversation
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
force-pushed
the
claude/win-fluent-rs-architecture-lm1os8
branch
from
August 4, 2026 14:40
b8f96a3 to
0281ed2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
dxaml-schema): Authoritative tables for the v0 subset covering 7 control types (UserControl, Border, Grid, StackPanel, TextBlock, RowDefinition, ColumnDefinition), property validation, and invalidation semanticsdxaml-syntax): XML lexing via quick-xml with precise span tracking for diagnostics; produces untyped CSTdxaml-ast): Resolves XML namespaces, classifies attributes (directives, properties, attached properties), drops ignorable markup; no schema validation yetdxaml-hir): Type-checks against schema, parses attribute values (lengths, colors, thicknesses, grid lengths, resources), enforces v0 subset constraints; produces typed treedxaml-lower): Converts HIR to backend-neutral IR with resource interning and deterministic outputdxaml-ir): JSON schema defining compiled document structure (nodes, properties, named slots, resources, actions, semantics)dxamlc): Command-line compiler driver with diagnostic renderingMinimalServiceResultItem.xamlfixtureRuntime (.NET)
IrLoader): Deserializes and validates compiled IR; refuses unknown versions/features outrightIrModel): C# mirror of JSON schema with explicit property naming (snake_case for document level, camelCase for values)LayoutEngine): Two-pass measure/arrange over compiled tree; delegates text layout to Polyglot.TextLayout for CJK kinsoku rules and punctuation groupingDisplayListBuilder,DisplayList): Walks arranged tree, emits backend-neutral drawing instructions in paint orderDirectXamlCanvas,DisplayListExecutor): Hosts CompiledView on canvas; replays display list onto Win2D sessionWin2DTextMeasurer): Supplies real DirectWrite metrics via Win2DIResourceResolver,ThemeResourceResolver): Resolves theme and static resources at runtimeDirectServiceResultItem): Replaces FrameworkElement tree for translation cards; ports update logic fromMinimalServiceResultItem.xaml.csas slot writesDocumentation & Configuration
spec/direct-xaml-v0.md): Frozen contract for v0; total compilation (all constructs either supported or hard error)spec/compatibility.md): What survives the move, what breaks, porting costsNotable Implementation Details
https://claude.ai/code/session_019m94Wzb5rJkrrqRXXoqB3A