diff --git a/README.md b/README.md
index bae5da1..f3cffa1 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,7 @@ pinned `avalonia-src` producer submodule at commit
| `interop/` | `Avalonia.Rust` and `Avalonia.Rust.Interop` - the managed-side view-model interop layer |
| `apps/system-monitor/` | NeoHtop, a real Rust-owned system monitor with compiled AXAML presentation and a packaged NativeAOT host |
| `apps/pdf-viewer/` | A PDF Oxide-powered sample viewer with rendered page navigation and extracted text |
+| `apps/hash-calculator/` | A streaming checksum utility (MD5, SHA-1, SHA-256, SHA-512, BLAKE3) with compare and copy |
| `tests/` | Host, IR, and generator test suites |
| `samples/` | `RustViewModelSample.Managed` - the sample presentation project the host consumes |
| `build/` | Vendored MSBuild configuration (versioning, signing, analyzers, xunit) |
diff --git a/apps/hash-calculator/.cargo/config.toml b/apps/hash-calculator/.cargo/config.toml
new file mode 100644
index 0000000..81b00ec
--- /dev/null
+++ b/apps/hash-calculator/.cargo/config.toml
@@ -0,0 +1,5 @@
+[target.x86_64-pc-windows-msvc]
+rustflags = ["-C", "target-feature=+crt-static"]
+
+[target.aarch64-pc-windows-msvc]
+rustflags = ["-C", "target-feature=+crt-static"]
diff --git a/apps/hash-calculator/.gitignore b/apps/hash-calculator/.gitignore
new file mode 100644
index 0000000..0559651
--- /dev/null
+++ b/apps/hash-calculator/.gitignore
@@ -0,0 +1,6 @@
+.avalonia/
+bin/
+obj/
+target/
+*.user
+*.suo
diff --git a/apps/hash-calculator/Cargo.lock b/apps/hash-calculator/Cargo.lock
new file mode 100644
index 0000000..e437488
--- /dev/null
+++ b/apps/hash-calculator/Cargo.lock
@@ -0,0 +1,212 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "arrayvec"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
+
+[[package]]
+name = "avalonia"
+version = "0.1.0"
+dependencies = [
+ "avalonia-sys",
+]
+
+[[package]]
+name = "avalonia-sys"
+version = "0.1.0"
+dependencies = [
+ "libloading",
+]
+
+[[package]]
+name = "blake3"
+version = "1.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
+dependencies = [
+ "arrayvec",
+ "cc",
+ "cfg-if",
+ "constant_time_eq",
+ "cpufeatures 0.3.1",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "cc"
+version = "1.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "constant_time_eq"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "hash-calculator"
+version = "0.1.0"
+dependencies = [
+ "avalonia",
+ "blake3",
+ "digest",
+ "hex",
+ "md-5",
+ "sha1",
+ "sha2",
+]
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "libloading"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link",
+]
+
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
diff --git a/apps/hash-calculator/Cargo.toml b/apps/hash-calculator/Cargo.toml
new file mode 100644
index 0000000..db32fc7
--- /dev/null
+++ b/apps/hash-calculator/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "hash-calculator"
+version = "0.1.0"
+edition = "2021"
+publish = false
+description = "A sample Rustolonia hash calculator for local files"
+
+[dependencies]
+avalonia = { path = "../../rust/avalonia" }
+blake3 = "1"
+digest = "0.10"
+hex = "0.4"
+md-5 = "0.10"
+sha1 = "0.10"
+sha2 = "0.10"
+
+[workspace]
diff --git a/apps/hash-calculator/README.md b/apps/hash-calculator/README.md
new file mode 100644
index 0000000..8dbbe3a
--- /dev/null
+++ b/apps/hash-calculator/README.md
@@ -0,0 +1,50 @@
+# Rustolonia hash calculator
+
+This sample is a sibling external consumer under `apps/` that combines
+Rustolonia, Avalonia and streaming checksum crates (`sha2`, `sha1`, `md-5`,
+`blake3`). Rust owns file hashing, algorithm selection, digest comparison and
+the file list; the generated view-model bridge exposes that state to the
+compiled Avalonia presentation. Hashing runs on a dedicated worker thread and
+reads files in 64 KiB chunks so large inputs keep the UI responsive.
+
+With no arguments, the app writes a small sample file to the system temporary
+directory and hashes it with SHA-256. Passing local file paths hashes those
+instead. **Open files** uses Avalonia's platform picker (multi-select). Toggle
+MD5, SHA-1, SHA-256, SHA-512 and BLAKE3 independently. Paste an expected digest
+to mark matching files; **Copy** / **Copy report** write hex to the clipboard.
+
+## Build on Windows x64
+
+From the repository root:
+
+```powershell
+pwsh ./rust/build-app.ps1 `
+ -ProducerRoot ./avalonia-src `
+ -Manifest ./apps/hash-calculator/avalonia-app.json `
+ -UpdateLockFile
+```
+
+The portable bundle is written to `apps/hash-calculator/artifacts/win-x64`.
+Subsequent locked builds omit `-UpdateLockFile`.
+
+## Tests
+
+Run the Rust hashing tests:
+
+```powershell
+Push-Location ./apps/hash-calculator
+cargo test --locked
+Pop-Location
+```
+
+After building, the Windows UI Automation smoke test exercises startup sample
+generation, SHA-256 output, enabling MD5, compare-against-digest and natural
+shutdown:
+
+```powershell
+powershell.exe -NoProfile -STA -ExecutionPolicy Bypass `
+ -File ./apps/hash-calculator/tests/test-bundle.ps1
+```
+
+The app is a portable Win32 GUI bundle, not an installer. File-association
+metadata snippets are under `file-associations/`.
diff --git a/apps/hash-calculator/avalonia-app.json b/apps/hash-calculator/avalonia-app.json
new file mode 100644
index 0000000..adfd5fc
--- /dev/null
+++ b/apps/hash-calculator/avalonia-app.json
@@ -0,0 +1,15 @@
+{
+ "version": 1,
+ "presentationProject": "managed/Consumer.Presentation.csproj",
+ "viewModelIr": "view-model.ir.json",
+ "generatedAdaptersDirectory": "managed/Generated",
+ "generatedRegistryFile": "generated/RustViewRegistry.g.cs",
+ "generatedRustFile": "generated/generated_view_models.rs",
+ "generatedContractFile": "generated/view-model.contract.md",
+ "cargoManifest": "Cargo.toml",
+ "packageName": "hash-calculator",
+ "binary": "hash-calculator",
+ "rid": "win-x64",
+ "configuration": "Release",
+ "outputDirectory": "artifacts/win-x64"
+}
diff --git a/apps/hash-calculator/file-associations/README.md b/apps/hash-calculator/file-associations/README.md
new file mode 100644
index 0000000..d420b75
--- /dev/null
+++ b/apps/hash-calculator/file-associations/README.md
@@ -0,0 +1,38 @@
+# File type association metadata
+
+These are packaging metadata snippets, not installers. They register a document
+type so the desktop shell launches this application with the selected file path
+as a command-line argument; the runtime side of "open with" is already wired,
+because `App::run` forwards this process's arguments to the managed desktop
+lifetime and `AppScope::activation_items()` returns them normalized (see
+`rust/DESKTOP_FILES.md` in the pinned Avalonia producer checkout).
+
+Deliberately out of scope here: MSIX packaging, `.msi`/`.pkg`/`.deb`/`.rpm`
+installers, notarization, and any store submission. Those belong to whatever
+distribution channel a consumer chooses; this workflow ships a deterministic
+per-RID directory (see `PRODUCTIZATION.md`).
+
+Replace `hash-calculator` (already substituted by `new-app`), the
+extension `.myapp`, and the install path before shipping any of these.
+
+| Platform | File | Applied by |
+| --- | --- | --- |
+| Windows | `windows-file-association.reg` | Your installer writing the same keys, or `reg import` for local testing. |
+| Linux | `linux-desktop-entry.desktop`, `linux-mime-type.xml` | `desktop-file-install` / `xdg-mime install` from your package's post-install step. |
+| macOS | `macos-Info.plist.snippet` | Merged into the `.app` bundle's `Info.plist`. |
+
+## Verifying the runtime side
+
+Once an association is registered, launching a file through the shell reaches
+Rust as an activation item:
+
+```rust
+for item in scope.activation_items()? {
+ // `uri()` is always present; `local_path()` may be `None` on platforms
+ // that hand the application a non-local document reference.
+ println!("open with: {} ({})", item.uri(), item.name());
+}
+```
+
+macOS additionally raises later activations while the application is already
+running; subscribe with `AppScope::on_activation` to receive them.
diff --git a/apps/hash-calculator/file-associations/linux-desktop-entry.desktop b/apps/hash-calculator/file-associations/linux-desktop-entry.desktop
new file mode 100644
index 0000000..df9609d
--- /dev/null
+++ b/apps/hash-calculator/file-associations/linux-desktop-entry.desktop
@@ -0,0 +1,16 @@
+[Desktop Entry]
+Type=Application
+Name=hash-calculator
+Comment=Rustolonia hash calculator for local files
+# %F passes the selected file paths as separate arguments. Use %U instead if
+# your application should also receive non-local URIs; both reach Rust through
+# AppScope::activation_items(), which keeps a URI when there is no local path.
+Exec=/opt/hash-calculator/hash-calculator %F
+Icon=hash-calculator
+Terminal=false
+Categories=Utility;
+MimeType=application/x-hash-calculator;
+
+# Install with (from a package post-install step, not from the application):
+# desktop-file-install --dir="$HOME/.local/share/applications" this-file.desktop
+# update-desktop-database "$HOME/.local/share/applications"
diff --git a/apps/hash-calculator/file-associations/linux-mime-type.xml b/apps/hash-calculator/file-associations/linux-mime-type.xml
new file mode 100644
index 0000000..76f72c2
--- /dev/null
+++ b/apps/hash-calculator/file-associations/linux-mime-type.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ hash-calculator document
+
+
+
+
diff --git a/apps/hash-calculator/file-associations/macos-Info.plist.snippet b/apps/hash-calculator/file-associations/macos-Info.plist.snippet
new file mode 100644
index 0000000..6b4bb18
--- /dev/null
+++ b/apps/hash-calculator/file-associations/macos-Info.plist.snippet
@@ -0,0 +1,54 @@
+
+
+
+
+ CFBundleDocumentTypes
+
+
+ CFBundleTypeName
+ hash-calculator document
+ CFBundleTypeRole
+ Editor
+ LSHandlerRank
+ Owner
+ LSItemContentTypes
+
+ com.example.hash-calculator.document
+
+
+
+ UTExportedTypeDeclarations
+
+
+ UTTypeIdentifier
+ com.example.hash-calculator.document
+ UTTypeDescription
+ hash-calculator document
+ UTTypeConformsTo
+
+ public.data
+
+ UTTypeTagSpecification
+
+ public.filename-extension
+
+ myapp
+
+
+
+
+
+
diff --git a/apps/hash-calculator/file-associations/windows-file-association.reg b/apps/hash-calculator/file-associations/windows-file-association.reg
new file mode 100644
index 0000000..6157c91
--- /dev/null
+++ b/apps/hash-calculator/file-associations/windows-file-association.reg
@@ -0,0 +1,27 @@
+Windows Registry Editor Version 5.00
+
+; File type association for hash-calculator.
+;
+; Replace ".myapp" with your extension and the install path with the real one.
+; "%1" is what makes the shell pass the selected file to the executable; the
+; Rust entry point forwards it to the managed desktop lifetime, so it arrives
+; through AppScope::activation_items().
+;
+; Written under HKEY_CURRENT_USER so it can be applied and undone without
+; elevation while developing. A real installer writes the same keys under
+; HKEY_LOCAL_MACHINE\Software\Classes and owns their removal on uninstall.
+
+[HKEY_CURRENT_USER\Software\Classes\.myapp]
+@="hash-calculator.Document"
+
+[HKEY_CURRENT_USER\Software\Classes\.myapp\OpenWithProgids]
+"hash-calculator.Document"=""
+
+[HKEY_CURRENT_USER\Software\Classes\hash-calculator.Document]
+@="hash-calculator document"
+
+[HKEY_CURRENT_USER\Software\Classes\hash-calculator.Document\DefaultIcon]
+@="C:\\Program Files\\hash-calculator\\hash-calculator.exe,0"
+
+[HKEY_CURRENT_USER\Software\Classes\hash-calculator.Document\shell\open\command]
+@="\"C:\\Program Files\\hash-calculator\\hash-calculator.exe\" \"%1\""
diff --git a/apps/hash-calculator/generated/.avalonia-viewmodel.owned.json b/apps/hash-calculator/generated/.avalonia-viewmodel.owned.json
new file mode 100644
index 0000000..b540094
--- /dev/null
+++ b/apps/hash-calculator/generated/.avalonia-viewmodel.owned.json
@@ -0,0 +1,17 @@
+{
+ "generator": "avalonia-viewmodel",
+ "files": [
+ {
+ "path": "generated_view_models.rs",
+ "sha256": "cfd1c2fc7d9982baec51e290d316f4297b85e82513d9561ecb2f8da04353598f"
+ },
+ {
+ "path": "RustViewRegistry.g.cs",
+ "sha256": "434e460c14fe7b82b96605d85037532ede47f71a50557b05ba7d00cd5f3698fe"
+ },
+ {
+ "path": "view-model.contract.md",
+ "sha256": "3417da743765c13538e564e9a757b1d945b1825eb67d4c20d1da6b80db7bfa9d"
+ }
+ ]
+}
diff --git a/apps/hash-calculator/generated/RustViewRegistry.g.cs b/apps/hash-calculator/generated/RustViewRegistry.g.cs
new file mode 100644
index 0000000..0bc28f0
--- /dev/null
+++ b/apps/hash-calculator/generated/RustViewRegistry.g.cs
@@ -0,0 +1,45 @@
+//
+#nullable enable
+using System;
+using Avalonia.Controls;
+using Avalonia.Rust;
+using Avalonia.Rust.Interop;
+using Avalonia.Threading;
+
+namespace Avalonia.Host.Generated.ViewModels;
+
+internal static class RustViewRegistry
+{
+ internal static Window Create(int viewId, IAvnRustViewModel model) => viewId switch
+ {
+ 1 => new global::HashCalculator.Presentation.Views.MainWindow(model),
+ _ => throw new global::System.ArgumentOutOfRangeException(nameof(viewId)),
+ };
+
+ private static TWindow CreateDynamic(
+ IAvnRustViewModel model,
+ RustViewModelDescriptor descriptor)
+ where TWindow : Window, new()
+ {
+ var adapter = new ReflectableRustViewModelAdapter(model, descriptor, Dispatch);
+ try
+ {
+ var window = new TWindow { DataContext = adapter };
+ window.Closed += (_, _) => adapter.Dispose();
+ return window;
+ }
+ catch
+ {
+ adapter.Dispose();
+ throw;
+ }
+ }
+
+ private static void Dispatch(Action action)
+ {
+ if (Dispatcher.UIThread.CheckAccess())
+ action();
+ else
+ Dispatcher.UIThread.Invoke(action);
+ }
+}
diff --git a/apps/hash-calculator/generated/generated_view_models.rs b/apps/hash-calculator/generated/generated_view_models.rs
new file mode 100644
index 0000000..119cd0f
--- /dev/null
+++ b/apps/hash-calculator/generated/generated_view_models.rs
@@ -0,0 +1,345 @@
+//! Generated from view-model.ir.json. Do not edit.
+
+#![allow(dead_code)]
+
+#[derive(Clone, Debug)]
+pub struct MainViewModelSink(crate::view_model::ViewModelSink);
+
+/// Declared capacity of the `MainViewModel` recent-file list.
+pub const MAIN_VIEW_MODEL_RECENT_FILES_CAPACITY: usize = 8;
+
+impl MainViewModelSink {
+ pub fn set_title(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(1, value) }
+ pub fn set_status(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(2, value) }
+ pub fn set_file_count_label(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(3, value) }
+ pub fn set_expected_hash(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(4, value) }
+ pub fn set_compare_status(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(5, value) }
+ pub fn set_include_md5(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(6, value) }
+ pub fn set_include_sha1(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(7, value) }
+ pub fn set_include_sha256(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(8, value) }
+ pub fn set_include_sha512(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(9, value) }
+ pub fn set_include_blake3(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(10, value) }
+ pub fn set_is_busy(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(11, value) }
+ pub fn set_can_clear(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(12, value) }
+ pub fn set_can_copy_report(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(13, value) }
+ pub fn set_primary_digest(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(14, value) }
+ pub fn set_empty_visible(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(15, value) }
+ pub fn add_recent_files(&self, value: impl AsRef) -> crate::Result<()> { self.0.add_string(1, value) }
+ pub fn insert_recent_files(&self, index: i32, value: impl AsRef) -> crate::Result<()> { self.0.insert_string(1, index, value) }
+ pub fn replace_recent_files(&self, index: i32, value: impl AsRef) -> crate::Result<()> { self.0.replace_string(1, index, value) }
+ pub fn add_files(&self, value: impl FileRowViewModel) -> crate::Result<()> { self.0.add_model(2, FileRowViewModelDispatch { model: value }) }
+ pub fn insert_files(&self, index: i32, value: impl FileRowViewModel) -> crate::Result<()> { self.0.insert_model(2, index, FileRowViewModelDispatch { model: value }) }
+ pub fn replace_files(&self, index: i32, value: impl FileRowViewModel) -> crate::Result<()> { self.0.replace_model(2, index, FileRowViewModelDispatch { model: value }) }
+ pub fn remove_recent_files(&self, index: i32) -> crate::Result<()> { self.0.remove_string_at(1, index) }
+ pub fn move_recent_files(&self, from_index: i32, to_index: i32) -> crate::Result<()> { self.0.move_string_item(1, from_index, to_index) }
+ pub fn clear_recent_files(&self) -> crate::Result<()> { self.0.clear_string_collection(1) }
+ pub fn remove_files(&self, index: i32) -> crate::Result<()> { self.0.remove_model_at(2, index) }
+ pub fn move_files(&self, from_index: i32, to_index: i32) -> crate::Result<()> { self.0.move_model_item(2, from_index, to_index) }
+ pub fn clear_files(&self) -> crate::Result<()> { self.0.clear_model_collection(2) }
+ pub fn set_open_files_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(1, enabled) }
+ pub fn set_clear_files_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(2, enabled) }
+ pub fn set_copy_report_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(3, enabled) }
+ pub fn set_open_recent_file_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(4, enabled) }
+ pub fn set_exit_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(5, enabled) }
+ /// Publishes the most-recently-used storage URIs into `RecentFiles`.
+ ///
+ /// The list is bounded by its own capacity (8), so this replaces a
+ /// handful of entries rather than a data set; the generated menu derives
+ /// each header from the URI and passes the URI back as the command parameter.
+ pub fn publish_recent_files(&self, recent: &crate::RecentFileList) -> crate::Result<()> {
+ self.0.clear_string_collection(1)?;
+ for uri in recent.entries() { self.0.add_string(1, uri)?; }
+ Ok(())
+ }
+ pub fn set_title_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(1, message) }
+ pub fn set_status_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(2, message) }
+ pub fn set_file_count_label_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(3, message) }
+ pub fn set_expected_hash_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(4, message) }
+ pub fn set_compare_status_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(5, message) }
+ pub fn set_include_md5_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(6, message) }
+ pub fn set_include_sha1_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(7, message) }
+ pub fn set_include_sha256_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(8, message) }
+ pub fn set_include_sha512_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(9, message) }
+ pub fn set_include_blake3_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(10, message) }
+ pub fn set_is_busy_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(11, message) }
+ pub fn set_can_clear_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(12, message) }
+ pub fn set_can_copy_report_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(13, message) }
+ pub fn set_primary_digest_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(14, message) }
+ pub fn set_empty_visible_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(15, message) }
+ /// Creates a worker-safe immutable update batch with a monotonic generation.
+ pub fn batch(&self, generation: i64) -> MainViewModelSinkBatch { MainViewModelSinkBatch(crate::view_model::ViewModelBatch::new(generation)) }
+ pub fn submit_batch(&self, batch: MainViewModelSinkBatch) -> crate::Result { self.0.submit_batch(batch.0) }
+}
+
+pub struct MainViewModelSinkBatch(crate::view_model::ViewModelBatch);
+
+impl MainViewModelSinkBatch {
+ pub fn set_title(&mut self, value: impl AsRef) { self.0.push_string(1, 1, 0, value); }
+ pub fn set_title_error(&mut self, message: impl AsRef) { self.0.push_string(18, 1, 0, message); }
+ pub fn clear_title_error(&mut self) { self.0.push_clear_error(1); }
+ pub fn set_status(&mut self, value: impl AsRef) { self.0.push_string(1, 2, 0, value); }
+ pub fn set_status_error(&mut self, message: impl AsRef) { self.0.push_string(18, 2, 0, message); }
+ pub fn clear_status_error(&mut self) { self.0.push_clear_error(2); }
+ pub fn set_file_count_label(&mut self, value: impl AsRef) { self.0.push_string(1, 3, 0, value); }
+ pub fn set_file_count_label_error(&mut self, message: impl AsRef) { self.0.push_string(18, 3, 0, message); }
+ pub fn clear_file_count_label_error(&mut self) { self.0.push_clear_error(3); }
+ pub fn set_expected_hash(&mut self, value: impl AsRef) { self.0.push_string(1, 4, 0, value); }
+ pub fn set_expected_hash_error(&mut self, message: impl AsRef) { self.0.push_string(18, 4, 0, message); }
+ pub fn clear_expected_hash_error(&mut self) { self.0.push_clear_error(4); }
+ pub fn set_compare_status(&mut self, value: impl AsRef) { self.0.push_string(1, 5, 0, value); }
+ pub fn set_compare_status_error(&mut self, message: impl AsRef) { self.0.push_string(18, 5, 0, message); }
+ pub fn clear_compare_status_error(&mut self) { self.0.push_clear_error(5); }
+ pub fn set_include_md5(&mut self, value: bool) { self.0.push_boolean(3, 6, value); }
+ pub fn set_include_md5_error(&mut self, message: impl AsRef) { self.0.push_string(18, 6, 0, message); }
+ pub fn clear_include_md5_error(&mut self) { self.0.push_clear_error(6); }
+ pub fn set_include_sha1(&mut self, value: bool) { self.0.push_boolean(3, 7, value); }
+ pub fn set_include_sha1_error(&mut self, message: impl AsRef) { self.0.push_string(18, 7, 0, message); }
+ pub fn clear_include_sha1_error(&mut self) { self.0.push_clear_error(7); }
+ pub fn set_include_sha256(&mut self, value: bool) { self.0.push_boolean(3, 8, value); }
+ pub fn set_include_sha256_error(&mut self, message: impl AsRef) { self.0.push_string(18, 8, 0, message); }
+ pub fn clear_include_sha256_error(&mut self) { self.0.push_clear_error(8); }
+ pub fn set_include_sha512(&mut self, value: bool) { self.0.push_boolean(3, 9, value); }
+ pub fn set_include_sha512_error(&mut self, message: impl AsRef) { self.0.push_string(18, 9, 0, message); }
+ pub fn clear_include_sha512_error(&mut self) { self.0.push_clear_error(9); }
+ pub fn set_include_blake3(&mut self, value: bool) { self.0.push_boolean(3, 10, value); }
+ pub fn set_include_blake3_error(&mut self, message: impl AsRef) { self.0.push_string(18, 10, 0, message); }
+ pub fn clear_include_blake3_error(&mut self) { self.0.push_clear_error(10); }
+ pub fn set_is_busy(&mut self, value: bool) { self.0.push_boolean(3, 11, value); }
+ pub fn set_is_busy_error(&mut self, message: impl AsRef) { self.0.push_string(18, 11, 0, message); }
+ pub fn clear_is_busy_error(&mut self) { self.0.push_clear_error(11); }
+ pub fn set_can_clear(&mut self, value: bool) { self.0.push_boolean(3, 12, value); }
+ pub fn set_can_clear_error(&mut self, message: impl AsRef) { self.0.push_string(18, 12, 0, message); }
+ pub fn clear_can_clear_error(&mut self) { self.0.push_clear_error(12); }
+ pub fn set_can_copy_report(&mut self, value: bool) { self.0.push_boolean(3, 13, value); }
+ pub fn set_can_copy_report_error(&mut self, message: impl AsRef) { self.0.push_string(18, 13, 0, message); }
+ pub fn clear_can_copy_report_error(&mut self) { self.0.push_clear_error(13); }
+ pub fn set_primary_digest(&mut self, value: impl AsRef) { self.0.push_string(1, 14, 0, value); }
+ pub fn set_primary_digest_error(&mut self, message: impl AsRef) { self.0.push_string(18, 14, 0, message); }
+ pub fn clear_primary_digest_error(&mut self) { self.0.push_clear_error(14); }
+ pub fn set_empty_visible(&mut self, value: bool) { self.0.push_boolean(3, 15, value); }
+ pub fn set_empty_visible_error(&mut self, message: impl AsRef) { self.0.push_string(18, 15, 0, message); }
+ pub fn clear_empty_visible_error(&mut self) { self.0.push_clear_error(15); }
+ pub fn add_recent_files(&mut self, value: impl AsRef) { self.0.push_string(7, 1, 0, value); }
+ pub fn insert_recent_files(&mut self, index: i32, value: impl AsRef) { self.0.push_string(9, 1, index, value); }
+ pub fn replace_recent_files(&mut self, index: i32, value: impl AsRef) { self.0.push_string(11, 1, index, value); }
+ pub fn replace_recent_files_snapshot>(&mut self, values: impl IntoIterator- ) { self.0.push_string_snapshot(1, values); }
+ pub fn remove_recent_files(&mut self, index: i32) { self.0.push_indices(13, 1, index, 0); }
+ pub fn move_recent_files(&mut self, from_index: i32, to_index: i32) { self.0.push_indices(14, 1, from_index, to_index); }
+ pub fn clear_recent_files(&mut self) { self.0.push_indices(19, 1, 0, 0); }
+ pub fn add_files(&mut self, value: impl FileRowViewModel) { self.0.push_model(8, 2, 0, FileRowViewModelDispatch { model: value }); }
+ pub fn insert_files(&mut self, index: i32, value: impl FileRowViewModel) { self.0.push_model(10, 2, index, FileRowViewModelDispatch { model: value }); }
+ pub fn replace_files(&mut self, index: i32, value: impl FileRowViewModel) { self.0.push_model(12, 2, index, FileRowViewModelDispatch { model: value }); }
+ pub fn replace_files_snapshot(&mut self, values: impl IntoIterator
- ) { self.0.push_model_snapshot(2, values.into_iter().map(|value| FileRowViewModelDispatch { model: value })); }
+ pub fn remove_files(&mut self, index: i32) { self.0.push_model_indices(13, 2, index, 0); }
+ pub fn move_files(&mut self, from_index: i32, to_index: i32) { self.0.push_model_indices(14, 2, from_index, to_index); }
+ pub fn clear_files(&mut self) { self.0.push_model_clear(2); }
+ pub fn set_open_files_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 1, enabled); }
+ pub fn set_clear_files_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 2, enabled); }
+ pub fn set_copy_report_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 3, enabled); }
+ pub fn set_open_recent_file_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 4, enabled); }
+ pub fn set_exit_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 5, enabled); }
+ /// Stages the most-recently-used storage URIs as one `RecentFiles` snapshot.
+ pub fn set_recent_files(&mut self, recent: &crate::RecentFileList) { self.0.push_string_snapshot(1, recent.entries()); }
+}
+
+pub trait MainViewModel: Send + 'static {
+ fn attach(&mut self, sink: MainViewModelSink) -> crate::Result<()>;
+ fn detach(&mut self) -> crate::Result<()>;
+ fn set_expected_hash(&mut self, value: String) -> crate::Result<()>;
+ fn set_include_md5(&mut self, value: bool) -> crate::Result<()>;
+ fn set_include_sha1(&mut self, value: bool) -> crate::Result<()>;
+ fn set_include_sha256(&mut self, value: bool) -> crate::Result<()>;
+ fn set_include_sha512(&mut self, value: bool) -> crate::Result<()>;
+ fn set_include_blake3(&mut self, value: bool) -> crate::Result<()>;
+ fn open_files(&mut self) -> crate::Result<()>;
+ fn clear_files(&mut self) -> crate::Result<()>;
+ fn copy_report(&mut self) -> crate::Result<()>;
+ fn open_recent_file(&mut self, value: String) -> crate::Result<()>;
+ fn exit(&mut self) -> crate::Result<()>;
+}
+
+struct MainViewModelDispatch { model: T }
+
+impl crate::view_model::DynamicViewModel for MainViewModelDispatch {
+ fn attach(&mut self, sink: crate::view_model::ViewModelSink) -> crate::Result<()> { self.model.attach(MainViewModelSink(sink)) }
+ fn detach(&mut self) -> crate::Result<()> { self.model.detach() }
+ fn set_string(&mut self, property_id: i32, value: String) -> crate::Result<()> {
+ match property_id {
+ 4 => self.model.set_expected_hash(value),
+ _ => Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id }),
+ }
+ }
+ fn set_integer(&mut self, property_id: i32, _value: i64) -> crate::Result<()> {
+ Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id })
+ }
+ fn set_boolean(&mut self, property_id: i32, value: bool) -> crate::Result<()> {
+ match property_id {
+ 6 => self.model.set_include_md5(value),
+ 7 => self.model.set_include_sha1(value),
+ 8 => self.model.set_include_sha256(value),
+ 9 => self.model.set_include_sha512(value),
+ 10 => self.model.set_include_blake3(value),
+ _ => Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id }),
+ }
+ }
+ fn set_double(&mut self, property_id: i32, _value: f64) -> crate::Result<()> {
+ Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id })
+ }
+ fn execute(&mut self, command_id: i32, parameter: Option) -> crate::Result<()> {
+ match command_id {
+ 2 => self.model.clear_files(),
+ 4 => self.model.open_recent_file(parameter.unwrap_or_default()),
+ 5 => self.model.exit(),
+ _ => Err(crate::Error::InvalidViewModelMember { kind: "command", id: command_id }),
+ }
+ }
+ fn begin_async(&mut self, command_id: i32, _parameter: Option) -> crate::Result<()> {
+ match command_id {
+ 1 => self.model.open_files(),
+ 3 => self.model.copy_report(),
+ _ => Err(crate::Error::InvalidViewModelMember { kind: "command", id: command_id }),
+ }
+ }
+}
+
+pub fn mount_main_window(scope: &crate::AppScope, model: impl MainViewModel) -> crate::Result<()> { scope.mount_dynamic_view_model(1, MainViewModelDispatch { model }) }
+
+#[derive(Clone, Debug)]
+pub struct FileRowViewModelSink(crate::view_model::ViewModelSink);
+
+impl FileRowViewModelSink {
+ pub fn set_name(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(1, value) }
+ pub fn set_file_path(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(2, value) }
+ pub fn set_size_label(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(3, value) }
+ pub fn set_status(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(4, value) }
+ pub fn set_match_label(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(5, value) }
+ pub fn set_md5(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(6, value) }
+ pub fn set_sha1(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(7, value) }
+ pub fn set_sha256(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(8, value) }
+ pub fn set_sha512(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(9, value) }
+ pub fn set_blake3(&self, value: impl AsRef) -> crate::Result<()> { self.0.set_string(10, value) }
+ pub fn set_show_md5(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(11, value) }
+ pub fn set_show_sha1(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(12, value) }
+ pub fn set_show_sha256(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(13, value) }
+ pub fn set_show_sha512(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(14, value) }
+ pub fn set_show_blake3(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(15, value) }
+ pub fn set_can_copy_sha256(&self, value: bool) -> crate::Result<()> { self.0.set_boolean(16, value) }
+ pub fn set_copy_sha256_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(1, enabled) }
+ pub fn set_copy_row_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(2, enabled) }
+ pub fn set_remove_enabled(&self, enabled: bool) -> crate::Result<()> { self.0.set_command_enabled(3, enabled) }
+ pub fn set_name_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(1, message) }
+ pub fn set_file_path_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(2, message) }
+ pub fn set_size_label_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(3, message) }
+ pub fn set_status_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(4, message) }
+ pub fn set_match_label_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(5, message) }
+ pub fn set_md5_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(6, message) }
+ pub fn set_sha1_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(7, message) }
+ pub fn set_sha256_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(8, message) }
+ pub fn set_sha512_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(9, message) }
+ pub fn set_blake3_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(10, message) }
+ pub fn set_show_md5_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(11, message) }
+ pub fn set_show_sha1_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(12, message) }
+ pub fn set_show_sha256_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(13, message) }
+ pub fn set_show_sha512_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(14, message) }
+ pub fn set_show_blake3_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(15, message) }
+ pub fn set_can_copy_sha256_error(&self, message: Option<&str>) -> crate::Result<()> { self.0.set_property_error(16, message) }
+ /// Creates a worker-safe immutable update batch with a monotonic generation.
+ pub fn batch(&self, generation: i64) -> FileRowViewModelSinkBatch { FileRowViewModelSinkBatch(crate::view_model::ViewModelBatch::new(generation)) }
+ pub fn submit_batch(&self, batch: FileRowViewModelSinkBatch) -> crate::Result { self.0.submit_batch(batch.0) }
+}
+
+pub struct FileRowViewModelSinkBatch(crate::view_model::ViewModelBatch);
+
+impl FileRowViewModelSinkBatch {
+ pub fn set_name(&mut self, value: impl AsRef) { self.0.push_string(1, 1, 0, value); }
+ pub fn set_name_error(&mut self, message: impl AsRef) { self.0.push_string(18, 1, 0, message); }
+ pub fn clear_name_error(&mut self) { self.0.push_clear_error(1); }
+ pub fn set_file_path(&mut self, value: impl AsRef) { self.0.push_string(1, 2, 0, value); }
+ pub fn set_file_path_error(&mut self, message: impl AsRef) { self.0.push_string(18, 2, 0, message); }
+ pub fn clear_file_path_error(&mut self) { self.0.push_clear_error(2); }
+ pub fn set_size_label(&mut self, value: impl AsRef) { self.0.push_string(1, 3, 0, value); }
+ pub fn set_size_label_error(&mut self, message: impl AsRef) { self.0.push_string(18, 3, 0, message); }
+ pub fn clear_size_label_error(&mut self) { self.0.push_clear_error(3); }
+ pub fn set_status(&mut self, value: impl AsRef) { self.0.push_string(1, 4, 0, value); }
+ pub fn set_status_error(&mut self, message: impl AsRef) { self.0.push_string(18, 4, 0, message); }
+ pub fn clear_status_error(&mut self) { self.0.push_clear_error(4); }
+ pub fn set_match_label(&mut self, value: impl AsRef) { self.0.push_string(1, 5, 0, value); }
+ pub fn set_match_label_error(&mut self, message: impl AsRef) { self.0.push_string(18, 5, 0, message); }
+ pub fn clear_match_label_error(&mut self) { self.0.push_clear_error(5); }
+ pub fn set_md5(&mut self, value: impl AsRef) { self.0.push_string(1, 6, 0, value); }
+ pub fn set_md5_error(&mut self, message: impl AsRef) { self.0.push_string(18, 6, 0, message); }
+ pub fn clear_md5_error(&mut self) { self.0.push_clear_error(6); }
+ pub fn set_sha1(&mut self, value: impl AsRef) { self.0.push_string(1, 7, 0, value); }
+ pub fn set_sha1_error(&mut self, message: impl AsRef) { self.0.push_string(18, 7, 0, message); }
+ pub fn clear_sha1_error(&mut self) { self.0.push_clear_error(7); }
+ pub fn set_sha256(&mut self, value: impl AsRef) { self.0.push_string(1, 8, 0, value); }
+ pub fn set_sha256_error(&mut self, message: impl AsRef) { self.0.push_string(18, 8, 0, message); }
+ pub fn clear_sha256_error(&mut self) { self.0.push_clear_error(8); }
+ pub fn set_sha512(&mut self, value: impl AsRef) { self.0.push_string(1, 9, 0, value); }
+ pub fn set_sha512_error(&mut self, message: impl AsRef) { self.0.push_string(18, 9, 0, message); }
+ pub fn clear_sha512_error(&mut self) { self.0.push_clear_error(9); }
+ pub fn set_blake3(&mut self, value: impl AsRef) { self.0.push_string(1, 10, 0, value); }
+ pub fn set_blake3_error(&mut self, message: impl AsRef) { self.0.push_string(18, 10, 0, message); }
+ pub fn clear_blake3_error(&mut self) { self.0.push_clear_error(10); }
+ pub fn set_show_md5(&mut self, value: bool) { self.0.push_boolean(3, 11, value); }
+ pub fn set_show_md5_error(&mut self, message: impl AsRef) { self.0.push_string(18, 11, 0, message); }
+ pub fn clear_show_md5_error(&mut self) { self.0.push_clear_error(11); }
+ pub fn set_show_sha1(&mut self, value: bool) { self.0.push_boolean(3, 12, value); }
+ pub fn set_show_sha1_error(&mut self, message: impl AsRef) { self.0.push_string(18, 12, 0, message); }
+ pub fn clear_show_sha1_error(&mut self) { self.0.push_clear_error(12); }
+ pub fn set_show_sha256(&mut self, value: bool) { self.0.push_boolean(3, 13, value); }
+ pub fn set_show_sha256_error(&mut self, message: impl AsRef) { self.0.push_string(18, 13, 0, message); }
+ pub fn clear_show_sha256_error(&mut self) { self.0.push_clear_error(13); }
+ pub fn set_show_sha512(&mut self, value: bool) { self.0.push_boolean(3, 14, value); }
+ pub fn set_show_sha512_error(&mut self, message: impl AsRef) { self.0.push_string(18, 14, 0, message); }
+ pub fn clear_show_sha512_error(&mut self) { self.0.push_clear_error(14); }
+ pub fn set_show_blake3(&mut self, value: bool) { self.0.push_boolean(3, 15, value); }
+ pub fn set_show_blake3_error(&mut self, message: impl AsRef) { self.0.push_string(18, 15, 0, message); }
+ pub fn clear_show_blake3_error(&mut self) { self.0.push_clear_error(15); }
+ pub fn set_can_copy_sha256(&mut self, value: bool) { self.0.push_boolean(3, 16, value); }
+ pub fn set_can_copy_sha256_error(&mut self, message: impl AsRef) { self.0.push_string(18, 16, 0, message); }
+ pub fn clear_can_copy_sha256_error(&mut self) { self.0.push_clear_error(16); }
+ pub fn set_copy_sha256_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 1, enabled); }
+ pub fn set_copy_row_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 2, enabled); }
+ pub fn set_remove_enabled(&mut self, enabled: bool) { self.0.push_boolean(17, 3, enabled); }
+}
+
+pub trait FileRowViewModel: Send + 'static {
+ fn attach(&mut self, sink: FileRowViewModelSink) -> crate::Result<()>;
+ fn detach(&mut self) -> crate::Result<()>;
+ fn copy_sha256(&mut self) -> crate::Result<()>;
+ fn copy_row(&mut self) -> crate::Result<()>;
+ fn remove(&mut self) -> crate::Result<()>;
+}
+
+struct FileRowViewModelDispatch { model: T }
+
+impl crate::view_model::DynamicViewModel for FileRowViewModelDispatch {
+ fn attach(&mut self, sink: crate::view_model::ViewModelSink) -> crate::Result<()> { self.model.attach(FileRowViewModelSink(sink)) }
+ fn detach(&mut self) -> crate::Result<()> { self.model.detach() }
+ fn set_string(&mut self, property_id: i32, _value: String) -> crate::Result<()> {
+ Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id })
+ }
+ fn set_integer(&mut self, property_id: i32, _value: i64) -> crate::Result<()> {
+ Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id })
+ }
+ fn set_boolean(&mut self, property_id: i32, _value: bool) -> crate::Result<()> {
+ Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id })
+ }
+ fn set_double(&mut self, property_id: i32, _value: f64) -> crate::Result<()> {
+ Err(crate::Error::InvalidViewModelMember { kind: "property", id: property_id })
+ }
+ fn execute(&mut self, command_id: i32, _parameter: Option) -> crate::Result<()> {
+ match command_id {
+ 3 => self.model.remove(),
+ _ => Err(crate::Error::InvalidViewModelMember { kind: "command", id: command_id }),
+ }
+ }
+ fn begin_async(&mut self, command_id: i32, _parameter: Option) -> crate::Result<()> {
+ match command_id {
+ 1 => self.model.copy_sha256(),
+ 2 => self.model.copy_row(),
+ _ => Err(crate::Error::InvalidViewModelMember { kind: "command", id: command_id }),
+ }
+ }
+}
diff --git a/apps/hash-calculator/generated/view-model.contract.md b/apps/hash-calculator/generated/view-model.contract.md
new file mode 100644
index 0000000..89033b7
--- /dev/null
+++ b/apps/hash-calculator/generated/view-model.contract.md
@@ -0,0 +1,82 @@
+# Generated Rust view-model contract
+
+Schema version: `5`
+
+## Model `MainViewModel` (`1`)
+
+| Kind | ID | Name | Type | Direction |
+| --- | ---: | --- | --- | --- |
+| Property | 1 | `Title` | `String` | Rust to managed |
+| Property | 2 | `Status` | `String` | Rust to managed |
+| Property | 3 | `FileCountLabel` | `String` | Rust to managed |
+| Property | 4 | `ExpectedHash` | `String` | Rust and managed |
+| Property | 5 | `CompareStatus` | `String` | Rust to managed |
+| Property | 6 | `IncludeMd5` | `Boolean` | Rust and managed |
+| Property | 7 | `IncludeSha1` | `Boolean` | Rust and managed |
+| Property | 8 | `IncludeSha256` | `Boolean` | Rust and managed |
+| Property | 9 | `IncludeSha512` | `Boolean` | Rust and managed |
+| Property | 10 | `IncludeBlake3` | `Boolean` | Rust and managed |
+| Property | 11 | `IsBusy` | `Boolean` | Rust to managed |
+| Property | 12 | `CanClear` | `Boolean` | Rust to managed |
+| Property | 13 | `CanCopyReport` | `Boolean` | Rust to managed |
+| Property | 14 | `PrimaryDigest` | `String` | Rust to managed |
+| Property | 15 | `EmptyVisible` | `Boolean` | Rust to managed |
+| Collection | 1 | `RecentFiles` | `String` | Rust to managed |
+| Collection | 2 | `Files` | Model `FileRowViewModel` | Rust to managed |
+| Async command | 1 | `OpenFiles` | None | Managed to Rust |
+| Command | 2 | `ClearFiles` | None | Managed to Rust |
+| Async command | 3 | `CopyReport` | None | Managed to Rust |
+| Command | 4 | `OpenRecentFile` | None | Managed to Rust |
+| Command | 5 | `Exit` | None | Managed to Rust |
+
+### Recent files `RecentFiles`
+
+Storage URIs published into collection `RecentFiles`, capacity 8, activated by `OpenRecentFileCommand` with the chosen URI as its command parameter.
+
+### Application menu `Main` (`1`)
+
+| ID | Item | Kind | Header | Command | Gesture | Bound member |
+| ---: | --- | --- | --- | --- | --- | --- |
+| 1 | `File` | Submenu | _File | - | - | - |
+| 2 | `OpenFiles` | Command | _Open files... | `OpenFilesCommand` | `Ctrl+O` | - |
+| 3 | `Recent` | RecentFiles | Recent _files | - | - | recent files |
+| 4 | `CopyReport` | Command | _Copy report | `CopyReportCommand` | `Ctrl+Shift+C` | - |
+| 5 | `ClearFiles` | Command | C_lear list | `ClearFilesCommand` | `Ctrl+Shift+N` | - |
+| 6 | `FileSeparator` | Separator | - | - | - | - |
+| 7 | `Exit` | Command | E_xit | `ExitCommand` | `Ctrl+Q` | - |
+| 8 | `Hash` | Submenu | _Hash | - | - | - |
+| 9 | `IncludeMd5` | Toggle | _MD5 | - | - | `IncludeMd5` |
+| 10 | `IncludeSha1` | Toggle | SHA-_1 | - | - | `IncludeSha1` |
+| 11 | `IncludeSha256` | Toggle | SHA-_256 | - | - | `IncludeSha256` |
+| 12 | `IncludeSha512` | Toggle | SHA-_512 | - | - | `IncludeSha512` |
+| 13 | `IncludeBlake3` | Toggle | _BLAKE3 | - | - | `IncludeBlake3` |
+
+## Model `FileRowViewModel` (`2`)
+
+| Kind | ID | Name | Type | Direction |
+| --- | ---: | --- | --- | --- |
+| Property | 1 | `Name` | `String` | Rust to managed |
+| Property | 2 | `FilePath` | `String` | Rust to managed |
+| Property | 3 | `SizeLabel` | `String` | Rust to managed |
+| Property | 4 | `Status` | `String` | Rust to managed |
+| Property | 5 | `MatchLabel` | `String` | Rust to managed |
+| Property | 6 | `Md5` | `String` | Rust to managed |
+| Property | 7 | `Sha1` | `String` | Rust to managed |
+| Property | 8 | `Sha256` | `String` | Rust to managed |
+| Property | 9 | `Sha512` | `String` | Rust to managed |
+| Property | 10 | `Blake3` | `String` | Rust to managed |
+| Property | 11 | `ShowMd5` | `Boolean` | Rust to managed |
+| Property | 12 | `ShowSha1` | `Boolean` | Rust to managed |
+| Property | 13 | `ShowSha256` | `Boolean` | Rust to managed |
+| Property | 14 | `ShowSha512` | `Boolean` | Rust to managed |
+| Property | 15 | `ShowBlake3` | `Boolean` | Rust to managed |
+| Property | 16 | `CanCopySha256` | `Boolean` | Rust to managed |
+| Async command | 1 | `CopySha256` | None | Managed to Rust |
+| Async command | 2 | `CopyRow` | None | Managed to Rust |
+| Command | 3 | `Remove` | None | Managed to Rust |
+
+## Views
+
+| ID | Name | Model | Managed type | Binding path |
+| ---: | --- | --- | --- | --- |
+| 1 | `MainWindow` | `MainViewModel` | `HashCalculator.Presentation.Views.MainWindow` | Generated CLR properties |
diff --git a/apps/hash-calculator/global.json b/apps/hash-calculator/global.json
new file mode 100644
index 0000000..c745e9d
--- /dev/null
+++ b/apps/hash-calculator/global.json
@@ -0,0 +1,9 @@
+{
+ "sdk": {
+ "version": "10.0.201",
+ "rollForward": "latestFeature"
+ },
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ }
+}
diff --git a/apps/hash-calculator/managed/Consumer.Presentation.csproj b/apps/hash-calculator/managed/Consumer.Presentation.csproj
new file mode 100644
index 0000000..8b53fa4
--- /dev/null
+++ b/apps/hash-calculator/managed/Consumer.Presentation.csproj
@@ -0,0 +1,24 @@
+
+
+ net10.0
+ enable
+ true
+ false
+ true
+ true
+
+ $(MSBuildThisFileDirectory)../../../avalonia-src
+ $(MSBuildThisFileDirectory)../../..
+
+
+
+
+
+
+
+
+
diff --git a/apps/hash-calculator/managed/Generated/.avalonia-viewmodel.owned.json b/apps/hash-calculator/managed/Generated/.avalonia-viewmodel.owned.json
new file mode 100644
index 0000000..d00f631
--- /dev/null
+++ b/apps/hash-calculator/managed/Generated/.avalonia-viewmodel.owned.json
@@ -0,0 +1,25 @@
+{
+ "generator": "avalonia-viewmodel",
+ "files": [
+ {
+ "path": "FileRowViewModelAdapter.g.cs",
+ "sha256": "f0ed9a0fa23bf02cfc83d43eafc5638e67c63d1546ad2f4905d9d8d5cba772c4"
+ },
+ {
+ "path": "FileRowViewModelMetadata.g.cs",
+ "sha256": "141ae5acf10764b7d3a414d55b6891ab39efaa7845909b6241a8e62695414c44"
+ },
+ {
+ "path": "MainViewModelAdapter.g.cs",
+ "sha256": "9e162bb8fa023fa8c85eb1182c62afac44235db38ae01201bc20b4c2a877efdc"
+ },
+ {
+ "path": "MainViewModelMenus.g.cs",
+ "sha256": "f4efd045eb92113ab0db6b838532976b583ece7ea2bdf1ba5ced1475f8996efa"
+ },
+ {
+ "path": "MainViewModelMetadata.g.cs",
+ "sha256": "735a3d9e1af62469f8dbd1e50803680dab5d1a429910a46bfff5037b6b2be1fc"
+ }
+ ]
+}
diff --git a/apps/hash-calculator/managed/Generated/FileRowViewModelAdapter.g.cs b/apps/hash-calculator/managed/Generated/FileRowViewModelAdapter.g.cs
new file mode 100644
index 0000000..6a77682
--- /dev/null
+++ b/apps/hash-calculator/managed/Generated/FileRowViewModelAdapter.g.cs
@@ -0,0 +1,629 @@
+//
+#nullable enable
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.Marshalling;
+using System.Windows.Input;
+using Avalonia.Rust;
+using Avalonia.Rust.Interop;
+using Avalonia.Threading;
+
+namespace HashCalculator.Presentation.Generated;
+
+[GeneratedComClass]
+public sealed partial class FileRowViewModelAdapter : IAvnRustVmSink, IAvnRustVmSink2, IAvnRustVmSink3, IRustVmStringSnapshotSink, IRustVmModelSnapshotSink, IRustVmBatchTarget, IRustVmTableSelectionBatchTarget, INotifyPropertyChanged, INotifyDataErrorInfo, IDisposable
+{
+ private readonly IAvnRustViewModel _model;
+ private readonly Action _dispatch;
+ private readonly Action? _post;
+ private readonly RustVmBatchCoordinator _batch;
+ private readonly Dictionary _errors = new(StringComparer.Ordinal);
+ private readonly RustVmInboundWriteTracker _inboundWrites = new();
+ private string _name = "";
+ private string _filePath = "";
+ private string _sizeLabel = "";
+ private string _status = "";
+ private string _matchLabel = "";
+ private string _md5 = "";
+ private string _sha1 = "";
+ private string _sha256 = "";
+ private string _sha512 = "";
+ private string _blake3 = "";
+ private bool _showMd5 = false;
+ private bool _showSha1 = false;
+ private bool _showSha256 = true;
+ private bool _showSha512 = false;
+ private bool _showBlake3 = false;
+ private bool _canCopySha256 = false;
+
+ /// Creates an adapter that dispatches and posts through .
+ public FileRowViewModelAdapter(IAvnRustViewModel model) : this(model, null, null) { }
+
+ ///
+ /// Creates an adapter with a custom synchronous dispatch for the legacy v1/v2
+ /// sink path. Kept as a distinct CLR signature (not an optional parameter) so
+ /// already-compiled callers keep binding to it.
+ ///
+ public FileRowViewModelAdapter(IAvnRustViewModel model, Action? dispatch) : this(model, dispatch, null) { }
+
+ ///
+ /// Creates an adapter with a custom synchronous for the
+ /// legacy v1/v2 sink path and a custom nonblocking for
+ /// batch submission.
+ ///
+ public FileRowViewModelAdapter(IAvnRustViewModel model, Action? dispatch, Action? post)
+ {
+ _model = model;
+ _dispatch = dispatch ?? Dispatch;
+ _post = post;
+ _batch = new RustVmBatchCoordinator(this, post);
+ CopySha256Command = new DelegateCommand(parameter => Check(_model.BeginAsync(1, null)));
+ CopyRowCommand = new DelegateCommand(parameter => Check(_model.BeginAsync(2, null)));
+ RemoveCommand = new DelegateCommand(parameter => Check(_model.Execute(3, null)));
+ try
+ {
+ Check(_model.Attach(this));
+ }
+ catch
+ {
+ try { _model.Detach(); }
+ catch { }
+ DisposeNestedAdapters();
+ throw;
+ }
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+ public event EventHandler? ErrorsChanged;
+
+ public string Name
+ {
+ get => _name;
+ }
+
+ public string FilePath
+ {
+ get => _filePath;
+ }
+
+ public string SizeLabel
+ {
+ get => _sizeLabel;
+ }
+
+ public string Status
+ {
+ get => _status;
+ }
+
+ public string MatchLabel
+ {
+ get => _matchLabel;
+ }
+
+ public string Md5
+ {
+ get => _md5;
+ }
+
+ public string Sha1
+ {
+ get => _sha1;
+ }
+
+ public string Sha256
+ {
+ get => _sha256;
+ }
+
+ public string Sha512
+ {
+ get => _sha512;
+ }
+
+ public string Blake3
+ {
+ get => _blake3;
+ }
+
+ public bool ShowMd5
+ {
+ get => _showMd5;
+ }
+
+ public bool ShowSha1
+ {
+ get => _showSha1;
+ }
+
+ public bool ShowSha256
+ {
+ get => _showSha256;
+ }
+
+ public bool ShowSha512
+ {
+ get => _showSha512;
+ }
+
+ public bool ShowBlake3
+ {
+ get => _showBlake3;
+ }
+
+ public bool CanCopySha256
+ {
+ get => _canCopySha256;
+ }
+
+ public DelegateCommand CopySha256Command { get; }
+ public DelegateCommand CopyRowCommand { get; }
+ public DelegateCommand RemoveCommand { get; }
+
+ public bool HasErrors => _errors.Count > 0;
+
+ public IEnumerable GetErrors(string? propertyName) =>
+ propertyName is not null && _errors.TryGetValue(propertyName, out var message)
+ ? new[] { message }
+ : Array.Empty();
+
+ public int SetString(int propertyId, string? value)
+ {
+ var inbound = _inboundWrites.MarkPublication(propertyId);
+ return propertyId switch
+ {
+ 1 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_name, converted)) SetField(ref _name, converted, nameof(Name)); }),
+ 2 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_filePath, converted)) SetField(ref _filePath, converted, nameof(FilePath)); }),
+ 3 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_sizeLabel, converted)) SetField(ref _sizeLabel, converted, nameof(SizeLabel)); }),
+ 4 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_status, converted)) SetField(ref _status, converted, nameof(Status)); }),
+ 5 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_matchLabel, converted)) SetField(ref _matchLabel, converted, nameof(MatchLabel)); }),
+ 6 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_md5, converted)) SetField(ref _md5, converted, nameof(Md5)); }),
+ 7 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_sha1, converted)) SetField(ref _sha1, converted, nameof(Sha1)); }),
+ 8 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_sha256, converted)) SetField(ref _sha256, converted, nameof(Sha256)); }),
+ 9 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_sha512, converted)) SetField(ref _sha512, converted, nameof(Sha512)); }),
+ 10 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_blake3, converted)) SetField(ref _blake3, converted, nameof(Blake3)); }),
+ _ => unchecked((int)0x80070057),
+ };
+ }
+
+ public int SetInteger(int propertyId, long value)
+ {
+ var inbound = _inboundWrites.MarkPublication(propertyId);
+ return propertyId switch
+ {
+ _ => unchecked((int)0x80070057),
+ };
+ }
+
+ public int SetBoolean(int propertyId, int value)
+ {
+ var inbound = _inboundWrites.MarkPublication(propertyId);
+ return propertyId switch
+ {
+ 11 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_showMd5, converted)) SetField(ref _showMd5, converted, nameof(ShowMd5)); }),
+ 12 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_showSha1, converted)) SetField(ref _showSha1, converted, nameof(ShowSha1)); }),
+ 13 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_showSha256, converted)) SetField(ref _showSha256, converted, nameof(ShowSha256)); }),
+ 14 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_showSha512, converted)) SetField(ref _showSha512, converted, nameof(ShowSha512)); }),
+ 15 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_showBlake3, converted)) SetField(ref _showBlake3, converted, nameof(ShowBlake3)); }),
+ 16 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_canCopySha256, converted)) SetField(ref _canCopySha256, converted, nameof(CanCopySha256)); }),
+ _ => unchecked((int)0x80070057),
+ };
+ }
+
+ public int SetDouble(int propertyId, double value)
+ {
+ var inbound = _inboundWrites.MarkPublication(propertyId);
+ return propertyId switch
+ {
+ _ => unchecked((int)0x80070057),
+ };
+ }
+
+ public int SetNull(int propertyId)
+ {
+ var inbound = _inboundWrites.MarkPublication(propertyId);
+ return propertyId switch
+ {
+ _ => unchecked((int)0x80070057),
+ };
+ }
+
+ public int SetModel(int propertyId, IAvnRustViewModel? model) => propertyId switch
+ {
+ _ => unchecked((int)0x80070057),
+ };
+
+ public int AddModel(int collectionId, IAvnRustViewModel? model) => collectionId switch
+ {
+ _ => unchecked((int)0x80070057),
+ };
+
+ public int InsertString(int collectionId, int index, string? value) => collectionId switch
+ {
+ _ => unchecked((int)0x80070057),
+ };
+
+ public int InsertModel(int collectionId, int index, IAvnRustViewModel? model) => collectionId switch
+ {
+ _ => unchecked((int)0x80070057),
+ };
+
+ public int ReplaceString(int collectionId, int index, string? value) => collectionId switch
+ {
+ _ => unchecked((int)0x80070057),
+ };
+
+ public int ReplaceModel(int collectionId, int index, IAvnRustViewModel? model) => collectionId switch
+ {
+ _ => unchecked((int)0x80070057),
+ };
+
+ public int RemoveAt(int collectionId, int index) => collectionId switch
+ {
+ _ => unchecked((int)0x80070057),
+ };
+
+ public int MoveItem(int collectionId, int fromIndex, int toIndex) => collectionId switch
+ {
+ _ => unchecked((int)0x80070057),
+ };
+
+ public int ClearCollection(int collectionId) => collectionId switch
+ {
+ _ => unchecked((int)0x80070057),
+ };
+
+ public int SetCommandEnabled(int commandId, int enabled) => commandId switch
+ {
+ 1 => Apply(() => CopySha256Command.SetEnabled(enabled != 0)),
+ 2 => Apply(() => CopyRowCommand.SetEnabled(enabled != 0)),
+ 3 => Apply(() => RemoveCommand.SetEnabled(enabled != 0)),
+ _ => unchecked((int)0x80070057),
+ };
+
+ public int SetPropertyError(int propertyId, string? message) => propertyId switch
+ {
+ 1 => Apply(() => SetError(nameof(Name), message)),
+ 2 => Apply(() => SetError(nameof(FilePath), message)),
+ 3 => Apply(() => SetError(nameof(SizeLabel), message)),
+ 4 => Apply(() => SetError(nameof(Status), message)),
+ 5 => Apply(() => SetError(nameof(MatchLabel), message)),
+ 6 => Apply(() => SetError(nameof(Md5), message)),
+ 7 => Apply(() => SetError(nameof(Sha1), message)),
+ 8 => Apply(() => SetError(nameof(Sha256), message)),
+ 9 => Apply(() => SetError(nameof(Sha512), message)),
+ 10 => Apply(() => SetError(nameof(Blake3), message)),
+ 11 => Apply(() => SetError(nameof(ShowMd5), message)),
+ 12 => Apply(() => SetError(nameof(ShowSha1), message)),
+ 13 => Apply(() => SetError(nameof(ShowSha256), message)),
+ 14 => Apply(() => SetError(nameof(ShowSha512), message)),
+ 15 => Apply(() => SetError(nameof(ShowBlake3), message)),
+ 16 => Apply(() => SetError(nameof(CanCopySha256), message)),
+ _ => unchecked((int)0x80070057),
+ };
+
+ public int AddString(int collectionId, string? value) => collectionId switch
+ {
+ _ => unchecked((int)0x80070057),
+ };
+
+ public int ReplaceStringSnapshot(int collectionId, IReadOnlyList values) => collectionId switch
+ {
+ _ => unchecked((int)0x80070057),
+ };
+
+ public int ReplaceModelSnapshot(int collectionId, IReadOnlyList values) => collectionId switch
+ {
+ _ => unchecked((int)0x80070057),
+ };
+
+ bool IRustVmBatchTarget.TryGetProperty(int propertyId, out RustVmBatchProperty property)
+ {
+ property = propertyId switch
+ {
+ 1 => new RustVmBatchProperty(nameof(Name), RustVmValueWireKind.String, false, false),
+ 2 => new RustVmBatchProperty(nameof(FilePath), RustVmValueWireKind.String, false, false),
+ 3 => new RustVmBatchProperty(nameof(SizeLabel), RustVmValueWireKind.String, false, false),
+ 4 => new RustVmBatchProperty(nameof(Status), RustVmValueWireKind.String, false, false),
+ 5 => new RustVmBatchProperty(nameof(MatchLabel), RustVmValueWireKind.String, false, false),
+ 6 => new RustVmBatchProperty(nameof(Md5), RustVmValueWireKind.String, false, false),
+ 7 => new RustVmBatchProperty(nameof(Sha1), RustVmValueWireKind.String, false, false),
+ 8 => new RustVmBatchProperty(nameof(Sha256), RustVmValueWireKind.String, false, false),
+ 9 => new RustVmBatchProperty(nameof(Sha512), RustVmValueWireKind.String, false, false),
+ 10 => new RustVmBatchProperty(nameof(Blake3), RustVmValueWireKind.String, false, false),
+ 11 => new RustVmBatchProperty(nameof(ShowMd5), RustVmValueWireKind.Boolean, false, false),
+ 12 => new RustVmBatchProperty(nameof(ShowSha1), RustVmValueWireKind.Boolean, false, false),
+ 13 => new RustVmBatchProperty(nameof(ShowSha256), RustVmValueWireKind.Boolean, false, false),
+ 14 => new RustVmBatchProperty(nameof(ShowSha512), RustVmValueWireKind.Boolean, false, false),
+ 15 => new RustVmBatchProperty(nameof(ShowBlake3), RustVmValueWireKind.Boolean, false, false),
+ 16 => new RustVmBatchProperty(nameof(CanCopySha256), RustVmValueWireKind.Boolean, false, false),
+ _ => default,
+ };
+ return property.Name is not null;
+ }
+
+ bool IRustVmBatchTarget.TryGetCollection(int collectionId, out RustVmBatchCollectionInfo collection)
+ {
+ collection = collectionId switch
+ {
+ _ => default,
+ };
+ return collection.Items is not null;
+ }
+
+ bool IRustVmBatchTarget.TryGetCommand(int commandId, out IRustVmBatchCommand command)
+ {
+ command = commandId switch
+ {
+ 1 => CopySha256Command,
+ 2 => CopyRowCommand,
+ 3 => RemoveCommand,
+ _ => null!,
+ };
+ return command is not null;
+ }
+
+ bool IRustVmBatchTarget.IsEnumValueDefined(int propertyId, long value) => propertyId switch
+ {
+ _ => false,
+ };
+
+ IDisposable IRustVmBatchTarget.CreateNestedProperty(int propertyId, IAvnRustViewModel model) => propertyId switch
+ {
+ _ => throw new ArgumentOutOfRangeException(nameof(propertyId)),
+ };
+
+ IDisposable IRustVmBatchTarget.CreateNestedElement(int collectionId, IAvnRustViewModel model) => collectionId switch
+ {
+ _ => throw new ArgumentOutOfRangeException(nameof(collectionId)),
+ };
+
+ bool IRustVmBatchTarget.CommitProperty(int propertyId, in RustVmBatchValue value, out IDisposable? replaced)
+ {
+ replaced = null;
+ switch (propertyId)
+ {
+ case 1:
+ {
+ var next = value.Text ?? "";
+ if (Equals(_name, next)) return false;
+ _name = next;
+ return true;
+ }
+ case 2:
+ {
+ var next = value.Text ?? "";
+ if (Equals(_filePath, next)) return false;
+ _filePath = next;
+ return true;
+ }
+ case 3:
+ {
+ var next = value.Text ?? "";
+ if (Equals(_sizeLabel, next)) return false;
+ _sizeLabel = next;
+ return true;
+ }
+ case 4:
+ {
+ var next = value.Text ?? "";
+ if (Equals(_status, next)) return false;
+ _status = next;
+ return true;
+ }
+ case 5:
+ {
+ var next = value.Text ?? "";
+ if (Equals(_matchLabel, next)) return false;
+ _matchLabel = next;
+ return true;
+ }
+ case 6:
+ {
+ var next = value.Text ?? "";
+ if (Equals(_md5, next)) return false;
+ _md5 = next;
+ return true;
+ }
+ case 7:
+ {
+ var next = value.Text ?? "";
+ if (Equals(_sha1, next)) return false;
+ _sha1 = next;
+ return true;
+ }
+ case 8:
+ {
+ var next = value.Text ?? "";
+ if (Equals(_sha256, next)) return false;
+ _sha256 = next;
+ return true;
+ }
+ case 9:
+ {
+ var next = value.Text ?? "";
+ if (Equals(_sha512, next)) return false;
+ _sha512 = next;
+ return true;
+ }
+ case 10:
+ {
+ var next = value.Text ?? "";
+ if (Equals(_blake3, next)) return false;
+ _blake3 = next;
+ return true;
+ }
+ case 11:
+ {
+ var next = value.Boolean;
+ if (Equals(_showMd5, next)) return false;
+ _showMd5 = next;
+ return true;
+ }
+ case 12:
+ {
+ var next = value.Boolean;
+ if (Equals(_showSha1, next)) return false;
+ _showSha1 = next;
+ return true;
+ }
+ case 13:
+ {
+ var next = value.Boolean;
+ if (Equals(_showSha256, next)) return false;
+ _showSha256 = next;
+ return true;
+ }
+ case 14:
+ {
+ var next = value.Boolean;
+ if (Equals(_showSha512, next)) return false;
+ _showSha512 = next;
+ return true;
+ }
+ case 15:
+ {
+ var next = value.Boolean;
+ if (Equals(_showBlake3, next)) return false;
+ _showBlake3 = next;
+ return true;
+ }
+ case 16:
+ {
+ var next = value.Boolean;
+ if (Equals(_canCopySha256, next)) return false;
+ _canCopySha256 = next;
+ return true;
+ }
+ default: return false;
+ }
+ }
+
+ bool IRustVmBatchTarget.CommitError(string propertyName, string? message) =>
+ RustVmBatchErrors.Set(_errors, propertyName, message);
+
+ bool IRustVmTableSelectionBatchTarget.IsPostCollectionPropertyNotification(string propertyName, IReadOnlySet changedCollections) => propertyName switch
+ {
+ _ => false,
+ };
+
+ void IRustVmBatchTarget.RaisePropertyChanged(string propertyName) =>
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+
+ void IRustVmBatchTarget.RaiseErrorsChanged(string propertyName) =>
+ ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
+
+ ///
+ /// Enqueues one immutable batch. This call never reads the batch, applies it,
+ /// or completes it on the submitting (Rust worker) stack.
+ ///
+ public int SubmitBatch(IAvnRustVmUpdateBatch? batch) => _batch.Submit(batch);
+
+ ///
+ /// Detaches the model and disposes every nested adapter exactly once. When a
+ /// batch notification triggers this re-entrantly, the gate defers the cleanup
+ /// until the batch's commit and notifications have finished.
+ ///
+ public void Dispose() => _batch.Dispose(DisposeCore);
+
+ private void DisposeCore()
+ {
+ try
+ {
+ Check(_model.Detach());
+ }
+ finally
+ {
+ DisposeNestedAdapters();
+ }
+ }
+
+ private void DisposeNestedAdapters()
+ {
+ }
+
+ private static void TryDispose(IDisposable? value)
+ {
+ try { value?.Dispose(); }
+ catch { }
+ }
+
+ private int Apply(Action action) => Apply(() =>
+ {
+ action();
+ return 0;
+ });
+
+ private int Apply(Func action)
+ {
+ if (_batch.IsClosed) return 0;
+ var hresult = 0;
+ void ApplyIfAlive()
+ {
+ if (!_batch.IsClosed)
+ {
+ try { hresult = action(); }
+ catch { hresult = unchecked((int)0x80004005); }
+ }
+ }
+ try { _dispatch(ApplyIfAlive); }
+ catch { return unchecked((int)0x80004005); }
+ return hresult;
+ }
+
+ private static void Dispatch(Action action)
+ {
+ if (Dispatcher.UIThread.CheckAccess()) action();
+ else Dispatcher.UIThread.Invoke(action);
+ }
+
+ private static void Check(int hresult)
+ {
+ if (hresult < 0) Marshal.ThrowExceptionForHR(hresult);
+ }
+
+ private void SetField(ref T field, T value, [CallerMemberName] string? propertyName = null)
+ {
+ if (Equals(field, value)) return;
+ field = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ private void SetError(string propertyName, string? message)
+ {
+ if (RustVmBatchErrors.Set(_errors, propertyName, message))
+ ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
+ }
+
+ public sealed class DelegateCommand(Action