From 7445355e460676a51e4a466ea920cd24cece6e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Wed, 9 Sep 2026 12:16:10 -0400 Subject: [PATCH] Add hash calculator sample app Adds a sibling apps/hash-calculator consumer that streams MD5, SHA-1, SHA-256, SHA-512 and BLAKE3 checksums from Rust, with compare-against- digest, copy report, and a compiled Avalonia UI. ## What changed - Scaffold the external consumer (IR, AXAML, worker-thread hashing) - Default SHA-256 sample file; Open files picker and algorithm toggles - Defer per-row Remove so the file list is not rebuilt while the command is still running - Windows STA UI Automation smoke for sample SHA-256, MD5 toggle, and compare ## Verification - cargo test --locked in apps/hash-calculator (9 tests) - Windows portable NativeAOT bundle build - powershell.exe -NoProfile -STA test-bundle.ps1 --- README.md | 1 + apps/hash-calculator/.cargo/config.toml | 5 + apps/hash-calculator/.gitignore | 6 + apps/hash-calculator/Cargo.lock | 212 +++++ apps/hash-calculator/Cargo.toml | 17 + apps/hash-calculator/README.md | 50 ++ apps/hash-calculator/avalonia-app.json | 15 + .../file-associations/README.md | 38 + .../linux-desktop-entry.desktop | 16 + .../file-associations/linux-mime-type.xml | 18 + .../macos-Info.plist.snippet | 54 ++ .../windows-file-association.reg | 27 + .../generated/.avalonia-viewmodel.owned.json | 17 + .../generated/RustViewRegistry.g.cs | 45 + .../generated/generated_view_models.rs | 345 ++++++++ .../generated/view-model.contract.md | 82 ++ apps/hash-calculator/global.json | 9 + .../managed/Consumer.Presentation.csproj | 24 + .../Generated/.avalonia-viewmodel.owned.json | 25 + .../Generated/FileRowViewModelAdapter.g.cs | 629 +++++++++++++ .../Generated/FileRowViewModelMetadata.g.cs | 40 + .../Generated/MainViewModelAdapter.g.cs | 830 ++++++++++++++++++ .../managed/Generated/MainViewModelMenus.g.cs | 127 +++ .../Generated/MainViewModelMetadata.g.cs | 43 + .../managed/Views/MainWindow.axaml | 390 ++++++++ .../managed/Views/MainWindow.axaml.cs | 34 + apps/hash-calculator/src/hash.rs | 343 ++++++++ apps/hash-calculator/src/main.rs | 779 ++++++++++++++++ apps/hash-calculator/tests/test-bundle.ps1 | 169 ++++ apps/hash-calculator/view-model.ir.json | 478 ++++++++++ 30 files changed, 4868 insertions(+) create mode 100644 apps/hash-calculator/.cargo/config.toml create mode 100644 apps/hash-calculator/.gitignore create mode 100644 apps/hash-calculator/Cargo.lock create mode 100644 apps/hash-calculator/Cargo.toml create mode 100644 apps/hash-calculator/README.md create mode 100644 apps/hash-calculator/avalonia-app.json create mode 100644 apps/hash-calculator/file-associations/README.md create mode 100644 apps/hash-calculator/file-associations/linux-desktop-entry.desktop create mode 100644 apps/hash-calculator/file-associations/linux-mime-type.xml create mode 100644 apps/hash-calculator/file-associations/macos-Info.plist.snippet create mode 100644 apps/hash-calculator/file-associations/windows-file-association.reg create mode 100644 apps/hash-calculator/generated/.avalonia-viewmodel.owned.json create mode 100644 apps/hash-calculator/generated/RustViewRegistry.g.cs create mode 100644 apps/hash-calculator/generated/generated_view_models.rs create mode 100644 apps/hash-calculator/generated/view-model.contract.md create mode 100644 apps/hash-calculator/global.json create mode 100644 apps/hash-calculator/managed/Consumer.Presentation.csproj create mode 100644 apps/hash-calculator/managed/Generated/.avalonia-viewmodel.owned.json create mode 100644 apps/hash-calculator/managed/Generated/FileRowViewModelAdapter.g.cs create mode 100644 apps/hash-calculator/managed/Generated/FileRowViewModelMetadata.g.cs create mode 100644 apps/hash-calculator/managed/Generated/MainViewModelAdapter.g.cs create mode 100644 apps/hash-calculator/managed/Generated/MainViewModelMenus.g.cs create mode 100644 apps/hash-calculator/managed/Generated/MainViewModelMetadata.g.cs create mode 100644 apps/hash-calculator/managed/Views/MainWindow.axaml create mode 100644 apps/hash-calculator/managed/Views/MainWindow.axaml.cs create mode 100644 apps/hash-calculator/src/hash.rs create mode 100644 apps/hash-calculator/src/main.rs create mode 100644 apps/hash-calculator/tests/test-bundle.ps1 create mode 100644 apps/hash-calculator/view-model.ir.json 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 execute) : ICommand, IRustVmBatchCommand + { + private bool _canExecute = true; + + public DelegateCommand(Action execute) : this(_ => execute()) { } + + public event EventHandler? CanExecuteChanged; + public bool CanExecute(object? parameter) => _canExecute; + public void Execute(object? parameter) => execute(parameter); + + public void SetEnabled(bool value) + { + if (SetEnabledCore(value)) RaiseCanExecuteChanged(); + } + + public bool SetEnabledCore(bool enabled) + { + if (_canExecute == enabled) return false; + _canExecute = enabled; + return true; + } + + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } +} diff --git a/apps/hash-calculator/managed/Generated/FileRowViewModelMetadata.g.cs b/apps/hash-calculator/managed/Generated/FileRowViewModelMetadata.g.cs new file mode 100644 index 0000000..c528baa --- /dev/null +++ b/apps/hash-calculator/managed/Generated/FileRowViewModelMetadata.g.cs @@ -0,0 +1,40 @@ +// +#nullable enable +using System.Collections.Generic; +using Avalonia.Rust; + +namespace HashCalculator.Presentation.Generated; + +public static class FileRowViewModelMetadata +{ + public static RustViewModelDescriptor Descriptor { get; } = new( + 2, + "FileRowViewModel", + [ + new(1, "Name", RustViewModelValueKind.String, false, false, "", null), + new(2, "FilePath", RustViewModelValueKind.String, false, false, "", null), + new(3, "SizeLabel", RustViewModelValueKind.String, false, false, "", null), + new(4, "Status", RustViewModelValueKind.String, false, false, "", null), + new(5, "MatchLabel", RustViewModelValueKind.String, false, false, "", null), + new(6, "Md5", RustViewModelValueKind.String, false, false, "", null), + new(7, "Sha1", RustViewModelValueKind.String, false, false, "", null), + new(8, "Sha256", RustViewModelValueKind.String, false, false, "", null), + new(9, "Sha512", RustViewModelValueKind.String, false, false, "", null), + new(10, "Blake3", RustViewModelValueKind.String, false, false, "", null), + new(11, "ShowMd5", RustViewModelValueKind.Boolean, false, false, false, null), + new(12, "ShowSha1", RustViewModelValueKind.Boolean, false, false, false, null), + new(13, "ShowSha256", RustViewModelValueKind.Boolean, false, false, true, null), + new(14, "ShowSha512", RustViewModelValueKind.Boolean, false, false, false, null), + new(15, "ShowBlake3", RustViewModelValueKind.Boolean, false, false, false, null), + new(16, "CanCopySha256", RustViewModelValueKind.Boolean, false, false, false, null), + ], + [ + ], + [ + new(1, "CopySha256Command", true, null, false, null, false, false), + new(2, "CopyRowCommand", true, null, false, null, false, false), + new(3, "RemoveCommand", false, null, false, null, false, false), + ], + [ + ]); +} diff --git a/apps/hash-calculator/managed/Generated/MainViewModelAdapter.g.cs b/apps/hash-calculator/managed/Generated/MainViewModelAdapter.g.cs new file mode 100644 index 0000000..44365b2 --- /dev/null +++ b/apps/hash-calculator/managed/Generated/MainViewModelAdapter.g.cs @@ -0,0 +1,830 @@ +// +#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 MainViewModelAdapter : 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 _title = "Hash Calculator"; + private string _status = "Preparing sample file..."; + private string _fileCountLabel = "No files"; + private string _expectedHash = ""; + private string _compareStatus = "Paste a digest to compare"; + private bool _includeMd5 = false; + private bool _includeSha1 = false; + private bool _includeSha256 = true; + private bool _includeSha512 = false; + private bool _includeBlake3 = false; + private bool _isBusy = true; + private bool _canClear = false; + private bool _canCopyReport = false; + private string _primaryDigest = ""; + private bool _emptyVisible = false; + + /// Creates an adapter that dispatches and posts through . + public MainViewModelAdapter(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 MainViewModelAdapter(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 MainViewModelAdapter(IAvnRustViewModel model, Action? dispatch, Action? post) + { + _model = model; + _dispatch = dispatch ?? Dispatch; + _post = post; + _batch = new RustVmBatchCoordinator(this, post); + OpenFilesCommand = new DelegateCommand(parameter => Check(_model.BeginAsync(1, null))); + ClearFilesCommand = new DelegateCommand(parameter => Check(_model.Execute(2, null))); + CopyReportCommand = new DelegateCommand(parameter => Check(_model.BeginAsync(3, null))); + OpenRecentFileCommand = new DelegateCommand(parameter => Check(_model.Execute(4, parameter as string))); + ExitCommand = new DelegateCommand(parameter => Check(_model.Execute(5, null))); + try + { + Check(_model.Attach(this)); + } + catch + { + try { _model.Detach(); } + catch { } + DisposeNestedAdapters(); + throw; + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + public event EventHandler? ErrorsChanged; + + public string Title + { + get => _title; + } + + public string Status + { + get => _status; + } + + public string FileCountLabel + { + get => _fileCountLabel; + } + + public string ExpectedHash + { + get => _expectedHash; + set + { + var accepted = value ?? ""; + if (Equals(_expectedHash, accepted)) + return; + var previous = _expectedHash; + var inbound = _inboundWrites.Begin(4); + try + { + Check(_model.SetString(4, accepted)); + if (!_inboundWrites.WasPublished(inbound)) + { + _inboundWrites.CommitLocal(4); + SetField(ref _expectedHash, accepted, nameof(ExpectedHash)); + } + } + catch + { + if (_inboundWrites.ShouldRollback(inbound)) + { + _inboundWrites.CommitLocal(4); + SetField(ref _expectedHash, previous, nameof(ExpectedHash)); + } + throw; + } + finally { _inboundWrites.End(inbound); } + } + } + + public string CompareStatus + { + get => _compareStatus; + } + + public bool IncludeMd5 + { + get => _includeMd5; + set + { + var accepted = value; + if (Equals(_includeMd5, accepted)) + return; + var previous = _includeMd5; + var inbound = _inboundWrites.Begin(6); + try + { + Check(_model.SetBoolean(6, (accepted ? 1 : 0))); + if (!_inboundWrites.WasPublished(inbound)) + { + _inboundWrites.CommitLocal(6); + SetField(ref _includeMd5, accepted, nameof(IncludeMd5)); + } + } + catch + { + if (_inboundWrites.ShouldRollback(inbound)) + { + _inboundWrites.CommitLocal(6); + SetField(ref _includeMd5, previous, nameof(IncludeMd5)); + } + throw; + } + finally { _inboundWrites.End(inbound); } + } + } + + public bool IncludeSha1 + { + get => _includeSha1; + set + { + var accepted = value; + if (Equals(_includeSha1, accepted)) + return; + var previous = _includeSha1; + var inbound = _inboundWrites.Begin(7); + try + { + Check(_model.SetBoolean(7, (accepted ? 1 : 0))); + if (!_inboundWrites.WasPublished(inbound)) + { + _inboundWrites.CommitLocal(7); + SetField(ref _includeSha1, accepted, nameof(IncludeSha1)); + } + } + catch + { + if (_inboundWrites.ShouldRollback(inbound)) + { + _inboundWrites.CommitLocal(7); + SetField(ref _includeSha1, previous, nameof(IncludeSha1)); + } + throw; + } + finally { _inboundWrites.End(inbound); } + } + } + + public bool IncludeSha256 + { + get => _includeSha256; + set + { + var accepted = value; + if (Equals(_includeSha256, accepted)) + return; + var previous = _includeSha256; + var inbound = _inboundWrites.Begin(8); + try + { + Check(_model.SetBoolean(8, (accepted ? 1 : 0))); + if (!_inboundWrites.WasPublished(inbound)) + { + _inboundWrites.CommitLocal(8); + SetField(ref _includeSha256, accepted, nameof(IncludeSha256)); + } + } + catch + { + if (_inboundWrites.ShouldRollback(inbound)) + { + _inboundWrites.CommitLocal(8); + SetField(ref _includeSha256, previous, nameof(IncludeSha256)); + } + throw; + } + finally { _inboundWrites.End(inbound); } + } + } + + public bool IncludeSha512 + { + get => _includeSha512; + set + { + var accepted = value; + if (Equals(_includeSha512, accepted)) + return; + var previous = _includeSha512; + var inbound = _inboundWrites.Begin(9); + try + { + Check(_model.SetBoolean(9, (accepted ? 1 : 0))); + if (!_inboundWrites.WasPublished(inbound)) + { + _inboundWrites.CommitLocal(9); + SetField(ref _includeSha512, accepted, nameof(IncludeSha512)); + } + } + catch + { + if (_inboundWrites.ShouldRollback(inbound)) + { + _inboundWrites.CommitLocal(9); + SetField(ref _includeSha512, previous, nameof(IncludeSha512)); + } + throw; + } + finally { _inboundWrites.End(inbound); } + } + } + + public bool IncludeBlake3 + { + get => _includeBlake3; + set + { + var accepted = value; + if (Equals(_includeBlake3, accepted)) + return; + var previous = _includeBlake3; + var inbound = _inboundWrites.Begin(10); + try + { + Check(_model.SetBoolean(10, (accepted ? 1 : 0))); + if (!_inboundWrites.WasPublished(inbound)) + { + _inboundWrites.CommitLocal(10); + SetField(ref _includeBlake3, accepted, nameof(IncludeBlake3)); + } + } + catch + { + if (_inboundWrites.ShouldRollback(inbound)) + { + _inboundWrites.CommitLocal(10); + SetField(ref _includeBlake3, previous, nameof(IncludeBlake3)); + } + throw; + } + finally { _inboundWrites.End(inbound); } + } + } + + public bool IsBusy + { + get => _isBusy; + } + + public bool CanClear + { + get => _canClear; + } + + public bool CanCopyReport + { + get => _canCopyReport; + } + + public string PrimaryDigest + { + get => _primaryDigest; + } + + public bool EmptyVisible + { + get => _emptyVisible; + } + + public BatchObservableCollection RecentFiles { get; } = []; + public BatchObservableCollection Files { get; } = []; + + public DelegateCommand OpenFilesCommand { get; } + public DelegateCommand ClearFilesCommand { get; } + public DelegateCommand CopyReportCommand { get; } + public DelegateCommand OpenRecentFileCommand { get; } + public DelegateCommand ExitCommand { 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(_title, converted)) SetField(ref _title, converted, nameof(Title)); }), + 2 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_status, converted)) SetField(ref _status, converted, nameof(Status)); }), + 3 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_fileCountLabel, converted)) SetField(ref _fileCountLabel, converted, nameof(FileCountLabel)); }), + 4 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_expectedHash, converted)) SetField(ref _expectedHash, converted, nameof(ExpectedHash)); }), + 5 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_compareStatus, converted)) SetField(ref _compareStatus, converted, nameof(CompareStatus)); }), + 14 => Apply(() => { var converted = value ?? ""; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_primaryDigest, converted)) SetField(ref _primaryDigest, converted, nameof(PrimaryDigest)); }), + _ => 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 + { + 6 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_includeMd5, converted)) SetField(ref _includeMd5, converted, nameof(IncludeMd5)); }), + 7 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_includeSha1, converted)) SetField(ref _includeSha1, converted, nameof(IncludeSha1)); }), + 8 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_includeSha256, converted)) SetField(ref _includeSha256, converted, nameof(IncludeSha256)); }), + 9 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_includeSha512, converted)) SetField(ref _includeSha512, converted, nameof(IncludeSha512)); }), + 10 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_includeBlake3, converted)) SetField(ref _includeBlake3, converted, nameof(IncludeBlake3)); }), + 11 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_isBusy, converted)) SetField(ref _isBusy, converted, nameof(IsBusy)); }), + 12 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_canClear, converted)) SetField(ref _canClear, converted, nameof(CanClear)); }), + 13 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_canCopyReport, converted)) SetField(ref _canCopyReport, converted, nameof(CanCopyReport)); }), + 15 => Apply(() => { var converted = value != 0; _inboundWrites.CommitPublication(propertyId, inbound); if (!Equals(_emptyVisible, converted)) SetField(ref _emptyVisible, converted, nameof(EmptyVisible)); }), + _ => 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 + { + 2 => model is null ? unchecked((int)0x80070057) : Apply(() => Files.Add(new global::HashCalculator.Presentation.Generated.FileRowViewModelAdapter(model, _dispatch, _post))), + _ => unchecked((int)0x80070057), + }; + + public int InsertString(int collectionId, int index, string? value) => collectionId switch + { + 1 => Apply(() => { if ((uint)index > (uint)RecentFiles.Count) return unchecked((int)0x80070057); RecentFiles.Insert(index, value ?? ""); return 0; }), + _ => unchecked((int)0x80070057), + }; + + public int InsertModel(int collectionId, int index, IAvnRustViewModel? model) => collectionId switch + { + 2 => model is null ? unchecked((int)0x80070057) : Apply(() => { if ((uint)index > (uint)Files.Count) return unchecked((int)0x80070057); Files.Insert(index, new global::HashCalculator.Presentation.Generated.FileRowViewModelAdapter(model, _dispatch, _post)); return 0; }), + _ => unchecked((int)0x80070057), + }; + + public int ReplaceString(int collectionId, int index, string? value) => collectionId switch + { + 1 => Apply(() => { if ((uint)index >= (uint)RecentFiles.Count) return unchecked((int)0x80070057); RecentFiles[index] = value ?? ""; return 0; }), + _ => unchecked((int)0x80070057), + }; + + public int ReplaceModel(int collectionId, int index, IAvnRustViewModel? model) => collectionId switch + { + 2 => model is null ? unchecked((int)0x80070057) : Apply(() => + { + if ((uint)index >= (uint)Files.Count) return unchecked((int)0x80070057); + var previous = Files[index]; + Files[index] = new global::HashCalculator.Presentation.Generated.FileRowViewModelAdapter(model, _dispatch, _post); + previous.Dispose(); + return 0; + }), + _ => unchecked((int)0x80070057), + }; + + public int RemoveAt(int collectionId, int index) => collectionId switch + { + 1 => Apply(() => { if ((uint)index >= (uint)RecentFiles.Count) return unchecked((int)0x80070057); RecentFiles.RemoveAt(index); return 0; }), + 2 => Apply(() => + { + if ((uint)index >= (uint)Files.Count) return unchecked((int)0x80070057); + var item = Files[index]; + Files.RemoveAt(index); + item.Dispose(); + return 0; + }), + _ => unchecked((int)0x80070057), + }; + + public int MoveItem(int collectionId, int fromIndex, int toIndex) => collectionId switch + { + 1 => Apply(() => { if ((uint)fromIndex >= (uint)RecentFiles.Count || (uint)toIndex >= (uint)RecentFiles.Count) return unchecked((int)0x80070057); RecentFiles.Move(fromIndex, toIndex); return 0; }), + 2 => Apply(() => { if ((uint)fromIndex >= (uint)Files.Count || (uint)toIndex >= (uint)Files.Count) return unchecked((int)0x80070057); Files.Move(fromIndex, toIndex); return 0; }), + _ => unchecked((int)0x80070057), + }; + + public int ClearCollection(int collectionId) => collectionId switch + { + 1 => Apply(RecentFiles.Clear), + 2 => Apply(() => + { + foreach (var item in Files) item.Dispose(); + Files.Clear(); + }), + _ => unchecked((int)0x80070057), + }; + + public int SetCommandEnabled(int commandId, int enabled) => commandId switch + { + 1 => Apply(() => OpenFilesCommand.SetEnabled(enabled != 0)), + 2 => Apply(() => ClearFilesCommand.SetEnabled(enabled != 0)), + 3 => Apply(() => CopyReportCommand.SetEnabled(enabled != 0)), + 4 => Apply(() => OpenRecentFileCommand.SetEnabled(enabled != 0)), + 5 => Apply(() => ExitCommand.SetEnabled(enabled != 0)), + _ => unchecked((int)0x80070057), + }; + + public int SetPropertyError(int propertyId, string? message) => propertyId switch + { + 1 => Apply(() => SetError(nameof(Title), message)), + 2 => Apply(() => SetError(nameof(Status), message)), + 3 => Apply(() => SetError(nameof(FileCountLabel), message)), + 4 => Apply(() => SetError(nameof(ExpectedHash), message)), + 5 => Apply(() => SetError(nameof(CompareStatus), message)), + 6 => Apply(() => SetError(nameof(IncludeMd5), message)), + 7 => Apply(() => SetError(nameof(IncludeSha1), message)), + 8 => Apply(() => SetError(nameof(IncludeSha256), message)), + 9 => Apply(() => SetError(nameof(IncludeSha512), message)), + 10 => Apply(() => SetError(nameof(IncludeBlake3), message)), + 11 => Apply(() => SetError(nameof(IsBusy), message)), + 12 => Apply(() => SetError(nameof(CanClear), message)), + 13 => Apply(() => SetError(nameof(CanCopyReport), message)), + 14 => Apply(() => SetError(nameof(PrimaryDigest), message)), + 15 => Apply(() => SetError(nameof(EmptyVisible), message)), + _ => unchecked((int)0x80070057), + }; + + public int AddString(int collectionId, string? value) => collectionId switch + { + 1 => Apply(() => RecentFiles.Add(value ?? "")), + _ => unchecked((int)0x80070057), + }; + + public int ReplaceStringSnapshot(int collectionId, IReadOnlyList values) => collectionId switch + { + 1 => Apply(() => RecentFiles.ReplaceSnapshot(values)), + _ => unchecked((int)0x80070057), + }; + + public int ReplaceModelSnapshot(int collectionId, IReadOnlyList values) => collectionId switch + { + 2 => Apply(() => + { + var staged = new List(); + try { foreach (var value in values) staged.Add(new global::HashCalculator.Presentation.Generated.FileRowViewModelAdapter(value, _dispatch, _post)); } + catch { foreach (var value in staged) TryDispose(value); throw; } + var previous = Files.ToArray(); + Files.ReplaceSnapshot(staged); + foreach (var value in previous) TryDispose(value); + }), + _ => unchecked((int)0x80070057), + }; + + bool IRustVmBatchTarget.TryGetProperty(int propertyId, out RustVmBatchProperty property) + { + property = propertyId switch + { + 1 => new RustVmBatchProperty(nameof(Title), RustVmValueWireKind.String, false, false), + 2 => new RustVmBatchProperty(nameof(Status), RustVmValueWireKind.String, false, false), + 3 => new RustVmBatchProperty(nameof(FileCountLabel), RustVmValueWireKind.String, false, false), + 4 => new RustVmBatchProperty(nameof(ExpectedHash), RustVmValueWireKind.String, false, false), + 5 => new RustVmBatchProperty(nameof(CompareStatus), RustVmValueWireKind.String, false, false), + 6 => new RustVmBatchProperty(nameof(IncludeMd5), RustVmValueWireKind.Boolean, false, false), + 7 => new RustVmBatchProperty(nameof(IncludeSha1), RustVmValueWireKind.Boolean, false, false), + 8 => new RustVmBatchProperty(nameof(IncludeSha256), RustVmValueWireKind.Boolean, false, false), + 9 => new RustVmBatchProperty(nameof(IncludeSha512), RustVmValueWireKind.Boolean, false, false), + 10 => new RustVmBatchProperty(nameof(IncludeBlake3), RustVmValueWireKind.Boolean, false, false), + 11 => new RustVmBatchProperty(nameof(IsBusy), RustVmValueWireKind.Boolean, false, false), + 12 => new RustVmBatchProperty(nameof(CanClear), RustVmValueWireKind.Boolean, false, false), + 13 => new RustVmBatchProperty(nameof(CanCopyReport), RustVmValueWireKind.Boolean, false, false), + 14 => new RustVmBatchProperty(nameof(PrimaryDigest), RustVmValueWireKind.String, false, false), + 15 => new RustVmBatchProperty(nameof(EmptyVisible), RustVmValueWireKind.Boolean, false, false), + _ => default, + }; + return property.Name is not null; + } + + bool IRustVmBatchTarget.TryGetCollection(int collectionId, out RustVmBatchCollectionInfo collection) + { + collection = collectionId switch + { + 1 => new RustVmBatchCollectionInfo(nameof(RecentFiles), RustVmValueWireKind.String, RecentFiles), + 2 => new RustVmBatchCollectionInfo(nameof(Files), RustVmValueWireKind.Model, Files), + _ => default, + }; + return collection.Items is not null; + } + + bool IRustVmBatchTarget.TryGetCommand(int commandId, out IRustVmBatchCommand command) + { + command = commandId switch + { + 1 => OpenFilesCommand, + 2 => ClearFilesCommand, + 3 => CopyReportCommand, + 4 => OpenRecentFileCommand, + 5 => ExitCommand, + _ => 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 + { + 2 => new global::HashCalculator.Presentation.Generated.FileRowViewModelAdapter(model, _dispatch, _post), + _ => 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(_title, next)) return false; + _title = next; + return true; + } + case 2: + { + var next = value.Text ?? ""; + if (Equals(_status, next)) return false; + _status = next; + return true; + } + case 3: + { + var next = value.Text ?? ""; + if (Equals(_fileCountLabel, next)) return false; + _fileCountLabel = next; + return true; + } + case 4: + { + var next = value.Text ?? ""; + if (Equals(_expectedHash, next)) return false; + _expectedHash = next; + return true; + } + case 5: + { + var next = value.Text ?? ""; + if (Equals(_compareStatus, next)) return false; + _compareStatus = next; + return true; + } + case 6: + { + var next = value.Boolean; + if (Equals(_includeMd5, next)) return false; + _includeMd5 = next; + return true; + } + case 7: + { + var next = value.Boolean; + if (Equals(_includeSha1, next)) return false; + _includeSha1 = next; + return true; + } + case 8: + { + var next = value.Boolean; + if (Equals(_includeSha256, next)) return false; + _includeSha256 = next; + return true; + } + case 9: + { + var next = value.Boolean; + if (Equals(_includeSha512, next)) return false; + _includeSha512 = next; + return true; + } + case 10: + { + var next = value.Boolean; + if (Equals(_includeBlake3, next)) return false; + _includeBlake3 = next; + return true; + } + case 11: + { + var next = value.Boolean; + if (Equals(_isBusy, next)) return false; + _isBusy = next; + return true; + } + case 12: + { + var next = value.Boolean; + if (Equals(_canClear, next)) return false; + _canClear = next; + return true; + } + case 13: + { + var next = value.Boolean; + if (Equals(_canCopyReport, next)) return false; + _canCopyReport = next; + return true; + } + case 14: + { + var next = value.Text ?? ""; + if (Equals(_primaryDigest, next)) return false; + _primaryDigest = next; + return true; + } + case 15: + { + var next = value.Boolean; + if (Equals(_emptyVisible, next)) return false; + _emptyVisible = 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() + { + foreach (var item in Files) TryDispose(item); + } + + 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 execute) : ICommand, IRustVmBatchCommand + { + private bool _canExecute = true; + + public DelegateCommand(Action execute) : this(_ => execute()) { } + + public event EventHandler? CanExecuteChanged; + public bool CanExecute(object? parameter) => _canExecute; + public void Execute(object? parameter) => execute(parameter); + + public void SetEnabled(bool value) + { + if (SetEnabledCore(value)) RaiseCanExecuteChanged(); + } + + public bool SetEnabledCore(bool enabled) + { + if (_canExecute == enabled) return false; + _canExecute = enabled; + return true; + } + + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } +} diff --git a/apps/hash-calculator/managed/Generated/MainViewModelMenus.g.cs b/apps/hash-calculator/managed/Generated/MainViewModelMenus.g.cs new file mode 100644 index 0000000..d82be2e --- /dev/null +++ b/apps/hash-calculator/managed/Generated/MainViewModelMenus.g.cs @@ -0,0 +1,127 @@ +// +#nullable enable +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Rust; + +namespace HashCalculator.Presentation.Generated; + +/// +/// Generated menus, keyboard accelerators and recent-file projection for MainViewModel. +/// +public static class MainViewModelMenus +{ + /// Maximum recent-file entries projected into a submenu. + public const int RecentFilesCapacity = 8; + + /// Placeholder header shown while the recent-file list is empty. + public const string RecentFilesEmptyHeader = "(no recent files)"; + + /// + /// Builds the Main application menu, appending every declared + /// accelerator to . + /// + public static NativeMenu CreateMain(MainViewModelAdapter model, RustMenuScope scope, IList? keyBindings = null) + { + ArgumentNullException.ThrowIfNull(model); + ArgumentNullException.ThrowIfNull(scope); + var menu = new NativeMenu(); + var fileItem = new NativeMenuItem("_File"); + var fileMenu = new NativeMenu(); + fileItem.Menu = fileMenu; + var openFilesCommand = new RustMenuCommand(parameter => { model.OpenFilesCommand.Execute(parameter); }, () => model.OpenFilesCommand.CanExecute(null)); + openFilesCommand.TrackSource(model.OpenFilesCommand, scope); + var openFilesItem = new NativeMenuItem("_Open files...") { Command = openFilesCommand }; + openFilesItem.Gesture = RustMenu.ParseGesture("Ctrl+O"); + fileMenu.Items.Add(openFilesItem); + keyBindings?.Add(new KeyBinding { Gesture = RustMenu.ParseGesture("Ctrl+O")!, Command = openFilesCommand }); + var recentItem = new NativeMenuItem("Recent _files"); + var recentMenu = new NativeMenu(); + recentItem.Menu = recentMenu; + var recentActivate = new RustMenuCommand(parameter => model.OpenRecentFileCommand.Execute(parameter)); + recentActivate.TrackSource(model.OpenRecentFileCommand, scope); + scope.ObserveCollection(model.RecentFiles, () => RustMenu.FillRecentFiles(recentMenu.Items, model.RecentFiles, recentActivate, RecentFilesEmptyHeader, RecentFilesCapacity)); + fileMenu.Items.Add(recentItem); + var copyReportCommand = new RustMenuCommand(parameter => { model.CopyReportCommand.Execute(parameter); }, () => model.CopyReportCommand.CanExecute(null)); + copyReportCommand.TrackSource(model.CopyReportCommand, scope); + var copyReportItem = new NativeMenuItem("_Copy report") { Command = copyReportCommand }; + copyReportItem.Gesture = RustMenu.ParseGesture("Ctrl+Shift+C"); + fileMenu.Items.Add(copyReportItem); + keyBindings?.Add(new KeyBinding { Gesture = RustMenu.ParseGesture("Ctrl+Shift+C")!, Command = copyReportCommand }); + var clearFilesCommand = new RustMenuCommand(parameter => { model.ClearFilesCommand.Execute(parameter); }, () => model.ClearFilesCommand.CanExecute(null)); + clearFilesCommand.TrackSource(model.ClearFilesCommand, scope); + var clearFilesItem = new NativeMenuItem("C_lear list") { Command = clearFilesCommand }; + clearFilesItem.Gesture = RustMenu.ParseGesture("Ctrl+Shift+N"); + fileMenu.Items.Add(clearFilesItem); + keyBindings?.Add(new KeyBinding { Gesture = RustMenu.ParseGesture("Ctrl+Shift+N")!, Command = clearFilesCommand }); + fileMenu.Items.Add(new NativeMenuItemSeparator()); + var exitCommand = new RustMenuCommand(parameter => { model.ExitCommand.Execute(parameter); }, () => model.ExitCommand.CanExecute(null)); + exitCommand.TrackSource(model.ExitCommand, scope); + var exitItem = new NativeMenuItem("E_xit") { Command = exitCommand }; + exitItem.Gesture = RustMenu.ParseGesture("Ctrl+Q"); + fileMenu.Items.Add(exitItem); + keyBindings?.Add(new KeyBinding { Gesture = RustMenu.ParseGesture("Ctrl+Q")!, Command = exitCommand }); + menu.Items.Add(fileItem); + var hashItem = new NativeMenuItem("_Hash"); + var hashMenu = new NativeMenu(); + hashItem.Menu = hashMenu; + var includeMd5Command = new RustMenuCommand(parameter => { model.IncludeMd5 = !model.IncludeMd5; }, null); + var includeMd5Item = new NativeMenuItem("_MD5") { Command = includeMd5Command }; + includeMd5Item.ToggleType = MenuItemToggleType.CheckBox; + scope.Observe("IncludeMd5", () => includeMd5Item.IsChecked = model.IncludeMd5); + hashMenu.Items.Add(includeMd5Item); + var includeSha1Command = new RustMenuCommand(parameter => { model.IncludeSha1 = !model.IncludeSha1; }, null); + var includeSha1Item = new NativeMenuItem("SHA-_1") { Command = includeSha1Command }; + includeSha1Item.ToggleType = MenuItemToggleType.CheckBox; + scope.Observe("IncludeSha1", () => includeSha1Item.IsChecked = model.IncludeSha1); + hashMenu.Items.Add(includeSha1Item); + var includeSha256Command = new RustMenuCommand(parameter => { model.IncludeSha256 = !model.IncludeSha256; }, null); + var includeSha256Item = new NativeMenuItem("SHA-_256") { Command = includeSha256Command }; + includeSha256Item.ToggleType = MenuItemToggleType.CheckBox; + scope.Observe("IncludeSha256", () => includeSha256Item.IsChecked = model.IncludeSha256); + hashMenu.Items.Add(includeSha256Item); + var includeSha512Command = new RustMenuCommand(parameter => { model.IncludeSha512 = !model.IncludeSha512; }, null); + var includeSha512Item = new NativeMenuItem("SHA-_512") { Command = includeSha512Command }; + includeSha512Item.ToggleType = MenuItemToggleType.CheckBox; + scope.Observe("IncludeSha512", () => includeSha512Item.IsChecked = model.IncludeSha512); + hashMenu.Items.Add(includeSha512Item); + var includeBlake3Command = new RustMenuCommand(parameter => { model.IncludeBlake3 = !model.IncludeBlake3; }, null); + var includeBlake3Item = new NativeMenuItem("_BLAKE3") { Command = includeBlake3Command }; + includeBlake3Item.ToggleType = MenuItemToggleType.CheckBox; + scope.Observe("IncludeBlake3", () => includeBlake3Item.IsChecked = model.IncludeBlake3); + hashMenu.Items.Add(includeBlake3Item); + menu.Items.Add(hashItem); + return menu; + } + + /// + /// Attaches the Main menu and its accelerators to a top-level. + /// + /// + /// The menu is exported natively where the platform has a menu bar (macOS) + /// and rendered in-window by a NativeMenuBar control everywhere else; + /// both read the same attached value, so there is no platform branch. Only a + /// real native menu bar handles its own shortcuts, so the declared gestures + /// are additionally installed as key bindings on the top-level. + /// + public static RustMenuAttachment AttachMain(TopLevel target, MainViewModelAdapter model) + { + ArgumentNullException.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(model); + var scope = new RustMenuScope(model); + try + { + var keyBindings = new List(); + var menu = CreateMain(model, scope, keyBindings); + return RustMenuAttachment.Attach(target, menu, keyBindings, scope); + } + catch + { + scope.Dispose(); + throw; + } + } + +} diff --git a/apps/hash-calculator/managed/Generated/MainViewModelMetadata.g.cs b/apps/hash-calculator/managed/Generated/MainViewModelMetadata.g.cs new file mode 100644 index 0000000..325059d --- /dev/null +++ b/apps/hash-calculator/managed/Generated/MainViewModelMetadata.g.cs @@ -0,0 +1,43 @@ +// +#nullable enable +using System.Collections.Generic; +using Avalonia.Rust; + +namespace HashCalculator.Presentation.Generated; + +public static class MainViewModelMetadata +{ + public static RustViewModelDescriptor Descriptor { get; } = new( + 1, + "MainViewModel", + [ + new(1, "Title", RustViewModelValueKind.String, false, false, "Hash Calculator", null), + new(2, "Status", RustViewModelValueKind.String, false, false, "Preparing sample file...", null), + new(3, "FileCountLabel", RustViewModelValueKind.String, false, false, "No files", null), + new(4, "ExpectedHash", RustViewModelValueKind.String, true, false, "", null), + new(5, "CompareStatus", RustViewModelValueKind.String, false, false, "Paste a digest to compare", null), + new(6, "IncludeMd5", RustViewModelValueKind.Boolean, true, false, false, null), + new(7, "IncludeSha1", RustViewModelValueKind.Boolean, true, false, false, null), + new(8, "IncludeSha256", RustViewModelValueKind.Boolean, true, false, true, null), + new(9, "IncludeSha512", RustViewModelValueKind.Boolean, true, false, false, null), + new(10, "IncludeBlake3", RustViewModelValueKind.Boolean, true, false, false, null), + new(11, "IsBusy", RustViewModelValueKind.Boolean, false, false, true, null), + new(12, "CanClear", RustViewModelValueKind.Boolean, false, false, false, null), + new(13, "CanCopyReport", RustViewModelValueKind.Boolean, false, false, false, null), + new(14, "PrimaryDigest", RustViewModelValueKind.String, false, false, "", null), + new(15, "EmptyVisible", RustViewModelValueKind.Boolean, false, false, false, null), + ], + [ + new(1, "RecentFiles", RustViewModelValueKind.String, null, null, null, null, false), + new(2, "Files", RustViewModelValueKind.Model, global::HashCalculator.Presentation.Generated.FileRowViewModelMetadata.Descriptor, null, null, null, false), + ], + [ + new(1, "OpenFilesCommand", true, null, false, null, false, false), + new(2, "ClearFilesCommand", false, null, false, null, false, false), + new(3, "CopyReportCommand", true, null, false, null, false, false), + new(4, "OpenRecentFileCommand", false, null, true, null, false, false), + new(5, "ExitCommand", false, null, false, null, false, false), + ], + [ + ]); +} diff --git a/apps/hash-calculator/managed/Views/MainWindow.axaml b/apps/hash-calculator/managed/Views/MainWindow.axaml new file mode 100644 index 0000000..ae0d798 --- /dev/null +++ b/apps/hash-calculator/managed/Views/MainWindow.axaml @@ -0,0 +1,390 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +