diff --git a/.github/workflows/editor.yml b/.github/workflows/editor.yml new file mode 100644 index 0000000..8e91fbf --- /dev/null +++ b/.github/workflows/editor.yml @@ -0,0 +1,43 @@ +name: Web editor +on: + push: + pull_request: +permissions: + contents: read +jobs: + editor: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: '1.90.0' + targets: wasm32-unknown-unknown + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --all -- --check + - run: cargo test --workspace --locked + - run: cargo clippy --workspace --all-targets --locked -- -D warnings + - run: cargo check -p web_view --target wasm32-unknown-unknown --locked + - uses: taiki-e/install-action@v2 + with: + tool: trunk@0.21.14 + - run: trunk build --release --locked + env: + NO_COLOR: 'true' + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + - run: npm ci + - run: npx playwright install --with-deps chromium + - run: npm run test:e2e + - uses: actions/upload-artifact@v4 + if: always() + with: + name: browser-test-results + path: test-results/ + - uses: actions/upload-artifact@v4 + with: + name: pace-web-editor + path: dist/ diff --git a/.gitignore b/.gitignore index ea8c4bf..fe50fe7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ /target +/dist +/node_modules +/test-results +/playwright-report diff --git a/Cargo.lock b/Cargo.lock index a482430..2b16dc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,21 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +version = 4 [[package]] name = "ahash" @@ -29,42 +14,12 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - [[package]] name = "allocator-api2" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anymap" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344" - [[package]] name = "anymap2" version = "0.13.0" @@ -75,49 +30,14 @@ checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" name = "app" version = "0.1.0" dependencies = [ - "base_resolver", - "desktop_resolver", + "base64 0.22.1", "dioxus", - "dioxus-logger", - "serde", - "tracing", - "web_resolver", -] - -[[package]] -name = "ashpd" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" -dependencies = [ - "async-fs", - "async-net", - "enumflags2", - "futures-channel", - "futures-util", - "rand 0.8.5", + "imagesize", + "js-sys", "serde", - "serde_repr", - "url", - "zbus", -] - -[[package]] -name = "askama_escape" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" - -[[package]] -name = "async-broadcast" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", + "serde_json", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -132,133 +52,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-compression" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" -dependencies = [ - "flate2", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "async-executor" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-io" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" -dependencies = [ - "async-lock", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "async-lock" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-net" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" -dependencies = [ - "async-io", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-process" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7eda79bbd84e29c2b308d1dc099d7de8dcc7035e48f4bf5dc4a531a44ff5e2a" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix", - "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "async-signal" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794f185324c2f00e771cd9f1ae8b5ac68be2ca7abb129a87afd6e86d228bc54d" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix", - "signal-hook-registry", - "slab", - "windows-sys 0.52.0", -] - [[package]] name = "async-task" version = "4.7.1" @@ -273,30 +66,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", -] - -[[package]] -name = "atk" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4" -dependencies = [ - "atk-sys", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", + "syn", ] [[package]] @@ -311,93 +81,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" -[[package]] -name = "axum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" -dependencies = [ - "async-trait", - "axum-core", - "axum-macros", - "base64", - "bytes", - "futures-util", - "http 1.1.0", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa 1.0.11", - "matchit", - "memchr", - "mime", - "multer", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sha1", - "sync_wrapper 1.0.1", - "tokio", - "tokio-tungstenite", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.1.0", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper 0.1.2", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "backtrace" -version = "0.3.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - [[package]] name = "base64" version = "0.21.7" @@ -405,12 +88,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] -name = "base_resolver" -version = "0.1.0" -dependencies = [ - "dioxus", - "serde", -] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bincode" @@ -421,12 +102,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.5.0" @@ -436,21 +111,6 @@ dependencies = [ "serde", ] -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[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 = "blocking" version = "1.6.1" @@ -470,43 +130,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" -[[package]] -name = "cairo-rs" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" -dependencies = [ - "bitflags 2.5.0", - "cairo-sys-rs", - "glib", - "libc", - "once_cell", - "thiserror", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - [[package]] name = "camino" version = "1.1.7" @@ -539,29 +168,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "cc" -version = "1.0.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfb" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" -dependencies = [ - "byteorder", - "fnv", - "uuid", -] - [[package]] name = "cfg-expr" version = "0.15.8" @@ -569,7 +175,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", - "target-lexicon", ] [[package]] @@ -579,39 +184,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "cfg_aliases" -version = "0.1.1" +name = "ciborium" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] [[package]] -name = "chrono" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-targets 0.52.5", -] - -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" +name = "ciborium-io" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" @@ -625,46 +210,6 @@ dependencies = [ "half", ] -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation", - "core-foundation", - "core-graphics", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation", - "core-graphics-types", - "libc", - "objc", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -710,12 +255,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd7e35aee659887cbfb97aaf227ac12cad1a9d7c71e55ff3376839ed4e282d08" -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "convert_case" version = "0.6.0" @@ -725,73 +264,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" - -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -804,43 +276,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "cssparser" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa 0.4.8", - "matches", - "phf 0.8.0", - "proc-macro2", - "quote", - "smallvec", - "syn 1.0.109", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.66", -] - [[package]] name = "darling" version = "0.20.9" @@ -861,7 +296,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -872,7 +307,7 @@ checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -882,100 +317,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.5", + "hashbrown", "lock_api", "once_cell", "parking_lot_core", ] -[[package]] -name = "data-encoding" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" - -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive_more" -version = "0.99.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" -dependencies = [ - "convert_case 0.4.0", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.66", -] - -[[package]] -name = "desktop_resolver" -version = "0.1.0" -dependencies = [ - "base_resolver", - "dioxus", - "dioxus-logger", - "serde", - "tracing", -] - -[[package]] -name = "desktop_view" -version = "0.1.0" -dependencies = [ - "app", - "dioxus", - "dioxus-logger", - "tracing", -] - -[[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 = "dioxus" -version = "0.5.1" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e50735a28b303b0d67e1b5637fb57e4711bf2776266290cbc987c0adfdabb55" +checksum = "b8e7fe217b50d43b27528b0f24c89b411f742a3e7564d1cfbf85253f967954db" dependencies = [ "dioxus-config-macro", "dioxus-core", "dioxus-core-macro", - "dioxus-desktop", "dioxus-fullstack", "dioxus-hooks", - "dioxus-hot-reload", "dioxus-html", - "dioxus-liveview", "dioxus-router", "dioxus-signals", - "dioxus-ssr", "dioxus-web", - "serde", ] [[package]] name = "dioxus-cli-config" -version = "0.5.0" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d4661064bad2f0b12929faf6c9cea4d94e60217ba6b11ff4146b505a57124b" +checksum = "c7dffc452ed91af6ef772b0d9a5899573f6785314e97c533733ec55413c01df3" dependencies = [ "once_cell", "serde", @@ -985,9 +354,9 @@ dependencies = [ [[package]] name = "dioxus-config-macro" -version = "0.5.0" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe244197b320dec9e9f38742985fe98c058136ada770df73e9429878ed92863" +checksum = "cb1a1aa34cc04c1f7fcbb7a10791ba773cc02d834fe3ec1fe05647699f3b101f" dependencies = [ "proc-macro2", "quote", @@ -995,9 +364,9 @@ dependencies = [ [[package]] name = "dioxus-core" -version = "0.5.1" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "088daa3f45aaa729e9eef32dc0a9393dd709ee906b092089e5839cad1cad7c85" +checksum = "3730d2459ab66951cedf10b09eb84141a6eda7f403c28057cbe010495be156b7" dependencies = [ "futures-channel", "futures-util", @@ -1006,23 +375,24 @@ dependencies = [ "rustc-hash", "serde", "slab", + "slotmap", "tracing", "tracing-subscriber", ] [[package]] name = "dioxus-core-macro" -version = "0.5.1" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e29a07448245451334eec2883a394e207f28caedf0a57fd1a903e9ccea0b9531" +checksum = "0d9c0dfe0e6a46626fa716c4aa1d2ccb273441337909cfeacad5bb6fcfb947d2" dependencies = [ "constcat", - "convert_case 0.6.0", + "convert_case", "dioxus-rsx", "prettyplease", "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1031,90 +401,34 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ea539174bb236e0e7dc9c12b19b88eae3cb574dedbd0252a2d43ea7e6de13e2" -[[package]] -name = "dioxus-desktop" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85f5008be950e1dd255d947d8037e82e424ccc538892cf251b09a22558dc296" -dependencies = [ - "async-trait", - "cocoa", - "core-foundation", - "dioxus-cli-config", - "dioxus-core", - "dioxus-hooks", - "dioxus-hot-reload", - "dioxus-html", - "dioxus-interpreter-js", - "dunce", - "futures-channel", - "futures-util", - "generational-box", - "global-hotkey", - "infer", - "muda", - "objc", - "objc_id", - "rfd", - "rustc-hash", - "serde", - "serde_json", - "signal-hook", - "slab", - "tao", - "thiserror", - "tokio", - "tracing", - "urlencoding", - "webbrowser", - "wry", -] - [[package]] name = "dioxus-fullstack" -version = "0.5.2" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7fb1a22ff7af8756bc9506eebfbecf374b1c8c57f087c85c752ba8bd767fce" +checksum = "b80f0ac18166302341164e681322e0385131c08a11c3cc1c51ee8df799ab0d3d" dependencies = [ - "anymap", - "async-compression", "async-trait", - "axum", - "base64", + "base64 0.21.7", "bytes", "ciborium", - "dioxus-cli-config", - "dioxus-desktop", "dioxus-hot-reload", "dioxus-lib", - "dioxus-ssr", "dioxus-web", "dioxus_server_macro", "futures-util", - "http 1.1.0", - "hyper", "once_cell", - "pin-project", "serde", "serde_json", "server_fn", - "thiserror", - "tokio", - "tokio-stream", - "tokio-util", - "tower", - "tower-http", - "tower-layer", "tracing", - "tracing-futures", "web-sys", ] [[package]] name = "dioxus-hooks" -version = "0.5.1" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a8c7019308a6d8381fce84a51006f207407af265aebc5425871399c98d788e4" +checksum = "fa8f9c661eea82295219d25555d5c0b597e74186b029038ceb5e3700ccbd4380" dependencies = [ "dioxus-core", "dioxus-debug-cell", @@ -1129,9 +443,9 @@ dependencies = [ [[package]] name = "dioxus-hot-reload" -version = "0.5.0" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d5a28a2af6655473c6521fb5a428538807b985e8e5f1a8c30e2ab71bd54e637" +checksum = "77d01246cb1b93437fb0bbd0dd11cfc66342d86b4311819e76654f2017ce1473" dependencies = [ "dioxus-core", "dioxus-html", @@ -1143,9 +457,9 @@ dependencies = [ [[package]] name = "dioxus-html" -version = "0.5.1" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d612d9732f32adc1852e13e1387a9d5baa710b0b004641b5123def53065c8d" +checksum = "f01a0826f179adad6ea8d6586746e8edde0c602cc86f4eb8e5df7a3b204c4018" dependencies = [ "async-trait", "dioxus-core", @@ -1159,7 +473,6 @@ dependencies = [ "serde-value", "serde_json", "serde_repr", - "tokio", "tracing", "wasm-bindgen", "web-sys", @@ -1167,37 +480,36 @@ dependencies = [ [[package]] name = "dioxus-html-internal-macro" -version = "0.5.0" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1799f34affdb158f6ebec23b46b11f9e65de0bbadbbb781dc68c3eddfe6fd32b" +checksum = "0b96f35a608d0ab8f4ca6f66ce1828354e4ebd41580b12454f490221a11da93c" dependencies = [ - "convert_case 0.6.0", + "convert_case", "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] name = "dioxus-interpreter-js" -version = "0.5.1" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc68a22e33562317b40ccc7b2d140017e510745c5d7e062e911c6a4f9042e4b1" +checksum = "351fad098c657d14f3ac2900362d2b86e83c22c4c620a404839e1ab628f3395b" dependencies = [ - "dioxus-core", - "dioxus-html", "js-sys", "md5", "sledgehammer_bindgen", "sledgehammer_utils", "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", ] [[package]] name = "dioxus-lib" -version = "0.5.1" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9af36a9f985ad68783acf62dc276b0a8f0aa928f1c3b42f78e2ae222b19d445c" +checksum = "8bd39b2c41dd1915dcb91d914ea72d8b646f1f8995aaeff82816b862ec586ecd" dependencies = [ "dioxus-core", "dioxus-core-macro", @@ -1207,59 +519,18 @@ dependencies = [ "dioxus-signals", ] -[[package]] -name = "dioxus-liveview" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc132aacc3eb3d11db8f20afa5f3cfc74147374cc41285488083476a0ed1f17" -dependencies = [ - "axum", - "dioxus-cli-config", - "dioxus-core", - "dioxus-hot-reload", - "dioxus-html", - "dioxus-interpreter-js", - "futures-channel", - "futures-util", - "generational-box", - "rustc-hash", - "serde", - "serde_json", - "slab", - "thiserror", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", -] - -[[package]] -name = "dioxus-logger" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe09dc9773dc1f1bb0d38529203d6555d08f67aadca5cf955ac3d1a9e69880" -dependencies = [ - "console_error_panic_hook", - "tracing", - "tracing-subscriber", - "tracing-wasm", -] - [[package]] name = "dioxus-router" -version = "0.5.0" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4cbbc1aff811aa3715c94a7ca0375be0be34356aba7f3897328d844323519b" +checksum = "c235c5dbeb528c0c2b0424763da812e7500df69b82eddac54db6f4975e065c5f" dependencies = [ "dioxus-cli-config", - "dioxus-fullstack", "dioxus-lib", "dioxus-router-macro", - "dioxus-ssr", "gloo", "gloo-utils 0.1.7", "js-sys", - "tokio", "tracing", "url", "urlencoding", @@ -1269,36 +540,36 @@ dependencies = [ [[package]] name = "dioxus-router-macro" -version = "0.5.1" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fcb6e4a203dc816bca720c638562d5a782695dc71d6598de088ce50ba2a0f8" +checksum = "2e7cd1c5137ba361f2150cdea6b3bc9ddda7b1af84b22c9ee6b5499bf43e1381" dependencies = [ "proc-macro2", "quote", "slab", - "syn 2.0.66", + "syn", ] [[package]] name = "dioxus-rsx" -version = "0.5.1" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa83056104f63fdc6f7f9fc1137208c7b7648bf88d6c86db1e095f15297a0f3" +checksum = "15c400bc8a779107d8f3a67b14375db07dbd2bc31163bf085a8e9097f36f7179" dependencies = [ "dioxus-core", "internment", "krates", "proc-macro2", "quote", - "syn 2.0.66", + "syn", "tracing", ] [[package]] name = "dioxus-signals" -version = "0.5.1" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fd04e2b3739d5c12255005cbf3185446e750fc2b3eeee9fa4e83c989132415c" +checksum = "7e3e224cd3d3713f159f0199fc088c292a0f4adb94996b48120157f6a8f8342d" dependencies = [ "dioxus-core", "futures-channel", @@ -1310,32 +581,11 @@ dependencies = [ "tracing", ] -[[package]] -name = "dioxus-ssr" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ba8e16b3c7e154a61d814a39505c910403b9df29a113e2920cd23334a4e71c" -dependencies = [ - "askama_escape", - "async-trait", - "chrono", - "dioxus-core", - "dioxus-html", - "generational-box", - "http 1.1.0", - "lru", - "rustc-hash", - "serde_json", - "thiserror", - "tokio", - "tracing", -] - [[package]] name = "dioxus-web" -version = "0.5.1" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75cfbe115b193a05c649a80a54a90a6bdd88779694a617daf27d287d6fb944f" +checksum = "e0855ac81fcc9252a0863930a7a7cbb2504fc1b6efe893489c8d0e23aaeb2cb9" dependencies = [ "async-trait", "console_error_panic_hook", @@ -1358,23 +608,17 @@ dependencies = [ [[package]] name = "dioxus_server_macro" -version = "0.5.1" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b723da95503f739f9fc5fb23b6ad1e456f1438a496a0427210fa94e4e0d5fe9" +checksum = "b5ef2cad17001c1155f019cb69adbacd620644566d78a77d0778807bb106a337" dependencies = [ - "convert_case 0.6.0", + "convert_case", "proc-macro2", "quote", "server_fn_macro", - "syn 2.0.66", + "syn", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "displaydoc" version = "0.2.4" @@ -1383,139 +627,49 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] -name = "dlopen2" -version = "0.7.0" +name = "enumset" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6" +checksum = "226c0da7462c13fb57e5cc9e0dc8f0635e7d27f276a3a7fd30054647f669007d" dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", + "enumset_derive", ] [[package]] -name = "dlopen2_derive" -version = "0.4.0" +name = "enumset_derive" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" +checksum = "e08b6c6ab82d70f08844964ba10c7babb716de2ecaeab9be5717918a5177d3af" dependencies = [ + "darling", "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] -name = "dtoa" -version = "1.0.9" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] -name = "dtoa-short" -version = "0.3.5" +name = "euclid" +version = "0.22.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +checksum = "e0f0eb73b934648cd7a4a61f1b15391cd95dab0b4da6e2e66c2a072c144b4a20" dependencies = [ - "dtoa", + "num-traits", + "serde", ] [[package]] -name = "dunce" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" - -[[package]] -name = "encoding_rs" -version = "0.8.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "endi" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" - -[[package]] -name = "enumflags2" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "enumset" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226c0da7462c13fb57e5cc9e0dc8f0635e7d27f276a3a7fd30054647f669007d" -dependencies = [ - "enumset_derive", -] - -[[package]] -name = "enumset_derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08b6c6ab82d70f08844964ba10c7babb716de2ecaeab9be5717918a5177d3af" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "euclid" -version = "0.22.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f0eb73b934648cd7a4a61f1b15391cd95dab0b4da6e2e66c2a072c144b4a20" -dependencies = [ - "num-traits", - "serde", -] - -[[package]] -name = "event-listener" -version = "5.3.1" +name = "event-listener" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ @@ -1540,74 +694,18 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" -[[package]] -name = "fdeflate" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset", - "rustc_version", -] - [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" -[[package]] -name = "flate2" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1617,16 +715,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures" version = "0.3.30" @@ -1681,10 +769,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand", "futures-core", - "futures-io", - "parking", "pin-project-lite", ] @@ -1696,7 +781,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1729,257 +814,15 @@ dependencies = [ "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "gdk" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ba081bdef3b75ebcdbfc953699ed2d7417d6bd853347a42a37d76406a33646" -dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" -dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", - "libc", - "once_cell", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkwayland-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90fbf5c033c65d93792192a49a8efb5bb1e640c419682a58bb96f5ae77f3d4a" -dependencies = [ - "gdk-sys", - "glib-sys", - "gobject-sys", - "libc", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkx11" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2ea8a4909d530f79921290389cbd7c34cb9d623bfe970eaae65ca5f9cd9cce" -dependencies = [ - "gdk", - "gdkx11-sys", - "gio", - "glib", - "libc", - "x11", -] - -[[package]] -name = "gdkx11-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee8f00f4ee46cad2939b8990f5c70c94ff882c3028f3cc5abf950fa4ab53043" -dependencies = [ - "gdk-sys", - "glib-sys", - "libc", - "system-deps", - "x11", -] - [[package]] name = "generational-box" -version = "0.5.1" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f132919c96b85c02a067ceae965fd50ace57111e2f55c7384b95ac191f4d966b" +checksum = "557cf2cbacd0504c6bf8c29f52f8071e0de1d9783346713dc6121d7fa1e5d0e0" dependencies = [ "parking_lot", ] -[[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 = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "gimli" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" - -[[package]] -name = "gio" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "once_cell", - "pin-project-lite", - "smallvec", - "thiserror", -] - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "glib" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" -dependencies = [ - "bitflags 2.5.0", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror", -] - -[[package]] -name = "glib-macros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" -dependencies = [ - "heck 0.4.1", - "proc-macro-crate 2.0.2", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "global-hotkey" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cb13e8c52c87e28a46eae3e5e65b8f0cd465c4c9e67b13d56c70412e792bc3" -dependencies = [ - "bitflags 2.5.0", - "cocoa", - "crossbeam-channel", - "keyboard-types", - "objc", - "once_cell", - "thiserror", - "windows-sys 0.52.0", - "x11-dl", -] - [[package]] name = "gloo" version = "0.8.1" @@ -2181,147 +1024,25 @@ dependencies = [ ] [[package]] -name = "gobject-sys" -version = "0.18.0" +name = "half" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ - "glib-sys", - "libc", - "system-deps", + "cfg-if", + "crunchy", ] [[package]] -name = "gtk" -version = "0.18.1" +name = "hashbrown" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93c4f5e0e20b60e10631a5f06da7fe3dda744b05ad0ea71fee2f47adf865890c" -dependencies = [ - "atk", - "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", - "glib", - "gtk-sys", - "gtk3-macros", - "libc", - "pango", - "pkg-config", -] - -[[package]] -name = "gtk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "gtk3-macros" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "half" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" -dependencies = [ - "cfg-if", - "crunchy", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "html5ever" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" -dependencies = [ - "log", - "mac", - "markup5ever", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "http" version = "0.2.12" @@ -2330,7 +1051,7 @@ checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", - "itoa 1.0.11", + "itoa", ] [[package]] @@ -2341,105 +1062,7 @@ checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", - "itoa 1.0.11", -] - -[[package]] -name = "http-body" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" -dependencies = [ - "bytes", - "http 1.1.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http 1.1.0", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "http-range-header" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" - -[[package]] -name = "httparse" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.1.0", - "http-body", - "httparse", - "httpdate", - "itoa 1.0.11", - "pin-project-lite", - "smallvec", - "tokio", -] - -[[package]] -name = "hyper-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" -dependencies = [ - "bytes", - "futures-util", - "http 1.1.0", - "http-body", - "hyper", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core 0.52.0", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", + "itoa", ] [[package]] @@ -2557,7 +1180,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -2579,14 +1202,10 @@ dependencies = [ ] [[package]] -name = "indexmap" -version = "1.9.3" +name = "imagesize" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] name = "indexmap" @@ -2595,25 +1214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.5", -] - -[[package]] -name = "infer" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6c16b11a665b26aeeb9b1d7f954cdeb034be38dd00adab4f2ae921a8fee804" -dependencies = [ - "cfb", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", + "hashbrown", ] [[package]] @@ -2622,7 +1223,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04e8e537b529b8674e97e9fb82c10ff168a290ac3867a0295f112061ffbca1ef" dependencies = [ - "hashbrown 0.14.5", + "hashbrown", "parking_lot", ] @@ -2652,69 +1253,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae52f28f45ac2bc96edb7714de995cffc174a395fb0abf5bff453587c980d7b9" -[[package]] -name = "inventory" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" - -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" -[[package]] -name = "javascriptcore-rs" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" -dependencies = [ - "bitflags 1.3.2", - "glib", - "javascriptcore-rs-sys", -] - -[[package]] -name = "javascriptcore-rs-sys" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - [[package]] name = "js-sys" version = "0.3.69" @@ -2730,7 +1274,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.5.0", + "bitflags", "serde", "unicode-segmentation", ] @@ -2748,19 +1292,6 @@ dependencies = [ "semver", ] -[[package]] -name = "kuchikiki" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" -dependencies = [ - "cssparser", - "html5ever", - "indexmap 1.9.3", - "matches", - "selectors", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -2773,31 +1304,6 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" -[[package]] -name = "libxdo" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" -dependencies = [ - "libxdo-sys", -] - -[[package]] -name = "libxdo-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" -dependencies = [ - "libc", - "x11", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" - [[package]] name = "litemap" version = "0.7.3" @@ -2832,2253 +1338,751 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ - "hashbrown 0.14.5", + "hashbrown", ] [[package]] -name = "mac" -version = "0.1.1" +name = "md5" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] -name = "malloc_buf" -version = "0.0.6" +name = "memchr" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] -name = "markup5ever" -version = "0.11.0" +name = "nu-ansi-term" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ - "log", - "phf 0.10.1", - "phf_codegen 0.10.0", - "string_cache", - "string_cache_codegen", - "tendril", + "overload", + "winapi", ] [[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "md5" -version = "0.7.0" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] [[package]] -name = "memchr" -version = "2.7.4" +name = "once_cell" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] -name = "memoffset" -version = "0.9.1" +name = "ordered-float" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" dependencies = [ - "autocfg", + "num-traits", ] [[package]] -name = "mime" -version = "0.3.17" +name = "overload" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] -name = "mime_guess" -version = "2.0.4" +name = "parking" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" -dependencies = [ - "mime", - "unicase", -] +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] -name = "miniz_oxide" -version = "0.7.3" +name = "parking_lot" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ - "adler", - "simd-adler32", + "lock_api", + "parking_lot_core", ] [[package]] -name = "mio" -version = "0.8.11" +name = "parking_lot_core" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ + "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", -] - -[[package]] -name = "muda" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c47e7625990fc1af2226ea4f34fb2412b03c12639fcb91868581eb3a6893453" -dependencies = [ - "cocoa", - "crossbeam-channel", - "gtk", - "keyboard-types", - "libxdo", - "objc", - "once_cell", - "png", - "thiserror", - "windows-sys 0.52.0", + "redox_syscall", + "smallvec", + "windows-targets", ] [[package]] -name = "multer" -version = "3.1.0" +name = "percent-encoding" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http 1.1.0", - "httparse", - "memchr", - "mime", - "spin", - "version_check", -] +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] -name = "ndk" -version = "0.7.0" +name = "petgraph" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "bitflags 1.3.2", - "jni-sys", - "ndk-sys", - "num_enum", - "raw-window-handle 0.5.2", - "thiserror", + "fixedbitset", + "indexmap", ] [[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.4.1+23.1.7779620" +name = "pin-project" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ - "jni-sys", + "pin-project-internal", ] [[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "nix" -version = "0.27.1" +name = "pin-project-internal" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ - "bitflags 2.5.0", - "cfg-if", - "libc", - "memoffset", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "nodrop" -version = "0.1.14" +name = "pin-project-lite" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] -name = "nu-ansi-term" -version = "0.46.0" +name = "pin-utils" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "num-traits" -version = "0.2.19" +name = "piper" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" dependencies = [ - "autocfg", + "atomic-waker", + "fastrand", + "futures-io", ] [[package]] -name = "num_cpus" -version = "1.16.0" +name = "prettyplease" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ - "hermit-abi 0.3.9", - "libc", + "proc-macro2", + "syn", ] [[package]] -name = "num_enum" -version = "0.5.11" +name = "proc-macro2" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ - "num_enum_derive", + "unicode-ident", ] [[package]] -name = "num_enum_derive" -version = "0.5.11" +name = "quote" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ - "proc-macro-crate 1.3.1", "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", - "objc_exception", ] [[package]] -name = "objc-foundation" -version = "0.1.1" +name = "redox_syscall" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "block", - "objc", - "objc_id", + "bitflags", ] [[package]] -name = "objc_exception" -version = "0.1.2" +name = "rustc-hash" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" -dependencies = [ - "cc", -] +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] -name = "objc_id" -version = "0.1.1" +name = "rustc_version" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "objc", + "semver", ] [[package]] -name = "object" -version = "0.36.0" +name = "ryu" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" -dependencies = [ - "memchr", -] +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] -name = "once_cell" -version = "1.19.0" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "ordered-float" -version = "2.10.1" +name = "semver" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" dependencies = [ - "num-traits", + "serde", ] [[package]] -name = "ordered-stream" -version = "0.2.0" +name = "send_wrapper" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" dependencies = [ "futures-core", - "pin-project-lite", ] [[package]] -name = "overload" -version = "0.1.1" +name = "serde" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] [[package]] -name = "pango" -version = "0.18.3" +name = "serde-value" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" dependencies = [ - "gio", - "glib", - "libc", - "once_cell", - "pango-sys", + "ordered-float", + "serde", ] [[package]] -name = "pango-sys" -version = "0.18.0" +name = "serde-wasm-bindgen" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", + "js-sys", + "serde", + "wasm-bindgen", ] [[package]] -name = "parking" -version = "2.2.0" +name = "serde_derive" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "parking_lot" -version = "0.12.3" +name = "serde_json" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ - "lock_api", - "parking_lot_core", + "itoa", + "ryu", + "serde", ] [[package]] -name = "parking_lot_core" -version = "0.9.10" +name = "serde_qs" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.5", + "percent-encoding", + "serde", + "thiserror", ] [[package]] -name = "percent-encoding" -version = "2.3.1" +name = "serde_repr" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "petgraph" -version = "0.6.5" +name = "serde_urlencoded" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ - "fixedbitset", - "indexmap 2.2.6", + "form_urlencoded", + "itoa", + "ryu", + "serde", ] [[package]] -name = "phf" -version = "0.8.0" +name = "server_fn" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +checksum = "b06e6e5467a2cd93ce1accfdfd8b859404f0b3b2041131ffd774fabf666b8219" dependencies = [ - "phf_macros", - "phf_shared 0.8.0", - "proc-macro-hack", + "bytes", + "const_format", + "dashmap", + "futures", + "gloo-net 0.5.0", + "http 1.1.0", + "js-sys", + "once_cell", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "thiserror", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", ] [[package]] -name = "phf" -version = "0.10.1" +name = "server_fn_macro" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +checksum = "09c216bb1c1ac890151397643c663c875a1836adf0b269be4e389cb1b48c173c" dependencies = [ - "phf_shared 0.10.0", + "const_format", + "convert_case", + "proc-macro2", + "quote", + "syn", + "xxhash-rust", ] [[package]] -name = "phf_codegen" -version = "0.8.0" +name = "server_fn_macro_default" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +checksum = "00783df297ec85ea605779f2fef9cbec98981dffe2e01e1a9845c102ee1f1ae6" dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", + "server_fn_macro", + "syn", ] [[package]] -name = "phf_codegen" -version = "0.10.0" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", + "lazy_static", ] [[package]] -name = "phf_generator" -version = "0.8.0" +name = "slab" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", + "autocfg", ] [[package]] -name = "phf_generator" -version = "0.10.0" +name = "sledgehammer_bindgen" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +checksum = "fcfaf791ff02f48f3518ce825d32cf419c13a43c1d8b1232f74ac89f339c46d2" dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.5", + "sledgehammer_bindgen_macro", + "wasm-bindgen", ] [[package]] -name = "phf_macros" -version = "0.8.0" +name = "sledgehammer_bindgen_macro" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +checksum = "bdd941cc539bd3dc694edaf9d0c4e1221d02baa67c6b45ec04fad1024d9e8139" dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", - "proc-macro-hack", - "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] -name = "phf_shared" -version = "0.8.0" +name = "sledgehammer_utils" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +checksum = "f20798defa0e9d4eff9ca451c7f84774c7378a9c3b5a40112cfa2b3eadb97ae2" dependencies = [ - "siphasher", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" -dependencies = [ - "pin-project-internal", + "lru", + "once_cell", + "rustc-hash", ] [[package]] -name = "pin-project-internal" -version = "1.1.5" +name = "slotmap" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", + "serde", + "version_check", ] [[package]] -name = "pin-project-lite" -version = "0.2.14" +name = "smallvec" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] -name = "pin-utils" +name = "spinning" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - -[[package]] -name = "pkg-config" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" - -[[package]] -name = "png" -version = "0.17.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "polling" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi 0.4.0", - "pin-project-lite", - "rustix", - "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "pollster" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "prettyplease" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" -dependencies = [ - "proc-macro2", - "syn 2.0.66", -] - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "2d4f0e86297cad2658d92a707320d87bf4e6ae1050287f51d19b67ef3f153a7b" dependencies = [ - "once_cell", - "toml_edit 0.19.15", + "lock_api", ] [[package]] -name = "proc-macro-crate" -version = "2.0.2" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" -dependencies = [ - "toml_datetime", - "toml_edit 0.20.2", -] +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "syn" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ - "proc-macro-error-attr", "proc-macro2", "quote", - "syn 1.0.109", - "version_check", + "unicode-ident", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "synstructure" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "version_check", + "syn", ] [[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - -[[package]] -name = "proc-macro2" -version = "1.0.85" +name = "thiserror" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ - "unicode-ident", + "thiserror-impl", ] [[package]] -name = "quote" -version = "1.0.36" +name = "thiserror-impl" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", + "quote", + "syn", ] [[package]] -name = "rand" -version = "0.7.3" +name = "thread_local" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", + "cfg-if", + "once_cell", ] [[package]] -name = "rand" -version = "0.8.5" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", + "displaydoc", + "zerovec", ] [[package]] -name = "rand_chacha" -version = "0.2.2" +name = "to_method" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] +checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "tracing" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "pin-project-lite", + "tracing-attributes", + "tracing-core", ] [[package]] -name = "rand_core" -version = "0.5.1" +name = "tracing-attributes" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ - "getrandom 0.1.16", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "rand_core" -version = "0.6.4" +name = "tracing-core" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ - "getrandom 0.2.15", + "once_cell", + "valuable", ] [[package]] -name = "rand_hc" +name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "raw-window-handle" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "redox_syscall" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" -dependencies = [ - "bitflags 2.5.0", -] - -[[package]] -name = "regex" -version = "1.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" - -[[package]] -name = "rfd" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a73a7337fc24366edfca76ec521f51877b114e42dab584008209cca6719251" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "ashpd", - "block", - "dispatch", - "js-sys", "log", - "objc", - "objc-foundation", - "objc_id", - "pollster", - "raw-window-handle 0.6.2", - "urlencoding", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-sys 0.48.0", + "once_cell", + "tracing-core", ] [[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc_version" -version = "0.4.0" +name = "tracing-subscriber" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ - "semver", + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] -name = "rustix" -version = "0.38.34" +name = "unicode-ident" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" -dependencies = [ - "bitflags 2.5.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] -name = "rustversion" -version = "1.0.17" +name = "unicode-segmentation" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] -name = "ryu" -version = "1.0.18" +name = "unicode-xid" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] -name = "same-file" -version = "1.0.6" +name = "url" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "selectors" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" -dependencies = [ - "bitflags 1.3.2", - "cssparser", - "derive_more", - "fxhash", - "log", - "matches", - "phf 0.8.0", - "phf_codegen 0.8.0", - "precomputed-hash", - "servo_arc", - "smallvec", - "thin-slice", -] - -[[package]] -name = "semver" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" -dependencies = [ - "serde", -] - -[[package]] -name = "send_wrapper" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" -dependencies = [ - "futures-core", -] - -[[package]] -name = "serde" -version = "1.0.203" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - -[[package]] -name = "serde-wasm-bindgen" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" -dependencies = [ - "js-sys", - "serde", - "wasm-bindgen", -] - -[[package]] -name = "serde_derive" -version = "1.0.203" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "serde_json" -version = "1.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" -dependencies = [ - "itoa 1.0.11", - "ryu", - "serde", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" -dependencies = [ - "itoa 1.0.11", - "serde", -] - -[[package]] -name = "serde_qs" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" -dependencies = [ - "percent-encoding", - "serde", - "thiserror", -] - -[[package]] -name = "serde_repr" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "serde_spanned" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa 1.0.11", - "ryu", - "serde", -] - -[[package]] -name = "server_fn" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06e6e5467a2cd93ce1accfdfd8b859404f0b3b2041131ffd774fabf666b8219" -dependencies = [ - "axum", - "bytes", - "const_format", - "dashmap", - "futures", - "gloo-net 0.5.0", - "http 1.1.0", - "http-body-util", - "hyper", - "inventory", - "js-sys", - "once_cell", - "send_wrapper", - "serde", - "serde_json", - "serde_qs", - "server_fn_macro_default", - "thiserror", - "tower", - "tower-layer", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "xxhash-rust", -] - -[[package]] -name = "server_fn_macro" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c216bb1c1ac890151397643c663c875a1836adf0b269be4e389cb1b48c173c" -dependencies = [ - "const_format", - "convert_case 0.6.0", - "proc-macro2", - "quote", - "syn 2.0.66", - "xxhash-rust", -] - -[[package]] -name = "server_fn_macro_default" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00783df297ec85ea605779f2fef9cbec98981dffe2e01e1a9845c102ee1f1ae6" -dependencies = [ - "server_fn_macro", - "syn 2.0.66", -] - -[[package]] -name = "servo_arc" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" -dependencies = [ - "nodrop", - "stable_deref_trait", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "signal-hook" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "sledgehammer_bindgen" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcfaf791ff02f48f3518ce825d32cf419c13a43c1d8b1232f74ac89f339c46d2" -dependencies = [ - "sledgehammer_bindgen_macro", - "wasm-bindgen", -] - -[[package]] -name = "sledgehammer_bindgen_macro" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd941cc539bd3dc694edaf9d0c4e1221d02baa67c6b45ec04fad1024d9e8139" -dependencies = [ - "quote", - "syn 2.0.66", -] - -[[package]] -name = "sledgehammer_utils" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f20798defa0e9d4eff9ca451c7f84774c7378a9c3b5a40112cfa2b3eadb97ae2" -dependencies = [ - "lru", - "once_cell", - "rustc-hash", -] - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "soup3" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" -dependencies = [ - "futures-channel", - "gio", - "glib", - "libc", - "soup3-sys", -] - -[[package]] -name = "soup3-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "spinning" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d4f0e86297cad2658d92a707320d87bf4e6ae1050287f51d19b67ef3f153a7b" -dependencies = [ - "lock_api", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "string_cache" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" -dependencies = [ - "new_debug_unreachable", - "once_cell", - "parking_lot", - "phf_shared 0.10.0", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro2", - "quote", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "sync_wrapper" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" - -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "tao" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ebbccb78deb5a36744c079eea2981b4a48ecbbe6b1b2ffbaa528bea3f5e5db" -dependencies = [ - "bitflags 1.3.2", - "cocoa", - "core-foundation", - "core-graphics", - "crossbeam-channel", - "dispatch", - "dlopen2", - "gdkwayland-sys", - "gdkx11-sys", - "gtk", - "instant", - "jni", - "lazy_static", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "objc", - "once_cell", - "parking_lot", - "raw-window-handle 0.5.2", - "raw-window-handle 0.6.2", - "scopeguard", - "tao-macros", - "unicode-segmentation", - "url", - "windows 0.54.0", - "windows-version", - "x11-dl", -] - -[[package]] -name = "tao-macros" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec114582505d158b669b136e6851f85840c109819d77c42bb7c0709f727d18c2" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "target-lexicon" -version = "0.12.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" - -[[package]] -name = "tempfile" -version = "3.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" -dependencies = [ - "cfg-if", - "fastrand", - "rustix", - "windows-sys 0.52.0", -] - -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - -[[package]] -name = "thin-slice" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" - -[[package]] -name = "thiserror" -version = "1.0.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "to_method" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" - -[[package]] -name = "tokio" -version = "1.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "num_cpus", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.48.0", -] - -[[package]] -name = "tokio-macros" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "tokio-stream" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite", -] - -[[package]] -name = "tokio-util" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "futures-util", - "hashbrown 0.14.5", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.20.2", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.2.6", - "toml_datetime", - "winnow", -] - -[[package]] -name = "toml_edit" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" -dependencies = [ - "indexmap 2.2.6", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" -dependencies = [ - "bitflags 2.5.0", - "bytes", - "futures-util", - "http 1.1.0", - "http-body", - "http-body-util", - "http-range-header", - "httpdate", - "mime", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "tracing-wasm" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" -dependencies = [ - "tracing", - "tracing-subscriber", - "wasm-bindgen", -] - -[[package]] -name = "tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http 1.1.0", - "httparse", - "log", - "rand 0.8.5", - "sha1", - "thiserror", - "url", - "utf-8", -] - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "uds_windows" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" -dependencies = [ - "memoffset", - "tempfile", - "winapi", -] - -[[package]] -name = "unicase" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-segmentation" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" - -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - -[[package]] -name = "url" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" dependencies = [ "form_urlencoded", "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "version-compare" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.66", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" - -[[package]] -name = "wasm-streams" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web_resolver" -version = "0.1.0" -dependencies = [ - "base_resolver", - "dioxus", - "dioxus-logger", - "serde", - "tracing", -] - -[[package]] -name = "web_view" -version = "0.1.0" -dependencies = [ - "app", - "dioxus", - "dioxus-logger", - "serde", - "tracing", -] - -[[package]] -name = "webbrowser" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b" -dependencies = [ - "core-foundation", - "home", - "jni", - "log", - "ndk-context", - "objc", - "raw-window-handle 0.5.2", - "url", - "web-sys", -] - -[[package]] -name = "webkit2gtk" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" -dependencies = [ - "bitflags 1.3.2", - "cairo-rs", - "gdk", - "gdk-sys", - "gio", - "gio-sys", - "glib", - "glib-sys", - "gobject-sys", - "gtk", - "gtk-sys", - "javascriptcore-rs", - "libc", - "once_cell", - "soup3", - "webkit2gtk-sys", -] - -[[package]] -name = "webkit2gtk-sys" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" -dependencies = [ - "bitflags 1.3.2", - "cairo-sys-rs", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk-sys", - "javascriptcore-rs-sys", - "libc", - "pkg-config", - "soup3-sys", - "system-deps", -] - -[[package]] -name = "webview2-com" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0ae9c7e420783826cf769d2c06ac9ba462f450eca5893bb8c6c6529a4e5dd33" -dependencies = [ - "webview2-com-macros", - "webview2-com-sys", - "windows 0.52.0", - "windows-core 0.52.0", - "windows-implement 0.52.0", - "windows-interface 0.52.0", -] - -[[package]] -name = "webview2-com-macros" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "webview2-com-sys" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ad85fceee6c42fa3d61239eba5a11401bf38407a849ed5ea1b407df08cca72" -dependencies = [ - "thiserror", - "windows 0.52.0", - "windows-core 0.52.0", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "percent-encoding", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "urlencoding" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] -name = "winapi-util" -version = "0.1.8" +name = "utf16_iter" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" -dependencies = [ - "windows-sys 0.52.0", -] +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "utf8_iter" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "windows" -version = "0.52.0" +name = "valuable" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core 0.52.0", - "windows-implement 0.52.0", - "windows-interface 0.52.0", - "windows-targets 0.52.5", -] +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] -name = "windows" -version = "0.54.0" +name = "version_check" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-implement 0.53.0", - "windows-interface 0.53.0", - "windows-targets 0.52.5", -] +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] -name = "windows-core" -version = "0.52.0" +name = "wasm-bindgen" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ - "windows-targets 0.52.5", + "cfg-if", + "wasm-bindgen-macro", ] [[package]] -name = "windows-core" -version = "0.54.0" +name = "wasm-bindgen-backend" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ - "windows-result", - "windows-targets 0.52.5", + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", ] [[package]] -name = "windows-implement" -version = "0.52.0" +name = "wasm-bindgen-futures" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "windows-implement" -version = "0.53.0" +name = "wasm-bindgen-macro" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ - "proc-macro2", "quote", - "syn 2.0.66", + "wasm-bindgen-macro-support", ] [[package]] -name = "windows-interface" -version = "0.52.0" +name = "wasm-bindgen-macro-support" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", ] [[package]] -name = "windows-interface" -version = "0.53.0" +name = "wasm-bindgen-shared" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] -name = "windows-result" -version = "0.1.2" +name = "wasm-streams" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" dependencies = [ - "windows-targets 0.52.5", + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] -name = "windows-sys" -version = "0.45.0" +name = "web-sys" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ - "windows-targets 0.42.2", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +name = "web_view" +version = "0.1.0" dependencies = [ - "windows-targets 0.48.5", + "app", + "dioxus", ] [[package]] -name = "windows-sys" -version = "0.52.0" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "windows-targets 0.52.5", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] -name = "windows-targets" -version = "0.42.2" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "windows-targets" -version = "0.48.5" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-targets" @@ -5086,73 +2090,28 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", -] - -[[package]] -name = "windows-version" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6998aa457c9ba8ff2fb9f13e9d2a930dabcea28f1d0ab94d687d8b3654844515" -dependencies = [ - "windows-targets 0.52.5", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.5" @@ -5165,87 +2124,30 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - [[package]] name = "write16" version = "1.0.0" @@ -5258,82 +2160,6 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" -[[package]] -name = "wry" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b717040ba9771fd88eb428c6ea6b555f8e734ff8534f02c13e8f10d97f5935e" -dependencies = [ - "base64", - "block", - "cfg_aliases", - "cocoa", - "core-graphics", - "crossbeam-channel", - "dunce", - "gdkx11", - "gtk", - "html5ever", - "http 0.2.12", - "javascriptcore-rs", - "jni", - "kuchikiki", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "objc", - "objc_id", - "once_cell", - "percent-encoding", - "raw-window-handle 0.6.2", - "serde", - "serde_json", - "sha2", - "soup3", - "tao-macros", - "thiserror", - "webkit2gtk", - "webkit2gtk-sys", - "webview2-com", - "windows 0.52.0", - "windows-implement 0.52.0", - "windows-version", - "x11-dl", -] - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "xdg-home" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca91dcf8f93db085f3a0a29358cd0b9d670915468f4290e8b85d118a34211ab8" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "xxhash-rust" version = "0.8.10" @@ -5360,74 +2186,10 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", "synstructure", ] -[[package]] -name = "zbus" -version = "4.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b8e3d6ae3342792a6cc2340e4394334c7402f3d793b390d2c5494a4032b3030" -dependencies = [ - "async-broadcast", - "async-executor", - "async-fs", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "derivative", - "enumflags2", - "event-listener", - "futures-core", - "futures-sink", - "futures-util", - "hex", - "nix", - "ordered-stream", - "rand 0.8.5", - "serde", - "serde_repr", - "sha1", - "static_assertions", - "tracing", - "uds_windows", - "windows-sys 0.52.0", - "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "4.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a3e850ff1e7217a3b7a07eba90d37fe9bb9e89a310f718afcde5885ca9b6d7" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "regex", - "syn 1.0.109", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" -dependencies = [ - "serde", - "static_assertions", - "zvariant", -] - [[package]] name = "zerocopy" version = "0.7.34" @@ -5445,7 +2207,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -5465,7 +2227,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", "synstructure", ] @@ -5488,43 +2250,5 @@ checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", -] - -[[package]] -name = "zvariant" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e09e8be97d44eeab994d752f341e67b3b0d80512a8b315a0671d47232ef1b65" -dependencies = [ - "endi", - "enumflags2", - "serde", - "static_assertions", - "url", - "zvariant_derive", -] - -[[package]] -name = "zvariant_derive" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a5857e2856435331636a9fbb415b09243df4521a267c5bedcd5289b4d5799e" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn 1.0.109", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index 93484cc..87fe6bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ [workspace] - -members = ["app", "base_resolver", "desktop_resolver", "desktop_view", "web_resolver", "web_view"] +resolver = "2" +members = ["app", "web_view"] +# The initial supported platform is browser/WASM. Legacy desktop scaffolding is +# retained outside the workspace until its platform adapter is implemented. +exclude = ["base_resolver", "desktop_resolver", "desktop_view", "web_resolver"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e44192 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# PACE adventure editor + +A Dioxus 0.5.6 editor targeting **web / WebAssembly** first. Rust is pinned to +1.90.0. Desktop and the old resolver crates are retained but excluded from the +supported workspace. + +## Run + +```sh +rustup show # installs rust-toolchain.toml's toolchain and WASM target +cargo install trunk --version 0.21.14 --locked +trunk serve +``` + +Open http://localhost:8080. `trunk build --release` produces a static site in +`dist/`. No server or account is needed for project authoring. + +```sh +cargo test --workspace +cargo check -p web_view --target wasm32-unknown-unknown +cargo fmt --all -- --check +``` + +## Core dependency — blocked + +The inspected `pace_core` commit is +[`6ccfd6ea60534f3c3312882458e1c83308a76b81`](https://github.com/pace-kit/pace_core/tree/6ccfd6ea60534f3c3312882458e1c83308a76b81). +It contains empty build/debug modules and Bevy templates, but no versioned scene +format, loader, executable runtime, or three-room fixture. No other branches or +PRs were available during implementation. `runtime.lock.json` records this +fact; its null runtime revision is **not** a runnable runtime pin. + +Projects currently use **`pace-editor-draft`, version 1**, an explicitly +editor-owned contract, not a claimed implementation of the missing core format. +Core save/import, runtime preview, and game export cannot be completed until +that dependency exists. Draft files are deliberately named `.pace-draft.json`. +No upstream compatibility is implied. + +## Project files + +The JSON envelope includes its format/version, project title, start-room ID, +stable object IDs, embedded PNG/JPEG/WebP backgrounds, normalized hotspot +rectangles, room links, items, dialogue nodes and choices. Embedded images make +files portable without absolute paths or missing asset folders. Limits: 8 MiB +per background, 16,384 pixels per dimension, 64 MiB per project file. + +Unknown fields, unknown versions, duplicate IDs, and malformed files are rejected +before replacing the current project. Broken references remain editable and are +reported against their owning objects. Deleting a referenced object deliberately +leaves those references visible; undo restores it. History retains 50 edits. +Save downloads the full project and attempts an atomic browser recovery copy. +Recovery is origin-specific and limited by browser quota; a warning instructs you +to save a file if it is unavailable. Project files are the portable source of +truth. Keep a downloaded copy before clearing browser data. + +## Room tools + +Add a room and import its background. Use **Draw hotspot** to drag a rectangle +in either direction, or **Add hotspot** for keyboard-friendly placement. Drag a +selected hotspot to move it; arrow keys nudge it by 1% of the image. Set exact +X/Y/width/height percentages in the inspector. Image replacement preserves the +normalized bounds. A **Go to room** action links to a stable room ID, so renaming +a destination does not break the link. Add a separate return hotspot for a +bidirectional connection. + +Validation entries identify the object ID and field; click an entry to select +its room, hotspot, item, or dialogue. Saves may contain incomplete drafts. +Invalid drafts cannot be rehearsed. + +## Browser checks + +```sh +npm ci +npx playwright install --with-deps chromium +trunk build +npm run test:e2e +``` + +For an existing Chromium installation, set `CHROMIUM_PATH=/usr/bin/chromium`. + +## Dialogue, inventory, and walkthrough + +Create inventory definitions and dialogue nodes from their sidebar sections. +Dialogue choices can branch to another node, grant an item, or end the +conversation. Hotspots can inspect text, link rooms, pick up an item, or start a +dialogue; any action can require an inventory item. Names are editable while IDs +stay stable. + +Follow [the illustrated walkthrough](docs/walkthrough.md) to author **The Garden +Key**, a three-room example, using only the editor controls. The browser test +reproduces the walkthrough, including save/reopen equality for the whole project. + +**Rehearse draft** runs an isolated editor simulation to exercise the draft's +links, dialogue branches and item gates. Restart clears its session. It never +modifies the source project and is **not** the PACE core runtime. **Core preview** +and **Export game** remain disabled. This implementation does not yet meet the +core-format serialization or pinned-runtime preview/export requirements. diff --git a/Trunk.toml b/Trunk.toml new file mode 100644 index 0000000..ed8ada4 --- /dev/null +++ b/Trunk.toml @@ -0,0 +1,6 @@ +[build] +target = "web_view/index.html" +dist = "dist" +[serve] +addresses = ["0.0.0.0"] +port = 8080 diff --git a/app/Cargo.toml b/app/Cargo.toml index a5738ae..d0e407b 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -4,19 +4,16 @@ version = "0.1.0" edition = "2021" [dependencies] +dioxus = { version = "=0.5.6", default-features = false, features = ["macro", "html", "signals", "hooks"] } +serde = { version = "1", features = ["derive", "rc"] } +serde_json = "1" +base64 = "0.22" +imagesize = "0.13" -base_resolver = { path = "../base_resolver" } -web_resolver = { path = "../web_resolver" } -desktop_resolver = { path = "../desktop_resolver" } - -serde = { version = "1.0.197", features = ["derive"] } - -dioxus = { version = "0.5", features = ["router"] } - -# Debug -tracing = "0.1.40" -dioxus-logger = "0.5.0" +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" +web-sys = { version = "0.3", features = ["Window", "Storage", "Document", "Element", "DomRect", "HtmlElement", "HtmlAnchorElement", "Blob", "BlobPropertyBag", "Url"] } +js-sys = "0.3" [features] web = [] -desktop = [] diff --git a/app/src/browser.rs b/app/src/browser.rs new file mode 100644 index 0000000..33e1291 --- /dev/null +++ b/app/src/browser.rs @@ -0,0 +1,121 @@ +//! Small browser boundary; the editor model and history are platform independent. +#[cfg(target_arch = "wasm32")] +mod web { + use wasm_bindgen::JsCast; + const RECOVERY_KEY: &str = "pace.editor.draft.v1"; + pub fn load() -> Result, String> { + web_sys::window() + .ok_or("Browser window unavailable")? + .local_storage() + .map_err(|_| "Recovery storage is unavailable")? + .ok_or("Recovery storage is unavailable")? + .get_item(RECOVERY_KEY) + .map_err(|_| "Cannot read browser recovery".into()) + } + pub fn recover(text: &str) -> Result<(), String> { + web_sys::window().ok_or("Browser window unavailable")?.local_storage().map_err(|_| "Recovery storage is unavailable")? + .ok_or("Recovery storage is unavailable")?.set_item(RECOVERY_KEY, text) + .map_err(|_| "Browser recovery is full or unavailable. Save a project file now; your edits remain open.".into()) + } + pub fn confirm(message: &str) -> bool { + web_sys::window() + .and_then(|w| w.confirm_with_message(message).ok()) + .unwrap_or(false) + } + pub fn download(text: &str) -> Result<(), String> { + let doc = web_sys::window() + .and_then(|w| w.document()) + .ok_or("Document unavailable")?; + let chunks = js_sys::Array::new(); + chunks.push(&text.into()); + let mut options = web_sys::BlobPropertyBag::new(); + options.type_("application/json"); + let blob = web_sys::Blob::new_with_str_sequence_and_options(&chunks, &options) + .map_err(|_| "Cannot prepare project file")?; + let url = web_sys::Url::create_object_url_with_blob(&blob) + .map_err(|_| "Cannot prepare download")?; + let anchor: web_sys::HtmlAnchorElement = doc + .create_element("a") + .map_err(|_| "Cannot prepare download")? + .dyn_into() + .map_err(|_| "Cannot create download link")?; + anchor.set_href(&url); + anchor.set_download("adventure.pace-draft.json"); + let body = doc.body().ok_or("Document body unavailable")?; + body.append_child(&anchor) + .map_err(|_| "Cannot attach download")?; + anchor.click(); + anchor.remove(); + // Revoke after the browser has consumed the click, not before navigation. + let callback = wasm_bindgen::closure::Closure::once_into_js(move || { + let _ = web_sys::Url::revoke_object_url(&url); + }); + if let Some(window) = web_sys::window() { + let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0( + callback.unchecked_ref(), + 1000, + ); + } + Ok(()) + } + pub fn capture_pointer(id: i32) { + if let Some(element) = web_sys::window() + .and_then(|w| w.document()) + .and_then(|d| d.get_element_by_id("room-stage")) + { + let _ = element.set_pointer_capture(id); + } + } + #[wasm_bindgen::prelude::wasm_bindgen( + inline_js = "export function setDirty(dirty) { window.onbeforeunload = dirty ? (event) => { event.preventDefault(); event.returnValue = ''; } : null; } export function openRehearsal() { const dialog = document.getElementById('draft-rehearsal'); if (!dialog) return; dialog.oncancel = event => { event.preventDefault(); dialog.querySelector('#close-rehearsal').click(); }; dialog.showModal(); }" + )] + extern "C" { + #[wasm_bindgen::prelude::wasm_bindgen(js_name = setDirty)] + pub fn set_dirty(dirty: bool); + #[wasm_bindgen::prelude::wasm_bindgen(js_name = openRehearsal)] + pub fn open_rehearsal(); + } + pub fn stage_point(x: f64, y: f64) -> Option<(f64, f64)> { + let rect = web_sys::window()? + .document()? + .get_element_by_id("room-stage")? + .get_bounding_client_rect(); + if rect.width() <= 0. || rect.height() <= 0. { + return None; + } + Some(( + ((x - rect.x()) / rect.width()).clamp(0., 1.), + ((y - rect.y()) / rect.height()).clamp(0., 1.), + )) + } +} +#[cfg(target_arch = "wasm32")] +pub use web::*; +#[cfg(not(target_arch = "wasm32"))] +pub fn load() -> Result, String> { + Ok(None) +} +#[cfg(not(target_arch = "wasm32"))] +pub fn recover(_: &str) -> Result<(), String> { + Ok(()) +} +#[cfg(not(target_arch = "wasm32"))] +pub fn confirm(_: &str) -> bool { + false +} +#[cfg(not(target_arch = "wasm32"))] +pub fn download(_: &str) -> Result<(), String> { + Err("Downloads require the web platform".into()) +} +#[cfg(not(target_arch = "wasm32"))] +pub fn stage_point(_: f64, _: f64) -> Option<(f64, f64)> { + None +} + +#[cfg(not(target_arch = "wasm32"))] +pub fn capture_pointer(_: i32) {} +#[cfg(not(target_arch = "wasm32"))] +pub fn set_dirty(_: bool) {} + +#[cfg(not(target_arch = "wasm32"))] +pub fn open_rehearsal() {} diff --git a/app/src/editor.css b/app/src/editor.css new file mode 100644 index 0000000..967ae5c --- /dev/null +++ b/app/src/editor.css @@ -0,0 +1,3 @@ +:root{font-family:'DM Sans',system-ui,sans-serif;color:#263d3c;background:#f5f6f3;font-synthesis:none;font-size:14px}*{box-sizing:border-box}body{margin:0}button,input,select,textarea{font:inherit}button,.button{display:inline-flex;align-items:center;justify-content:center;gap:6px;border:1px solid #d9dfda;background:#fff;color:#263d3c;border-radius:7px;padding:9px 13px;cursor:pointer;text-decoration:none;font-weight:600}button:hover,.button:hover{background:#edf2e9;border-color:#9cae9d}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,.stage:focus-visible{outline:3px solid #85b2ea;outline-offset:2px}button:disabled{opacity:.45;cursor:not-allowed}.primary{background:#224c43;color:#fff;border-color:#224c43}.primary:hover{background:#16392f}.button input[type=file]{position:absolute;width:1px;height:1px;opacity:0}.button:focus-within{outline:3px solid #85b2ea}h1,h2,h3,p{margin:0}h1,h2,h3{font-family:Manrope,system-ui,sans-serif}h2{font-size:17px}h3{font-size:12px;letter-spacing:.06em;margin:24px 0 12px}p{line-height:1.65;color:#6d7d76}label{display:flex;flex-direction:column;gap:7px;font-size:12px;font-weight:600;margin:16px 0}input:not([type=file]),select,textarea{width:100%;padding:10px;border:1px solid #d9dfda;border-radius:6px;background:#fff;color:#263d3c}textarea{min-height:90px;resize:vertical}.app{display:flex;flex-direction:column;min-height:100vh}.topbar{height:76px;display:flex;align-items:center;gap:12px;padding:0 24px;background:#fff;border-bottom:1px solid #e1e6e0}.brand{font-family:Manrope,sans-serif;font-weight:800;background:#224c43;color:#d9e8aa;width:38px;height:42px;border-radius:12px 12px 12px 2px;font-size:27px;text-align:center;padding-top:2px}.brand-copy{display:flex;flex-direction:column;gap:2px}.brand-copy strong{font-size:16px;letter-spacing:.13em}.brand-copy span{font-size:10px;color:#78867f}.project-title{margin:auto;font-weight:600}.dependency-banner{font-size:11px;padding:9px 24px;background:#edf0df;color:#69734b;border-bottom:1px solid #dfe3ce}.workspace{display:grid;grid-template-columns:220px minmax(320px,1fr) 290px;flex:1;min-height:calc(100vh - 149px)}.sidebar{background:#f9faf7;border-right:1px solid #e0e5dc;padding:24px 14px;display:flex;flex-direction:column}.section-heading{display:flex;align-items:center;justify-content:space-between;margin:28px 10px 12px}.section-heading h2{font-family:inherit;font-size:10px;letter-spacing:.12em;color:#849087}.section-heading button{padding:0;border:0;background:none;font-size:21px}.project-link{justify-content:flex-start;background:none;border:0;font-size:12px}.tree-row{display:flex;justify-content:flex-start;gap:9px;text-align:left;width:100%;border:0;background:none;margin:3px 0;padding:12px}.tree-row.active{background:#e8eddf;color:#234b40}.sidebar-foot{margin-top:auto;padding:48px 10px 0;font-size:11px;color:#849087;line-height:1.5}.main-panel{padding:20px 28px;min-width:0;background:radial-gradient(#dce3d9 .7px,transparent .7px);background-size:16px 16px}.toolbar{display:flex;align-items:center;gap:8px;margin-bottom:24px}.eyebrow{margin-right:auto;color:#859086;font-size:10px;letter-spacing:.12em;font-weight:700}.toolbar button{font-size:11px;padding:7px 11px}.empty-state{padding:90px 20px;text-align:center;max-width:530px;margin:auto}.empty-state h1{font-size:25px;line-height:1.5;margin:18px 0 12px}.empty-icon{display:inline-flex;align-items:center;justify-content:center;font-size:52px;width:90px;height:90px;border-radius:50%;background:#e5ecdd;color:#506c4a}.inspector{padding:26px 22px;background:#fff;border-left:1px solid #e0e5dc;overflow-wrap:anywhere}.issue{display:flex;flex-direction:column;align-items:flex-start;text-align:left;width:100%;font-size:11px;background:#fff8ef;border-color:#f1dfc5;color:#805d28;margin:8px 0;line-height:1.5}.issue span{font-weight:400}.success{color:#357350;font-size:12px}footer{min-height:36px;background:#fff;border-top:1px solid #e0e5dc;padding:9px 24px;font-size:11px;color:#64756c}.muted{font-size:11px;color:#829087;line-height:1.5}.danger{color:#a4473d;border-color:#e9d2cb;margin-top:18px}.room-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}.room-heading h1{font-size:25px}.room-heading p{font-size:12px;margin-top:6px}.stage{position:relative;box-shadow:0 12px 40px #263d3c18;background:#e2e8df;border-radius:8px;overflow:hidden;user-select:none;touch-action:none}.stage>img{display:block;width:100%;height:100%;pointer-events:none;object-fit:fill}.hotspot{position:absolute;background:#e5f29530;border:2px solid #e5f295;border-radius:3px;padding:0;color:#fff;text-shadow:0 1px 3px #000;font-size:11px;overflow:hidden;min-width:0}.hotspot:hover,.hotspot.selected{background:#e5f29555;border-color:#fff}.stage.drawing .hotspot{pointer-events:none}.ghost{position:absolute;border:2px dashed #fff;background:#e5f29555;pointer-events:none}.stage-caption{display:flex;justify-content:space-between;font-size:11px;color:#829087;margin-top:14px}.hotspot-list{margin-top:30px}.hotspot-row{display:flex;gap:12px;align-items:center;background:#fff;border:1px solid #e1e6dc;border-radius:7px;margin:7px 0;padding:12px;width:100%;text-align:left;font-size:12px;justify-content:flex-start}.hotspot-row span{margin-left:auto;color:#849087;font-size:10px}.fields-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 12px}.fields-grid label{margin:8px 0}.segmented{display:flex;gap:7px;margin:16px 0}.segmented button{font-size:11px}.card{background:#fff;border:1px solid #e1e6dc;border-radius:10px;padding:24px;margin-bottom:18px}.choice{padding:14px;background:#f6f8f1;border-radius:8px;margin:14px 0}.preview-overlay{position:fixed;inset:0;z-index:20;background:#132624e8;display:flex;align-items:center;justify-content:center;padding:24px}.preview{background:#f8f9f4;border-radius:12px;padding:24px;width:min(960px,95vw);max-height:95vh;overflow:auto}.preview .stage{max-height:56vh}.preview-head{display:flex;align-items:center;gap:12px;margin-bottom:16px}.preview-head h2{margin-right:auto}.inventory{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:16px 0;font-size:12px}.chip{background:#e6eddb;padding:7px 11px;border-radius:20px}.dialogue{padding:18px;background:#fff;border:1px solid #d9dfda;border-radius:8px}.dialogue p{margin:8px 0 14px}.dialogue button{margin-right:8px}.preview-note{font-size:11px;color:#93734e;margin:10px 0}.error{color:#a4473d}.inline-actions{display:flex;gap:8px;flex-wrap:wrap}@media(max-width:1050px){.workspace{grid-template-columns:180px minmax(280px,1fr) 250px}.main-panel{padding:18px}.topbar{gap:8px;padding:0 15px}.project-title{font-size:12px}.brand-copy span{display:none}}@media(max-width:760px){.workspace{grid-template-columns:150px 1fr}.inspector{grid-column:1/-1;border-top:1px solid #e0e5dc}.topbar{height:auto;min-height:76px;flex-wrap:wrap;padding:12px}.project-title{flex:1}.brand-copy{display:none}.topbar button,.topbar .button{font-size:11px}.sidebar{padding:12px 6px}.main-panel{padding:14px}.room-heading{align-items:flex-start;gap:8px}.toolbar{flex-wrap:wrap}.eyebrow{width:100%}.empty-state{padding:40px 10px}} + +.preview-overlay{width:100vw;height:100vh;max-width:none;max-height:none;border:0;margin:0;color:inherit}.preview-overlay::backdrop{background:transparent} diff --git a/app/src/editor/mod.rs b/app/src/editor/mod.rs new file mode 100644 index 0000000..52b6ebb --- /dev/null +++ b/app/src/editor/mod.rs @@ -0,0 +1,144 @@ +use crate::model::Project; +use std::sync::atomic::{AtomicUsize, Ordering}; +static NEXT_DOCUMENT: AtomicUsize = AtomicUsize::new(1); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Selection { + Project, + Room(String), + Hotspot(String, String), + Item(String), + Dialogue(String), +} + +#[derive(Clone)] +pub struct Editor { + pub document_id: usize, + pub project: Project, + pub selection: Selection, + pub dirty: bool, + undo: Vec, + redo: Vec, +} + +impl Default for Editor { + fn default() -> Self { + Self::new(Project::default()) + } +} + +impl Editor { + pub fn new(project: Project) -> Self { + let selection = project + .rooms + .first() + .map(|r| Selection::Room(r.id.clone())) + .unwrap_or(Selection::Project); + Self { + document_id: NEXT_DOCUMENT.fetch_add(1, Ordering::Relaxed), + project, + selection, + dirty: false, + undo: vec![], + redo: vec![], + } + } + pub fn edit(&mut self, change: impl FnOnce(&mut Project)) { + let before = self.project.clone(); + change(&mut self.project); + if before != self.project { + self.undo.push(before); + if self.undo.len() > 50 { + self.undo.remove(0); + } + self.redo.clear(); + self.dirty = true; + } + } + pub fn can_undo(&self) -> bool { + !self.undo.is_empty() + } + pub fn can_redo(&self) -> bool { + !self.redo.is_empty() + } + pub fn undo(&mut self) { + if let Some(previous) = self.undo.pop() { + self.redo + .push(std::mem::replace(&mut self.project, previous)); + self.reconcile_selection(); + self.dirty = true; + } + } + pub fn redo(&mut self) { + if let Some(next) = self.redo.pop() { + self.undo.push(std::mem::replace(&mut self.project, next)); + self.reconcile_selection(); + self.dirty = true; + } + } + pub fn open(&mut self, text: &str) -> Result<(), String> { + let project = Project::from_json(text)?; + *self = Self::new(project); + Ok(()) + } + pub fn selected_room(&self) -> Option<&str> { + match &self.selection { + Selection::Room(id) | Selection::Hotspot(id, _) => Some(id), + _ => None, + } + } + pub fn focus(&mut self, id: Option<&str>) { + self.selection = Selection::Project; + if let Some(id) = id { + for room in &self.project.rooms { + if room.id == id { + self.selection = Selection::Room(id.into()); + return; + } + if room.hotspots.iter().any(|h| h.id == id) { + self.selection = Selection::Hotspot(room.id.clone(), id.into()); + return; + } + } + if self.project.items.iter().any(|i| i.id == id) { + self.selection = Selection::Item(id.into()); + } + if let Some(d) = self + .project + .dialogues + .iter() + .find(|d| d.id == id || d.choices.iter().any(|c| c.id == id)) + { + self.selection = Selection::Dialogue(d.id.clone()); + } + } + } + pub fn delete_selection(&mut self) { + let selection = self.selection.clone(); + self.edit(|p| match &selection { + Selection::Room(id) => p.rooms.retain(|r| &r.id != id), + Selection::Hotspot(room_id, id) => { + if let Some(r) = p.room_mut(room_id) { + r.hotspots.retain(|h| &h.id != id); + } + } + Selection::Item(id) => p.items.retain(|i| &i.id != id), + Selection::Dialogue(id) => p.dialogues.retain(|d| &d.id != id), + Selection::Project => {} + }); + // Keep references to deleted objects so validation exposes each affected + // object instead of silently changing the author's intended story. + self.reconcile_selection(); + } + fn reconcile_selection(&mut self) { + let selection = self.selection.clone(); + let id = match &selection { + Selection::Project => None, + Selection::Room(id) + | Selection::Item(id) + | Selection::Dialogue(id) + | Selection::Hotspot(_, id) => Some(id.as_str()), + }; + self.focus(id); + } +} diff --git a/app/src/lib.rs b/app/src/lib.rs index 847174d..d4304c5 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -1,59 +1,11 @@ #![allow(non_snake_case)] -mod no_resolver; - -use base_resolver::PartialsResolver; -use dioxus::prelude::*; - -// If neither feature is enabled, -#[cfg(not(any(feature = "web", feature = "desktop")))] -use no_resolver::Resolver; - -// Base case for when both features are enabled -#[cfg(all(feature = "web", feature = "desktop"))] -use no_resolver::Resolver; - -// If feature "web" is enabled, use web_resolver -#[cfg(all(feature = "web", not(feature = "desktop")))] -use web_resolver::Resolver; - -// If feature "desktop" is enabled, use desktop_resolver -#[cfg(all(feature = "desktop", not(feature = "web")))] -use desktop_resolver::Resolver; - -#[derive(Clone, Routable, Debug, PartialEq)] -enum Route { - #[route("/")] - Home {}, - #[route("/blog/:id")] - Blog { id: i32 }, -} - -#[component] -pub fn App() -> Element { - rsx! { - Router:: {} - } -} - -#[component] -fn Blog(id: i32) -> Element { - rsx! { - Link { to: Route::Home {}, "Go to counter" } - "Blog post {id}" - } -} - -#[component] -fn Home() -> Element { - let mut count = use_signal(|| 0); - - rsx! { - Link { to: Route::Blog { id: count() }, "Go to blog" } - div { - h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } - {Resolver::HomeSub()} - } - } -} +mod browser; +pub mod editor; +pub mod model; +mod rooms; +mod ui; +pub use ui::App; + +mod preview; +pub mod rehearsal; +mod story; diff --git a/app/src/model.rs b/app/src/model.rs new file mode 100644 index 0000000..6112864 --- /dev/null +++ b/app/src/model.rs @@ -0,0 +1,476 @@ +//! Editor-owned draft contract. This is deliberately NOT named the PACE core +//! format: the inspected upstream revision has no serializable model or runtime. +use base64::{engine::general_purpose::STANDARD, Engine}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashSet, sync::Arc}; + +pub const FORMAT: &str = "pace-editor-draft"; +pub const VERSION: u32 = 1; +pub const MAX_IMAGE_BYTES: usize = 8 * 1024 * 1024; +pub const MAX_PROJECT_BYTES: usize = 64 * 1024 * 1024; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Project { + pub format: String, + pub version: u32, + pub title: String, + pub start_room: Option, + pub rooms: Vec, + pub items: Vec, + pub dialogues: Vec, + next_id: u64, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Room { + pub id: String, + pub name: String, + pub background: Option, + pub hotspots: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Background { + pub name: String, + pub data_url: Arc, + pub width: u32, + pub height: u32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Rect { + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, +} + +impl Rect { + /// Coordinates are normalized to the original image, independent of zoom. + pub fn from_drag(start: (f64, f64), end: (f64, f64)) -> Option { + let (ax, ay) = (start.0.clamp(0., 1.), start.1.clamp(0., 1.)); + let (bx, by) = (end.0.clamp(0., 1.), end.1.clamp(0., 1.)); + let rect = Self { + x: ax.min(bx), + y: ay.min(by), + width: (ax - bx).abs(), + height: (ay - by).abs(), + }; + (rect.valid() && rect.width >= 0.005 && rect.height >= 0.005).then_some(rect) + } + + pub fn valid(&self) -> bool { + [self.x, self.y, self.width, self.height] + .iter() + .all(|n| n.is_finite()) + && self.x >= 0. + && self.y >= 0. + && self.width > 0. + && self.height > 0. + && self.x + self.width <= 1.000001 + && self.y + self.height <= 1.000001 + } + + pub fn translated(self, dx: f64, dy: f64) -> Self { + Self { + x: (self.x + dx).clamp(0., (1. - self.width).max(0.)), + y: (self.y + dy).clamp(0., (1. - self.height).max(0.)), + ..self + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Hotspot { + pub id: String, + pub name: String, + pub rect: Rect, + pub action: Action, + pub requires_item: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub enum Action { + Look { text: String }, + GoTo { room_id: String }, + Take { item_id: String }, + Talk { dialogue_id: String }, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Item { + pub id: String, + pub name: String, + pub description: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Dialogue { + pub id: String, + pub name: String, + pub speaker: String, + pub text: String, + pub choices: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Choice { + pub id: String, + pub text: String, + pub next: Option, + pub give_item: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Issue { + pub object_id: Option, + pub field: String, + pub message: String, +} + +impl Default for Project { + fn default() -> Self { + Self { + format: FORMAT.into(), + version: VERSION, + title: "Untitled adventure".into(), + start_room: None, + rooms: vec![], + items: vec![], + dialogues: vec![], + next_id: 1, + } + } +} + +impl Project { + pub fn to_json(&self) -> Result { + if self.rooms.iter().flat_map(|r| &r.hotspots).any(|h| { + [h.rect.x, h.rect.y, h.rect.width, h.rect.height] + .iter() + .any(|n| !n.is_finite()) + }) { + return Err( + "Cannot save a hotspot with non-finite coordinates. Correct its bounds first." + .into(), + ); + } + let text = serde_json::to_string_pretty(self).map_err(|e| e.to_string())?; + if text.len() > MAX_PROJECT_BYTES { + return Err("Project exceeds the 64 MiB file limit. Reduce background sizes.".into()); + } + Ok(text) + } + + pub fn from_json(text: &str) -> Result { + if text.len() > MAX_PROJECT_BYTES { + return Err("Project exceeds the 64 MiB file limit.".into()); + } + let value: serde_json::Value = + serde_json::from_str(text).map_err(|e| format!("Invalid project JSON: {e}"))?; + if value.get("format").and_then(|v| v.as_str()) != Some(FORMAT) { + return Err( + "This is not a PACE editor draft. Core-format import awaits the upstream contract." + .into(), + ); + } + if value.get("version").and_then(|v| v.as_u64()) != Some(VERSION as u64) { + return Err("Unsupported draft version. Open it with the editor that created it; no data was replaced.".into()); + } + // Strict decoding prevents silent loss of fields from a newer writer. + let project: Self = serde_json::from_str(text) + .map_err(|e| format!("Unsupported or malformed project: {e}"))?; + // Invalid references are editable, but ambiguous IDs cannot safely be edited. + let mut ids = HashSet::new(); + for id in project.ids() { + if id.trim().is_empty() || !ids.insert(id) { + return Err(format!( + "Empty or duplicate object ID: {id:?}. No data was replaced." + )); + } + } + Ok(project) + } + + fn ids(&self) -> Vec<&str> { + self.rooms + .iter() + .flat_map(|r| { + std::iter::once(r.id.as_str()).chain(r.hotspots.iter().map(|h| h.id.as_str())) + }) + .chain(self.items.iter().map(|i| i.id.as_str())) + .chain(self.dialogues.iter().flat_map(|d| { + std::iter::once(d.id.as_str()).chain(d.choices.iter().map(|c| c.id.as_str())) + })) + .collect() + } + + pub fn allocate_id(&mut self, kind: &str) -> String { + loop { + let id = format!("{kind}-{}", self.next_id); + self.next_id = self.next_id.wrapping_add(1); + if !self.ids().contains(&id.as_str()) { + return id; + } + } + } + + pub fn add_room(&mut self) -> String { + let id = self.allocate_id("room"); + self.rooms.push(Room { + id: id.clone(), + name: format!("Room {}", self.rooms.len() + 1), + background: None, + hotspots: vec![], + }); + if self.start_room.is_none() { + self.start_room = Some(id.clone()); + } + id + } + + pub fn room(&self, id: &str) -> Option<&Room> { + self.rooms.iter().find(|r| r.id == id) + } + pub fn room_mut(&mut self, id: &str) -> Option<&mut Room> { + self.rooms.iter_mut().find(|r| r.id == id) + } + + pub fn add_hotspot(&mut self, room_id: &str, rect: Rect) -> Option { + if !rect.valid() || self.room(room_id)?.background.is_none() { + return None; + } + let id = self.allocate_id("hotspot"); + let room = self.room_mut(room_id)?; + room.hotspots.push(Hotspot { + id: id.clone(), + name: format!("Hotspot {}", room.hotspots.len() + 1), + rect, + action: Action::Look { + text: "Something catches your eye.".into(), + }, + requires_item: None, + }); + Some(id) + } + + pub fn add_item(&mut self) -> String { + let id = self.allocate_id("item"); + self.items.push(Item { + id: id.clone(), + name: "New item".into(), + description: String::new(), + }); + id + } + + pub fn add_dialogue(&mut self) -> String { + let id = self.allocate_id("dialogue"); + self.dialogues.push(Dialogue { + id: id.clone(), + name: "New dialogue".into(), + speaker: "Narrator".into(), + text: "Hello, traveler.".into(), + choices: vec![], + }); + id + } + + pub fn validate(&self) -> Vec { + let mut issues = vec![]; + let mut push = |id: Option<&str>, field: &str, message: String| { + issues.push(Issue { + object_id: id.map(str::to_owned), + field: field.into(), + message, + }) + }; + if self.title.trim().is_empty() { + push(None, "title", "Give the project a title.".into()); + } + if self + .start_room + .as_deref() + .is_none_or(|id| self.room(id).is_none()) + { + push( + None, + "start_room", + "Choose an existing starting room.".into(), + ); + } + let has_item = |id: &str| self.items.iter().any(|i| i.id == id); + let has_dialogue = |id: &str| self.dialogues.iter().any(|d| d.id == id); + for r in &self.rooms { + if r.name.trim().is_empty() { + push(Some(&r.id), "name", "Room name is empty.".into()); + } + match &r.background { + None => push( + Some(&r.id), + "background", + "Import a background for this room.".into(), + ), + Some(b) => { + if let Err(e) = b.validate() { + push(Some(&r.id), "background", e); + } + } + } + for h in &r.hotspots { + if h.name.trim().is_empty() { + push(Some(&h.id), "name", "Hotspot name is empty.".into()); + } + if !h.rect.valid() { + push( + Some(&h.id), + "rect", + "Hotspot must have a positive size and fit inside the background.".into(), + ); + } + if let Some(id) = &h.requires_item { + if !has_item(id) { + push( + Some(&h.id), + "requires_item", + format!("Required item {id:?} does not exist."), + ); + } + } + match &h.action { + Action::GoTo { room_id } if self.room(room_id).is_none() => push( + Some(&h.id), + "action.room_id", + format!("Linked room {room_id:?} does not exist."), + ), + Action::Take { item_id } if !has_item(item_id) => push( + Some(&h.id), + "action.item_id", + format!("Item {item_id:?} does not exist."), + ), + Action::Talk { dialogue_id } if !has_dialogue(dialogue_id) => push( + Some(&h.id), + "action.dialogue_id", + format!("Dialogue {dialogue_id:?} does not exist."), + ), + Action::Look { text } if text.trim().is_empty() => { + push(Some(&h.id), "action.text", "Enter inspection text.".into()) + } + _ => {} + } + } + } + for item in &self.items { + if item.name.trim().is_empty() { + push(Some(&item.id), "name", "Item name is empty.".into()); + } + } + for d in &self.dialogues { + if d.name.trim().is_empty() { + push(Some(&d.id), "name", "Dialogue name is empty.".into()); + } + if d.text.trim().is_empty() { + push(Some(&d.id), "text", "Enter dialogue text.".into()); + } + for c in &d.choices { + if c.text.trim().is_empty() { + push(Some(&c.id), "text", "Enter choice text.".into()); + } + if let Some(id) = &c.next { + if !has_dialogue(id) { + push( + Some(&c.id), + "next", + format!("Next dialogue {id:?} does not exist."), + ); + } + } + if let Some(id) = &c.give_item { + if !has_item(id) { + push( + Some(&c.id), + "give_item", + format!("Reward item {id:?} does not exist."), + ); + } + } + } + } + issues + } +} + +impl Background { + /// Only embedded raster data is rendered; malformed imports cannot load a + /// remote URL or an active document through the image element. + pub fn display_url(&self) -> &str { + if [ + "data:image/png;base64,", + "data:image/jpeg;base64,", + "data:image/webp;base64,", + ] + .iter() + .any(|prefix| self.data_url.starts_with(prefix)) + { + &self.data_url + } else { + "" + } + } + + pub fn import(name: String, bytes: &[u8]) -> Result { + if bytes.len() > MAX_IMAGE_BYTES { + return Err("Background exceeds 8 MiB. Resize it before importing.".into()); + } + let mime = if bytes.starts_with(b"\x89PNG\r\n\x1a\n") { + "image/png" + } else if bytes.starts_with(&[0xff, 0xd8, 0xff]) { + "image/jpeg" + } else if bytes.starts_with(b"RIFF") && bytes.get(8..12) == Some(b"WEBP") { + "image/webp" + } else { + return Err("Use a PNG, JPEG, or WebP image.".into()); + }; + let size = imagesize::blob_size(bytes) + .map_err(|_| "Cannot read this image. It may be corrupt.".to_string())?; + if size.width == 0 || size.height == 0 || size.width > 16384 || size.height > 16384 { + return Err("Image dimensions must be between 1 and 16384 pixels.".into()); + } + Ok(Self { + name, + data_url: format!("data:{mime};base64,{}", STANDARD.encode(bytes)).into(), + width: size.width as u32, + height: size.height as u32, + }) + } + + fn validate(&self) -> Result<(), String> { + let (header, data) = self + .data_url + .split_once(',') + .ok_or("Invalid embedded background.")?; + if data.len() > MAX_IMAGE_BYTES * 4 / 3 + 4 { + return Err("Embedded background exceeds 8 MiB.".into()); + } + let bytes = STANDARD + .decode(data) + .map_err(|_| "Invalid background encoding.")?; + let parsed = Self::import(self.name.clone(), &bytes)?; + if parsed.data_url.split_once(',').map(|p| p.0) != Some(header) + || parsed.width != self.width + || parsed.height != self.height + { + return Err("Background type or dimensions do not match its image data.".into()); + } + Ok(()) + } +} diff --git a/app/src/preview.rs b/app/src/preview.rs new file mode 100644 index 0000000..a09f37b --- /dev/null +++ b/app/src/preview.rs @@ -0,0 +1,47 @@ +use crate::rehearsal::Rehearsal; +use dioxus::prelude::*; + +#[component] +pub fn DraftPreview(mut session: Signal>) -> Element { + use_effect(crate::browser::open_rehearsal); + let state = session.read().clone()?; + let room = state.project.room(&state.room_id)?; + let dialogue = state + .dialogue_id + .as_ref() + .and_then(|id| state.project.dialogues.iter().find(|d| &d.id == id)); + rsx! { + dialog { id: "draft-rehearsal", class: "preview-overlay", role: "dialog", "aria-modal": "true", "aria-label": "Draft rehearsal", + div { class: "preview", + div { class: "preview-head", h2 { "{room.name}" } + button { onclick: move |_| { let project = session.read().as_ref().map(|s| s.project.clone()); if let Some(p) = project { session.set(Rehearsal::new(&p).ok()); } }, "Restart" } + button { id: "close-rehearsal", autofocus: true, onclick: move |_| session.set(None), "Close rehearsal" } + } + p { class: "preview-note", "Editor rehearsal only · PACE runtime integration is pending. This does not verify runtime compatibility." } + if let Some(bg) = &room.background { + div { class: "stage", style: "aspect-ratio:{bg.width}/{bg.height}", + img { src: bg.display_url(), alt: "{room.name} background" } + for hotspot in room.hotspots.clone() { + button { class: "hotspot", disabled: dialogue.is_some(), style: format!("left:{}%;top:{}%;width:{}%;height:{}%",hotspot.rect.x*100.,hotspot.rect.y*100.,hotspot.rect.width*100.,hotspot.rect.height*100.), "aria-label": "Interact with {hotspot.name}", + onclick: move |_| { if let Some(state) = session.write().as_mut() { if let Err(e) = state.activate(&hotspot.id) { state.message = e; } } }, "{hotspot.name}" + } + } + } + } + div { class: "inventory", strong { "INVENTORY" } + if state.inventory.is_empty() { span { class: "muted", "Nothing collected yet" } } + for item in state.project.items.iter().filter(|i| state.inventory.contains(&i.id)) { span { class: "chip", title: "{item.description}", "{item.name}" } } + } + if let Some(d) = dialogue { + div { class: "dialogue", strong { "{d.speaker}" } p { "{d.text}" } + for choice in d.choices.clone() { + button { onclick: move |_| { if let Some(state) = session.write().as_mut() { if let Err(e) = state.choose(&choice.id) { state.message = e; } } }, "{choice.text}" } + } + if d.choices.is_empty() { button { onclick: move |_| { if let Some(state) = session.write().as_mut() { if let Err(e) = state.continue_dialogue() { state.message = e; } } }, "Continue" } } + } + } + p { role: "status", "{state.message}" } + } + } + } +} diff --git a/app/src/rehearsal.rs b/app/src/rehearsal.rs new file mode 100644 index 0000000..75b0c45 --- /dev/null +++ b/app/src/rehearsal.rs @@ -0,0 +1,120 @@ +//! An editor-only interaction rehearsal, not the unavailable PACE runtime. +//! It runs on an isolated snapshot and must never be used as game export. +use crate::model::{Action, Project}; +use std::collections::BTreeSet; + +#[derive(Clone)] +pub struct Rehearsal { + pub project: Project, + pub room_id: String, + pub inventory: BTreeSet, + pub dialogue_id: Option, + pub message: String, +} +impl Rehearsal { + pub fn new(project: &Project) -> Result { + let issues = project.validate(); + if !issues.is_empty() { + return Err(format!( + "Resolve {} validation error(s) before rehearsing.", + issues.len() + )); + } + Ok(Self { + project: project.clone(), + room_id: project.start_room.clone().ok_or("Choose a starting room")?, + inventory: BTreeSet::new(), + dialogue_id: None, + message: "Click a hotspot to try its action.".into(), + }) + } + pub fn activate(&mut self, hotspot_id: &str) -> Result<(), String> { + if self.dialogue_id.is_some() { + return Err("Finish the conversation first.".into()); + } + let h = self + .project + .room(&self.room_id) + .and_then(|r| r.hotspots.iter().find(|h| h.id == hotspot_id)) + .ok_or("Hotspot is not in the current room")? + .clone(); + if let Some(id) = &h.requires_item { + if !self.inventory.contains(id) { + let name = self + .project + .items + .iter() + .find(|i| &i.id == id) + .map(|i| i.name.as_str()) + .unwrap_or(id); + self.message = format!("You need {name} first."); + return Ok(()); + } + } + match h.action { + Action::Look { text } => self.message = text, + Action::GoTo { room_id } => { + if self.project.room(&room_id).is_none() { + return Err("Destination room is missing".into()); + } + self.room_id = room_id; + self.message = "You enter another room.".into(); + } + Action::Take { item_id } => { + let item = self + .project + .items + .iter() + .find(|i| i.id == item_id) + .ok_or("Item is missing")?; + self.message = if self.inventory.insert(item_id) { + format!("Picked up {}. {}", item.name, item.description) + } else { + format!("You already have {}.", item.name) + }; + } + Action::Talk { dialogue_id } => { + if !self.project.dialogues.iter().any(|d| d.id == dialogue_id) { + return Err("Dialogue is missing".into()); + } + self.dialogue_id = Some(dialogue_id); + self.message.clear(); + } + } + Ok(()) + } + pub fn choose(&mut self, choice_id: &str) -> Result<(), String> { + let choice = self + .project + .dialogues + .iter() + .find(|d| Some(&d.id) == self.dialogue_id.as_ref()) + .and_then(|d| d.choices.iter().find(|c| c.id == choice_id)) + .ok_or("Choice is not in the active dialogue")? + .clone(); + if let Some(item_id) = choice.give_item { + self.inventory.insert(item_id); + } + self.dialogue_id = choice.next; + self.message = if self.dialogue_id.is_none() { + "The conversation ends.".into() + } else { + String::new() + }; + Ok(()) + } + pub fn continue_dialogue(&mut self) -> Result<(), String> { + let d = self + .project + .dialogues + .iter() + .find(|d| Some(&d.id) == self.dialogue_id.as_ref()) + .ok_or("No active conversation")?; + if !d.choices.is_empty() { + return Err("Choose a response first".into()); + } + self.dialogue_id = None; + self.message = "The conversation ends.".into(); + Ok(()) + } +} diff --git a/app/src/rooms.rs b/app/src/rooms.rs new file mode 100644 index 0000000..43e2c21 --- /dev/null +++ b/app/src/rooms.rs @@ -0,0 +1,237 @@ +use crate::{ + browser, + editor::{Editor, Selection}, + model::{Action, Background, Hotspot, Rect, Room}, +}; +use dioxus::prelude::*; + +pub fn update_room(mut editor: Signal, change: impl FnOnce(&mut Room)) { + let id = editor.read().selected_room().map(str::to_owned); + if let Some(id) = id { + editor.write().edit(|p| { + if let Some(r) = p.room_mut(&id) { + change(r); + } + }); + } +} +pub fn update_hotspot(mut editor: Signal, change: impl FnOnce(&mut Hotspot)) { + let selection = editor.read().selection.clone(); + if let Selection::Hotspot(room_id, id) = selection { + editor.write().edit(|p| { + if let Some(h) = p + .room_mut(&room_id) + .and_then(|r| r.hotspots.iter_mut().find(|h| h.id == id)) + { + change(h); + } + }); + } +} +fn position(rect: Rect) -> String { + format!( + "left:{}%;top:{}%;width:{}%;height:{}%", + rect.x * 100., + rect.y * 100., + rect.width * 100., + rect.height * 100. + ) +} +#[derive(Clone)] +enum Drag { + Draw { + start: (f64, f64), + end: (f64, f64), + }, + Move { + id: String, + start: (f64, f64), + end: (f64, f64), + original: Rect, + }, +} +impl Drag { + fn rect(&self) -> Option { + match self { + Self::Draw { start, end } => Rect::from_drag(*start, *end), + Self::Move { + start, + end, + original, + .. + } => Some(original.translated(end.0 - start.0, end.1 - start.1)), + } + } + fn set_end(&mut self, point: (f64, f64)) { + match self { + Self::Draw { end, .. } | Self::Move { end, .. } => *end = point, + } + } +} + +#[component] +pub fn RoomWorkspace(mut editor: Signal, mut notice: Signal) -> Element { + let mut drawing = use_signal(|| false); + let mut drag: Signal> = use_signal(|| None); + let room = editor + .read() + .selected_room() + .and_then(|id| editor.read().project.room(id).cloned()); + let Some(room) = room else { + return rsx! { div { class: "empty-state", span { class: "empty-icon", "◇" } h1 { "Every adventure starts somewhere." } p { "Add a room, import its background, and bring your world to life." } } }; + }; + let selected_id = match &editor.read().selection { + Selection::Hotspot(_, id) => Some(id.clone()), + _ => None, + }; + let dimensions = room + .background + .as_ref() + .map(|b| format!("{} × {}", b.width, b.height)) + .unwrap_or_default(); + rsx! { + div { class: "room-workspace", onkeydown: move |event| { if event.key() == Key::Escape { drag.set(None); drawing.set(false); } }, + div { class: "room-heading", div { h1 { "{room.name}" } p { "Set the scene. Give every detail a story." } } + label { class: "button", "Import background" + input { r#type: "file", accept: "image/png,image/jpeg,image/webp", "aria-label": "Import background", onchange: move |event: Event| async move { + let room_id = editor.read().selected_room().map(str::to_owned); + let document_id = editor.read().document_id; + if let (Some(files), Some(room_id)) = (event.files(), room_id) { + if let Some(name) = files.files().first() { + match files.read_file(name).await { + Some(_) if editor.read().document_id != document_id => notice.set("Image import cancelled because a different project was opened.".into()), + Some(bytes) => match Background::import(name.clone(), &bytes) { + Ok(background) => { editor.write().edit(|p| { if let Some(room) = p.room_mut(&room_id) { room.background = Some(background); } }); notice.set("Background imported. Hotspot bounds scale with the image.".into()); }, + Err(e) => notice.set(e), + }, + None => notice.set("Cannot read this image.".into()), + } + } + } + } } + } + } + if let Some(background) = &room.background { + div { class: "segmented", + button { class: if !drawing() { "primary" } else { "" }, onclick: move |_| { drawing.set(false); drag.set(None); }, "Select / move" } + button { class: if drawing() { "primary" } else { "" }, onclick: move |_| { drawing.set(true); drag.set(None); }, "Draw hotspot" } + button { onclick: move |_| { + let room_id = editor.read().selected_room().map(str::to_owned); + if let Some(room_id) = room_id { let mut id = None; editor.write().edit(|p| id = p.add_hotspot(&room_id, Rect { x:0.35,y:0.35,width:0.2,height:0.2 })); if let Some(id) = id { editor.write().selection = Selection::Hotspot(room_id,id); } } + }, "Add hotspot" } + } + div { id: "room-stage", tabindex: "0", class: if drawing() { "stage drawing" } else { "stage" }, style: "aspect-ratio:{background.width}/{background.height}", "aria-label": "Room canvas", + onkeydown: move |event| { if event.key() == Key::Escape { drag.set(None); drawing.set(false); } }, + onpointerdown: move |event| { if drawing() && event.trigger_button() == Some(dioxus::html::input_data::MouseButton::Primary) { + let point = event.client_coordinates(); if let Some(point) = browser::stage_point(point.x,point.y) { browser::capture_pointer(event.pointer_id()); drag.set(Some(Drag::Draw {start:point,end:point})); } + } }, + onpointermove: move |event| { let point = event.client_coordinates(); if let Some(point) = browser::stage_point(point.x,point.y) { if let Some(drag) = drag.write().as_mut() { drag.set_end(point); } } }, + onpointercancel: move |_| drag.set(None), + onpointerup: move |_| { + let gesture = drag.write().take(); + if let Some(gesture) = gesture { if let Some(rect) = gesture.rect() { + let room_id = editor.read().selected_room().map(str::to_owned); + if let Some(room_id) = room_id { match gesture { + Drag::Draw {..} => { let mut id = None; editor.write().edit(|p| id = p.add_hotspot(&room_id,rect)); if let Some(id) = id { editor.write().selection = Selection::Hotspot(room_id,id); } drawing.set(false); }, + Drag::Move {id,..} => editor.write().edit(|p| { if let Some(h) = p.room_mut(&room_id).and_then(|r| r.hotspots.iter_mut().find(|h| h.id == id)) { h.rect = rect; } }), + } } + } } + }, + img { src: background.display_url(), alt: "{room.name} background", draggable: "false" } + for hotspot in room.hotspots.clone() { + HotspotButton { key: "{hotspot.id}", hotspot: hotspot.clone(), selected: selected_id.as_deref() == Some(&hotspot.id), editor, drag } + } + if let Some(rect) = drag.read().as_ref().and_then(Drag::rect) { div { class: "ghost", style: position(rect) } } + } + div { class: "stage-caption", span { "{dimensions} · Original image coordinates" } span { "{room.hotspots.len()} hotspots" } } + p { class: "muted", style: "margin-top:14px", "Draw a rectangle, drag to move, or use Add hotspot and edit its bounds. Escape cancels a gesture." } + } else { + div { class: "empty-state", span { class: "empty-icon", "▧" } h1 { "A place for your story." } p { "Import a PNG, JPEG, or WebP background to start placing hotspots." } p { class: "muted", "Up to 8 MiB per image. Images travel with your project." } } + } + if !room.hotspots.is_empty() { + div { class: "hotspot-list", h3 { "IN THIS ROOM" } + for hotspot in room.hotspots { + button { class: "hotspot-row", onclick: move |_| { + let room_id = editor.read().selected_room().map(str::to_owned); if let Some(room_id) = room_id { editor.write().selection = Selection::Hotspot(room_id,hotspot.id.clone()); } + }, "◇ {hotspot.name}" span { {match hotspot.action { Action::Look {..} => "INSPECT", Action::GoTo {..} => "GO TO ROOM", Action::Take {..} => "PICK UP ITEM", Action::Talk {..} => "DIALOGUE" }} } } + } + } + } + } + } +} + +#[component] +fn HotspotButton( + hotspot: Hotspot, + selected: bool, + mut editor: Signal, + mut drag: Signal>, +) -> Element { + let id = hotspot.id.clone(); + let key_id = id.clone(); + let click_id = id.clone(); + rsx! { button { class: if selected { "hotspot selected" } else { "hotspot" }, style: position(hotspot.rect), "aria-label": "Select hotspot {hotspot.name}", + onclick: move |_| { let room_id = editor.read().selected_room().map(str::to_owned); if let Some(room_id) = room_id { editor.write().selection = Selection::Hotspot(room_id,click_id.clone()); } }, + onpointerdown: move |event| { + event.stop_propagation(); + let room_id = editor.read().selected_room().map(str::to_owned); + if let Some(room_id) = room_id { editor.write().selection = Selection::Hotspot(room_id,id.clone()); } + let point = event.client_coordinates(); if let Some(point) = browser::stage_point(point.x,point.y) { browser::capture_pointer(event.pointer_id()); drag.set(Some(Drag::Move { id:id.clone(),start:point,end:point,original:hotspot.rect })); } + }, + onkeydown: move |event| { + let delta = match event.key() { Key::ArrowLeft => Some((-0.01,0.)), Key::ArrowRight => Some((0.01,0.)), Key::ArrowUp => Some((0.,-0.01)), Key::ArrowDown => Some((0.,0.01)), Key::Escape => { drag.set(None); None }, _ => None }; + if let Some((dx,dy)) = delta { let room_id = editor.read().selected_room().map(str::to_owned); if let Some(room_id) = room_id { editor.write().edit(|p| { if let Some(h) = p.room_mut(&room_id).and_then(|r| r.hotspots.iter_mut().find(|h| h.id == key_id)) { h.rect = h.rect.translated(dx,dy); } }); } } + }, + "{hotspot.name}" + } } +} + +#[component] +pub fn RoomInspector(mut editor: Signal) -> Element { + let room = editor + .read() + .selected_room() + .and_then(|id| editor.read().project.room(id).cloned()); + let room = room?; + let hotspot = match &editor.read().selection { + Selection::Hotspot(_, id) => room.hotspots.iter().find(|h| &h.id == id).cloned(), + _ => None, + }; + if let Some(h) = hotspot { + let action = match h.action { + Action::Look { .. } => "look", + Action::GoTo { .. } => "go_to", + Action::Take { .. } => "take", + Action::Talk { .. } => "talk", + }; + rsx! { + h2 { "Hotspot" } p { class: "muted", "{h.id}" } + label { "Hotspot name" input { value: "{h.name}", oninput: move |e| update_hotspot(editor,|h| h.name = e.value()) } } + h3 { "BOUNDS · % OF IMAGE" } + div { class: "fields-grid", + for (field,value) in [("X",h.rect.x),("Y",h.rect.y),("Width",h.rect.width),("Height",h.rect.height)] { + label { "{field} (%)" input { r#type: "number", min: "0", max: "100", step: "0.1", value: format!("{:.2}",value*100.), oninput: move |e| { if let Ok(value) = e.value().parse::() { update_hotspot(editor, |h| match field { "X" => h.rect.x = value/100., "Y" => h.rect.y = value/100., "Width" => h.rect.width = value/100., _ => h.rect.height = value/100. }); } } } } + } + } + label { "Action" select { value: action, onchange: move |e| update_hotspot(editor, |h| h.action = match e.value().as_str() { "go_to" => Action::GoTo { room_id: String::new() }, "take" => Action::Take { item_id: String::new() }, "talk" => Action::Talk { dialogue_id: String::new() }, _ => Action::Look { text: String::new() } }), + option { value: "look", selected: action == "look", "Inspect" } option { value: "go_to", selected: action == "go_to", "Go to room" } option { value: "take", selected: action == "take", "Pick up item" } option { value: "talk", selected: action == "talk", "Start dialogue" } + } } + match h.action { + Action::Look {text} => rsx! { label { "Inspection text" textarea { value: "{text}", oninput: move |e| update_hotspot(editor, |h| h.action = Action::Look { text:e.value() }) } } }, + Action::GoTo {room_id} => rsx! { label { "Destination room" select { value: "{room_id}", onchange: move |e| update_hotspot(editor, |h| h.action = Action::GoTo { room_id:e.value() }), option { value: "", selected: room_id.is_empty(), "Choose destination" } for r in editor.read().project.rooms.clone() { option { value: "{r.id}", selected: r.id == room_id, "{r.name}" } } } } }, + Action::Take {item_id} => rsx! { label { "Item to pick up" select { value: "{item_id}", onchange: move |e| update_hotspot(editor, |h| h.action = Action::Take { item_id:e.value() }), option { value: "", selected: item_id.is_empty(), "Choose item" } for item in editor.read().project.items.clone() { option { value: "{item.id}", selected: item.id == item_id, "{item.name}" } } } } }, + Action::Talk {dialogue_id} => rsx! { label { "Dialogue to start" select { value: "{dialogue_id}", onchange: move |e| update_hotspot(editor, |h| h.action = Action::Talk { dialogue_id:e.value() }), option { value: "", selected: dialogue_id.is_empty(), "Choose dialogue" } for d in editor.read().project.dialogues.clone() { option { value: "{d.id}", selected: d.id == dialogue_id, "{d.name}" } } } } }, + } + label { "Requires item" select { value: h.requires_item.clone().unwrap_or_default(), onchange: move |e| update_hotspot(editor, |h| h.requires_item = if e.value().is_empty() { None } else { Some(e.value()) }), option { value: "", selected: h.requires_item.is_none(), "No requirement" } for item in editor.read().project.items.clone() { option { value: "{item.id}", selected: h.requires_item.as_deref() == Some(item.id.as_str()), "{item.name}" } } } } + p { class: "muted", "Room links are one-way. Add a return hotspot in the destination room." } + button { class: "danger", onclick: move |_| editor.write().delete_selection(), "Delete hotspot" } + } + } else { + rsx! { h2 { "Room" } p { class: "muted", "{room.id}" } + label { "Room name" input { value: "{room.name}", oninput: move |e| update_room(editor, |r| r.name = e.value()) } } + p { class: "muted", "Select a hotspot on the canvas or in the list to edit its action and bounds." } + button { class: "danger", onclick: move |_| { if browser::confirm("Delete this room and its hotspots? Links from other objects will be reported in validation. You can undo this.") { editor.write().delete_selection(); } }, "Delete room" } + } + } +} diff --git a/app/src/story.rs b/app/src/story.rs new file mode 100644 index 0000000..272b02b --- /dev/null +++ b/app/src/story.rs @@ -0,0 +1,102 @@ +use crate::{ + browser, + editor::{Editor, Selection}, + model::{Choice, Dialogue, Item}, +}; +use dioxus::prelude::*; + +fn update_item(mut editor: Signal, change: impl FnOnce(&mut Item)) { + let selection = editor.read().selection.clone(); + if let Selection::Item(id) = selection { + editor.write().edit(|p| { + if let Some(item) = p.items.iter_mut().find(|i| i.id == id) { + change(item); + } + }); + } +} +fn update_dialogue(mut editor: Signal, change: impl FnOnce(&mut Dialogue)) { + let selection = editor.read().selection.clone(); + if let Selection::Dialogue(id) = selection { + editor.write().edit(|p| { + if let Some(dialogue) = p.dialogues.iter_mut().find(|d| d.id == id) { + change(dialogue); + } + }); + } +} + +#[component] +pub fn StoryWorkspace(mut editor: Signal) -> Element { + let selection = editor.read().selection.clone(); + match selection { + Selection::Item(id) => { + let item = editor + .read() + .project + .items + .iter() + .find(|i| i.id == id) + .cloned(); + let item = item?; + rsx! { div { class: "room-heading", div { h1 { "{item.name}" } p { "Something to find. Something to take along." } } } + div { class: "card", h2 { "Inventory item" } p { class: "muted", "{item.id}" } + label { "Item name" input { value: "{item.name}", oninput: move |e| update_item(editor, |i| i.name = e.value()) } } + label { "Item description" textarea { value: "{item.description}", oninput: move |e| update_item(editor, |i| i.description = e.value()) } } + p { "Use a Pick up item hotspot or a dialogue reward to add this item to the player's inventory. A hotspot can require it before its action is available." } + button { class: "danger", onclick: move |_| { if browser::confirm("Delete this item? References will appear in validation. You can undo this.") { editor.write().delete_selection(); } }, "Delete item" } + } + } + } + Selection::Dialogue(id) => { + let dialogue = editor + .read() + .project + .dialogues + .iter() + .find(|d| d.id == id) + .cloned(); + let d = dialogue?; + rsx! { div { class: "room-heading", div { h1 { "{d.name}" } p { "A conversation can open a whole new world." } } } + div { class: "card", h2 { "Dialogue node" } p { class: "muted", "{d.id}" } + label { "Dialogue name" input { value: "{d.name}", oninput: move |e| update_dialogue(editor, |d| d.name = e.value()) } } + label { "Speaker" input { value: "{d.speaker}", oninput: move |e| update_dialogue(editor, |d| d.speaker = e.value()) } } + label { "Dialogue text" textarea { value: "{d.text}", oninput: move |e| update_dialogue(editor, |d| d.text = e.value()) } } + h3 { "PLAYER CHOICES" } + p { class: "muted", "Each choice can lead to another node and grant an item. No choices gives the player a Continue button to end the conversation." } + for choice in d.choices { ChoiceEditor { key: "{choice.id}", choice, editor } } + button { onclick: move |_| { + let selection = editor.read().selection.clone(); + if let Selection::Dialogue(dialogue_id) = selection { editor.write().edit(|p| { + let id = p.allocate_id("choice"); + if let Some(d) = p.dialogues.iter_mut().find(|d| d.id == dialogue_id) { d.choices.push(Choice { id, text: "New choice".into(), next: None, give_item: None }); } + }); } + }, "Add choice" } + br {} + button { class: "danger", onclick: move |_| { if browser::confirm("Delete this dialogue? References will appear in validation. You can undo this.") { editor.write().delete_selection(); } }, "Delete dialogue" } + } + } + } + _ => rsx! {}, + } +} + +#[component] +fn ChoiceEditor(choice: Choice, editor: Signal) -> Element { + let id = choice.id.clone(); + let next_id = id.clone(); + let item_id = id.clone(); + let delete_id = id.clone(); + rsx! { div { class: "choice", "data-choice-id": "{choice.id}", + label { "Choice text" input { value: "{choice.text}", oninput: move |e| update_dialogue(editor, |d| { if let Some(c) = d.choices.iter_mut().find(|c| c.id == id) { c.text = e.value(); } }) } } + label { "Next dialogue" select { value: choice.next.clone().unwrap_or_default(), onchange: move |e| update_dialogue(editor, |d| { if let Some(c) = d.choices.iter_mut().find(|c| c.id == next_id) { c.next = if e.value().is_empty() { None } else { Some(e.value()) }; } }), + option { value: "", selected: choice.next.is_none(), "End conversation" } + for dialogue in editor.read().project.dialogues.clone() { option { value: "{dialogue.id}", selected: choice.next.as_deref() == Some(dialogue.id.as_str()), "{dialogue.name}" } } + } } + label { "Give item" select { value: choice.give_item.clone().unwrap_or_default(), onchange: move |e| update_dialogue(editor, |d| { if let Some(c) = d.choices.iter_mut().find(|c| c.id == item_id) { c.give_item = if e.value().is_empty() { None } else { Some(e.value()) }; } }), + option { value: "", selected: choice.give_item.is_none(), "No item" } + for item in editor.read().project.items.clone() { option { value: "{item.id}", selected: choice.give_item.as_deref() == Some(item.id.as_str()), "{item.name}" } } + } } + button { onclick: move |_| update_dialogue(editor, |d| d.choices.retain(|c| c.id != delete_id)), "Remove choice" } + } } +} diff --git a/app/src/ui.rs b/app/src/ui.rs new file mode 100644 index 0000000..0fd8583 --- /dev/null +++ b/app/src/ui.rs @@ -0,0 +1,117 @@ +use crate::{ + browser, + editor::{Editor, Selection}, +}; +use dioxus::prelude::*; + +#[component] +pub fn App() -> Element { + let mut notice = use_signal(String::new); + let mut editor = use_signal(|| { + let mut editor = Editor::default(); + match browser::load() { + Ok(Some(text)) => match editor.open(&text) { + Ok(()) => notice.set("Recovered your last browser draft.".into()), + Err(e) => notice.set(format!("Recovery was not opened: {e}")), + }, + Err(e) => notice.set(e), + _ => {} + } + editor + }); + use_effect(move || browser::set_dirty(editor.read().dirty)); + let issues = editor.read().project.validate(); + let has_issues = !issues.is_empty(); + let mut session = use_signal(|| None::); + rsx! { + style { {include_str!("editor.css")} } + div { class: "app", + header { class: "topbar", + div { class: "brand", "P" } + div { class: "brand-copy", strong { "PACE" } span { "Adventure editor" } } + div { class: "project-title", "{editor.read().project.title}" if editor.read().dirty { span { title: "Changes not yet saved to file", " •" } } } + button { onclick: move |_| { if !editor.read().dirty || browser::confirm("Discard changes that have not been saved to a project file?") { editor.set(Editor::default()); } }, "New project" } + label { class: "button", "Open project" + input { r#type: "file", accept: ".json", "aria-label": "Open project", onchange: move |event: Event| async move { + if editor.read().dirty && !browser::confirm("Replace this project? Save a file first to keep your changes.") { return; } + let document_id = editor.read().document_id; + let original = editor.read().project.clone(); + if let Some(files) = event.files() { + if let Some(name) = files.files().first() { + match files.read_file_to_string(name).await { + Some(_) if editor.read().document_id != document_id || editor.read().project != original => notice.set("Open cancelled because the project changed while the file was being read. Try opening it again.".into()), + Some(text) => match editor.write().open(&text) { Ok(()) => notice.set("Project opened.".into()), Err(e) => notice.set(e) }, + None => notice.set("Could not read project file.".into()), + } + } + } + } } + } + button { class: "primary", onclick: move |_| { + let result = editor.read().project.to_json().and_then(|text| { browser::download(&text)?; Ok(text) }); + match result { + Ok(text) => match browser::recover(&text) { + Ok(()) => { editor.write().dirty = false; notice.set("Project download requested and browser recovery saved.".into()); }, + Err(e) => notice.set(format!("Project download requested. {e}")), + }, + Err(e) => notice.set(e), + } + }, "Save project" } + } + div { class: "dependency-banner", "Draft format v1 · PACE core integration pending. Your project files include all imported images." } + div { class: "workspace", + aside { class: "sidebar", + button { class: "project-link", onclick: move |_| editor.write().selection = Selection::Project, "Project settings" } + div { class: "section-heading", h2 { "ROOMS" } button { "aria-label": "Add room", onclick: move |_| { + let mut id = String::new(); editor.write().edit(|p| id = p.add_room()); editor.write().selection = Selection::Room(id); + }, "+" } } + for room in editor.read().project.rooms.clone() { + button { class: if editor.read().selected_room() == Some(room.id.as_str()) { "tree-row active" } else { "tree-row" }, onclick: move |_| editor.write().selection = Selection::Room(room.id.clone()), "{room.name}" } + } + div { class: "section-heading", h2 { "INVENTORY" } button { "aria-label": "Add item", onclick: move |_| { + let mut id = String::new(); editor.write().edit(|p| id = p.add_item()); editor.write().selection = Selection::Item(id); + }, "+" } } + for item in editor.read().project.items.clone() { + button { class: if editor.read().selection == Selection::Item(item.id.clone()) { "tree-row active" } else { "tree-row" }, onclick: move |_| editor.write().selection = Selection::Item(item.id.clone()), "{item.name}" } + } + div { class: "section-heading", h2 { "DIALOGUE" } button { "aria-label": "Add dialogue", onclick: move |_| { + let mut id = String::new(); editor.write().edit(|p| id = p.add_dialogue()); editor.write().selection = Selection::Dialogue(id); + }, "+" } } + for dialogue in editor.read().project.dialogues.clone() { + button { class: if editor.read().selection == Selection::Dialogue(dialogue.id.clone()) { "tree-row active" } else { "tree-row" }, onclick: move |_| editor.write().selection = Selection::Dialogue(dialogue.id.clone()), "{dialogue.name}" } + } + div { class: "sidebar-foot", "Build a world, one room at a time." } + } + main { class: "main-panel", + div { class: "toolbar", + span { class: "eyebrow", "YOUR ADVENTURE" } + button { disabled: !editor.read().can_undo(), onclick: move |_| editor.write().undo(), "Undo" } + button { disabled: !editor.read().can_redo(), onclick: move |_| editor.write().redo(), "Redo" } + button { disabled: has_issues, onclick: move |_| { + match crate::rehearsal::Rehearsal::new(&editor.read().project) { Ok(state) => session.set(Some(state)), Err(e) => notice.set(e) } + }, "Rehearse draft" } + button { disabled: true, title: "No PACE core runtime is available at the inspected revision.", "Core preview" } + button { disabled: true, title: "Game export requires a pinned PACE core runtime.", "Export game" } + } + if matches!(editor.read().selection, Selection::Item(_) | Selection::Dialogue(_)) { crate::story::StoryWorkspace { editor } } else { + crate::rooms::RoomWorkspace { key: "{editor.read().selected_room().unwrap_or_default()}", editor, notice } + } + } + aside { class: "inspector", + if editor.read().selected_room().is_some() { crate::rooms::RoomInspector { editor } } else { + h2 { "Project" } + label { "Project title" input { value: "{editor.read().project.title}", oninput: move |e| editor.write().edit(|p| p.title = e.value()) } } + label { "Starting room" select { value: editor.read().project.start_room.clone().unwrap_or_default(), onchange: move |e| editor.write().edit(|p| p.start_room = if e.value().is_empty() { None } else { Some(e.value()) }), option { value: "", selected: editor.read().project.start_room.is_none(), "Choose a room" } for room in editor.read().project.rooms.clone() { option { value: "{room.id}", selected: editor.read().project.start_room.as_deref() == Some(room.id.as_str()), "{room.name}" } } } } + } + h3 { "Validation · {issues.len()}" } + if issues.is_empty() { p { class: "success", "Ready for draft authoring." } } + for issue in issues { + button { class: "issue", onclick: move |_| editor.write().focus(issue.object_id.as_deref()), strong { {format!("{} · {}", issue.object_id.as_deref().unwrap_or("Project"), issue.field)} } span { "{issue.message}" } } + } + } + } + footer { role: "status", "{notice}" } + if session.read().is_some() { crate::preview::DraftPreview { session } } + } + } +} diff --git a/app/tests/project.rs b/app/tests/project.rs new file mode 100644 index 0000000..86ca37c --- /dev/null +++ b/app/tests/project.rs @@ -0,0 +1,217 @@ +use app::{ + editor::{Editor, Selection}, + model::*, +}; +fn background() -> Background { + Background::import( + "foyer.png".into(), + include_bytes!("../../fixtures/backgrounds/foyer.png"), + ) + .unwrap() +} +fn project() -> Project { + let mut p = Project::default(); + let room = p.add_room(); + p.room_mut(&room).unwrap().background = Some(background()); + let item = p.add_item(); + let dialogue = p.add_dialogue(); + let choice = p.allocate_id("choice"); + p.dialogues[0].choices.push(Choice { + id: choice, + text: "Thanks!".into(), + next: Some(dialogue.clone()), + give_item: Some(item.clone()), + }); + for action in [ + Action::Look { + text: "A quiet room 🗝".into(), + }, + Action::GoTo { + room_id: room.clone(), + }, + Action::Take { + item_id: item.clone(), + }, + Action::Talk { + dialogue_id: dialogue, + }, + ] { + let id = p + .add_hotspot( + &room, + Rect { + x: 0.1, + y: 0.2, + width: 0.25, + height: 0.3, + }, + ) + .unwrap(); + let h = p.rooms[0].hotspots.iter_mut().find(|h| h.id == id).unwrap(); + h.action = action; + h.requires_item = Some(item.clone()); + } + p +} +#[test] +fn portable_roundtrip_preserves_every_field_and_asset() { + let p = project(); + assert!(p.validate().is_empty()); + let text = p.to_json().unwrap(); + let loaded = Project::from_json(&text).unwrap(); + assert_eq!(loaded, p); + assert_eq!(loaded.to_json().unwrap(), text); +} +#[test] +fn rejects_future_versions_and_unknown_fields_without_replacing_open_project() { + let p = project(); + let mut editor = Editor::new(p.clone()); + for text in [ + p.to_json() + .unwrap() + .replace("\"version\": 1", "\"version\": 2"), + p.to_json() + .unwrap() + .replace("\"title\":", "\"future_field\": true, \"title\":"), + "not json".into(), + ] { + assert!(editor.open(&text).is_err()); + assert_eq!(editor.project, p); + } + let text = p + .to_json() + .unwrap() + .replace("\"speaker\":", "\"future_nested\": 42, \"speaker\":"); + assert!(Project::from_json(&text).is_err()); +} +#[test] +fn duplicate_ids_are_rejected_and_allocated_ids_never_collide_after_reopen() { + let mut p = project(); + p.items[0].id = p.rooms[0].id.clone(); + assert!(Project::from_json(&p.to_json().unwrap()).is_err()); + let mut p = Project::from_json(&project().to_json().unwrap()).unwrap(); + let old = p.rooms[0].id.clone(); + assert_ne!(p.add_room(), old); +} +#[test] +fn missing_references_are_preserved_and_point_to_affected_objects() { + let mut p = project(); + let removed = p.items.remove(0); + let loaded = Project::from_json(&p.to_json().unwrap()).unwrap(); + let issues = loaded.validate(); + assert!(issues.iter().any(|i| i.object_id.as_deref() + == Some(p.rooms[0].hotspots[2].id.as_str()) + && i.field == "action.item_id")); + assert!(issues.iter().any(|i| i.object_id.as_deref() + == Some(p.dialogues[0].choices[0].id.as_str()) + && i.field == "give_item")); + assert_eq!( + loaded.rooms[0].hotspots[0].requires_item.as_deref(), + Some(removed.id.as_str()) + ); +} +#[test] +fn edits_delete_undo_redo_and_failed_open_are_transactional() { + let mut e = Editor::new(project()); + let initial = e.project.clone(); + e.selection = Selection::Room(e.project.rooms[0].id.clone()); + e.delete_selection(); + assert!(e.project.rooms.is_empty()); + assert!(e.project.validate().iter().any(|i| i.field == "start_room")); + e.undo(); + assert_eq!(e.project, initial); + e.redo(); + assert!(e.project.rooms.is_empty()); + e.undo(); + e.edit(|p| p.title = "A new story".into()); + assert!(!e.can_redo()); + assert!(e.dirty); +} +#[test] +fn import_rejects_unsupported_corrupt_or_oversize_images() { + assert!(Background::import("a.svg".into(), b"").is_err()); + assert!(Background::import("bad.png".into(), b"\x89PNG\r\n\x1a\n").is_err()); + assert!(Background::import("huge.png".into(), &vec![0; MAX_IMAGE_BYTES + 1]).is_err()); + let mut p = project(); + p.rooms[0].background.as_mut().unwrap().width += 1; + assert!(p.validate().iter().any(|i| i.field == "background")); +} +#[test] +fn canvas_geometry_handles_reverse_drag_clamping_and_tiny_gestures() { + assert_eq!( + Rect::from_drag((0.8, 0.9), (0.2, 0.3)).unwrap(), + Rect { + x: 0.2, + y: 0.3, + width: 0.6000000000000001, + height: 0.6000000000000001 + } + ); + let rect = Rect::from_drag((-1., -1.), (2., 2.)).unwrap(); + assert_eq!( + rect, + Rect { + x: 0., + y: 0., + width: 1., + height: 1. + } + ); + assert!(Rect::from_drag((0.5, 0.5), (0.5, 0.6)).is_none()); + assert!(!Rect { + x: f64::NAN, + y: 0., + width: 0.1, + height: 0.1 + } + .valid()); + let moved = Rect { + x: 0.1, + y: 0.2, + width: 0.25, + height: 0.3, + } + .translated(2., -2.); + assert_eq!(moved.x, 0.75); + assert_eq!(moved.y, 0.); +} +#[test] +fn room_and_hotspot_operations_preserve_links_across_rename_and_report_deletion() { + let mut p = project(); + let destination = p.add_room(); + p.rooms[0].hotspots[1].action = Action::GoTo { + room_id: destination.clone(), + }; + p.room_mut(&destination).unwrap().name = "Renamed".into(); + assert!(!p.validate().iter().any(|i| i.field == "action.room_id")); + let affected = p.rooms[0].hotspots[1].id.clone(); + let mut e = Editor::new(p.clone()); + e.selection = Selection::Room(destination); + e.delete_selection(); + assert!(e + .project + .validate() + .iter() + .any(|i| i.object_id == Some(affected.clone()) && i.field == "action.room_id")); + e.focus(Some(&affected)); + assert_eq!( + e.selection, + Selection::Hotspot(p.rooms[0].id.clone(), affected) + ); + e.undo(); + assert_eq!(e.project, p); +} +#[test] +fn non_finite_bounds_cannot_silently_serialize_as_null() { + let mut p = project(); + p.rooms[0].hotspots[0].rect.x = f64::INFINITY; + assert!(p.to_json().unwrap_err().contains("non-finite")); +} +#[test] +fn renderer_does_not_load_external_or_active_background_urls() { + let mut bg = background(); + bg.data_url = "https://example.com/tracking.png".into(); + assert_eq!(bg.display_url(), ""); + bg.data_url = "data:image/svg+xml,".into(); + assert_eq!(bg.display_url(), ""); +} diff --git a/app/tests/rehearsal.rs b/app/tests/rehearsal.rs new file mode 100644 index 0000000..f1f34fe --- /dev/null +++ b/app/tests/rehearsal.rs @@ -0,0 +1,90 @@ +use app::{model::*, rehearsal::Rehearsal}; +fn sample() -> Project { + let mut p = Project::default(); + let a = p.add_room(); + let b = p.add_room(); + for r in &mut p.rooms { + r.background = Some( + Background::import( + "test.png".into(), + include_bytes!("../../fixtures/backgrounds/foyer.png"), + ) + .unwrap(), + ); + } + let item = p.add_item(); + p.items[0].name = "Brass key".into(); + let dialogue = p.add_dialogue(); + let choice = p.allocate_id("choice"); + p.dialogues[0].choices.push(Choice { + id: choice, + text: "Take the key".into(), + next: None, + give_item: Some(item.clone()), + }); + let rect = Rect { + x: 0.1, + y: 0.1, + width: 0.2, + height: 0.2, + }; + p.add_hotspot(&a, rect); + p.add_hotspot(&a, rect); + p.add_hotspot(&a, rect); + p.rooms[0].hotspots[0].action = Action::GoTo { room_id: b }; + p.rooms[0].hotspots[0].requires_item = Some(item.clone()); + p.rooms[0].hotspots[1].action = Action::Talk { + dialogue_id: dialogue, + }; + p.rooms[0].hotspots[2].action = Action::Take { item_id: item }; + p +} +#[test] +fn item_gate_dialogue_reward_and_room_transition_do_not_mutate_project() { + let p = sample(); + let original = p.clone(); + let mut s = Rehearsal::new(&p).unwrap(); + let door = &p.rooms[0].hotspots[0].id; + s.activate(door).unwrap(); + assert_eq!(s.room_id, p.rooms[0].id); + assert_eq!(s.message, "You need Brass key first."); + assert!(s.choose("not active").is_err()); + s.activate(&p.rooms[0].hotspots[1].id).unwrap(); + assert!(s.activate(door).is_err()); + assert!(s.continue_dialogue().is_err()); + s.choose(&p.dialogues[0].choices[0].id).unwrap(); + assert!(s.inventory.contains(&p.items[0].id)); + assert_eq!(s.dialogue_id, None); + s.activate(door).unwrap(); + assert_eq!(s.room_id, p.rooms[1].id); + assert!(s.activate(door).is_err()); + assert_eq!(p, original); + assert_eq!(s.project, original); + let reset = Rehearsal::new(&p).unwrap(); + assert!(reset.inventory.is_empty()); + assert_eq!(reset.room_id, p.rooms[0].id); +} +#[test] +fn pickup_is_idempotent_and_invalid_projects_cannot_start() { + let mut p = sample(); + let mut s = Rehearsal::new(&p).unwrap(); + let pickup = &p.rooms[0].hotspots[2].id; + s.activate(pickup).unwrap(); + s.activate(pickup).unwrap(); + assert_eq!(s.inventory.len(), 1); + assert!(s.message.starts_with("You already have")); + p.items.clear(); + assert!(Rehearsal::new(&p).is_err()); +} +#[test] +fn dialogue_branches_and_terminal_continue_are_explicit() { + let mut p = sample(); + let terminal = p.add_dialogue(); + p.dialogues[0].choices[0].next = Some(terminal.clone()); + let mut s = Rehearsal::new(&p).unwrap(); + s.activate(&p.rooms[0].hotspots[1].id).unwrap(); + s.choose(&p.dialogues[0].choices[0].id).unwrap(); + assert_eq!(s.dialogue_id, Some(terminal)); + s.continue_dialogue().unwrap(); + assert!(s.dialogue_id.is_none()); +} diff --git a/docs/core-integration.md b/docs/core-integration.md new file mode 100644 index 0000000..457ce45 --- /dev/null +++ b/docs/core-integration.md @@ -0,0 +1,21 @@ +# Core integration boundary + +Do not reinterpret the draft schema as an upstream PACE format. The dependency +must supply a versioned project contract, validation diagnostics with object IDs, +an asset-loading contract, and a runnable web target before runtime integration +can be completed. + +1. Set `runtime.lock.json.runtime_revision` to the actual immutable core commit. +2. Add the core crate using `git` + `rev` (and commit Cargo.lock), or pin its WASM + artifact with a checksum. Never use a moving branch in shipped exports. +3. Add a separate conversion adapter from `model::Project` to the core model. + Check all conversions; report unsupported fields on their owning object. +4. Test a canonical core three-room fixture against that pinned loader, including + assets, room transitions, conditional item actions and branching dialogue. +5. Route both preview and export through that same loader/runtime. Only then + enable core export. Export must contain the pinned runtime and all assets. +6. Keep a versioned draft migration path; do not silently rename old files into + the core format or discard fields the core version cannot represent. + +The three-room authoring example in this repository is editor-owned. Replace or +compare it with the canonical core fixture when the latter is available. diff --git a/docs/images/01-room-editor.png b/docs/images/01-room-editor.png new file mode 100644 index 0000000..0ff144c Binary files /dev/null and b/docs/images/01-room-editor.png differ diff --git a/docs/images/02-dialogue-editor.png b/docs/images/02-dialogue-editor.png new file mode 100644 index 0000000..d564aa8 Binary files /dev/null and b/docs/images/02-dialogue-editor.png differ diff --git a/docs/images/03-draft-rehearsal.png b/docs/images/03-draft-rehearsal.png new file mode 100644 index 0000000..cb4e35d Binary files /dev/null and b/docs/images/03-draft-rehearsal.png differ diff --git a/docs/walkthrough.md b/docs/walkthrough.md new file mode 100644 index 0000000..a32d373 --- /dev/null +++ b/docs/walkthrough.md @@ -0,0 +1,122 @@ +# Author The Garden Key + +This walkthrough authors an **editor-owned example** through the UI. It is not +the missing upstream core fixture. Allow about ten minutes. No scene-data editing +is required. Run `trunk serve`, open http://localhost:8080, and keep the three PNGs +in [`fixtures/backgrounds`](../fixtures/backgrounds) handy. + +## 1. Set the scene + +In **Project settings**, name the adventure **The Garden Key**. Click **+** beside +**ROOMS**, name the room **Foyer**, and use **Import background** to select +`foyer.png`. Repeat for **Study** (`study.png`) and **Garden** (`garden.png`). The +first room becomes the starting room; check **Project settings → Starting room** +to change it. Each room can be selected in the left sidebar. + +## 2. Create the things the player can carry + +Click **+** beside **INVENTORY**. Name the item **Brass key**, with description +“Opens the study door.” Create a second item named **Garden pass**, with description +“A gift from the gardener.” These are definitions: the player starts with neither. + +## 3. Write the conversation + +Click **+** beside **DIALOGUE**. Name the node **Farewell**, set **Speaker** to +“Gardener”, and **Dialogue text** to “The garden is waiting for you.” Leave it +without choices; the player will get a Continue button. + +Create another node, **Meet the gardener**, with speaker “Gardener” and text +“Would you like to visit the garden?” Click **Add choice**: + +- **Choice text:** May I enter? +- **Next dialogue:** Farewell +- **Give item:** Garden pass + +Add a second choice, **Not yet.**, leaving its next dialogue as **End conversation** +and its reward as **No item**. Both branches are now authored. + +![Dialogue and branching choices](images/02-dialogue-editor.png) + +## 4. Place and connect hotspots + +Select a room, click **Draw hotspot**, and drag over the intended area. Reverse +drags work too. Use **Select / move** to reposition it. For keyboard-only or exact +placement, click **Add hotspot**, then set its name and percentage bounds in the +right inspector. Arrow keys nudge a focused canvas hotspot by 1%. + +The following exact bounds reproduce the example. All coordinates are percentages +of the original background, so they stay aligned when the editor is resized. + +| Room | Hotspot name | X, Y, width, height | Action | Target | Requires item | +|---|---|---|---|---|---| +| Foyer | Brass key on table | 44, 60, 16, 15 | Pick up item | Brass key | No requirement | +| Foyer | Study door | 72, 20, 20, 48 | Go to room | Study | Brass key | +| Study | Return to foyer | 8, 20, 18, 48 | Go to room | Foyer | No requirement | +| Study | Talk to gardener | 40, 55, 24, 18 | Start dialogue | Meet the gardener | No requirement | +| Study | Garden gate | 72, 20, 20, 48 | Go to room | Garden | Garden pass | +| Garden | Return to study | 8, 20, 18, 48 | Go to room | Study | No requirement | +| Garden | Statue | 42, 40, 18, 30 | Inspect | The garden remembers every visitor. | No requirement | + +For the Statue, enter the target text in **Inspection text**. Links are one-way; +the return hotspots above explicitly create the reverse paths. + +![A room with its hotspot inspector](images/01-room-editor.png) + +## 5. Validate and save + +The inspector should show **Validation · 0**. To try an error, temporarily change +Study door's destination to **Choose destination**. Its validation entry includes +the hotspot's stable ID and `action.room_id`. Click the error to select that +hotspot, then choose Study again (or Undo). + +Click **Save project** and keep the downloaded `adventure.pace-draft.json`. It +contains every background and all authored fields. Click **New project**, then +**Open project** and select the downloaded file. Everything reappears; importing +the original image files again is unnecessary. Invalid and unsupported files do +not replace the open project. The saved browser recovery copy also opens on a +refresh; it is a convenience, not a substitute for a downloaded project file. + +## 6. Rehearse the draft + +Click **Rehearse draft**. This is an editor-only simulation on a copy of the +project, not the PACE runtime: + +1. Click **Study door** before taking the key. It tells you what is missing. +2. Click **Brass key on table**, then **Study door**. You arrive in the Study. +3. Try **Garden gate**. It requires the Garden pass. +4. Click **Talk to gardener**, then **Not yet.** to try the non-reward branch. +5. Talk again; choose **May I enter?** The pass enters inventory and Farewell runs. +6. Click **Continue**, then **Garden gate**, then **Statue**. +7. Return to Study and Foyer using their return hotspots. **Restart** resets the + inventory and current room. **Close rehearsal** returns to the unchanged draft. + +![Draft rehearsal after reaching the garden](images/03-draft-rehearsal.png) + +**Core preview** and **Export game** remain disabled until the upstream core +format and executable runtime exist and can be pinned. See +[the integration boundary](core-integration.md). Do not treat rehearsal as proof +of core compatibility. + +## Reproduce the evidence + +`tests/e2e/authoring.spec.ts` performs the entire authoring workflow using browser +controls, verifies both dialogue choices and the gated room transitions, then +saves, reopens, and deeply compares the project—including embedded backgrounds. +No test writes scene data into the page to create the example. + +```sh +trunk build +npm ci +npx playwright install chromium +npm run test:e2e +``` + +To regenerate the committed example and screenshots from those same UI actions: + +```sh +UPDATE_WALKTHROUGH=1 npm run test:e2e -- authoring.spec.ts +``` + +The fixture is [`three-rooms.pace-draft.json`](../fixtures/three-rooms.pace-draft.json). +The background illustrations were generated for this repository and contain no +external assets. diff --git a/fixtures/backgrounds/foyer.png b/fixtures/backgrounds/foyer.png new file mode 100644 index 0000000..c1d3029 Binary files /dev/null and b/fixtures/backgrounds/foyer.png differ diff --git a/fixtures/backgrounds/garden.png b/fixtures/backgrounds/garden.png new file mode 100644 index 0000000..7de8649 Binary files /dev/null and b/fixtures/backgrounds/garden.png differ diff --git a/fixtures/backgrounds/study.png b/fixtures/backgrounds/study.png new file mode 100644 index 0000000..9057187 Binary files /dev/null and b/fixtures/backgrounds/study.png differ diff --git a/fixtures/three-rooms.pace-draft.json b/fixtures/three-rooms.pace-draft.json new file mode 100644 index 0000000..cfe3646 --- /dev/null +++ b/fixtures/three-rooms.pace-draft.json @@ -0,0 +1,191 @@ +{ + "format": "pace-editor-draft", + "version": 1, + "title": "The Garden Key", + "start_room": "room-1", + "rooms": [ + { + "id": "room-1", + "name": "Foyer", + "background": { + "name": "foyer.png", + "data_url": "data:image/png;base64,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", + "width": 960, + "height": 600 + }, + "hotspots": [ + { + "id": "hotspot-10", + "name": "Brass key on table", + "rect": { + "x": 0.44, + "y": 0.6, + "width": 0.16, + "height": 0.15 + }, + "action": { + "kind": "take", + "item_id": "item-4" + }, + "requires_item": null + }, + { + "id": "hotspot-11", + "name": "Study door", + "rect": { + "x": 0.72, + "y": 0.2, + "width": 0.2, + "height": 0.48 + }, + "action": { + "kind": "go_to", + "room_id": "room-2" + }, + "requires_item": "item-4" + } + ] + }, + { + "id": "room-2", + "name": "Study", + "background": { + "name": "study.png", + "data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA8AAAAJYCAIAAAAi9hhWAAAWRklEQVR4nO3da4xmd13A8bO7Q7F10wqNFCNYV5DaoG2sYrfGRuuFDZDGEgFJIxoJBS8vgDThFTHGNBpNjOILwRskGm0bbdRqxFapJUW6zRLrBgtdS2naCmULu724pHZLd3wxm+nu7Mwz53ee8z//y/l8XvUynf2d85zn/L/nv093dlx93fUdAADQz87cAwAAQE0ENAAABAhoAAAIENAAABAgoAEAIEBAAwBAgIAGAIAAAQ0AAAECGgAAAgQ0AAAECGgAAAgQ0AAAECCgAQAgQEADAECAgAYAgAABDQAAAQIaAAACBDQAAAQIaAAACBDQAAAQIKABACBAQAMAQICABgCAAAENAAABAhoAAAIENAAABAhoAAAIWEn0ffd8+7de+6a9u3btfP7E6kduvuO7XvGyN1x5Sdd137Pn2+5/6LGu627798++521XvfODf7r29R+94V1rf/3nv/XuLzzyeNetruza9Yl7PvfJA/e/4oKXvPcd+z7wuzevrq52Xfeb73vrh2/6xKNfOZpocgAAWCBVQP/Sz17123/2T0efOnb5Ja/6uat/+EN/cfuB//pi13UfveFdv/Hhv1v7mve87aoz/8NvPH9i7QtefNaLPvDONz57/Ln9Bx989CtHrrj01Z/+zwe+/+ILDx95Sj0DAJBLqo9wnLf7nLNetKvrus/c99Btn/rsgO/w7PHn/vIfP722b33Lv3zmzT/5Azt27LjmJy675fYDI88KAAC9pdqBvvHj+3/9V9987+cf/tR//Pd9X/jSsG/y8GNHXn7+eV3XfenwE488duS6t/zo15449j+Hnxh1UgAACEi1A/3JA/df/zs3HnrosV/46R95y+tfN+yb7Nq58/kTJ9b++pbbD1z1QxfbfgYAIK8kAX3u7rNf850v//ozz9554P4b/ujWn7ritZt+2erq6s6dO7rTQ/lUr/6Olz3y2MmPO3/5q08+8+xzX/7qkykGBgCAnpIE9Orq6vvese/8b9nddd3uc77pa08e2/TLHnz08e97zSu7rrvkold+8ZHHN/zbbz77xde+6Ypb/+3eFBMCAMAwST4D/b9f/78/+Zs73//z+44/940TJ1Y/cvMdm37Zx/72rne/9ceu+fHLuq7747++8+RAu3b+2i9f03Wru3bt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", + "width": 960, + "height": 600 + }, + "hotspots": [ + { + "id": "hotspot-12", + "name": "Return to foyer", + "rect": { + "x": 0.08, + "y": 0.2, + "width": 0.18, + "height": 0.48 + }, + "action": { + "kind": "go_to", + "room_id": "room-1" + }, + "requires_item": null + }, + { + "id": "hotspot-13", + "name": "Talk to gardener", + "rect": { + "x": 0.4, + "y": 0.55, + "width": 0.24, + "height": 0.18 + }, + "action": { + "kind": "talk", + "dialogue_id": "dialogue-7" + }, + "requires_item": null + }, + { + "id": "hotspot-14", + "name": "Garden gate", + "rect": { + "x": 0.72, + "y": 0.2, + "width": 0.2, + "height": 0.48 + }, + "action": { + "kind": "go_to", + "room_id": "room-3" + }, + "requires_item": "item-5" + } + ] + }, + { + "id": "room-3", + "name": "Garden", + "background": { + "name": "garden.png", + "data_url": "data:image/png;base64,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", + "width": 960, + "height": 600 + }, + "hotspots": [ + { + "id": "hotspot-15", + "name": "Return to study", + "rect": { + "x": 0.08, + "y": 0.2, + "width": 0.18, + "height": 0.48 + }, + "action": { + "kind": "go_to", + "room_id": "room-2" + }, + "requires_item": null + }, + { + "id": "hotspot-16", + "name": "Statue", + "rect": { + "x": 0.42, + "y": 0.4, + "width": 0.18, + "height": 0.3 + }, + "action": { + "kind": "look", + "text": "The garden remembers every visitor." + }, + "requires_item": null + } + ] + } + ], + "items": [ + { + "id": "item-4", + "name": "Brass key", + "description": "Opens the study door." + }, + { + "id": "item-5", + "name": "Garden pass", + "description": "A gift from the gardener." + } + ], + "dialogues": [ + { + "id": "dialogue-6", + "name": "Farewell", + "speaker": "Gardener", + "text": "The garden is waiting for you.", + "choices": [] + }, + { + "id": "dialogue-7", + "name": "Meet the gardener", + "speaker": "Gardener", + "text": "Would you like to visit the garden?", + "choices": [ + { + "id": "choice-8", + "text": "May I enter?", + "next": "dialogue-6", + "give_item": "item-5" + }, + { + "id": "choice-9", + "text": "Not yet.", + "next": null, + "give_item": null + } + ] + } + ], + "next_id": 17 +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ea46827 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,76 @@ +{ + "name": "pace-editor-browser-tests", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pace-editor-browser-tests", + "devDependencies": { + "@playwright/test": "1.56.1" + } + }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..429ce01 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "name": "pace-editor-browser-tests", + "private": true, + "scripts": { "test:e2e": "playwright test" }, + "devDependencies": { "@playwright/test": "1.56.1" } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..4806be4 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from '@playwright/test'; +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: false, + workers: 1, + use: { + baseURL: 'http://127.0.0.1:8080', + actionTimeout: 10_000, + viewport: { width: 1440, height: 1000 }, + launchOptions: process.env.CHROMIUM_PATH ? { executablePath: process.env.CHROMIUM_PATH } : {}, + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + webServer: { command: 'python3 -m http.server 8080 --bind 127.0.0.1 --directory dist', url: 'http://127.0.0.1:8080', reuseExistingServer: !process.env.CI }, +}); diff --git a/runtime.lock.json b/runtime.lock.json new file mode 100644 index 0000000..d93276e --- /dev/null +++ b/runtime.lock.json @@ -0,0 +1,9 @@ +{ + "platform": "web-wasm", + "core_repository": "https://github.com/pace-kit/pace_core", + "inspected_core_revision": "6ccfd6ea60534f3c3312882458e1c83308a76b81", + "runtime_revision": null, + "status": "blocked: upstream contains no format or runtime API", + "editor_format": "pace-editor-draft", + "editor_format_version": 1 +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..4b46634 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +channel = "1.90.0" +components = ["rustfmt", "clippy"] +targets = ["wasm32-unknown-unknown"] +profile = "minimal" diff --git a/tests/e2e/authoring.spec.ts b/tests/e2e/authoring.spec.ts new file mode 100644 index 0000000..e2ec2c1 --- /dev/null +++ b/tests/e2e/authoring.spec.ts @@ -0,0 +1,132 @@ +import { test, expect, Page } from '@playwright/test'; +import fs from 'node:fs/promises'; + +async function room(page: Page, name: string, image: string) { + await page.getByRole('button', { name: 'Add room', exact: true }).click(); + await page.getByLabel('Room name', { exact: true }).fill(name); + await page.getByLabel('Import background', { exact: true }).setInputFiles(`fixtures/backgrounds/${image}.png`); + await expect(page.getByRole('img', { name: `${name} background` })).toBeVisible(); +} +async function hotspot(page: Page, room: string, name: string, bounds: number[], action: string, target: string) { + await page.getByRole('button', { name: room, exact: true }).click(); + await page.getByRole('button', { name: 'Add hotspot', exact: true }).click(); + await page.getByLabel('Hotspot name', { exact: true }).fill(name); + for (const [i, field] of ['X', 'Y', 'Width', 'Height'].entries()) + await page.getByLabel(`${field} (%)`, { exact: true }).fill(String(bounds[i])); + await page.getByRole('combobox', { name: 'Action', exact: true }).selectOption(action); + if (action === 'look') await page.getByLabel('Inspection text', { exact: true }).fill(target); + else await page.getByRole('combobox', { name: { go_to: 'Destination room', take: 'Item to pick up', talk: 'Dialogue to start' }[action]!, exact: true }).selectOption({ label: target }); +} + +test('authors the three-room adventure entirely through controls and rehearses the story', async ({ page }, testInfo) => { + test.setTimeout(90_000); + const errors: string[] = []; + page.on('pageerror', e => errors.push(e.message)); + page.on('dialog', d => d.accept()); + await page.goto('/'); + await page.getByLabel('Project title', { exact: true }).fill('The Garden Key'); + await room(page, 'Foyer', 'foyer'); + await room(page, 'Study', 'study'); + await room(page, 'Garden', 'garden'); + for (const [name, description] of [['Brass key', 'Opens the study door.'], ['Garden pass', 'A gift from the gardener.']]) { + await page.getByRole('button', { name: 'Add item', exact: true }).click(); + await page.getByLabel('Item name', { exact: true }).fill(name); + await page.getByLabel('Item description', { exact: true }).fill(description); + } + await page.getByRole('button', { name: 'Add dialogue', exact: true }).click(); + await page.getByLabel('Dialogue name', { exact: true }).fill('Farewell'); + await page.getByLabel('Speaker', { exact: true }).fill('Gardener'); + await page.getByLabel('Dialogue text', { exact: true }).fill('The garden is waiting for you.'); + await page.getByRole('button', { name: 'Add dialogue', exact: true }).click(); + await page.getByLabel('Dialogue name', { exact: true }).fill('Meet the gardener'); + await page.getByLabel('Speaker', { exact: true }).fill('Gardener'); + await page.getByLabel('Dialogue text', { exact: true }).fill('Would you like to visit the garden?'); + await page.getByRole('button', { name: 'Add choice', exact: true }).click(); + await page.getByLabel('Choice text', { exact: true }).fill('May I enter?'); + await page.getByRole('combobox', { name: 'Next dialogue', exact: true }).selectOption({ label: 'Farewell' }); + await page.getByRole('combobox', { name: 'Give item', exact: true }).selectOption({ label: 'Garden pass' }); + await page.getByRole('button', { name: 'Add choice', exact: true }).click(); + await page.getByLabel('Choice text', { exact: true }).nth(1).fill('Not yet.'); + await hotspot(page, 'Foyer', 'Brass key on table', [44, 60, 16, 15], 'take', 'Brass key'); + await hotspot(page, 'Foyer', 'Study door', [72, 20, 20, 48], 'go_to', 'Study'); + await page.getByRole('combobox', { name: 'Requires item', exact: true }).selectOption({ label: 'Brass key' }); + await hotspot(page, 'Study', 'Return to foyer', [8, 20, 18, 48], 'go_to', 'Foyer'); + await hotspot(page, 'Study', 'Talk to gardener', [40, 55, 24, 18], 'talk', 'Meet the gardener'); + await hotspot(page, 'Study', 'Garden gate', [72, 20, 20, 48], 'go_to', 'Garden'); + await page.getByRole('combobox', { name: 'Requires item', exact: true }).selectOption({ label: 'Garden pass' }); + await hotspot(page, 'Garden', 'Return to study', [8, 20, 18, 48], 'go_to', 'Study'); + await hotspot(page, 'Garden', 'Statue', [42, 40, 18, 30], 'look', 'The garden remembers every visitor.'); + await expect(page.getByText('Validation · 0', { exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Core preview', exact: true })).toBeDisabled(); + await expect(page.getByRole('button', { name: 'Export game', exact: true })).toBeDisabled(); + const downloadPromise = page.waitForEvent('download'); + await page.getByRole('button', { name: 'Save project', exact: true }).click(); + const download = await downloadPromise; + const file = testInfo.outputPath('three-rooms.pace-draft.json'); + await download.saveAs(file); + const saved = JSON.parse(await fs.readFile(file, 'utf8')); + expect(saved.rooms.map((r: any) => r.name)).toEqual(['Foyer', 'Study', 'Garden']); + expect(saved.rooms.flatMap((r: any) => r.hotspots)).toHaveLength(7); + expect(saved.items).toHaveLength(2); expect(saved.dialogues).toHaveLength(2); + await page.getByRole('button', { name: 'Foyer', exact: true }).click(); + await page.getByRole('button', { name: 'Select hotspot Study door', exact: true }).click(); + await page.screenshot({ path: testInfo.outputPath('01-room-editor.png'), fullPage: true }); + if (process.env.UPDATE_WALKTHROUGH) { + await fs.copyFile(file, 'fixtures/three-rooms.pace-draft.json'); + await page.screenshot({ path: 'docs/images/01-room-editor.png', fullPage: true }); + } + await page.getByRole('button', { name: 'Meet the gardener', exact: true }).click(); + await expect(page.getByRole('combobox', { name: 'Starting room', exact: true }).locator('option:checked')).toHaveText('Foyer'); + await expect(page.getByRole('combobox', { name: 'Next dialogue', exact: true }).first().locator('option:checked')).toHaveText('Farewell'); + await expect(page.getByRole('combobox', { name: 'Give item', exact: true }).first().locator('option:checked')).toHaveText('Garden pass'); + await page.screenshot({ path: testInfo.outputPath('02-dialogue-editor.png'), fullPage: true }); + if (process.env.UPDATE_WALKTHROUGH) await page.screenshot({ path: 'docs/images/02-dialogue-editor.png', fullPage: true }); + await page.getByRole('button', { name: 'Rehearse draft', exact: true }).click(); + const preview = page.getByRole('dialog', { name: 'Draft rehearsal' }); + await preview.getByRole('button', { name: 'Interact with Study door', exact: true }).click(); + await expect(preview.getByRole('status')).toHaveText('You need Brass key first.'); + await preview.getByRole('button', { name: 'Interact with Brass key on table', exact: true }).click(); + await preview.getByRole('button', { name: 'Interact with Study door', exact: true }).click(); + await expect(preview.getByRole('heading', { name: 'Study', exact: true })).toBeVisible(); + await preview.getByRole('button', { name: 'Interact with Garden gate', exact: true }).click(); + await expect(preview.getByRole('status')).toHaveText('You need Garden pass first.'); + await preview.getByRole('button', { name: 'Interact with Talk to gardener', exact: true }).click(); + await preview.getByRole('button', { name: 'Not yet.', exact: true }).click(); + await expect(preview.getByRole('status')).toHaveText('The conversation ends.'); + await preview.getByRole('button', { name: 'Interact with Talk to gardener', exact: true }).click(); + await preview.getByRole('button', { name: 'May I enter?', exact: true }).click(); + await expect(preview.getByText('The garden is waiting for you.', { exact: true })).toBeVisible(); + await preview.getByRole('button', { name: 'Continue', exact: true }).click(); + await preview.getByRole('button', { name: 'Interact with Garden gate', exact: true }).click(); + await expect(preview.getByRole('heading', { name: 'Garden', exact: true })).toBeVisible(); + await preview.getByRole('button', { name: 'Interact with Statue', exact: true }).click(); + await expect(preview.getByRole('status')).toHaveText('The garden remembers every visitor.'); + if (process.env.UPDATE_WALKTHROUGH) await page.screenshot({ path: 'docs/images/03-draft-rehearsal.png' }); + await preview.getByRole('button', { name: 'Interact with Return to study', exact: true }).click(); + await preview.getByRole('button', { name: 'Interact with Return to foyer', exact: true }).click(); + await preview.getByRole('button', { name: 'Restart', exact: true }).click(); + await expect(preview.getByText('Nothing collected yet', { exact: true })).toBeVisible(); + await preview.getByRole('button', { name: 'Close rehearsal', exact: true }).click(); + await expect(preview).toHaveCount(0); + await page.getByRole('button', { name: 'Rehearse draft', exact: true }).click(); + // The native modal keeps keyboard focus away from editing controls. + for (let i = 0; i < 7; i++) { + await page.keyboard.press('Tab'); + expect(await page.evaluate(() => document.activeElement === document.body || !!document.activeElement?.closest('#draft-rehearsal'))).toBe(true); + } + await page.keyboard.press('Escape'); + await expect(preview).toHaveCount(0); + // Rehearsal must not dirty or change the authored data. + const afterPromise = page.waitForEvent('download'); + await page.getByRole('button', { name: 'Save project', exact: true }).click(); + await (await afterPromise).saveAs(testInfo.outputPath('after-rehearsal.json')); + expect(JSON.parse(await fs.readFile(testInfo.outputPath('after-rehearsal.json'), 'utf8'))).toEqual(saved); + await page.getByRole('button', { name: 'New project', exact: true }).click(); + await page.getByLabel('Open project', { exact: true }).setInputFiles(file); + await expect(page.getByText('Validation · 0', { exact: true })).toBeVisible(); + const reopenedPromise = page.waitForEvent('download'); + await page.getByRole('button', { name: 'Save project', exact: true }).click(); + await (await reopenedPromise).saveAs(testInfo.outputPath('reopened.json')); + expect(JSON.parse(await fs.readFile(testInfo.outputPath('reopened.json'), 'utf8'))).toEqual(saved); + expect(errors).toEqual([]); +}); diff --git a/tests/e2e/recovery.spec.ts b/tests/e2e/recovery.spec.ts new file mode 100644 index 0000000..56c3e35 --- /dev/null +++ b/tests/e2e/recovery.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '@playwright/test'; +import fs from 'node:fs/promises'; + +test('quota failure preserves edits and offers a portable file; malformed open is non-destructive', async ({ page }, testInfo) => { + page.on('dialog', d => d.accept()); + await page.addInitScript(() => { + Storage.prototype.setItem = () => { throw new DOMException('Quota exceeded', 'QuotaExceededError'); }; + }); + await page.goto('/'); + await page.getByLabel('Project title', { exact: true }).fill('Keep this draft'); + const downloadPromise = page.waitForEvent('download'); + await page.getByRole('button', { name: 'Save project', exact: true }).click(); + const file = testInfo.outputPath('quota-draft.json'); + await (await downloadPromise).saveAs(file); + expect(JSON.parse(await fs.readFile(file, 'utf8')).title).toBe('Keep this draft'); + await expect(page.getByRole('status')).toContainText('Browser recovery is full or unavailable'); + await expect(page.locator('.project-title')).toContainText('•'); + await page.getByLabel('Open project', { exact: true }).setInputFiles({ name: 'broken.json', mimeType: 'application/json', buffer: Buffer.from('{broken') }); + await expect(page.getByRole('status')).toContainText('Invalid project JSON'); + await expect(page.getByLabel('Project title', { exact: true })).toHaveValue('Keep this draft'); +}); + +test('cancelled destructive action retains the current draft', async ({ page }) => { + page.on('dialog', d => d.dismiss()); + await page.goto('/'); + await page.getByLabel('Project title', { exact: true }).fill('Unsaved story'); + await page.getByRole('button', { name: 'New project', exact: true }).click(); + await expect(page.getByLabel('Project title', { exact: true })).toHaveValue('Unsaved story'); +}); diff --git a/tests/e2e/rooms.spec.ts b/tests/e2e/rooms.spec.ts new file mode 100644 index 0000000..e40749b --- /dev/null +++ b/tests/e2e/rooms.spec.ts @@ -0,0 +1,84 @@ +import { test, expect } from '@playwright/test'; +import fs from 'node:fs/promises'; + +test('import, draw, move, link, validate, undo, save and reopen rooms', async ({ page }, testInfo) => { + const errors: string[] = []; + page.on('pageerror', error => errors.push(error.message)); + page.on('dialog', dialog => dialog.accept()); + await page.goto('/'); + await page.getByRole('button', { name: 'Add room', exact: true }).click(); + await page.getByLabel('Room name', { exact: true }).fill('Foyer'); + await page.getByLabel('Import background', { exact: true }).setInputFiles('fixtures/backgrounds/foyer.png'); + await expect(page.getByRole('img', { name: 'Foyer background' })).toBeVisible(); + await page.getByRole('button', { name: 'Draw hotspot', exact: true }).click(); + const canvas = await page.locator('#room-stage').boundingBox(); + if (!canvas) throw new Error('Missing canvas'); + await page.mouse.move(canvas.x + canvas.width * .3, canvas.y + canvas.height * .7); + await page.mouse.down(); + await page.mouse.move(canvas.x + canvas.width * .1, canvas.y + canvas.height * .2, { steps: 8 }); + await page.mouse.up(); + await page.getByLabel('Hotspot name', { exact: true }).fill('Study door'); + const drawnX = Number(await page.getByLabel('X (%)', { exact: true }).inputValue()); + expect(Math.abs(drawnX - 10)).toBeLessThan(.2); + expect(Math.abs(Number(await page.getByLabel('Width (%)', { exact: true }).inputValue()) - 20)).toBeLessThan(.2); + await page.getByRole('button', { name: 'Select hotspot Study door', exact: true }).press('ArrowRight'); + await expect(page.getByLabel('X (%)', { exact: true })).toHaveValue((drawnX + 1).toFixed(2)); + await page.getByRole('button', { name: 'Undo', exact: true }).click(); + await expect(page.getByLabel('X (%)', { exact: true })).toHaveValue(drawnX.toFixed(2)); + const hit = await page.getByRole('button', { name: 'Select hotspot Study door', exact: true }).boundingBox(); + if (!hit) throw new Error('Missing hotspot'); + await page.mouse.move(hit.x + hit.width / 2, hit.y + hit.height / 2); + await page.mouse.down(); + await page.mouse.move(hit.x + hit.width / 2 + canvas.width * .05, hit.y + hit.height / 2, { steps: 6 }); + await page.mouse.up(); + expect(Math.abs(Number(await page.getByLabel('X (%)', { exact: true }).inputValue()) - drawnX - 5)).toBeLessThan(.2); + await page.getByRole('button', { name: 'Undo', exact: true }).click(); + await expect(page.getByLabel('X (%)', { exact: true })).toHaveValue(drawnX.toFixed(2)); + await page.getByRole('button', { name: 'Draw hotspot', exact: true }).click(); + await page.mouse.move(canvas.x + canvas.width * .5, canvas.y + canvas.height * .5); + await page.mouse.down(); + await page.mouse.move(canvas.x + canvas.width * .8, canvas.y + canvas.height * .8); + await page.keyboard.press('Escape'); + await page.mouse.up(); + await expect(page.locator('#room-stage .hotspot')).toHaveCount(1); + await page.getByRole('button', { name: 'Add room', exact: true }).click(); + await page.getByLabel('Room name', { exact: true }).fill('Study'); + await page.getByLabel('Import background', { exact: true }).setInputFiles('fixtures/backgrounds/study.png'); + await expect(page.getByRole('img', { name: 'Study background' })).toBeVisible(); + await page.getByRole('button', { name: 'Foyer', exact: true }).click(); + await page.getByRole('button', { name: 'Select hotspot Study door', exact: true }).click(); + await page.getByRole('combobox', { name: 'Action', exact: true }).selectOption('go_to'); + await page.getByRole('combobox', { name: 'Destination room', exact: true }).selectOption({ label: 'Study' }); + await expect(page.getByText('Validation · 0', { exact: true })).toBeVisible(); + await page.getByRole('button', { name: 'Study', exact: true }).click(); + await page.getByRole('button', { name: 'Delete room', exact: true }).click(); + await page.locator('.issue').filter({ hasText: 'action.room_id' }).click(); + await expect(page.getByLabel('Hotspot name', { exact: true })).toHaveValue('Study door'); + await page.getByRole('button', { name: 'Undo', exact: true }).click(); + await expect(page.getByText('Validation · 0', { exact: true })).toBeVisible(); + const downloadPromise = page.waitForEvent('download'); + await page.getByRole('button', { name: 'Save project', exact: true }).click(); + const download = await downloadPromise; + const file = testInfo.outputPath('rooms.pace-draft.json'); + await download.saveAs(file); + const saved = JSON.parse(await fs.readFile(file, 'utf8')); + expect(saved.rooms).toHaveLength(2); + expect(saved.rooms[0].hotspots[0].action.room_id).toBe(saved.rooms[1].id); + expect(saved.rooms[0].background.data_url).toMatch(/^data:image\/png;base64,/); + await page.getByRole('button', { name: 'New project', exact: true }).click(); + await page.getByLabel('Open project', { exact: true }).setInputFiles(file); + await expect(page.getByRole('heading', { name: 'Foyer', exact: true })).toBeVisible(); + const secondPromise = page.waitForEvent('download'); + await page.getByRole('button', { name: 'Save project', exact: true }).click(); + const second = await secondPromise; + await second.saveAs(testInfo.outputPath('roundtrip.json')); + expect(JSON.parse(await fs.readFile(testInfo.outputPath('roundtrip.json'), 'utf8'))).toEqual(saved); + await page.reload(); + await expect(page.getByRole('heading', { name: 'Foyer', exact: true })).toBeVisible(); + const malformed = testInfo.outputPath('unsupported.json'); + await fs.writeFile(malformed, JSON.stringify({ ...saved, version: 999 })); + await page.getByLabel('Open project', { exact: true }).setInputFiles(malformed); + await expect(page.getByRole('status')).toContainText('Unsupported draft version'); + await expect(page.getByRole('heading', { name: 'Foyer', exact: true })).toBeVisible(); + expect(errors).toEqual([]); +}); diff --git a/web_view/Cargo.toml b/web_view/Cargo.toml index ab4058d..e074527 100644 --- a/web_view/Cargo.toml +++ b/web_view/Cargo.toml @@ -1,24 +1,8 @@ [package] name = "web_view" version = "0.1.0" -authors = ["WilliamRagstad "] edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] - app = { path = "../app", features = ["web"] } - -serde = { version = "1.0.197", features = ["derive"] } - -dioxus = { version = "0.5", features = ["fullstack", "router"] } - -# Debug -tracing = "0.1.40" -dioxus-logger = "0.5.0" - -[features] -default = [] -server = ["dioxus/axum"] -web = ["dioxus/web"] +dioxus = { version = "=0.5.6", default-features = false, features = ["web", "launch"] } diff --git a/web_view/index.html b/web_view/index.html new file mode 100644 index 0000000..034e039 --- /dev/null +++ b/web_view/index.html @@ -0,0 +1,12 @@ + + + + + + + + PACE — Adventure editor + + +
+ diff --git a/web_view/src/main.rs b/web_view/src/main.rs index c662a34..5edaaf9 100644 --- a/web_view/src/main.rs +++ b/web_view/src/main.rs @@ -1,10 +1,3 @@ -use tracing::Level; - -use app::App; - fn main() { - // Init logger - dioxus_logger::init(Level::INFO).expect("failed to init logger"); - - dioxus::launch(App); + dioxus::launch(app::App); }