Skip to content
Merged
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
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
[workspace]
members = ["rusty", "rusty-filter", "rusty-macros", "rusty-ivyml", "rusty-server", "rusty-docs", "rusty-desktop"]
members = [
"rusty",
"rusty-filter",
"rusty-macros",
"rusty-ivyml",
"rusty-xaml",
"rusty-server",
"rusty-docs",
"rusty-desktop",
]
resolver = "2"

[workspace.package]
Expand All @@ -21,3 +30,4 @@ tower-http = { version = "0.6", features = ["fs"] }
clap = { version = "4", features = ["derive", "env"] }
tokio-tungstenite = "0.29"
bytes = "1"
roxmltree = "0.21"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Rusty-Framework follows the same architecture as Ivy-Framework:
| `rusty-macros` | Proc macros for `#[derive(Widget)]`, `#[prop]`, `#[event]` |
| `rusty-ivyml` | Proc macros for `ivyml!` / `ivyml_file!` declarative markup |
| `rusty-filter` | Filter query lexer/parser/AST and evaluator |
| `rusty-xaml` | Parses XAML markup into widget trees at runtime |
| `rusty-server` | Standalone server binary |
| `rusty-docs` | Docs site binary; `build.rs` generates `src/generated/` |
| `rusty-desktop` | Native shell via wry/tao behind the optional `shell` feature |
Expand Down
20 changes: 20 additions & 0 deletions rusty-xaml/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "rusty-xaml"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "Parses XAML UI documents into Rusty widget trees at runtime"

[dependencies]
rusty = { path = "../rusty" }
roxmltree.workspace = true
# `value.rs` deserializes XAML enum words straight into `rusty`'s enums, which
# needs the `DeserializeOwned` bound that `serde_json` does not re-export.
serde.workspace = true
serde_json.workspace = true

[dev-dependencies]
# For `examples/xaml_counter.rs`, which serves the parsed tree like the
# examples in `rusty/examples`.
tokio.workspace = true
82 changes: 82 additions & 0 deletions rusty-xaml/examples/xaml_counter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//! `rusty/examples/counter.rs`, with the UI in XAML instead of builder calls.
//!
//! Run it with `cargo run -p rusty-xaml --example xaml_counter`, then open
//! <http://127.0.0.1:3000>.
//!
//! The point of interest is where the parse happens: *inside* `build`, against a
//! context rebuilt from the current state. A binding is resolved once, when the
//! document is parsed, so parsing per build is what makes `{Binding Count}` track
//! the counter — see the crate docs. The markup itself could equally come from a
//! file with `parse_file_with`, which is the case this crate exists for.

use rusty::prelude::*;
use rusty_xaml::XamlContext;

/// Hand-written here to keep the example self-contained; a real app would load
/// this from disk, from a database, or from an editor.
const MARKUP: &str = r#"
<StackPanel Spacing="16" Padding="24">
<TextBlock Text="XAML Counter" Variant="Heading1" />
<TextBlock Text="{Binding CountLabel}" />

<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="Increment" Click="OnIncrement" />
<Button Content="Decrement" Variant="Secondary" Click="OnDecrement" />
<Button Content="Reset" Variant="Ghost" Click="OnReset" />
</StackPanel>

<Separator />

<ProgressBar Value="{Binding Progress}" Label="Progress to 10" />
</StackPanel>
"#;

struct XamlCounterApp;

#[rusty::view]
impl View for XamlCounterApp {
fn build(&self, ctx: &mut BuildContext) -> Element {
let count = use_state(ctx, 0i32);

let count_inc = count.clone();
let count_dec = count.clone();
let count_reset = count.clone();

let xaml = XamlContext::new()
.value("CountLabel", format!("Count: {}", count.get()))
.value("Progress", f64::from(count.get().clamp(0, 10)) / 10.0)
.handler("OnIncrement", move || {
count_inc.update(|v| v + 1);
})
.handler("OnDecrement", move || {
count_dec.update(|v| v - 1);
})
.handler("OnReset", move || {
count_reset.set(0);
});

// A runtime parser can fail at runtime, so the app has to say something
// rather than panic: the error already names the element, the attribute
// and the line it came from.
match rusty_xaml::parse_with(MARKUP, &xaml) {
Ok(element) => element,
Err(err) => Layout::vertical()
.gap(8.0)
.padding(24.0)
.child(TextBlock::h1("The markup did not parse"))
.child(TextBlock::code(&err.to_string()))
.into(),
}
}
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let port = std::env::var("PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(3000);

// `RustyServer` binds `DEFAULT_BIND_ADDRESS`, which is loopback.
RustyServer::new(port, || XamlCounterApp).serve().await
}
Loading