Skip to content

[00133] Create the rusty-xaml Runtime XAML Parser and Builder Crate - #133

Merged
rorychatt merged 4 commits into
mainfrom
tendril/00133-CreateTheRustyxamlRuntimeXAMLParserAndBuilderCrate
Aug 10, 2026
Merged

[00133] Create the rusty-xaml Runtime XAML Parser and Builder Crate#133
rorychatt merged 4 commits into
mainfrom
tendril/00133-CreateTheRustyxamlRuntimeXAMLParserAndBuilderCrate

Conversation

@rorychatt

Copy link
Copy Markdown
Contributor

Fixes #116

00133 — Create the rusty-xaml Runtime XAML Parser and Builder Crate

Implements issue #116.
Branch tendril/00133-CreateTheRustyxamlRuntimeXAMLParserAndBuilderCrate,
three commits: bcf22b8, a6c080b, bd00e0d.

Changes

A new leaf crate, rusty-xaml, that turns XAML markup into rusty widget trees
at runtime — the counterpart to rusty-ivyml, which does the same job at compile
time. A document that only exists once the program is running (read from disk,
from a database, or edited while the app is up) can now become a
rusty::views::Element.

  • Vocabulary. 15 elements in their WPF spelling (StackPanel, Grid,
    WrapPanel, Border, GroupBox, ItemsControl/ListView, ListViewItem,
    TextBlock, Button, Badge, TextBox, Image, ProgressBar, Separator,
    plus Spacer), each also accepting its Rusty name as an alias. Attributes are
    PascalCase and map onto the matching builder, with the XAML-specific
    translations spelled out (HorizontalAlignment="Right".align(Align::End),
    IsEnabled="False".disabled(true)).
  • Bindings. {Binding Count} and {Binding Path=Count} resolve against a
    XamlContext; {} is the literal-brace escape. Resolution happens at parse
    time, so a tree is a snapshot — the crate docs say to call parse_with inside
    View::build, which is also the whole of the #[rusty::view] integration.
  • Events. Click="OnIncrement" names a handler the XamlContext supplies from
    Rust. The value is a name, never an expression; there is no interpreter here.
  • Errors. Nothing is dropped silently. An unknown element, an unknown
    attribute, an unparseable value, a child on a leaf, a missing handler, an
    unresolved binding — each is a XamlError naming the element, the attribute and
    the line and column it came from. Markup that renders half its styling is worse
    than markup that refuses to render.
  • Drift guard. mapping_covers_every_ivyml_element scans
    rusty-ivyml/src/codegen.rs at test time, so an element added to the
    compile-time vocabulary cannot silently go missing from the runtime one. Same
    house style as every_widget_type_is_mapped in rusty/src/shared/widget_names.rs.

rusty-xaml is a leaf crate: it depends on rusty and rusty does not
re-export it, because that would be a dependency cycle. Unlike rusty-ivyml,
which rusty can re-export precisely because a proc-macro crate does not depend
on rusty. Consumers add a second dependency line, as they already do for
rusty-server and rusty-docs.

API Changes

Additive only. No existing signature changed, and no existing test moved.

// rusty-xaml
pub fn parse(xaml: &str) -> Result<Element, XamlError>;
pub fn parse_with(xaml: &str, ctx: &XamlContext) -> Result<Element, XamlError>;
pub fn parse_file(path: impl AsRef<Path>) -> Result<Element, XamlError>;
pub fn parse_file_with(path: impl AsRef<Path>, ctx: &XamlContext) -> Result<Element, XamlError>;

pub type Handler = Arc<dyn Fn() + Send + Sync>;

pub struct XamlContext { /* Clone, Default, Debug */ }
impl XamlContext {
    pub fn new() -> Self;
    pub fn value(self, name: impl Into<String>, value: impl Into<serde_json::Value>) -> Self;
    pub fn handler(self, name: impl Into<String>, handler: impl Fn() + Send + Sync + 'static) -> Self;
    pub fn value_of(&self, name: &str) -> Option<&serde_json::Value>;
    pub fn handler_of(&self, name: &str) -> Option<Handler>;
}

pub struct Position { pub line: u32, pub column: u32 }   // Display as `line:col`

pub enum XamlError {                    // Debug + Display + Error, not PartialEq
    Xml(roxmltree::Error),
    Io { path: PathBuf, source: std::io::Error },
    NoRoot,
    UnknownElement { element, pos },
    UnknownAttribute { element, attribute, pos },
    MissingAttribute { element, attribute, pos },
    UnsupportedValue { element, attribute, value, reason, pos },
    UnsupportedMarkupExtension { element, attribute, value, pos },
    UnresolvedBinding { element, attribute, path, pos },
    UnknownHandler { element, attribute, handler, pos },
    DuplicateContent { element, attribute, pos },
    NoChildrenAllowed { element, pos },
}

Workspace: roxmltree = "0.21" added to [workspace.dependencies];
"rusty-xaml" added to workspace.members. Cargo.lock gained one entry,
roxmltree 0.21.1 — its only transitive dependency, memchr, was already locked.

Deviations from the plan

Seven, each because an assumption the plan made about rusty's API did not hold.
All are resolved in the direction the plan's own rules point; Verification/CheckResult.md
has the full reasoning.

Plan said Actually Done instead
deps: rusty, roxmltree, serde_json routing enums through serde needs DeserializeOwned, which serde_json does not re-export added serde.workspace = true
Size::parse_css accepts a bare 120 it returns None — the same fn deserializes the wire format explicit bare-number → Size::Px fallback in as_size
FontWeight="Bold".bold(true) TextBlock::bold(self) takes no argument .bold(); Bold/Italic also accepted as booleans, False a no-op
Background has no widget equivalent, so error Container::background(Color) exists honoured on Border/Container, error everywhere else; Margin still errors everywhere
Header is consumed by Card's constructor Card::new() takes no argument Header/TitleCard::title(..)
add From<std::io::Error> the Io variant carries a PathBuf, which a bare From cannot supply variant built at the call site in parse_file_with
errors carry a byte offset an error outlives the Document that could interpret one resolved Position { line, column }, converted eagerly with text_pos_at

Two smaller judgement calls in the same spirit:

  • Columns is accepted only on Grid. Layout has a columns field but no
    columns builder, so <StackPanel Columns="2"> is an UnknownAttribute.
  • There is no generic PascalCase-to-snake_case attribute fallback — nothing in Rust
    can enumerate a type's builder methods at runtime. Each widget's applier lists
    the Rusty spelling next to the XAML one instead (Gap, Rounded, Border,
    Disabled, ReadOnly, Max, Color, Direction), which is what the fallback
    was there to provide.

Files Modified

New (rusty-xaml/, 2541 lines):

File Lines Contents
Cargo.toml 20 manifest; rusty, roxmltree, serde, serde_json, dev-dep tokio
src/lib.rs 110 the four parse* functions; crate docs on binding snapshots and x:Name
src/build.rs 1136 the element/attribute mapping and 41 unit tests
src/value.rs 431 {Binding ..} resolution and the as_* coercions; 15 unit tests
src/error.rs 262 XamlError, Position; 4 unit tests
src/context.rs 143 XamlContext; 4 unit tests
tests/parse.rs 344 7 integration tests, including the IvyML drift guard
tests/fixtures/dashboard.xaml 33 a document shaped like a real app's
examples/xaml_counter.rs 82 counter.rs with its UI in XAML

Modified:

  • Cargo.toml — one member, one workspace dependency.
  • Cargo.lockroxmltree 0.21.1.
  • README.md — one row in the crate table.

Manual Testing

Beyond the automated gates (807 workspace tests passing, 73 of them new — see
Verification/RustTest.md):

  • The example serves. PORT=3123 cargo run -p rusty-xaml --example xaml_counter
    starts and binds loopback, logging RUSTY_PORT=3123. curl http://127.0.0.1:3123/
    returns 404 because this checkout has no built frontend bundle — the
    pre-existing rusty example behaves identically
    (PORT=3124 cargo run -p rusty --example counter logs Rusty server listening on 127.0.0.1:3124 and also returns 404 at /), so the new example is on par
    with the ones already in the tree. Both processes were killed afterwards.
  • Loopback only. rusty::server::DEFAULT_BIND_ADDRESS is already 127.0.0.1
    (rusty/src/server/ws.rs:72), which the example relies on rather than choosing
    its own bind address. Nothing in this plan listens on 0.0.0.0.
  • The rusty-desktop CI job. cargo build -p rusty-desktop and
    cargo clippy -p rusty-desktop --all-targets -- -D warnings were both run with
    default features, since adding a workspace member affects every member. Clean.
  • The end-to-end wiring, in a test rather than by hand.
    ids_and_events_survive_assign_ids parses a document, runs assign_ids, then
    dispatches click through the real EventRegistry by widget id and asserts the
    context's handlers ran. That is the part a browser click would exercise, checked
    without one.

No screenshots: the change has no visual surface of its own — a parsed tree
serializes to the same JSON as a hand-built one, which the tests assert directly.


Commits

  • bd00e0d [00133] Add the xaml_counter example and list rusty-xaml in the README
  • a6c080b [00133] Cover whole documents with rusty-xaml integration tests
  • bcf22b8 [00133] Add the rusty-xaml crate: runtime XAML parser and widget builder

Created using Ivy Tendril.

rorychatt and others added 3 commits August 10, 2026 11:16
Parses XAML markup into `rusty` widget trees at runtime, where
`rusty-ivyml` does the same job at compile time. The vocabulary is a
table for the same reason `codegen::shape_for` is: the constructors are
not uniform, `List` attaches children with `.item`, and some attributes
are consumed by the constructor.

Nothing is dropped silently — an unknown element, an unknown attribute,
an unparseable value or a child on a leaf is a `XamlError` carrying the
element, the attribute and the line it came from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the tests that only mean something across several elements: a
parsed tree serializing identically to the equivalent builder chain,
`.child` and `.item` attachment three deep, `parse_file` against a
fixture, and ids and events surviving `assign_ids`.

`mapping_covers_every_ivyml_element` scans `rusty-ivyml`'s `shape_for`
so an element added to the compile-time vocabulary cannot silently go
missing from the runtime one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The example is `rusty/examples/counter.rs` with its UI in XAML, and
exists mainly to show where the parse belongs: inside `build`, against a
context rebuilt from current state, since a binding is resolved once when
the document is parsed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rorychatt rorychatt self-assigned this Aug 10, 2026
@rorychatt
rorychatt merged commit 4703d7b into main Aug 10, 2026
@rorychatt
rorychatt deleted the tendril/00133-CreateTheRustyxamlRuntimeXAMLParserAndBuilderCrate branch August 10, 2026 09:27
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.

[Subsystem] Create XAML UI Parser & Builder Crate (Port of Ivy.XamlBuilder)

1 participant