diff --git a/.github/workflows/rust-build.yml b/.github/workflows/rust-build.yml index 8dfcaae..a490af0 100644 --- a/.github/workflows/rust-build.yml +++ b/.github/workflows/rust-build.yml @@ -66,7 +66,7 @@ jobs: - name: Build run: | # Build - cargo build -p testangel --bin testangel --release --no-default-features --features next-ui + cargo build -p testangel --bin testangel --release cargo build -p testangel --bin testangel-executor --no-default-features --features cli --release cargo build -p testangel-arithmetic --release cargo build -p testangel-compare --release @@ -180,8 +180,6 @@ jobs: $env:INCLUDE="C:\gtk-build\gtk\x64\release\include;C:\gtk-build\gtk\x64\release\include\cairo;C:\gtk-build\gtk\x64\release\include\glib-2.0;C:\gtk-build\gtk\x64\release\include\gobject-introspection-1.0;C:\gtk-build\gtk\x64\release\lib\glib-2.0\include;" + $env:INCLUDE $env:PKG_CONFIG_PATH="C:\gtk-build\gtk\x64\release\lib\pkgconfig;" + $env:PKG_CONFIG_PATH - cargo build -p testangel --bin testangel --release --no-default-features --features next-ui,windows-keep-console-window - move target/release/testangel.exe target/release/testangel-new-ui.exe cargo build -p testangel --bin testangel --release cargo build -p testangel --bin testangel-executor --no-default-features --features cli --release cargo build -p testangel-arithmetic --release @@ -194,7 +192,6 @@ jobs: cargo build -p testangel-user-interaction --release mkdir build copy target/release/testangel.exe build/ - copy target/release/testangel-new-ui.exe build/ cargo build -p testangel --bin testangel --release --features windows-keep-console-window copy target/release/testangel.exe build/testangel-dbg.exe copy target/release/testangel-executor.exe build/ @@ -274,8 +271,6 @@ jobs: - name: Build for mac run: | # Build - cargo build -p testangel --bin testangel --release --no-default-features --features next-ui - mv target/release/testangel target/release/testangel-new-ui cargo build -p testangel --bin testangel --release cargo build -p testangel --bin testangel-executor --no-default-features --features cli --release cargo build -p testangel-arithmetic --release diff --git a/Cargo.lock b/Cargo.lock index a05ddbf..d3fdf14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,22 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "ab_glyph" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1061f3ff92c2f65800df1f12fc7b4ff44ee14783104187dd04dfee6f11b0fd2" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" - [[package]] name = "addr2line" version = "0.21.0" @@ -39,18 +23,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" -[[package]] -name = "ahash" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7d5a2cecb58716e47d67d5703a249964b14c7be1ec3cad3affc295b2d1c35d" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.2" @@ -60,42 +32,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "aliasable" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" - -[[package]] -name = "allocator-api2" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" - -[[package]] -name = "android-activity" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64529721f27c2314ced0890ce45e469574a73e5e6fdd6e9da1860eb29285f5e0" -dependencies = [ - "android-properties", - "bitflags 1.3.2", - "cc", - "jni-sys", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "num_enum 0.6.1", -] - -[[package]] -name = "android-properties" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -146,7 +82,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -156,7 +92,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -174,42 +110,12 @@ dependencies = [ "num-traits", ] -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - [[package]] name = "arc-swap" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - -[[package]] -name = "ash" -version = "0.37.3+1.3.251" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" -dependencies = [ - "libloading 0.7.4", -] - [[package]] name = "ashpd" version = "0.6.7" @@ -333,7 +239,7 @@ dependencies = [ "event-listener 3.0.0", "futures-lite", "rustix 0.38.20", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -362,7 +268,7 @@ dependencies = [ "rustix 0.38.20", "signal-hook-registry", "slab", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -453,27 +359,6 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bit_field" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" - [[package]] name = "bitflags" version = "1.3.2" @@ -504,25 +389,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-sys" -version = "0.1.0-beta.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146" -dependencies = [ - "objc-sys", -] - -[[package]] -name = "block2" -version = "0.2.0-alpha.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42" -dependencies = [ - "block-sys", - "objc2-encode", -] - [[package]] name = "blocking" version = "1.4.1" @@ -561,20 +427,6 @@ name = "bytemuck" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.38", -] [[package]] name = "byteorder" @@ -613,27 +465,12 @@ dependencies = [ "system-deps", ] -[[package]] -name = "calloop" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e0d00eb1ea24371a97d2da6201c6747a633dc6dc1988ef503403b4c59504a8" -dependencies = [ - "bitflags 1.3.2", - "log", - "nix 0.25.1", - "slotmap", - "thiserror", - "vec_map", -] - [[package]] name = "cc" version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ - "jobserver", "libc", ] @@ -653,12 +490,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "chrono" version = "0.4.31" @@ -671,7 +502,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.48.5", + "windows-targets", ] [[package]] @@ -714,87 +545,6 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" -[[package]] -name = "clipboard-win" -version = "4.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" -dependencies = [ - "error-code", - "str-buf", - "winapi", -] - -[[package]] -name = "clipboard_macos" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145a7f9e9b89453bc0a5e32d166456405d389cea5b578f57f1274b1397588a95" -dependencies = [ - "objc", - "objc-foundation", - "objc_id", -] - -[[package]] -name = "clipboard_wayland" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f6364a9f7a66f2ac1a1a098aa1c7f6b686f2496c6ac5e5c0d773445df912747" -dependencies = [ - "smithay-clipboard", -] - -[[package]] -name = "clipboard_x11" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "983a7010836ecd04dde2c6d27a0cb56ec5d21572177e782bdcb24a600124e921" -dependencies = [ - "thiserror", - "x11rb 0.9.0", -] - -[[package]] -name = "cocoa" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" -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 = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - [[package]] name = "color_quant" version = "1.1.0" @@ -807,12 +557,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" -[[package]] -name = "com-rs" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf43edc576402991846b093a7ca18a3477e0ef9c588cde84964b5d3e43016642" - [[package]] name = "concurrent-queue" version = "2.3.0" @@ -844,50 +588,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" -[[package]] -name = "core-graphics" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "libc", -] - -[[package]] -name = "cosmic-text" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b68966c2543609f8d92f9d33ac3b719b2a67529b0c6c0b3e025637b477eef9" -dependencies = [ - "aliasable", - "fontdb", - "libm", - "log", - "rangemap", - "rustybuzz", - "swash", - "sys-locale", - "unicode-bidi", - "unicode-linebreak", - "unicode-script", - "unicode-segmentation", -] - [[package]] name = "cpufeatures" version = "0.2.10" @@ -906,30 +606,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" -dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset 0.9.0", - "scopeguard", -] - [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -939,12 +615,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - [[package]] name = "crypto-common" version = "0.1.6" @@ -955,17 +625,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "d3d12" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8f0de2f5a8e7bd4a9eec0e3c781992a4ce1724f68aec7d7a3715344de8b39da" -dependencies = [ - "bitflags 1.3.2", - "libloading 0.7.4", - "winapi", -] - [[package]] name = "deflate" version = "0.8.6" @@ -1040,27 +699,12 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "dlib" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" -dependencies = [ - "libloading 0.8.1", -] - [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" -[[package]] -name = "downcast-rs" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" - [[package]] name = "dtoa" version = "0.4.8" @@ -1190,36 +834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "error-code" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" -dependencies = [ - "libc", - "str-buf", -] - -[[package]] -name = "etagere" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf22f748754352918e082e0039335ee92454a5d62bcaf69b5e8daf5907d9644" -dependencies = [ - "euclid", - "svg_fmt", -] - -[[package]] -name = "euclid" -version = "0.22.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787" -dependencies = [ - "num-traits", + "windows-sys", ] [[package]] @@ -1240,32 +855,10 @@ dependencies = [ ] [[package]] -name = "exr" -version = "1.6.4" +name = "fastrand" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279d3efcc55e19917fff7ab3ddd6c14afb6a90881a0078465196fe2f99d08c56" -dependencies = [ - "bit_field", - "flume", - "half", - "lebe", - "miniz_oxide 0.7.1", - "rayon-core", - "smallvec", - "zune-inflate", -] - -[[package]] -name = "fast-srgb8" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] @@ -1416,34 +1009,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "fontdb" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af8d8cbea8f21307d7e84bca254772981296f058a1d36b461bf4d83a7499fc9e" -dependencies = [ - "log", - "memmap2 0.6.2", - "slotmap", - "tinyvec", - "ttf-parser", -] - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.0" @@ -1499,7 +1064,6 @@ dependencies = [ "futures-core", "futures-task", "futures-util", - "num_cpus", ] [[package]] @@ -1656,16 +1220,6 @@ dependencies = [ "rusttype", ] -[[package]] -name = "gethostname" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "getrandom" version = "0.2.10" @@ -1689,16 +1243,6 @@ dependencies = [ "weezl", ] -[[package]] -name = "gif" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" -dependencies = [ - "color_quant", - "weezl", -] - [[package]] name = "gimli" version = "0.28.0" @@ -1738,12 +1282,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "glam" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945" - [[package]] name = "glib" version = "0.17.10" @@ -1823,30 +1361,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "glow" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0fe580e4b60a8ab24a868bc08e2f03cbcb20d3d676601fa909386713333728" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "glyphon" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e87caa7459145f5e5f167bf34db4532901404c679e62339fb712a0e3ccf722a" -dependencies = [ - "cosmic-text", - "etagere", - "lru", - "wgpu", -] - [[package]] name = "gobject-sys" version = "0.17.10" @@ -1858,58 +1372,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "gpu-alloc" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22beaafc29b38204457ea030f6fb7a84c9e4dd1b86e311ba0542533453d87f62" -dependencies = [ - "bitflags 1.3.2", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54804d0d6bc9d7f26db4eaec1ad10def69b599315f487d32c334a80d1efe67a5" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "gpu-allocator" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce95f9e2e11c2c6fadfce42b5af60005db06576f231f5c92550fdded43c423e8" -dependencies = [ - "backtrace", - "log", - "thiserror", - "winapi", - "windows", -] - -[[package]] -name = "gpu-descriptor" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" -dependencies = [ - "bitflags 2.4.1", - "gpu-descriptor-types", - "hashbrown 0.14.2", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" -dependencies = [ - "bitflags 2.4.1", -] - [[package]] name = "graphene-rs" version = "0.17.10" @@ -2021,16 +1483,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "guillotiere" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" -dependencies = [ - "euclid", - "svg_fmt", -] - [[package]] name = "gvdb" version = "0.4.2" @@ -2040,8 +1492,8 @@ dependencies = [ "byteorder", "flate2", "lazy_static", - "memmap2 0.7.1", - "quick-xml 0.29.0", + "memmap2", + "quick-xml", "safe-transmute", "serde", "serde_json", @@ -2049,46 +1501,11 @@ dependencies = [ "zvariant", ] -[[package]] -name = "half" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" -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.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" -dependencies = [ - "ahash", - "allocator-api2", -] - -[[package]] -name = "hassle-rs" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1397650ee315e8891a0df210707f0fc61771b0cc518c3023896064c5407cb3b0" -dependencies = [ - "bitflags 1.3.2", - "com-rs", - "libc", - "libloading 0.7.4", - "thiserror", - "widestring", - "winapi", -] [[package]] name = "heck" @@ -2108,12 +1525,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - [[package]] name = "http" version = "0.2.9" @@ -2234,174 +1645,6 @@ dependencies = [ "cc", ] -[[package]] -name = "iced" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c708807ec86f99dd729dc4d42db5239acf118cec14d3c5f57679dcfdbbc472b1" -dependencies = [ - "iced_core", - "iced_futures", - "iced_renderer", - "iced_widget", - "iced_winit", - "image 0.24.7", - "thiserror", -] - -[[package]] -name = "iced_core" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d0bc4fbf018576d08d93f838e6058cc6f10bbc05e04ae249a2a44dffb4ebc8" -dependencies = [ - "bitflags 1.3.2", - "instant", - "log", - "palette", - "thiserror", - "twox-hash", -] - -[[package]] -name = "iced_futures" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dab0054a9c7a1cbce227a8cd9ee4a094497b3d06094551ac6c1488d563802e" -dependencies = [ - "futures", - "iced_core", - "log", - "tokio", - "wasm-bindgen-futures", - "wasm-timer", -] - -[[package]] -name = "iced_graphics" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ff14447a221e9e9205a13d84d7bbdf0636a3b1daa02cfca690ed09689c4d2b" -dependencies = [ - "bitflags 1.3.2", - "bytemuck", - "glam", - "half", - "iced_core", - "image 0.24.7", - "kamadak-exif", - "log", - "raw-window-handle", - "thiserror", -] - -[[package]] -name = "iced_renderer" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1033385b0db0099a0d13178c9ff93c1ce11e7d0177522acf578bf79febdb2af8" -dependencies = [ - "iced_graphics", - "iced_tiny_skia", - "iced_wgpu", - "log", - "raw-window-handle", - "thiserror", -] - -[[package]] -name = "iced_runtime" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c6c89853e1250c6fac82c5015fa2144517be9b33d4b8e456f10e198b23e28bd" -dependencies = [ - "iced_core", - "iced_futures", - "thiserror", -] - -[[package]] -name = "iced_style" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85c47d9d13e2281f75ddf98c865daf2101632bd2b855c401dd0b1c8b81a31a0" -dependencies = [ - "iced_core", - "once_cell", - "palette", -] - -[[package]] -name = "iced_tiny_skia" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7715f6222c9470bbbd75a39f70478fa0d1bdfb81a377a34fd1b090ffccc480b" -dependencies = [ - "bytemuck", - "cosmic-text", - "iced_graphics", - "kurbo", - "log", - "raw-window-handle", - "rustc-hash", - "softbuffer", - "tiny-skia 0.10.0", - "twox-hash", -] - -[[package]] -name = "iced_wgpu" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f7c5de46b997ed7b18e05ec67059dcdf3beeac51e917c21071b021bb848b9" -dependencies = [ - "bitflags 1.3.2", - "bytemuck", - "futures", - "glam", - "glyphon", - "guillotiere", - "iced_graphics", - "log", - "once_cell", - "raw-window-handle", - "rustc-hash", - "twox-hash", - "wgpu", -] - -[[package]] -name = "iced_widget" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a177219ae51c3ba08f228ab932354b360cc669e94aec50c01e7c9b675f074c7c" -dependencies = [ - "iced_renderer", - "iced_runtime", - "iced_style", - "num-traits", - "thiserror", - "unicode-segmentation", -] - -[[package]] -name = "iced_winit" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ecea26fcc8074373f6011d2193d63445617d900a428eb4df66c0e5658408fb6" -dependencies = [ - "iced_graphics", - "iced_runtime", - "iced_style", - "log", - "raw-window-handle", - "thiserror", - "web-sys", - "winapi", - "window_clipboard", - "winit", -] - [[package]] name = "idna" version = "0.4.0" @@ -2438,13 +1681,13 @@ dependencies = [ "bytemuck", "byteorder", "color_quant", - "gif 0.11.4", - "jpeg-decoder 0.1.22", + "gif", + "jpeg-decoder", "num-iter", "num-rational 0.3.2", "num-traits", "png 0.16.8", - "tiff 0.6.1", + "tiff", ] [[package]] @@ -2456,24 +1699,9 @@ dependencies = [ "bytemuck", "byteorder", "color_quant", - "exr", - "gif 0.12.0", - "jpeg-decoder 0.3.0", "num-rational 0.4.1", "num-traits", "png 0.17.10", - "qoi", - "tiff 0.9.0", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", ] [[package]] @@ -2483,7 +1711,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown", ] [[package]] @@ -2493,9 +1721,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", ] [[package]] @@ -2525,7 +1750,7 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -2546,7 +1771,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", "rustix 0.38.20", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -2571,42 +1796,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] -name = "jni-sys" -version = "0.3.0" +name = "jpeg-decoder" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" [[package]] -name = "jobserver" -version = "0.1.27" +name = "js-sys" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ - "libc", -] - -[[package]] -name = "jpeg-decoder" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" - -[[package]] -name = "jpeg-decoder" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" -dependencies = [ - "rayon", -] - -[[package]] -name = "js-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" -dependencies = [ - "wasm-bindgen", + "wasm-bindgen", ] [[package]] @@ -2623,35 +1824,6 @@ dependencies = [ "simple_asn1", ] -[[package]] -name = "kamadak-exif" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" -dependencies = [ - "mutate_once", -] - -[[package]] -name = "khronos-egl" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2352bd1d0bceb871cb9d40f24360c8133c11d7486b68b5381c1dd1a32015e3" -dependencies = [ - "libc", - "libloading 0.7.4", - "pkg-config", -] - -[[package]] -name = "kurbo" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" -dependencies = [ - "arrayvec", -] - [[package]] name = "kv-log-macro" version = "1.0.7" @@ -2667,12 +1839,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lebe" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" - [[package]] name = "libadwaita" version = "0.4.4" @@ -2712,16 +1878,6 @@ version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - [[package]] name = "libloading" version = "0.8.1" @@ -2729,15 +1885,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" dependencies = [ "cfg-if", - "windows-sys 0.48.0", + "windows-sys", ] -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2793,15 +1943,6 @@ dependencies = [ "time 0.2.27", ] -[[package]] -name = "lru" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" -dependencies = [ - "hashbrown 0.14.2", -] - [[package]] name = "lzw" version = "0.10.0" @@ -2823,24 +1964,6 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" -[[package]] -name = "memmap2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" -dependencies = [ - "libc", -] - -[[package]] -name = "memmap2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" -dependencies = [ - "libc", -] - [[package]] name = "memmap2" version = "0.7.1" @@ -2850,15 +1973,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - [[package]] name = "memoffset" version = "0.7.1" @@ -2877,26 +1991,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "metal" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de11355d1f6781482d027a3b4d4de7825dcedb197bf573e0596d00008402d060" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-graphics-types", - "foreign-types", - "log", - "objc", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.3.7" @@ -2933,35 +2027,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", - "log", "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "mutate_once" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" - -[[package]] -name = "naga" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbcc2e0513220fd2b598e6068608d4462db20322c0e77e47f6f488dfcfc279cb" -dependencies = [ - "bit-set", - "bitflags 1.3.2", - "codespan-reporting", - "hexf-parse", - "indexmap 1.9.3", - "log", - "num-traits", - "rustc-hash", - "spirv", - "termcolor", - "thiserror", - "unicode-xid", + "windows-sys", ] [[package]] @@ -2973,73 +2040,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "ndk" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" -dependencies = [ - "bitflags 1.3.2", - "jni-sys", - "ndk-sys", - "num_enum 0.5.11", - "raw-window-handle", - "thiserror", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "nix" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf" -dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if", - "libc", - "memoffset 0.6.5", -] - -[[package]] -name = "nix" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.6.5", -] - -[[package]] -name = "nix" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.6.5", -] - [[package]] name = "nix" version = "0.26.4" @@ -3050,17 +2050,6 @@ dependencies = [ "cfg-if", "libc", "memoffset 0.7.1", - "pin-utils", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", ] [[package]] @@ -3069,7 +2058,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec60c60a693226186f5d6edf073232bfb6464ed97eb22cf3b01c1e8198fd97f5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -3145,48 +2134,6 @@ dependencies = [ "libc", ] -[[package]] -name = "num_enum" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" -dependencies = [ - "num_enum_derive 0.5.11", -] - -[[package]] -name = "num_enum" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" -dependencies = [ - "num_enum_derive 0.6.1", -] - -[[package]] -name = "num_enum_derive" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "num_enum_derive" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.38", -] - [[package]] name = "objc" version = "0.2.7" @@ -3194,7 +2141,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", - "objc_exception", ] [[package]] @@ -3208,41 +2154,6 @@ dependencies = [ "objc_id", ] -[[package]] -name = "objc-sys" -version = "0.2.0-beta.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7" - -[[package]] -name = "objc2" -version = "0.3.0-beta.3.patch-leaks.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e01640f9f2cb1220bbe80325e179e532cb3379ebcd1bf2279d703c19fe3a468" -dependencies = [ - "block2", - "objc-sys", - "objc2-encode", -] - -[[package]] -name = "objc2-encode" -version = "2.0.0-pre.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abfcac41015b00a120608fdaa6938c44cb983fee294351cc4bac7638b4e50512" -dependencies = [ - "objc-sys", -] - -[[package]] -name = "objc_exception" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" -dependencies = [ - "cc", -] - [[package]] name = "objc_id" version = "0.1.1" @@ -3321,15 +2232,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" -[[package]] -name = "orbclient" -version = "0.3.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8378ac0dfbd4e7895f2d2c1f1345cab3836910baf3a300b000d04250f0c8428f" -dependencies = [ - "redox_syscall 0.3.5", -] - [[package]] name = "ordered-float" version = "1.1.1" @@ -3349,38 +2251,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "owned_ttf_parser" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4" -dependencies = [ - "ttf-parser", -] - -[[package]] -name = "palette" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e2f34147767aa758aa649415b50a69eeb46a67f9dc7db8011eeb3d84b351dc" -dependencies = [ - "approx 0.5.1", - "fast-srgb8", - "palette_derive", - "phf", -] - -[[package]] -name = "palette_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7db010ec5ff3d4385e4f133916faacd9dad0f6a09394c92d825b3aed310fa0a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.38", -] - [[package]] name = "pango" version = "0.17.10" @@ -3414,109 +2284,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] -name = "parking_lot" -version = "0.11.2" +name = "pem" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", + "base64 0.13.1", ] [[package]] -name = "parking_lot" -version = "0.12.1" +name = "percent-encoding" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core 0.9.9", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.4.1", - "smallvec", - "windows-targets 0.48.5", -] - -[[package]] -name = "pem" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" -dependencies = [ - "base64 0.13.1", -] - -[[package]] -name = "percent-encoding" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.38", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher", -] +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project" @@ -3605,7 +2385,7 @@ dependencies = [ "libc", "log", "pin-project-lite", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -3707,30 +2487,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "profiling" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89dff0959d98c9758c88826cc002e2c3d0b9dfac4139711d1f30de442f1139b" - -[[package]] -name = "qoi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "quick-xml" -version = "0.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" -dependencies = [ - "memchr", -] - [[package]] name = "quick-xml" version = "0.29.0" @@ -3790,53 +2546,12 @@ dependencies = [ "regex-syntax 0.7.5", ] -[[package]] -name = "range-alloc" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" - -[[package]] -name = "rangemap" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "977b1e897f9d764566891689e642653e5ed90c6895106acd005eb4c1d0203991" - [[package]] name = "raw-window-handle" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" -[[package]] -name = "rayon" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.3.5" @@ -3846,15 +2561,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "regex" version = "1.10.2" @@ -3929,12 +2635,6 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "renderdoc-sys" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b" - [[package]] name = "rfd" version = "0.12.1" @@ -3957,7 +2657,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -4028,7 +2728,7 @@ dependencies = [ "io-lifetimes", "libc", "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -4041,7 +2741,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.10", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -4093,28 +2793,11 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f61411055101f7b60ecf1041d87fb74205fb20b0c7a723f07ef39174cf6b4c0" dependencies = [ - "approx 0.3.2", + "approx", "ordered-float", "stb_truetype", ] -[[package]] -name = "rustybuzz" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82eea22c8f56965eeaf3a209b3d24508256c7b920fb3b6211b8ba0f7c0583250" -dependencies = [ - "bitflags 1.3.2", - "bytemuck", - "libm", - "smallvec", - "ttf-parser", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-general-category", - "unicode-script", -] - [[package]] name = "ryu" version = "1.0.15" @@ -4142,7 +2825,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -4169,12 +2852,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -4191,19 +2868,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "sctk-adwaita" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda4e97be1fd174ccc2aae81c8b694e803fa99b34e8fd0f057a9d70698e3ed09" -dependencies = [ - "ab_glyph", - "log", - "memmap2 0.5.10", - "smithay-client-toolkit", - "tiny-skia 0.8.4", -] - [[package]] name = "secrecy" version = "0.8.0" @@ -4400,12 +3064,6 @@ dependencies = [ "time 0.3.30", ] -[[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" @@ -4415,50 +3073,12 @@ dependencies = [ "autocfg", ] -[[package]] -name = "slotmap" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" -dependencies = [ - "version_check", -] - [[package]] name = "smallvec" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" -[[package]] -name = "smithay-client-toolkit" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870427e30b8f2cbe64bf43ec4b86e88fe39b0a84b3f15efd9c9c2d020bc86eb9" -dependencies = [ - "bitflags 1.3.2", - "calloop", - "dlib", - "lazy_static", - "log", - "memmap2 0.5.10", - "nix 0.24.3", - "pkg-config", - "wayland-client 0.29.5", - "wayland-cursor", - "wayland-protocols", -] - -[[package]] -name = "smithay-clipboard" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a345c870a1fae0b1b779085e81b51e614767c239e93503588e54c5b17f4b0e8" -dependencies = [ - "smithay-client-toolkit", - "wayland-client 0.29.5", -] - [[package]] name = "snafu" version = "0.7.5" @@ -4499,35 +3119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "softbuffer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2b953f6ba7285f0af131eb748aabd8ddaf53e0b81dda3ba5d803b0847d6559f" -dependencies = [ - "bytemuck", - "cfg_aliases", - "cocoa", - "core-graphics", - "fastrand 1.9.0", - "foreign-types", - "log", - "nix 0.26.4", - "objc", - "raw-window-handle", - "redox_syscall 0.3.5", - "thiserror", - "wasm-bindgen", - "wayland-backend", - "wayland-client 0.30.2", - "wayland-sys 0.30.1", - "web-sys", - "windows-sys 0.48.0", - "x11-dl", - "x11rb 0.11.1", + "windows-sys", ] [[package]] @@ -4545,16 +3137,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "spirv" -version = "0.2.0+1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830" -dependencies = [ - "bitflags 1.3.2", - "num-traits", -] - [[package]] name = "standback" version = "0.2.17" @@ -4628,40 +3210,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" -[[package]] -name = "str-buf" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" - -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" - [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "svg_fmt" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2" - -[[package]] -name = "swash" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7c73c813353c347272919aa1af2885068b05e625e5532b43049e4f641ae77f" -dependencies = [ - "yazi", - "zeno", -] - [[package]] name = "syn" version = "1.0.109" @@ -4720,9 +3274,9 @@ checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand 2.0.1", - "redox_syscall 0.3.5", + "redox_syscall", "rustix 0.38.20", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -4736,7 +3290,7 @@ dependencies = [ [[package]] name = "testangel" -version = "0.19.0" +version = "0.20.0-pre.1" dependencies = [ "base64 0.21.5", "chrono", @@ -4747,10 +3301,9 @@ dependencies = [ "fuzzy-matcher", "genpdf", "glib-build-tools", - "iced", "image 0.24.7", "itertools", - "libloading 0.8.1", + "libloading", "log", "octocrab", "once_cell", @@ -4758,7 +3311,6 @@ dependencies = [ "pretty_env_logger", "relm4", "relm4-icons", - "rfd", "ron", "semver 1.0.20", "serde", @@ -4770,7 +3322,7 @@ dependencies = [ [[package]] name = "testangel-arithmetic" -version = "0.19.0" +version = "0.20.0-pre.1" dependencies = [ "lazy_static", "testangel-engine", @@ -4778,7 +3330,7 @@ dependencies = [ [[package]] name = "testangel-compare" -version = "0.19.0" +version = "0.20.0-pre.1" dependencies = [ "lazy_static", "testangel-engine", @@ -4786,7 +3338,7 @@ dependencies = [ [[package]] name = "testangel-convert" -version = "0.19.0" +version = "0.20.0-pre.1" dependencies = [ "lazy_static", "testangel-engine", @@ -4794,7 +3346,7 @@ dependencies = [ [[package]] name = "testangel-date" -version = "0.19.0" +version = "0.20.0-pre.1" dependencies = [ "chrono", "lazy_static", @@ -4803,7 +3355,7 @@ dependencies = [ [[package]] name = "testangel-engine" -version = "0.19.0" +version = "0.20.0-pre.1" dependencies = [ "testangel-engine-macros", "testangel-ipc", @@ -4811,11 +3363,11 @@ dependencies = [ [[package]] name = "testangel-engine-macros" -version = "0.19.0" +version = "0.20.0-pre.1" [[package]] name = "testangel-evidence" -version = "0.19.0" +version = "0.20.0-pre.1" dependencies = [ "lazy_static", "testangel-engine", @@ -4823,7 +3375,7 @@ dependencies = [ [[package]] name = "testangel-ipc" -version = "0.19.0" +version = "0.20.0-pre.1" dependencies = [ "schemars", "serde", @@ -4832,7 +3384,7 @@ dependencies = [ [[package]] name = "testangel-rand" -version = "0.19.0" +version = "0.20.0-pre.1" dependencies = [ "lazy_static", "rand", @@ -4843,7 +3395,7 @@ dependencies = [ [[package]] name = "testangel-regex" -version = "0.19.0" +version = "0.20.0-pre.1" dependencies = [ "lazy_static", "regex", @@ -4853,7 +3405,7 @@ dependencies = [ [[package]] name = "testangel-user-interaction" -version = "0.19.0" +version = "0.20.0-pre.1" dependencies = [ "lazy_static", "rfd", @@ -4897,22 +3449,11 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" dependencies = [ - "jpeg-decoder 0.1.22", + "jpeg-decoder", "miniz_oxide 0.4.4", "weezl", ] -[[package]] -name = "tiff" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211" -dependencies = [ - "flate2", - "jpeg-decoder 0.3.0", - "weezl", -] - [[package]] name = "time" version = "0.2.27" @@ -4980,57 +3521,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "tiny-skia" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8493a203431061e901613751931f047d1971337153f96d0e5e363d6dbf6a67" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "png 0.17.10", - "tiny-skia-path 0.8.4", -] - -[[package]] -name = "tiny-skia" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db11798945fa5c3e5490c794ccca7c6de86d3afdd54b4eb324109939c6f37bc" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "png 0.17.10", - "tiny-skia-path 0.10.0", -] - -[[package]] -name = "tiny-skia-path" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adbfb5d3f3dd57a0e11d12f4f13d4ebbbc1b5c15b7ab0a156d030b21da5f677c" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - -[[package]] -name = "tiny-skia-path" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f60aa35c89ac2687ace1a2556eaaea68e8c0d47408a2e3e7f5c98a489e7281c" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - [[package]] name = "tinystr" version = "0.7.4" @@ -5067,7 +3557,7 @@ dependencies = [ "num_cpus", "pin-project-lite", "socket2 0.5.5", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -5130,7 +3620,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.0.2", + "indexmap", "toml_datetime", "winnow", ] @@ -5141,7 +3631,7 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "380f9e8120405471f7c9ad1860a713ef5ece6a670c7eae39225e477340f32fc4" dependencies = [ - "indexmap 2.0.2", + "indexmap", "serde", "serde_spanned", "toml_datetime", @@ -5236,23 +3726,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" -[[package]] -name = "ttf-parser" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" - -[[package]] -name = "twox-hash" -version = "1.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" -dependencies = [ - "cfg-if", - "rand", - "static_assertions", -] - [[package]] name = "type-map" version = "0.4.0" @@ -5327,36 +3800,12 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" -[[package]] -name = "unicode-bidi-mirroring" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" - -[[package]] -name = "unicode-ccc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" - -[[package]] -name = "unicode-general-category" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" - [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - [[package]] name = "unicode-normalization" version = "0.1.22" @@ -5366,30 +3815,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-script" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc" - -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - -[[package]] -name = "unicode-width" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" - -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - [[package]] name = "untrusted" version = "0.7.1" @@ -5435,12 +3860,6 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version-compare" version = "0.1.1" @@ -5550,144 +3969,6 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" -[[package]] -name = "wasm-timer" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" -dependencies = [ - "futures", - "js-sys", - "parking_lot 0.11.2", - "pin-utils", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wayland-backend" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b48e27457e8da3b2260ac60d0a94512f5cba36448679f3747c0865b7893ed8" -dependencies = [ - "cc", - "downcast-rs", - "io-lifetimes", - "nix 0.26.4", - "scoped-tls", - "smallvec", - "wayland-sys 0.30.1", -] - -[[package]] -name = "wayland-client" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" -dependencies = [ - "bitflags 1.3.2", - "downcast-rs", - "libc", - "nix 0.24.3", - "scoped-tls", - "wayland-commons", - "wayland-scanner 0.29.5", - "wayland-sys 0.29.5", -] - -[[package]] -name = "wayland-client" -version = "0.30.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "489c9654770f674fc7e266b3c579f4053d7551df0ceb392f153adb1f9ed06ac8" -dependencies = [ - "bitflags 1.3.2", - "nix 0.26.4", - "wayland-backend", - "wayland-scanner 0.30.1", -] - -[[package]] -name = "wayland-commons" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" -dependencies = [ - "nix 0.24.3", - "once_cell", - "smallvec", - "wayland-sys 0.29.5", -] - -[[package]] -name = "wayland-cursor" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" -dependencies = [ - "nix 0.24.3", - "wayland-client 0.29.5", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" -dependencies = [ - "bitflags 1.3.2", - "wayland-client 0.29.5", - "wayland-commons", - "wayland-scanner 0.29.5", -] - -[[package]] -name = "wayland-scanner" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" -dependencies = [ - "proc-macro2", - "quote", - "xml-rs", -] - -[[package]] -name = "wayland-scanner" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b873b257fbc32ec909c0eb80dea312076a67014e65e245f5eb69a6b8ab330e" -dependencies = [ - "proc-macro2", - "quick-xml 0.28.2", - "quote", -] - -[[package]] -name = "wayland-sys" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" -dependencies = [ - "dlib", - "lazy_static", - "pkg-config", -] - -[[package]] -name = "wayland-sys" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b2a02ac608e07132978689a6f9bf4214949c85998c247abadd4f4129b1aa06" -dependencies = [ - "dlib", - "lazy_static", - "log", - "pkg-config", -] - [[package]] name = "web-sys" version = "0.3.64" @@ -5704,112 +3985,6 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" -[[package]] -name = "wgpu" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "480c965c9306872eb6255fa55e4b4953be55a8b64d57e61d7ff840d3dcc051cd" -dependencies = [ - "arrayvec", - "cfg-if", - "js-sys", - "log", - "naga", - "parking_lot 0.12.1", - "profiling", - "raw-window-handle", - "smallvec", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f478237b4bf0d5b70a39898a66fa67ca3a007d79f2520485b8b0c3dfc46f8c2" -dependencies = [ - "arrayvec", - "bit-vec", - "bitflags 2.4.1", - "codespan-reporting", - "log", - "naga", - "parking_lot 0.12.1", - "profiling", - "raw-window-handle", - "rustc-hash", - "smallvec", - "thiserror", - "web-sys", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-hal" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecb3258078e936deee14fd4e0febe1cfe9bbb5ffef165cb60218d2ee5eb4448" -dependencies = [ - "android_system_properties", - "arrayvec", - "ash", - "bit-set", - "bitflags 2.4.1", - "block", - "core-graphics-types", - "d3d12", - "foreign-types", - "glow", - "gpu-alloc", - "gpu-allocator", - "gpu-descriptor", - "hassle-rs", - "js-sys", - "khronos-egl", - "libc", - "libloading 0.8.1", - "log", - "metal", - "naga", - "objc", - "parking_lot 0.12.1", - "profiling", - "range-alloc", - "raw-window-handle", - "renderdoc-sys", - "rustc-hash", - "smallvec", - "thiserror", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "winapi", -] - -[[package]] -name = "wgpu-types" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c153280bb108c2979eb5c7391cb18c56642dd3c072e55f52065e13e2a1252a" -dependencies = [ - "bitflags 2.4.1", - "js-sys", - "web-sys", -] - -[[package]] -name = "widestring" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" - [[package]] name = "winapi" version = "0.3.9" @@ -5835,60 +4010,19 @@ dependencies = [ "winapi", ] -[[package]] -name = "winapi-wsapoll" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "window_clipboard" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63287c9c4396ccf5346d035a9b0fcaead9e18377637f5eaa78b7ac65c873ff7d" -dependencies = [ - "clipboard-win", - "clipboard_macos", - "clipboard_wayland", - "clipboard_x11", - "raw-window-handle", - "thiserror", -] - -[[package]] -name = "windows" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-core" version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", + "windows-targets", ] [[package]] @@ -5897,22 +4031,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -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", + "windows-targets", ] [[package]] @@ -5921,134 +4040,57 @@ version = "0.48.5" 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", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "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_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_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_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_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_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_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 = "winit" -version = "0.28.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9596d90b45384f5281384ab204224876e8e8bf7d58366d9b795ad99aa9894b94" -dependencies = [ - "android-activity", - "bitflags 1.3.2", - "cfg_aliases", - "core-foundation", - "core-graphics", - "dispatch", - "instant", - "libc", - "log", - "mio", - "ndk", - "objc2", - "once_cell", - "orbclient", - "percent-encoding", - "raw-window-handle", - "redox_syscall 0.3.5", - "sctk-adwaita", - "smithay-client-toolkit", - "wasm-bindgen", - "wayland-client 0.29.5", - "wayland-commons", - "wayland-protocols", - "wayland-scanner 0.29.5", - "web-sys", - "windows-sys 0.45.0", - "x11-dl", -] - [[package]] name = "winnow" version = "0.5.17" @@ -6058,85 +4100,16 @@ dependencies = [ "memchr", ] -[[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 = "x11rb" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e99be55648b3ae2a52342f9a870c0e138709a3493261ce9b469afe6e4df6d8a" -dependencies = [ - "gethostname", - "nix 0.22.3", - "winapi", - "winapi-wsapoll", -] - -[[package]] -name = "x11rb" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf3c79412dd91bae7a7366b8ad1565a85e35dd049affc3a6a2c549e97419617" -dependencies = [ - "gethostname", - "libc", - "libloading 0.7.4", - "nix 0.25.1", - "once_cell", - "winapi", - "winapi-wsapoll", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0b1513b141123073ce54d5bb1d33f801f17508fbd61e02060b1214e96d39c56" -dependencies = [ - "nix 0.25.1", -] - -[[package]] -name = "xcursor" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" -dependencies = [ - "nom", -] - [[package]] name = "xdg-home" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" dependencies = [ - "nix 0.26.4", + "nix", "winapi", ] -[[package]] -name = "xml-rs" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" - -[[package]] -name = "yazi" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" - [[package]] name = "zbus" version = "3.14.1" @@ -6161,7 +4134,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix 0.26.4", + "nix", "once_cell", "ordered-stream", "rand", @@ -6203,47 +4176,12 @@ dependencies = [ "zvariant", ] -[[package]] -name = "zeno" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" - -[[package]] -name = "zerocopy" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c19fae0c8a9efc6a8281f2e623db8af1db9e57852e04cde3e754dd2dc29340f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc56589e9ddd1f1c28d4b4b5c773ce232910a6bb67a70133d61c9e347585efe9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.38", -] - [[package]] name = "zeroize" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] - [[package]] name = "zvariant" version = "3.15.0" diff --git a/Cargo.toml b/Cargo.toml index 46e41f5..c7be49a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.19.0" +version = "0.20.0-pre.1" edition = "2021" [workspace] diff --git a/testangel/Cargo.toml b/testangel/Cargo.toml index 0bd2386..6559985 100644 --- a/testangel/Cargo.toml +++ b/testangel/Cargo.toml @@ -12,8 +12,7 @@ required-features = [ "cli" ] [features] default = [ "ui" ] -ui = [ "dep:iced", "dep:opener", "dep:rfd", "dep:fern" ] -next-ui = [ "dep:relm4", "dep:relm4-icons", "dep:opener", "dep:fern", "dep:fluent", "dep:fluent-templates", "dep:once_cell", "dep:fuzzy-matcher", "dep:sys-locale" ] +ui = [ "dep:relm4", "dep:relm4-icons", "dep:opener", "dep:fern", "dep:fluent", "dep:fluent-templates", "dep:once_cell", "dep:fuzzy-matcher", "dep:sys-locale" ] cli = [ "dep:clap", "dep:pretty_env_logger" ] windows-keep-console-window = [] @@ -33,9 +32,7 @@ genpdf = { version = "0.2.0", features = ["images"] } chrono = "0.4.26" base64 = "0.21.2" itertools = "0.11.0" -iced = { version = "0.10.0", features = [ "image", "tokio" ], optional = true } opener = { version = "0.6.1", optional = true } -rfd = { version = "0.12.0", optional = true, default-features = false, features = [ "xdg-portal" ] } octocrab = "0.31.2" semver = "1.0.19" relm4 = { version = "0.6", optional = true, features = [ "libadwaita", "gnome_44" ] } diff --git a/testangel/locales/en/main.ftl b/testangel/locales/en/main.ftl index 8b8d8e5..3043e79 100644 --- a/testangel/locales/en/main.ftl +++ b/testangel/locales/en/main.ftl @@ -8,6 +8,7 @@ value = Value nothing-open = Nothing is Open delete = Delete +header-about = About { app-name } acknowledgements-testing-title = Software Testing acknowledgements-translations-title = Translations @@ -44,7 +45,6 @@ flow-header-open = Open flow... flow-header-save = Save flow flow-header-save-as = Save flow as... flow-header-close = Close flow -flow-header-about = About { app-name } flow-nothing-open-description = Open a flow or add a step to get started flow-action-changed = Flow Actions Changed @@ -80,7 +80,6 @@ action-header-open = Open action... action-header-save = Save action action-header-save-as = Save action as... action-header-close = Close action -action-header-about = About { app-name } action-nothing-open-description = Open an action or add a step to get started action-save-before = Save this action? diff --git a/testangel/locales/sv/main.ftl b/testangel/locales/sv/main.ftl index a0b0b5d..8fcc166 100644 --- a/testangel/locales/sv/main.ftl +++ b/testangel/locales/sv/main.ftl @@ -8,6 +8,7 @@ value = Värde nothing-open = Inget är öppet delete = Ta bort +header-about = Om { app-name } acknowledgements-testing-title = Programtestning acknowledgements-translations-title = Översättningar @@ -44,7 +45,6 @@ flow-header-open = Öppna flöde... flow-header-save = Spara flöde flow-header-save-as = Spara flöde som... flow-header-close = Stäng flöde -flow-header-about = Om { app-name } flow-nothing-open-description = Öppna en flöde eller lägg till ett steg för att komma igÃ¥ng flow-action-changed = Ã…tgärder i flödet har ändrats @@ -77,7 +77,6 @@ action-header-open = Öppna Ã¥tgärd... action-header-save = Spara Ã¥tgärd action-header-save-as = Spara Ã¥tgärd som... action-header-close = Stäng Ã¥tgärd -action-header-about = Om { app-name } action-nothing-open-description = Öppna en Ã¥tgärd eller lägg till ett steg för att komma igÃ¥ng action-save-before = Spara denna Ã¥tgärden? diff --git a/testangel/src/main.rs b/testangel/src/main.rs index 788aabc..50e437b 100644 --- a/testangel/src/main.rs +++ b/testangel/src/main.rs @@ -3,8 +3,6 @@ windows_subsystem = "windows" )] -#[cfg(feature = "next-ui")] -mod next_ui; #[cfg(feature = "ui")] mod ui; @@ -26,21 +24,17 @@ fn main() { .apply() .expect("Couldn't start logger!"); - #[cfg(feature = "next-ui")] + #[cfg(feature = "ui")] { use relm4::tokio::runtime; use testangel::version; - log::info!("Using locale: {}", next_ui::lang::initialise_i18n()); + log::info!("Using locale: {}", ui::lang::initialise_i18n()); if let Ok(rt) = runtime::Builder::new_current_thread().enable_all().build() { let _is_latest = rt.block_on(version::check_is_latest()); } - next_ui::initialise_ui(); - } - #[cfg(feature = "ui")] - { ui::initialise_ui(); } } diff --git a/testangel/src/next_ui/mod.rs b/testangel/src/next_ui/mod.rs deleted file mode 100644 index 4f90448..0000000 --- a/testangel/src/next_ui/mod.rs +++ /dev/null @@ -1,185 +0,0 @@ -use std::{rc::Rc, sync::Arc}; - -use gtk::prelude::*; -use relm4::{ - adw, gtk, Component, ComponentController, ComponentParts, Controller, RelmApp, SimpleComponent, -}; -use testangel::{ - action_loader::{self, ActionMap}, - ipc::{self, EngineList}, -}; - -use self::header_bar::HeaderBarInput; - -mod about; -mod actions; -mod components; -mod file_filters; -mod flows; -mod header_bar; -pub(crate) mod lang; - -/// Initialise and open the UI. -pub fn initialise_ui() { - log::info!("Starting Next UI..."); - let app = RelmApp::new("lilopkins.testangel"); - relm4_icons::initialize_icons(); - initialise_icons(); - - let engines = Arc::new(ipc::get_engines()); - let actions = Arc::new(action_loader::get_actions(engines.clone())); - app.run::(AppInit { engines, actions }); -} - -fn initialise_icons() { - relm4::gtk::gio::resources_register_include!("icons.gresource").unwrap(); - log::info!("Loaded icon bundle."); - - let display = relm4::gtk::gdk::Display::default().unwrap(); - let theme = gtk::IconTheme::for_display(&display); - theme.add_resource_path("/uk/hpkns/testangel/icons"); -} - -pub struct AppInit { - engines: Arc, - actions: Arc, -} - -#[derive(Debug)] -enum AppInput { - NoOp, - /// The view has changed and should be read from visible_child_name, then components updated as needed. - ChangedView(Option), - /// The actions might have changed and should be reloaded - ReloadActionsMap, -} - -#[derive(Debug)] -struct AppModel { - stack: Rc, - header: Controller, - - flows: Controller, - actions: Controller, - - engines_list: Arc, - actions_map: Arc, -} - -#[relm4::component] -impl SimpleComponent for AppModel { - type Init = AppInit; - type Input = AppInput; - type Output = (); - - view! { - main_window = adw::Window { - set_title: Some(&lang::lookup("app-name")), - set_default_width: 800, - set_default_height: 600, - set_icon_name: Some("testangel"), - - gtk::Box { - set_orientation: gtk::Orientation::Vertical, - set_spacing: 0, - - model.header.widget(), - - #[local_ref] - stack -> adw::ViewStack { - connect_visible_child_name_notify[sender] => move |st| { - sender.input(AppInput::ChangedView(st.visible_child_name().map(|s| s.into()))); - }, - }, - } - } - } - - fn init( - init: Self::Init, - root: &Self::Root, - sender: relm4::ComponentSender, - ) -> relm4::ComponentParts { - // Initialise the sub-components (pages) - let flows = flows::FlowsModel::builder() - .launch((init.actions.clone(), init.engines.clone())) - .forward(sender.input_sender(), |_msg| AppInput::NoOp); - let actions = actions::ActionsModel::builder() - .launch((init.actions.clone(), init.engines.clone())) - .forward(sender.input_sender(), |msg| match msg { - actions::ActionOutputs::ReloadActions => AppInput::ReloadActionsMap, - }); - - let stack = Rc::new(adw::ViewStack::new()); - - // Initialise the headerbar - let header = header_bar::HeaderBarModel::builder() - .launch(( - actions.model().header_controller_rc(), - flows.model().header_controller_rc(), - stack.clone(), - )) - .forward(sender.input_sender(), |_msg| AppInput::NoOp); - - // Build model - let model = AppModel { - actions_map: init.actions, - engines_list: init.engines, - stack, - header, - flows, - actions, - }; - - // Render window parts - let stack = &*model.stack; - - // Add pages - stack.add_titled_with_icon( - model.flows.widget(), - Some("flows"), - &lang::lookup("tab-flows"), - relm4_icons::icon_name::PAPYRUS_VERTICAL, - ); - if !std::env::var("TA_HIDE_ACTION_EDITOR") - .unwrap_or("no".to_string()) - .eq_ignore_ascii_case("yes") - { - stack.add_titled_with_icon( - model.actions.widget(), - Some("actions"), - &lang::lookup("tab-actions"), - relm4_icons::icon_name::PUZZLE_PIECE, - ); - } - - let widgets = view_output!(); - log::debug!("Initialised model: {model:?}"); - - // Trigger initial header bar update - sender.input(AppInput::ChangedView( - stack.visible_child_name().map(|s| s.into()), - )); - - ComponentParts { model, widgets } - } - - fn update(&mut self, message: Self::Input, _sender: relm4::ComponentSender) { - match message { - AppInput::NoOp => (), - AppInput::ChangedView(new_view) => { - self.header - .emit(HeaderBarInput::ChangedView(new_view.unwrap_or_default())); - } - AppInput::ReloadActionsMap => { - self.actions_map = Arc::new(action_loader::get_actions(self.engines_list.clone())); - self.flows.emit(flows::FlowInputs::ActionsMapChanged( - self.actions_map.clone(), - )); - self.actions.emit(actions::ActionInputs::ActionsMapChanged( - self.actions_map.clone(), - )); - } - } - } -} diff --git a/testangel/src/next_ui/about.rs b/testangel/src/ui/about.rs similarity index 97% rename from testangel/src/next_ui/about.rs rename to testangel/src/ui/about.rs index ea51b01..de1f17a 100644 --- a/testangel/src/next_ui/about.rs +++ b/testangel/src/ui/about.rs @@ -1,79 +1,79 @@ -use std::sync::Arc; - -use relm4::{adw, gtk, SimpleComponent}; -use testangel::{action_loader::ActionMap, ipc::EngineList}; - -use crate::next_ui::lang; - -pub struct AppAbout; - -#[relm4::component(pub)] -impl SimpleComponent for AppAbout { - type Init = (Arc, Arc); - type Input = (); - type Output = (); - - view! { - #[root] - #[name = "about"] - adw::AboutWindow { - set_application_icon: "testangel", - set_application_name: &lang::lookup("app-name"), - set_version: env!("CARGO_PKG_VERSION"), - set_copyright: "© 2023 Lily Hopkins", - set_license_type: gtk::License::Gpl30Only, - set_issue_url: &support_url, - set_developer_name: "Lily Hopkins", - set_debug_info: &log_data, - - add_acknowledgement_section: (Some(&lang::lookup("acknowledgements-testing-title")), &["John Chander", "Eden Turner"]), - add_acknowledgement_section: (Some(&lang::lookup("acknowledgements-translations-title")), &["Lily Hopkins"]), - add_legal_section: ("GTK", None, gtk::License::Gpl20Only, None), - add_legal_section: ("Adwaita", None, gtk::License::Gpl20Only, None), - add_legal_section: ("clap", None, gtk::License::MitX11, None), - add_legal_section: ("fern", None, gtk::License::MitX11, None), - add_legal_section: ("libloading", None, gtk::License::Custom, Some("ISC")), - add_legal_section: ("log", None, gtk::License::MitX11, None), - add_legal_section: ("image", None, gtk::License::MitX11, None), - add_legal_section: ("thiserror", None, gtk::License::MitX11, None), - add_legal_section: ("pretty_env_logger", None, gtk::License::MitX11, None), - add_legal_section: ("serde", None, gtk::License::MitX11, None), - add_legal_section: ("uuid", None, gtk::License::MitX11, None), - add_legal_section: ("ron", None, gtk::License::MitX11, None), - add_legal_section: ("genpdf", None, gtk::License::MitX11, None), - add_legal_section: ("chrono", None, gtk::License::MitX11, None), - add_legal_section: ("base64", None, gtk::License::MitX11, None), - add_legal_section: ("itertools", None, gtk::License::MitX11, None), - add_legal_section: ("octocrab", None, gtk::License::MitX11, None), - add_legal_section: ("semver", None, gtk::License::MitX11, None), - add_legal_section: ("relm4", None, gtk::License::MitX11, None), - add_legal_section: ("relm4-icons", None, gtk::License::MitX11, None), - add_legal_section: ("rust-i18n", None, gtk::License::MitX11, None), - } - } - - fn init( - init: Self::Init, - root: &Self::Root, - _sender: relm4::ComponentSender, - ) -> relm4::ComponentParts { - let model = AppAbout; - - let engine_list = init.0; - let action_map = init.1; - let log_data = format!( - "Debug data generated at: {}\nSoftware version: {}\nLocale: {} (system wanted: {:?})\n\nEngines:\n{:#?}\n\nActions:\n{:#?}", - chrono::Local::now(), - env!("CARGO_PKG_VERSION"), - lang::current_locale(), - sys_locale::get_locales().collect::>(), - engine_list.inner(), - action_map.get_by_group(), - ); - let support_url = std::env::var("TA_LOCAL_SUPPORT_CONTACT") - .unwrap_or("https://github.com/lilopkins/testangel".to_string()); - let widgets = view_output!(); - - relm4::ComponentParts { model, widgets } - } -} +use std::sync::Arc; + +use relm4::{adw, gtk, SimpleComponent}; +use testangel::{action_loader::ActionMap, ipc::EngineList}; + +use crate::ui::lang; + +pub struct AppAbout; + +#[relm4::component(pub)] +impl SimpleComponent for AppAbout { + type Init = (Arc, Arc); + type Input = (); + type Output = (); + + view! { + #[root] + #[name = "about"] + adw::AboutWindow { + set_application_icon: "testangel", + set_application_name: &lang::lookup("app-name"), + set_version: env!("CARGO_PKG_VERSION"), + set_copyright: "© 2023 Lily Hopkins", + set_license_type: gtk::License::Gpl30Only, + set_issue_url: &support_url, + set_developer_name: "Lily Hopkins", + set_debug_info: &log_data, + + add_acknowledgement_section: (Some(&lang::lookup("acknowledgements-testing-title")), &["John Chander", "Eden Turner"]), + add_acknowledgement_section: (Some(&lang::lookup("acknowledgements-translations-title")), &["Lily Hopkins"]), + add_legal_section: ("GTK", None, gtk::License::Gpl20Only, None), + add_legal_section: ("Adwaita", None, gtk::License::Gpl20Only, None), + add_legal_section: ("clap", None, gtk::License::MitX11, None), + add_legal_section: ("fern", None, gtk::License::MitX11, None), + add_legal_section: ("libloading", None, gtk::License::Custom, Some("ISC")), + add_legal_section: ("log", None, gtk::License::MitX11, None), + add_legal_section: ("image", None, gtk::License::MitX11, None), + add_legal_section: ("thiserror", None, gtk::License::MitX11, None), + add_legal_section: ("pretty_env_logger", None, gtk::License::MitX11, None), + add_legal_section: ("serde", None, gtk::License::MitX11, None), + add_legal_section: ("uuid", None, gtk::License::MitX11, None), + add_legal_section: ("ron", None, gtk::License::MitX11, None), + add_legal_section: ("genpdf", None, gtk::License::MitX11, None), + add_legal_section: ("chrono", None, gtk::License::MitX11, None), + add_legal_section: ("base64", None, gtk::License::MitX11, None), + add_legal_section: ("itertools", None, gtk::License::MitX11, None), + add_legal_section: ("octocrab", None, gtk::License::MitX11, None), + add_legal_section: ("semver", None, gtk::License::MitX11, None), + add_legal_section: ("relm4", None, gtk::License::MitX11, None), + add_legal_section: ("relm4-icons", None, gtk::License::MitX11, None), + add_legal_section: ("rust-i18n", None, gtk::License::MitX11, None), + } + } + + fn init( + init: Self::Init, + root: &Self::Root, + _sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + let model = AppAbout; + + let engine_list = init.0; + let action_map = init.1; + let log_data = format!( + "Debug data generated at: {}\nSoftware version: {}\nLocale: {} (system wanted: {:?})\n\nEngines:\n{:#?}\n\nActions:\n{:#?}", + chrono::Local::now(), + env!("CARGO_PKG_VERSION"), + lang::current_locale(), + sys_locale::get_locales().collect::>(), + engine_list.inner(), + action_map.get_by_group(), + ); + let support_url = std::env::var("TA_LOCAL_SUPPORT_CONTACT") + .unwrap_or("https://github.com/lilopkins/testangel".to_string()); + let widgets = view_output!(); + + relm4::ComponentParts { model, widgets } + } +} diff --git a/testangel/src/ui/action_editor.rs b/testangel/src/ui/action_editor.rs deleted file mode 100644 index cd88a23..0000000 --- a/testangel/src/ui/action_editor.rs +++ /dev/null @@ -1,1304 +0,0 @@ -use std::{collections::HashMap, env, fmt, fs, path::PathBuf, sync::Arc}; - -use iced::{ - theme, - widget::{ - column, combo_box, row, Button, Checkbox, Column, ComboBox, Container, PickList, Rule, - Scrollable, Space, Text, TextInput, - }, - Length, -}; -use testangel::{ - ipc::EngineList, - types::{Action, InstructionConfiguration, InstructionParameterSource, VersionedFile}, -}; -use testangel_ipc::prelude::{Instruction, ParameterKind, ParameterValue}; - -use super::UiComponent; - -#[derive(Clone, Debug)] -pub enum ActionEditorMessage { - RunAction, - WriteFileToDisk(PathBuf, SaveActionThen), - SaveAction(SaveActionThen), - SaveAsAction(SaveActionThen), - DoPostSaveActions(SaveActionThen), - CloseAction, - - NameChanged(String), - GroupChanged(String), - DescriptionChanged(String), - AuthorChanged(String), - VisibleChanged(bool), - - ParameterCreate, - ParameterNameChange(usize, String), - ParameterTypeChange(usize, ParameterKind), - ParameterMoveUp(usize), - ParameterMoveDown(usize), - ParameterDelete(usize), - - StepCreate(AvailableInstruction), - StepChangeComment(usize, String), - StepChangeRunIf(usize, InstructionParameterSource), - StepParameterSourceChange(usize, String, InstructionParameterSource), - StepParameterValueChange(usize, String, String), - StepMoveUp(usize), - StepMoveDown(usize), - StepDelete(usize), - - OutputCreate, - OutputNameChange(usize, String), - OutputMoveUp(usize), - OutputMoveDown(usize), - OutputDelete(usize), - OutputSourceChange(usize, ParameterKind, InstructionParameterSource), -} - -#[derive(Clone, Debug)] -pub enum SaveActionThen { - DoNothing, - Close, -} - -#[derive(Clone, Debug)] -pub enum ActionEditorMessageOut { - RunAction(Action), -} - -pub enum SaveOrOpenActionError { - IoError(std::io::Error), - ParsingError(ron::error::SpannedError), - SerializingError(ron::Error), - ActionNotVersionCompatible, - MissingInstruction(String), -} - -impl fmt::Display for SaveOrOpenActionError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::IoError(e) => write!(f, "I/O Error: {e}"), - Self::ParsingError(e) => write!(f, "Parsing Error: {e}"), - Self::SerializingError(e) => write!(f, "Serializing error: {e}"), - Self::ActionNotVersionCompatible => write!( - f, - "This action is not compatible with this version of TestAngel." - ), - Self::MissingInstruction(instruction_id) => write!( - f, - "The action contains an instruction ({instruction_id}) which could not be loaded." - ), - } - } -} - -#[derive(Clone, Debug)] -pub struct AvailableInstruction { - /// The friendly name of this instruction. - engine_name: String, - /// The friendly name of this instruction. - friendly_name: String, - /// The instruction this is based on. - base_instruction: Instruction, -} - -impl fmt::Display for AvailableInstruction { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: {}", self.engine_name, self.friendly_name) - } -} - -#[derive(Clone, Debug)] -pub struct ComboInstructionParameterSource { - /// The friendly label of this source - friendly_label: String, - /// The actual source held by this entry - source: InstructionParameterSource, - /// The kind of parameters - kind: ParameterKind, -} - -impl fmt::Display for ComboInstructionParameterSource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.friendly_label) - } -} - -#[allow(clippy::from_over_into)] -impl Into for ComboInstructionParameterSource { - fn into(self) -> InstructionParameterSource { - self.source - } -} - -pub struct ActionEditor { - engines_list: Arc, - output_list: Vec<(isize, ParameterKind, InstructionParameterSource)>, - add_instruction_combo: combo_box::State, - - run_if_combo: Vec>, - parameter_source_combo: Vec>>, - output_combo: Vec>, - currently_open: Option, - current_path: Option, - needs_saving: bool, -} - -impl Default for ActionEditor { - fn default() -> Self { - Self { - engines_list: Arc::new(EngineList::default()), - output_list: vec![], - add_instruction_combo: combo_box::State::new(Vec::new()), - run_if_combo: vec![], - parameter_source_combo: vec![], - output_combo: vec![], - currently_open: None, - current_path: None, - needs_saving: false, - } - } -} - -impl ActionEditor { - /// Initialise a new ActionEditor with the provided [`ActionMap`]. - pub(crate) fn new(engines_list: Arc) -> Self { - let mut available_instructions = vec![]; - for engine in engines_list.inner() { - for instruction in &engine.instructions { - available_instructions.push(AvailableInstruction { - engine_name: engine.name.clone(), - friendly_name: instruction.friendly_name().clone(), - base_instruction: instruction.clone(), - }); - } - } - // Sort by engine name, then friendly name - available_instructions.sort_by(|a, b| match a.engine_name.cmp(&b.engine_name) { - std::cmp::Ordering::Equal => a.friendly_name.cmp(&b.friendly_name), - ord => ord, - }); - - Self { - engines_list, - add_instruction_combo: combo_box::State::new(available_instructions), - ..Default::default() - } - } - - /// Create a new action and open it - pub(crate) fn new_action(&mut self) { - self.currently_open = Some(Action::default()); - self.current_path = None; - self.needs_saving = true; - } - - /// Open an action - pub(crate) fn open_action(&mut self, file: PathBuf) -> Result<(), SaveOrOpenActionError> { - let data = &fs::read_to_string(&file).map_err(SaveOrOpenActionError::IoError)?; - - let versioned_file: VersionedFile = - ron::from_str(data).map_err(SaveOrOpenActionError::ParsingError)?; - if versioned_file.version() != 1 { - return Err(SaveOrOpenActionError::ActionNotVersionCompatible); - } - - let action: Action = ron::from_str(data).map_err(SaveOrOpenActionError::ParsingError)?; - for ic in &action.instructions { - if self - .engines_list - .get_instruction_by_id(&ic.instruction_id) - .is_none() - { - return Err(SaveOrOpenActionError::MissingInstruction( - ic.instruction_id.clone(), - )); - } - } - self.currently_open = Some(action); - self.current_path = Some(file); - self.needs_saving = false; - self.update_outputs(); - Ok(()) - } - - /// Offer to save if it is needed with default error handling - fn offer_to_save_default_error_handling( - &mut self, - then: SaveActionThen, - ) -> iced::Command { - if self.needs_saving { - iced::Command::perform( - rfd::AsyncMessageDialog::new() - .set_level(rfd::MessageLevel::Info) - .set_title("Do you want to save this action?") - .set_description("This action has been modified. Do you want to save it?") - .set_buttons(rfd::MessageButtons::YesNo) - .show(), - |wants_to_save| { - if wants_to_save == rfd::MessageDialogResult::Yes { - super::AppMessage::ActionEditor(ActionEditorMessage::SaveAction(then)) - } else { - super::AppMessage::ActionEditor(ActionEditorMessage::DoPostSaveActions( - then, - )) - } - }, - ) - } else { - self.do_then(then) - } - } - - fn write_to_disk(&mut self) -> Result<(), SaveOrOpenActionError> { - let save_path = self - .current_path - .as_ref() - .unwrap() - .with_extension("taaction"); - let data = ron::to_string(self.currently_open.as_ref().unwrap()) - .map_err(SaveOrOpenActionError::SerializingError)?; - fs::write(save_path, data).map_err(SaveOrOpenActionError::IoError)?; - self.needs_saving = false; - Ok(()) - } - - /// Save the currently opened action - fn save_action( - &mut self, - always_prompt_where: bool, - then: SaveActionThen, - ) -> iced::Command { - self.needs_saving = false; - - if always_prompt_where && self.current_path.is_some() { - // If this is a true 'save as' scenario, we need to generate a new UUID - self.currently_open.as_mut().unwrap().new_id(); - } - - if always_prompt_where || self.current_path.is_none() { - // Populate save path - return iced::Command::perform( - rfd::AsyncFileDialog::new() - .add_filter("TestAngel Actions", &["taaction"]) - .set_title("Save Action") - .set_directory(env::var("TA_ACTION_DIR").unwrap_or("./actions".to_owned())) - .save_file(), - |f| { - if let Some(file) = f { - return super::AppMessage::ActionEditor( - ActionEditorMessage::WriteFileToDisk(file.path().to_path_buf(), then), - ); - } - super::AppMessage::NoOp - }, - ); - } - - if let Err(e) = self.write_to_disk() { - return iced::Command::perform( - rfd::AsyncMessageDialog::new() - .set_title("Failed to save") - .set_description(format!("Failed to save file: {e}")) - .show(), - |_| super::AppMessage::NoOp, - ); - } - - self.do_then(then) - } - - fn do_then(&mut self, then: SaveActionThen) -> iced::Command { - match then { - SaveActionThen::DoNothing => iced::Command::none(), - SaveActionThen::Close => { - self.close_action(); - iced::Command::perform(async {}, |_| super::AppMessage::CloseEditor) - } - } - } - - /// Close the currently opened action - fn close_action(&mut self) { - self.currently_open = None; - self.current_path = None; - self.needs_saving = false; - } - - /// Hint that the open action has been modified. - fn modified(&mut self) { - self.needs_saving = true; - } - - /// Generate the UI for the action parameters - fn ui_parameters(&self) -> iced::Element<'_, ActionEditorMessage> { - let action = self.currently_open.as_ref().unwrap(); - - action - .parameters - .iter() - .enumerate() - .fold(Column::new().spacing(4), |col, (idx, (name, kind))| { - col.push( - row![ - Button::new("×").on_press(ActionEditorMessage::ParameterDelete(idx)), - Button::new("ÊŒ").on_press_maybe(if idx == 0 { - None - } else { - Some(ActionEditorMessage::ParameterMoveUp(idx)) - }), - Button::new("v").on_press_maybe(if (idx + 1) == action.parameters.len() { - None - } else { - Some(ActionEditorMessage::ParameterMoveDown(idx)) - }), - TextInput::new("Parameter Name", name) - .on_input(move |s| ActionEditorMessage::ParameterNameChange(idx, s)), - PickList::new( - &[ - ParameterKind::String, - ParameterKind::Integer, - ParameterKind::Decimal, - ParameterKind::Boolean, - ][..], - Some(*kind), - move |k| ActionEditorMessage::ParameterTypeChange(idx, k) - ) - .placeholder("Parameter Kind"), - ] - .spacing(4) - .align_items(iced::Alignment::Center), - ) - }) - .into() - } - - fn ui_instruction_inputs( - &self, - step_idx: usize, - instruction_config: InstructionConfiguration, - instruction: Instruction, - ) -> iced::Element<'_, ActionEditorMessage> { - instruction - .parameter_order() - .iter() - .enumerate() - .fold(Column::new().spacing(4), |col, (_param_idx, id)| { - let (name, kind) = &instruction.parameters()[id]; - let param_source = &instruction_config.parameter_sources[id]; - let param_value = &instruction_config.parameter_values[id]; - - let literal_input_id = id.clone(); - let literal_input: iced::Element<'_, _> = match param_source { - InstructionParameterSource::Literal => match kind { - ParameterKind::Boolean => { - Checkbox::new("Value", param_value.value_bool(), move |new_val| { - ActionEditorMessage::StepParameterValueChange( - step_idx, - literal_input_id.clone(), - if new_val { "yes" } else { "no" }.to_string(), - ) - }) - .into() - } - ParameterKind::Integer => TextInput::new( - "Literal number", - &if param_value.value_i32() == i32::MIN { - String::new() - } else { - param_value.to_string() - }, - ) - .on_input(move |new_val| { - if new_val.trim().is_empty() { - ActionEditorMessage::StepParameterValueChange( - step_idx, - literal_input_id.clone(), - i32::MIN.to_string(), - ) - } else { - ActionEditorMessage::StepParameterValueChange( - step_idx, - literal_input_id.clone(), - new_val, - ) - } - }) - .width(250) - .into(), - ParameterKind::Decimal => TextInput::new( - "Literal decimal", - &if param_value.value_f32() == f32::MIN { - String::new() - } else { - param_value.to_string() - }, - ) - .on_input(move |new_val| { - if new_val.trim().is_empty() { - ActionEditorMessage::StepParameterValueChange( - step_idx, - literal_input_id.clone(), - f32::MIN.to_string(), - ) - } else { - ActionEditorMessage::StepParameterValueChange( - step_idx, - literal_input_id.clone(), - new_val, - ) - } - }) - .width(250) - .into(), - _ => TextInput::new("Literal value", ¶m_value.to_string()) - .on_input(move |new_val| { - ActionEditorMessage::StepParameterValueChange( - step_idx, - literal_input_id.clone(), - new_val, - ) - }) - .width(250) - .into(), - }, - _ => Space::new(0, 0).into(), - }; - - let id = id.clone(); - col.push( - row![ - Text::new(format!("{name} ({kind}) use")), - ComboBox::new( - &self.parameter_source_combo[step_idx][&id.clone()], - "Source", - Some(&ComboInstructionParameterSource { - friendly_label: self.friendly_source_string(param_source), - kind: *kind, - source: param_source.clone(), - }), - move |src: ComboInstructionParameterSource| { - ActionEditorMessage::StepParameterSourceChange( - step_idx, - id.clone(), - src.into(), - ) - } - ), - literal_input, - ] - .spacing(4) - .align_items(iced::Alignment::Center), - ) - }) - .into() - } - - fn ui_steps(&self) -> iced::Element<'_, ActionEditorMessage> { - let action = self.currently_open.as_ref().unwrap(); - - action - .instructions - .iter() - .enumerate() - .fold( - Column::new().spacing(4), - |col, (idx, instruction_config)| { - let instruction = self - .engines_list - .get_instruction_by_id(&instruction_config.instruction_id) - .unwrap(); - - let mut outputs_text = String::new(); - for (name, kind) in instruction.outputs().values() { - outputs_text.push_str(&format!("{name}: {kind}")); - } - outputs_text = outputs_text.trim_end().to_string(); - col.push( - Container::new( - column![ - Text::new(format!( - "Step {}: {}", - idx + 1, - instruction.friendly_name() - )) - .size(20), - row![ - Button::new("×").on_press(ActionEditorMessage::StepDelete(idx)), - Button::new("ÊŒ").on_press_maybe(if idx == 0 { - None - } else { - Some(ActionEditorMessage::StepMoveUp(idx)) - }), - Button::new("v").on_press_maybe( - if (idx + 1) == action.instructions.len() { - None - } else { - Some(ActionEditorMessage::StepMoveDown(idx)) - } - ), - Text::new(instruction.description().clone()), - ] - .spacing(4), - TextInput::new("Comment", &instruction_config.comment).on_input( - move |new_comment| ActionEditorMessage::StepChangeComment( - idx, - new_comment - ) - ), - ComboBox::new( - &self.run_if_combo[idx], - "Run if...", - Some(&ComboInstructionParameterSource { - friendly_label: if instruction_config.run_if - == InstructionParameterSource::Literal - { - "Always run".to_string() - } else { - self.friendly_source_string(&instruction_config.run_if) - }, - kind: ParameterKind::Boolean, - source: instruction_config.run_if.clone(), - }), - move |src: ComboInstructionParameterSource| { - ActionEditorMessage::StepChangeRunIf(idx, src.into()) - } - ), - Space::with_height(4), - Text::new("Inputs").size(18), - self.ui_instruction_inputs( - idx, - instruction_config.clone(), - instruction.clone() - ), - Space::with_height(4), - Text::new("Outputs").size(18), - Text::new(outputs_text), - ] - .spacing(4), - ) - .padding(8) - .width(Length::Fill) - .style(theme::Container::Box), - ) - }, - ) - .into() - } - - fn ui_outputs(&self) -> iced::Element<'_, ActionEditorMessage> { - let action = self.currently_open.as_ref().unwrap(); - - action - .outputs - .iter() - .enumerate() - .fold( - Column::new().spacing(4), - |col, (idx, (name, kind, source))| { - col.push( - row![ - Button::new("×").on_press(ActionEditorMessage::OutputDelete(idx)), - Button::new("ÊŒ").on_press_maybe(if idx == 0 { - None - } else { - Some(ActionEditorMessage::OutputMoveUp(idx)) - }), - Button::new("v").on_press_maybe(if (idx + 1) == action.outputs.len() { - None - } else { - Some(ActionEditorMessage::OutputMoveDown(idx)) - }), - TextInput::new("Output Name", name) - .on_input(move |s| ActionEditorMessage::OutputNameChange(idx, s)), - Text::new(format!("({kind}) from")), - ComboBox::new( - &self.output_combo[idx], - "Output Source", - Some(&ComboInstructionParameterSource { - friendly_label: self.friendly_source_string(source), - kind: ParameterKind::String, // ! This doesn't matter as it isn't used for matching. - source: source.clone(), - }), - move |src: ComboInstructionParameterSource| { - ActionEditorMessage::OutputSourceChange( - idx, - src.kind, - src.into(), - ) - } - ) - ] - .spacing(4) - .align_items(iced::Alignment::Center), - ) - }, - ) - .into() - } - - /// Update the possible outputs - fn update_outputs(&mut self) { - self.output_list.clear(); - self.parameter_source_combo.clear(); - self.run_if_combo.clear(); - self.output_combo.clear(); - - if let Some(action) = &self.currently_open { - for (index, (_name, kind)) in action.parameters.iter().enumerate() { - self.output_list.push(( - -1, - *kind, - InstructionParameterSource::FromParameter(index), - )); - } - - for (step, instruction_config) in action.instructions.iter().enumerate() { - let instruction = self - .engines_list - .get_instruction_by_id(&instruction_config.instruction_id) - .unwrap(); - - // Build Run If source list - let run_if_options = self.output_list.iter().fold( - vec![ComboInstructionParameterSource { - friendly_label: "Always run".to_string(), - kind: ParameterKind::Boolean, - source: InstructionParameterSource::Literal, - }], - |mut list, (_step, kind, source)| { - if *kind == ParameterKind::Boolean { - list.push(ComboInstructionParameterSource { - friendly_label: self.friendly_source_string(source), - kind: *kind, - source: source.clone(), - }); - } - list - }, - ); - self.run_if_combo - .push(combo_box::State::new(run_if_options)); - - // Build parameter source list - let mut source_opts = HashMap::new(); - for (param_idx, (_, param_kind)) in instruction.parameters() { - let mut sources = vec![ComboInstructionParameterSource { - friendly_label: self - .friendly_source_string(&InstructionParameterSource::Literal), - kind: *param_kind, - source: InstructionParameterSource::Literal, - }]; - - for (_step, kind, source) in &self.output_list { - if kind == param_kind { - sources.push(ComboInstructionParameterSource { - friendly_label: self.friendly_source_string(source), - kind: *kind, - source: source.clone(), - }); - } - } - - source_opts.insert(param_idx.clone(), combo_box::State::new(sources)); - } - self.parameter_source_combo.push(source_opts); - - // Determine possible outputs from this step - for (id, (_name, kind)) in instruction.outputs() { - self.output_list.push(( - step as isize, - *kind, - InstructionParameterSource::FromOutput(step, id.clone()), - )); - } - } - - // Build output source list - let output_options: Vec<_> = self - .output_list - .iter() - .map(|(_step, kind, source)| ComboInstructionParameterSource { - friendly_label: self.friendly_source_string(source), - kind: *kind, - source: source.clone(), - }) - .collect(); - for _ in 0..action.outputs.len() { - self.output_combo - .push(combo_box::State::new(output_options.clone())); - } - } - } - - /// Convert an [`InstructionParameterSource`] to a friendly String by fetching names of parameters - /// and results from the currently opened action. - fn friendly_source_string(&self, source: &InstructionParameterSource) -> String { - if let Some(action) = &self.currently_open { - return match source { - InstructionParameterSource::Literal => "Literal value".to_owned(), - InstructionParameterSource::FromParameter(idx) => { - format!("Parameter {}", action.parameters[*idx].0) - } - InstructionParameterSource::FromOutput(step, id) => { - let ic = &action.instructions[*step]; - let instruction = self - .engines_list - .get_instruction_by_id(&ic.instruction_id) - .unwrap(); - format!("Step {} Output: {}", step + 1, instruction.outputs()[id].0) - } - }; - } - source.to_string() - } -} - -impl UiComponent for ActionEditor { - type Message = ActionEditorMessage; - type MessageOut = ActionEditorMessageOut; - - fn title(&self) -> Option<&str> { - Some("Action Editor") - } - - fn view(&self) -> iced::Element<'_, Self::Message> { - let action = self.currently_open.as_ref(); - if action.is_none() { - return Text::new("No action loaded.").into(); - } - let action = action.unwrap(); - - Scrollable::new( - Container::new( - column![ - // Toolbar - row![ - Button::new("Run Action").on_press(ActionEditorMessage::RunAction), - Button::new("Save") - .on_press(ActionEditorMessage::SaveAction(SaveActionThen::DoNothing)), - Button::new("Save as") - .on_press(ActionEditorMessage::SaveAsAction(SaveActionThen::DoNothing)), - Button::new("Close Action").on_press(ActionEditorMessage::CloseAction), - ] - .spacing(8), - // Metadata - TextInput::new("Action Name", &action.friendly_name) - .on_input(ActionEditorMessage::NameChanged), - TextInput::new("Action Group", &action.group) - .on_input(ActionEditorMessage::GroupChanged), - TextInput::new("Author", &action.author) - .on_input(ActionEditorMessage::AuthorChanged), - TextInput::new("Description", &action.description) - .on_input(ActionEditorMessage::DescriptionChanged), - Checkbox::new( - "Visible in Flow Editor", - action.visible, - ActionEditorMessage::VisibleChanged - ), - Rule::horizontal(2), - // Parameters - Text::new("Action Parameters"), - self.ui_parameters(), - Button::new("+ New parameter").on_press(ActionEditorMessage::ParameterCreate), - Rule::horizontal(2), - // Instructions - self.ui_steps(), - combo_box( - &self.add_instruction_combo, - "+ Add a step...", - None, - ActionEditorMessage::StepCreate - ), - Rule::horizontal(2), - // Outputs - Text::new("Action Outputs"), - self.ui_outputs(), - Button::new("+ New output").on_press(ActionEditorMessage::OutputCreate), - ] - .spacing(8), - ) - .padding(16), - ) - .into() - } - - fn update( - &mut self, - message: Self::Message, - ) -> ( - Option, - Option>, - ) { - match message { - ActionEditorMessage::WriteFileToDisk(path, then) => { - self.current_path = Some(path.with_extension("taaction")); - - if let Err(e) = self.write_to_disk() { - return ( - None, - Some(iced::Command::perform( - rfd::AsyncMessageDialog::new() - .set_title("Failed to save") - .set_description(format!("Failed to save file: {e}")) - .show(), - |_| super::AppMessage::NoOp, - )), - ); - } - - return (None, Some(self.do_then(then))); - } - ActionEditorMessage::DoPostSaveActions(then) => { - return (None, Some(self.do_then(then))); - } - ActionEditorMessage::SaveAction(then) => { - return (None, Some(self.save_action(false, then))); - } - ActionEditorMessage::SaveAsAction(then) => { - return (None, Some(self.save_action(true, then))); - } - ActionEditorMessage::CloseAction => { - return ( - None, - Some(self.offer_to_save_default_error_handling(SaveActionThen::Close)), - ); - } - - ActionEditorMessage::RunAction => { - return ( - Some(ActionEditorMessageOut::RunAction( - self.currently_open.clone().unwrap(), - )), - None, - ); - } - - ActionEditorMessage::NameChanged(new_name) => { - self.currently_open.as_mut().unwrap().friendly_name = new_name; - self.modified(); - } - ActionEditorMessage::GroupChanged(new_group) => { - self.currently_open.as_mut().unwrap().group = new_group; - self.modified(); - } - ActionEditorMessage::DescriptionChanged(new_description) => { - self.currently_open.as_mut().unwrap().description = new_description; - self.modified(); - } - ActionEditorMessage::AuthorChanged(new_author) => { - self.currently_open.as_mut().unwrap().author = new_author; - self.modified(); - } - ActionEditorMessage::VisibleChanged(now_visible) => { - self.currently_open.as_mut().unwrap().visible = now_visible; - self.modified(); - } - ActionEditorMessage::ParameterNameChange(idx, new_name) => { - let (_, kind) = &self.currently_open.as_mut().unwrap().parameters[idx]; - self.currently_open.as_mut().unwrap().parameters[idx] = (new_name, *kind); - self.modified(); - } - ActionEditorMessage::ParameterTypeChange(idx, new_type) => { - let action = self.currently_open.as_mut().unwrap(); - let (name, _) = &action.parameters[idx]; - action.parameters[idx] = (name.clone(), new_type); - - for instruction_config in action.instructions.iter_mut() { - // Update any run ifs to literals - if let InstructionParameterSource::FromParameter(p_idx) = - instruction_config.run_if - { - if idx == p_idx { - // Change to literal - instruction_config.run_if = InstructionParameterSource::Literal; - } - } - - // Updated any instruction parameters to literals - for (_, parameter_source) in instruction_config.parameter_sources.iter_mut() { - if let InstructionParameterSource::FromParameter(p_idx) = parameter_source { - if idx == *p_idx { - // Change to literal - *parameter_source = InstructionParameterSource::Literal; - } - } - } - } - - // Update any output kinds - for (_, kind, src) in action.outputs.iter_mut() { - if let InstructionParameterSource::FromParameter(p_idx) = src { - if idx == *p_idx { - // Change output type. - *kind = new_type; - } - } - } - - self.modified(); - } - ActionEditorMessage::ParameterCreate => { - self.currently_open - .as_mut() - .unwrap() - .parameters - .push((String::new(), ParameterKind::String)); - self.modified(); - } - ActionEditorMessage::ParameterMoveUp(idx) => { - let action = self.currently_open.as_mut().unwrap(); - let params = &mut action.parameters; - let val = params.remove(idx); - params.insert((idx - 1).max(0), val); - - // Swap idx and (idx - 1) - for instruction_config in action.instructions.iter_mut() { - if let InstructionParameterSource::FromParameter(p_idx) = - &mut instruction_config.run_if - { - if *p_idx == idx { - *p_idx = idx - 1; - } else if *p_idx == (idx - 1) { - *p_idx = idx; - } - } - - for (_, src) in instruction_config.parameter_sources.iter_mut() { - if let InstructionParameterSource::FromParameter(p_idx) = src { - if *p_idx == idx { - *p_idx = idx - 1; - } else if *p_idx == (idx - 1) { - *p_idx = idx; - } - } - } - } - for (_, _, src) in action.outputs.iter_mut() { - if let InstructionParameterSource::FromParameter(p_idx) = src { - if *p_idx == idx { - *p_idx = idx - 1; - } else if *p_idx == (idx - 1) { - *p_idx = idx; - } - } - } - - self.modified(); - } - ActionEditorMessage::ParameterMoveDown(idx) => { - let action = self.currently_open.as_mut().unwrap(); - let params = &mut action.parameters; - let val = params.remove(idx); - params.insert((idx + 1).min(params.len()), val); - - // Swap idx and (idx + 1) - for instruction_config in action.instructions.iter_mut() { - if let InstructionParameterSource::FromParameter(p_idx) = - &mut instruction_config.run_if - { - if *p_idx == idx { - *p_idx = idx + 1; - } else if *p_idx == (idx + 1) { - *p_idx = idx; - } - } - - for (_, src) in instruction_config.parameter_sources.iter_mut() { - if let InstructionParameterSource::FromParameter(p_idx) = src { - if *p_idx == idx { - *p_idx = idx + 1; - } else if *p_idx == (idx + 1) { - *p_idx = idx; - } - } - } - } - for (_, _, src) in action.outputs.iter_mut() { - if let InstructionParameterSource::FromParameter(p_idx) = src { - if *p_idx == idx { - *p_idx = idx + 1; - } else if *p_idx == (idx + 1) { - *p_idx = idx; - } - } - } - - self.modified(); - } - ActionEditorMessage::ParameterDelete(idx) => { - let action = self.currently_open.as_mut().unwrap(); - action.parameters.remove(idx); - - // Reset delete outputs referring to this parameter. - action.outputs.retain_mut(|(_, _, src)| match src { - InstructionParameterSource::FromParameter(p_idx) => *p_idx != idx, - _ => true, - }); - - // Renumber outputs referring to parameters afterwards - for (_, _, src) in action.outputs.iter_mut() { - if let InstructionParameterSource::FromParameter(p_idx) = src { - if *p_idx > idx { - *p_idx -= 1 - } - } - } - - // Reset instruction parameters that referred to idx to Literal - // Renumber all items after idx to (idx - 1). - for instruction_config in action.instructions.iter_mut() { - if let InstructionParameterSource::FromParameter(p_idx) = - &mut instruction_config.run_if - { - match (*p_idx).cmp(&idx) { - std::cmp::Ordering::Equal => { - instruction_config.run_if = InstructionParameterSource::Literal - } - std::cmp::Ordering::Greater => *p_idx -= 1, - _ => (), - } - } - - for src in instruction_config.parameter_sources.values_mut() { - if let InstructionParameterSource::FromParameter(p_idx) = src { - match (*p_idx).cmp(&idx) { - std::cmp::Ordering::Equal => { - *src = InstructionParameterSource::Literal - } - std::cmp::Ordering::Greater => *p_idx -= 1, - _ => (), - } - } - } - } - - self.modified(); - } - - ActionEditorMessage::StepCreate(instruction) => { - self.currently_open - .as_mut() - .unwrap() - .instructions - .push(InstructionConfiguration::from(instruction.base_instruction)); - self.modified(); - self.add_instruction_combo.unfocus(); - } - ActionEditorMessage::StepChangeComment(step, new_comment) => { - self.currently_open.as_mut().unwrap().instructions[step].comment = new_comment; - self.modified(); - } - ActionEditorMessage::StepChangeRunIf(step, new_run_if) => { - self.currently_open.as_mut().unwrap().instructions[step].run_if = new_run_if; - self.modified(); - } - ActionEditorMessage::StepParameterSourceChange(idx, id, new_source) => { - self.currently_open.as_mut().unwrap().instructions[idx] - .parameter_sources - .entry(id) - .and_modify(|v| { - *v = new_source; - }); - self.modified(); - } - ActionEditorMessage::StepParameterValueChange(idx, id, new_value) => { - self.currently_open.as_mut().unwrap().instructions[idx] - .parameter_values - .entry(id) - .and_modify(|v| match v { - ParameterValue::String(v) => *v = new_value, - ParameterValue::Integer(v) => *v = new_value.parse().unwrap_or(*v), - ParameterValue::Decimal(v) => *v = new_value.parse().unwrap_or(*v), - ParameterValue::Boolean(v) => *v = new_value.to_ascii_lowercase() == "yes", - }); - self.modified(); - } - ActionEditorMessage::StepMoveUp(idx) => { - let action = self.currently_open.as_mut().unwrap(); - let steps = &mut action.instructions; - let val = steps.remove(idx); - steps.insert((idx - 1).max(0), val); - - // Swap idx and (idx - 1) - for instruction_config in action.instructions.iter_mut() { - if let InstructionParameterSource::FromOutput(p_idx, _) = - &mut instruction_config.run_if - { - if *p_idx == idx { - *p_idx = idx - 1; - } else if *p_idx == (idx - 1) { - *p_idx = idx; - } - } - - for (_, src) in instruction_config.parameter_sources.iter_mut() { - if let InstructionParameterSource::FromOutput(p_idx, _) = src { - if *p_idx == idx { - *p_idx = idx - 1; - } else if *p_idx == (idx - 1) { - *p_idx = idx; - } - } - } - } - for (_, _, src) in action.outputs.iter_mut() { - if let InstructionParameterSource::FromOutput(p_idx, _) = src { - if *p_idx == idx { - *p_idx = idx - 1; - } else if *p_idx == (idx - 1) { - *p_idx = idx; - } - } - } - - self.modified(); - } - ActionEditorMessage::StepMoveDown(idx) => { - let action = self.currently_open.as_mut().unwrap(); - let steps = &mut action.instructions; - let val = steps.remove(idx); - steps.insert((idx + 1).min(steps.len()), val); - - // Swap idx and (idx + 1) - for instruction_config in action.instructions.iter_mut() { - if let InstructionParameterSource::FromOutput(p_idx, _) = - &mut instruction_config.run_if - { - if *p_idx == idx { - *p_idx = idx + 1; - } else if *p_idx == (idx + 1) { - *p_idx = idx; - } - } - - for (_, src) in instruction_config.parameter_sources.iter_mut() { - if let InstructionParameterSource::FromOutput(p_idx, _) = src { - if *p_idx == idx { - *p_idx = idx + 1; - } else if *p_idx == (idx + 1) { - *p_idx = idx; - } - } - } - } - for (_, _, src) in action.outputs.iter_mut() { - if let InstructionParameterSource::FromOutput(p_idx, _) = src { - if *p_idx == idx { - *p_idx = idx + 1; - } else if *p_idx == (idx + 1) { - *p_idx = idx; - } - } - } - - self.modified(); - } - ActionEditorMessage::StepDelete(idx) => { - let action = self.currently_open.as_mut().unwrap(); - action.instructions.remove(idx); - - // Reset delete outputs referring to this step. - action.outputs.retain_mut(|(_, _, src)| match src { - InstructionParameterSource::FromOutput(p_idx, _) => *p_idx != idx, - _ => true, - }); - - // Renumber outputs referring to steps afterwards - for (_, _, src) in action.outputs.iter_mut() { - if let InstructionParameterSource::FromOutput(p_idx, _) = src { - if *p_idx > idx { - *p_idx -= 1 - } - } - } - - for instruction_config in action.instructions.iter_mut() { - // Reset instruction parameters that referred to idx to Literal - if let InstructionParameterSource::FromOutput(p_idx, _) = - &mut instruction_config.run_if - { - match (*p_idx).cmp(&idx) { - std::cmp::Ordering::Equal => { - instruction_config.run_if = InstructionParameterSource::Literal - } - std::cmp::Ordering::Greater => *p_idx -= 1, - _ => (), - } - } - - // Renumber all items after idx to (idx - 1). - for src in instruction_config.parameter_sources.values_mut() { - if let InstructionParameterSource::FromOutput(p_idx, _) = src { - match (*p_idx).cmp(&idx) { - std::cmp::Ordering::Equal => { - log::debug!("Updated runif"); - *src = InstructionParameterSource::Literal - } - std::cmp::Ordering::Greater => *p_idx -= 1, - _ => (), - } - } - } - } - - self.modified(); - } - - ActionEditorMessage::OutputNameChange(idx, new_name) => { - let (_, kind, src) = &self.currently_open.as_mut().unwrap().outputs[idx]; - self.currently_open.as_mut().unwrap().outputs[idx] = (new_name, *kind, src.clone()); - self.modified(); - } - ActionEditorMessage::OutputSourceChange(idx, new_type, new_source) => { - let (name, _, _) = &self.currently_open.as_mut().unwrap().outputs[idx]; - self.currently_open.as_mut().unwrap().outputs[idx] = - (name.clone(), new_type, new_source); - self.modified(); - } - ActionEditorMessage::OutputCreate => { - if let Some((_, kind, default_output)) = self.output_list.get(0) { - self.currently_open.as_mut().unwrap().outputs.push(( - String::new(), - *kind, - default_output.clone(), - )); - self.modified(); - } else { - return ( - None, - Some(iced::Command::perform( - rfd::AsyncMessageDialog::new() - .set_level(rfd::MessageLevel::Warning) - .set_buttons(rfd::MessageButtons::Ok) - .set_title("No source") - .set_description( - "No source for output data. Add a parameter or a step.", - ) - .show(), - |_| super::AppMessage::NoOp, - )), - ); - } - } - ActionEditorMessage::OutputMoveUp(idx) => { - let outputs = &mut self.currently_open.as_mut().unwrap().outputs; - let val = outputs.remove(idx); - outputs.insert((idx - 1).max(0), val); - self.modified(); - } - ActionEditorMessage::OutputMoveDown(idx) => { - let outputs = &mut self.currently_open.as_mut().unwrap().outputs; - let val = outputs.remove(idx); - outputs.insert((idx + 1).min(outputs.len()), val); - self.modified(); - } - ActionEditorMessage::OutputDelete(idx) => { - self.currently_open.as_mut().unwrap().outputs.remove(idx); - self.modified(); - } - }; - self.update_outputs(); - (None, None) - } -} diff --git a/testangel/src/ui/action_running.rs b/testangel/src/ui/action_running.rs deleted file mode 100644 index cfce5fb..0000000 --- a/testangel/src/ui/action_running.rs +++ /dev/null @@ -1,294 +0,0 @@ -use std::{ - collections::HashMap, - path::PathBuf, - sync::Arc, - thread::{self, JoinHandle}, - time::Duration, -}; - -use iced::{ - widget::{column, row, Button, Checkbox, Column, Container, Rule, Space, Text, TextInput}, - Length, -}; -use testangel::{ - ipc::EngineList, - report_generation, - types::{Action, ActionConfiguration}, -}; -use testangel_ipc::prelude::{Evidence, EvidenceContent, ParameterKind, ParameterValue}; - -use super::UiComponent; - -#[derive(Clone, Debug)] -pub enum ActionRunningMessage { - Tick, - RunAction, - BackToEditor, - Save(Option, Vec), - ParameterValueChange(usize, String), -} - -#[derive(Clone, Debug)] -pub enum ActionRunningMessageOut { - BackToEditor, - SaveActionReport(Vec), -} - -#[derive(Default)] -pub struct ActionRunning { - engines_list: Arc, - action: Option, - parameters: HashMap, - - thread: Option>>>, - is_saving: bool, -} - -impl ActionRunning { - pub fn new(engines_list: Arc) -> Self { - Self { - engines_list, - action: None, - parameters: HashMap::new(), - thread: None, - is_saving: false, - } - } - - pub fn set_action(&mut self, action: Action) { - self.parameters.clear(); - for (idx, (_name, kind)) in action.parameters.iter().enumerate() { - self.parameters.insert(idx, kind.default_value()); - } - self.action = Some(action); - } - - fn start_flow(&mut self) { - self.is_saving = false; - let engines_list = self.engines_list.clone(); - let action = self.action.clone().unwrap(); - let parameters = self.parameters.clone(); - self.thread = Some(thread::spawn(move || { - let mut evidence = vec![]; - for engine in engines_list.inner() { - if engine.reset_state().is_err() { - evidence.push(Evidence { - label: String::from("WARNING: State Warning"), - content: EvidenceContent::Textual(String::from("For this test execution, the state couldn't be correctly reset. Some results may not be accurate.")) - }); - } - } - - let exec_result = - ActionConfiguration::execute_directly(engines_list.clone(), &action, parameters); - if let Err((step, e)) = exec_result { - rfd::MessageDialog::new() - .set_level(rfd::MessageLevel::Error) - .set_title("Failed to execute") - .set_description(format!( - "Failed to execute action at step {}: {e}", - step + 1 - )) - .show(); - return None; - } - let (outputs, ev) = exec_result.unwrap(); - evidence = [evidence, ev].concat(); - - // Add outputs as evidence - for (index, (name, kind, _src)) in action.outputs.iter().enumerate() { - let value = outputs[&index].clone(); - evidence.push(Evidence { - label: format!("Output '{name}' of kind '{kind}'"), - content: EvidenceContent::Textual(value.to_string()), - }); - } - - Some(evidence) - })); - } - - fn ui_action_inputs(&self, action: Action) -> iced::Element<'_, ActionRunningMessage> { - action - .parameters - .iter() - .enumerate() - .fold(Column::new().spacing(4), |col, (id, (name, kind))| { - let param_value = &self.parameters[&id]; - - let literal_input: iced::Element<'_, _> = match kind { - ParameterKind::Boolean => { - Checkbox::new("Value", param_value.value_bool(), move |new_val| { - ActionRunningMessage::ParameterValueChange( - id, - if new_val { "yes" } else { "no" }.to_string(), - ) - }) - .into() - } - ParameterKind::Integer => TextInput::new( - "Literal number", - &if param_value.value_i32() == i32::MIN { - String::new() - } else { - param_value.to_string() - }, - ) - .on_input(move |new_val| { - if new_val.trim().is_empty() { - ActionRunningMessage::ParameterValueChange(id, i32::MIN.to_string()) - } else { - ActionRunningMessage::ParameterValueChange(id, new_val) - } - }) - .width(250) - .into(), - ParameterKind::Decimal => TextInput::new( - "Literal decimal", - &if param_value.value_f32() == f32::MIN { - String::new() - } else { - param_value.to_string() - }, - ) - .on_input(move |new_val| { - if new_val.trim().is_empty() { - ActionRunningMessage::ParameterValueChange(id, f32::MIN.to_string()) - } else { - ActionRunningMessage::ParameterValueChange(id, new_val) - } - }) - .width(250) - .into(), - _ => TextInput::new("Literal value", ¶m_value.to_string()) - .on_input(move |new_val| { - ActionRunningMessage::ParameterValueChange(id, new_val) - }) - .width(250) - .into(), - }; - - col.push( - row![Text::new(format!("{name} ({kind}) use")), literal_input,] - .spacing(4) - .align_items(iced::Alignment::Center), - ) - }) - .into() - } -} - -impl UiComponent for ActionRunning { - type Message = ActionRunningMessage; - type MessageOut = ActionRunningMessageOut; - - fn title(&self) -> Option<&str> { - Some("Action Running") - } - - fn subscription(&self) -> iced::Subscription { - iced::time::every(Duration::from_millis(500)).map(|_| ActionRunningMessage::Tick) - } - - fn update( - &mut self, - message: Self::Message, - ) -> ( - Option, - Option>, - ) { - match message { - ActionRunningMessage::Tick => { - if let Some(thread) = &self.thread { - if thread.is_finished() { - self.is_saving = true; - if let Some(evidence) = self.thread.take().unwrap().join().unwrap() { - return ( - Some(ActionRunningMessageOut::SaveActionReport(evidence)), - None, - ); - } - return (Some(ActionRunningMessageOut::BackToEditor), None); - } - } - } - - ActionRunningMessage::ParameterValueChange(id, val) => { - self.parameters.insert( - id, - match self.parameters[&id].kind() { - ParameterKind::Boolean => ParameterValue::Boolean(val == "yes"), - ParameterKind::Integer => { - ParameterValue::Integer(val.parse().unwrap_or_default()) - } - ParameterKind::Decimal => { - ParameterValue::Decimal(val.parse().unwrap_or_default()) - } - ParameterKind::String => ParameterValue::String(val), - }, - ); - } - - ActionRunningMessage::RunAction => { - self.start_flow(); - } - - ActionRunningMessage::BackToEditor => { - return (Some(ActionRunningMessageOut::BackToEditor), None); - } - - ActionRunningMessage::Save(to, evidence) => { - if let Some(path) = to { - if let Err(e) = - report_generation::save_report(path.with_extension("pdf"), evidence) - { - return ( - None, - Some(iced::Command::perform( - rfd::AsyncMessageDialog::new() - .set_title("Failed") - .set_description(format!("Failed to generate report: {e}")) - .set_level(rfd::MessageLevel::Error) - .show(), - |_| super::AppMessage::NoOp, - )), - ); - } else if let Err(e) = opener::open(path.with_extension("pdf")) { - log::warn!("Failed to open evidence: {e}"); - } - } - return (Some(ActionRunningMessageOut::BackToEditor), None); - } - } - (None, None) - } - - fn view(&self) -> iced::Element<'_, Self::Message> { - if self.thread.is_none() { - Container::new( - column![ - Text::new("Run Action").size(24), - Text::new("This will execute an action alone and produce a report with the evidence and the outputs of the action."), - Rule::horizontal(1), - Text::new("Inputs").size(18), - self.ui_action_inputs(self.action.clone().unwrap()), - Space::with_height(4), - Button::new("Run Action").on_press(ActionRunningMessage::RunAction), - Button::new("Back to Editor").on_press(ActionRunningMessage::BackToEditor), - ] - .spacing(4), - ) - .padding(16) - .width(Length::Fill) - .into() - } else { - Container::new(Text::new(if self.is_saving { - "Saving report..." - } else { - "Action running..." - })) - .padding(32) - .into() - } - } -} diff --git a/testangel/src/next_ui/actions/header.rs b/testangel/src/ui/actions/header.rs similarity index 89% rename from testangel/src/next_ui/actions/header.rs rename to testangel/src/ui/actions/header.rs index ea47919..df21293 100644 --- a/testangel/src/next_ui/actions/header.rs +++ b/testangel/src/ui/actions/header.rs @@ -5,11 +5,11 @@ use relm4::{ actions::{AccelsPlus, RelmAction, RelmActionGroup}, adw, factory::FactoryVecDeque, - gtk, Component, ComponentController, ComponentParts, ComponentSender, RelmWidgetExt, + gtk, Component, ComponentParts, ComponentSender, RelmWidgetExt, }; use testangel::{action_loader::ActionMap, ipc::EngineList}; -use crate::next_ui::{ +use crate::ui::{ components::add_step_factory::{AddStepInit, AddStepResult, AddStepTrait}, lang, }; @@ -35,7 +35,6 @@ pub enum ActionsHeaderOutput { #[derive(Debug)] pub enum ActionsHeaderInput { - OpenAboutDialog, ActionsMapChanged(Arc), /// Add the step with the instruction ID given AddStep(String), @@ -131,7 +130,7 @@ impl Component for ActionsHeader { &lang::lookup("action-header-save-as") => ActionsSaveAsAction, &lang::lookup("action-header-close") => ActionsCloseAction, section! { - &lang::lookup("action-header-about") => ActionsAboutAction, + &lang::lookup("header-about") => crate::ui::header_bar::GeneralAboutAction, } } } @@ -193,20 +192,12 @@ impl Component for ActionsHeader { relm4::main_application() .set_accelerators_for_action::(&["W"]); - let sender_c = sender.clone(); - let about_action: RelmAction = RelmAction::new_stateless(move |_| { - sender_c.input(ActionsHeaderInput::OpenAboutDialog); - }); - relm4::main_application() - .set_accelerators_for_action::(&["A"]); - let mut group = RelmActionGroup::::new(); group.add_action(new_action); group.add_action(open_action); group.add_action(save_action); group.add_action(save_as_action); group.add_action(close_action); - group.add_action(about_action); group.register_for_widget(&widgets.end); ComponentParts { model, widgets } @@ -217,7 +208,7 @@ impl Component for ActionsHeader { widgets: &mut Self::Widgets, message: Self::Input, sender: ComponentSender, - root: &Self::Root, + _root: &Self::Root, ) { match message { ActionsHeaderInput::ChangeActionOpen(now) => { @@ -226,13 +217,6 @@ impl Component for ActionsHeader { ActionsHeaderInput::ActionsMapChanged(new_map) => { self.action_map = new_map; } - ActionsHeaderInput::OpenAboutDialog => { - crate::next_ui::about::AppAbout::builder() - .transient_for(root) - .launch((self.engine_list.clone(), self.action_map.clone())) - .widget() - .set_visible(true); - } ActionsHeaderInput::AddStep(step_id) => { // close popover self.add_button.popdown(); @@ -315,4 +299,3 @@ relm4::new_stateless_action!(ActionsOpenAction, ActionsActionGroup, "open"); relm4::new_stateless_action!(ActionsSaveAction, ActionsActionGroup, "save"); relm4::new_stateless_action!(ActionsSaveAsAction, ActionsActionGroup, "save-as"); relm4::new_stateless_action!(ActionsCloseAction, ActionsActionGroup, "close"); -relm4::new_stateless_action!(ActionsAboutAction, ActionsActionGroup, "about"); diff --git a/testangel/src/next_ui/actions/instruction_component.rs b/testangel/src/ui/actions/instruction_component.rs similarity index 97% rename from testangel/src/next_ui/actions/instruction_component.rs rename to testangel/src/ui/actions/instruction_component.rs index f672ee8..df0592f 100644 --- a/testangel/src/next_ui/actions/instruction_component.rs +++ b/testangel/src/ui/actions/instruction_component.rs @@ -11,7 +11,7 @@ use relm4::{ use testangel::types::{InstructionConfiguration, InstructionParameterSource}; use testangel_ipc::prelude::{Instruction, ParameterKind, ParameterValue}; -use crate::next_ui::{ +use crate::ui::{ components::variable_row::{ ParameterSourceTrait, VariableRow, VariableRowInit, VariableRowParentInput, }, diff --git a/testangel/src/next_ui/actions/metadata_component.rs b/testangel/src/ui/actions/metadata_component.rs similarity index 96% rename from testangel/src/next_ui/actions/metadata_component.rs rename to testangel/src/ui/actions/metadata_component.rs index b56d498..f3b9600 100644 --- a/testangel/src/next_ui/actions/metadata_component.rs +++ b/testangel/src/ui/actions/metadata_component.rs @@ -1,129 +1,129 @@ -use adw::prelude::*; -use relm4::{adw, gtk, Component}; -use testangel::types::Action; - -use crate::next_ui::lang; - -#[derive(Debug)] -pub enum MetadataInput { - /// Inform the metadata component that the action has changed and as such - /// it should reload the metadata values - ChangeAction(Action), -} - -#[derive(Clone, Debug, Default)] -pub struct MetadataOutput { - pub new_name: Option, - pub new_group: Option, - pub new_author: Option, - pub new_description: Option, - pub new_visible: Option, -} - -#[derive(Debug)] -pub struct Metadata; - -#[relm4::component(pub)] -impl Component for Metadata { - type Init = (); - type Input = MetadataInput; - type Output = MetadataOutput; - type CommandOutput = (); - - view! { - adw::PreferencesGroup { - set_title: &lang::lookup("action-metadata-label"), - - #[name = "name"] - adw::EntryRow { - set_title: &lang::lookup("action-metadata-name"), - - connect_changed[sender] => move |entry| { - let _ = sender.output(MetadataOutput { - new_name: Some(entry.text().to_string()), - ..Default::default() - }); - }, - }, - #[name = "group"] - adw::EntryRow { - set_title: &lang::lookup("action-metadata-group"), - - connect_changed[sender] => move |entry| { - let _ = sender.output(MetadataOutput { - new_group: Some(entry.text().to_string()), - ..Default::default() - }); - }, - }, - #[name = "author"] - adw::EntryRow { - set_title: &lang::lookup("action-metadata-author"), - - connect_changed[sender] => move |entry| { - let _ = sender.output(MetadataOutput { - new_author: Some(entry.text().to_string()), - ..Default::default() - }); - }, - }, - #[name = "description"] - adw::EntryRow { - set_title: &lang::lookup("action-metadata-description"), - - connect_changed[sender] => move |entry| { - let _ = sender.output(MetadataOutput { - new_description: Some(entry.text().to_string()), - ..Default::default() - }); - }, - }, - adw::ActionRow { - set_title: &lang::lookup("action-metadata-visible"), - - #[name = "visible"] - add_suffix = >k::Switch { - connect_state_set[sender] => move |_switch, state| { - let _ = sender.output(MetadataOutput { - new_visible: Some(state), - ..Default::default() - }); - gtk::Inhibit(false) - }, - }, - - } - }, - } - - fn init( - _init: Self::Init, - root: &Self::Root, - sender: relm4::ComponentSender, - ) -> relm4::ComponentParts { - let model = Metadata; - let widgets = view_output!(); - - relm4::ComponentParts { model, widgets } - } - - fn update_with_view( - &mut self, - widgets: &mut Self::Widgets, - message: Self::Input, - sender: relm4::ComponentSender, - _root: &Self::Root, - ) { - match message { - MetadataInput::ChangeAction(action) => { - widgets.name.set_text(&action.friendly_name); - widgets.group.set_text(&action.group); - widgets.author.set_text(&action.author); - widgets.description.set_text(&action.description); - widgets.visible.set_active(action.visible); - } - } - - self.update_view(widgets, sender) - } -} +use adw::prelude::*; +use relm4::{adw, gtk, Component}; +use testangel::types::Action; + +use crate::ui::lang; + +#[derive(Debug)] +pub enum MetadataInput { + /// Inform the metadata component that the action has changed and as such + /// it should reload the metadata values + ChangeAction(Action), +} + +#[derive(Clone, Debug, Default)] +pub struct MetadataOutput { + pub new_name: Option, + pub new_group: Option, + pub new_author: Option, + pub new_description: Option, + pub new_visible: Option, +} + +#[derive(Debug)] +pub struct Metadata; + +#[relm4::component(pub)] +impl Component for Metadata { + type Init = (); + type Input = MetadataInput; + type Output = MetadataOutput; + type CommandOutput = (); + + view! { + adw::PreferencesGroup { + set_title: &lang::lookup("action-metadata-label"), + + #[name = "name"] + adw::EntryRow { + set_title: &lang::lookup("action-metadata-name"), + + connect_changed[sender] => move |entry| { + let _ = sender.output(MetadataOutput { + new_name: Some(entry.text().to_string()), + ..Default::default() + }); + }, + }, + #[name = "group"] + adw::EntryRow { + set_title: &lang::lookup("action-metadata-group"), + + connect_changed[sender] => move |entry| { + let _ = sender.output(MetadataOutput { + new_group: Some(entry.text().to_string()), + ..Default::default() + }); + }, + }, + #[name = "author"] + adw::EntryRow { + set_title: &lang::lookup("action-metadata-author"), + + connect_changed[sender] => move |entry| { + let _ = sender.output(MetadataOutput { + new_author: Some(entry.text().to_string()), + ..Default::default() + }); + }, + }, + #[name = "description"] + adw::EntryRow { + set_title: &lang::lookup("action-metadata-description"), + + connect_changed[sender] => move |entry| { + let _ = sender.output(MetadataOutput { + new_description: Some(entry.text().to_string()), + ..Default::default() + }); + }, + }, + adw::ActionRow { + set_title: &lang::lookup("action-metadata-visible"), + + #[name = "visible"] + add_suffix = >k::Switch { + connect_state_set[sender] => move |_switch, state| { + let _ = sender.output(MetadataOutput { + new_visible: Some(state), + ..Default::default() + }); + gtk::Inhibit(false) + }, + }, + + } + }, + } + + fn init( + _init: Self::Init, + root: &Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + let model = Metadata; + let widgets = view_output!(); + + relm4::ComponentParts { model, widgets } + } + + fn update_with_view( + &mut self, + widgets: &mut Self::Widgets, + message: Self::Input, + sender: relm4::ComponentSender, + _root: &Self::Root, + ) { + match message { + MetadataInput::ChangeAction(action) => { + widgets.name.set_text(&action.friendly_name); + widgets.group.set_text(&action.group); + widgets.author.set_text(&action.author); + widgets.description.set_text(&action.description); + widgets.visible.set_active(action.visible); + } + } + + self.update_view(widgets, sender) + } +} diff --git a/testangel/src/next_ui/actions/mod.rs b/testangel/src/ui/actions/mod.rs similarity index 95% rename from testangel/src/next_ui/actions/mod.rs rename to testangel/src/ui/actions/mod.rs index 90b980f..8e00839 100644 --- a/testangel/src/next_ui/actions/mod.rs +++ b/testangel/src/ui/actions/mod.rs @@ -1,849 +1,865 @@ -use std::{cmp::Ordering, collections::HashMap, fs, path::PathBuf, rc::Rc, sync::Arc}; - -use adw::prelude::*; -use relm4::{ - adw, factory::FactoryVecDeque, gtk, prelude::DynamicIndex, Component, ComponentController, - ComponentParts, ComponentSender, Controller, RelmWidgetExt, -}; -use testangel::{ - action_loader::ActionMap, - ipc::EngineList, - types::{Action, InstructionConfiguration, InstructionParameterSource, VersionedFile}, -}; -use testangel_ipc::prelude::ParameterKind; - -use super::{file_filters, lang}; - -pub mod header; -mod instruction_component; -mod metadata_component; -mod outputs; -mod params; - -pub enum SaveOrOpenActionError { - IoError(std::io::Error), - ParsingError(ron::error::SpannedError), - SerializingError(ron::Error), - ActionNotVersionCompatible, - MissingInstruction(usize, String), -} - -impl ToString for SaveOrOpenActionError { - fn to_string(&self) -> String { - match self { - Self::IoError(e) => lang::lookup_with_args("action-save-open-error-io-error", { - let mut map = HashMap::new(); - map.insert("error", e.to_string().into()); - map - }), - Self::ParsingError(e) => { - lang::lookup_with_args("action-save-open-error-parsing-error", { - let mut map = HashMap::new(); - map.insert("error", e.to_string().into()); - map - }) - } - Self::SerializingError(e) => { - lang::lookup_with_args("action-save-open-error-serializing-error", { - let mut map = HashMap::new(); - map.insert("error", e.to_string().into()); - map - }) - } - Self::ActionNotVersionCompatible => { - lang::lookup("action-save-open-error-action-not-version-compatible") - } - Self::MissingInstruction(step, e) => { - lang::lookup_with_args("action-save-open-error-missing-instruction", { - let mut map = HashMap::new(); - map.insert("step", (step + 1).into()); - map.insert("error", e.to_string().into()); - map - }) - } - } - } -} - -#[derive(Clone, Debug)] -pub enum ActionInputs { - /// Do nothing - NoOp, - /// The map of actions has changed and should be updated - ActionsMapChanged(Arc), - /// Create a new action - NewAction, - /// Actually create the new action - _NewAction, - /// Prompt the user to open an action. This will ask to save first if needed. - OpenAction, - /// Actually show the user the open file dialog - _OpenAction, - /// Actually open an action after the user has finished selecting - __OpenAction(PathBuf), - /// Save the action, prompting if needed to set file path - SaveAction, - /// Save the action as a new file, always prompting for a file path - SaveAsAction, - /// Ask where to save if needed, then save - _SaveActionThen(Box), - /// Actually write the action to disk, then emit then input - __SaveActionThen(PathBuf, Box), - /// Close the action, prompting if needing to save first - CloseAction, - /// Actually close the action - _CloseAction, - /// Add the step with the ID provided - AddStep(String), - /// Update the UI steps from the open action. This will clear first and overwrite any changes! - UpdateStepsFromModel, - /// Remove the step with the provided index, resetting all references to it. - RemoveStep(DynamicIndex), - /// Remove the step with the provided index, but change references to it to a temporary value (`usize::MAX`) - /// that can be set again with [`ActionInputs::PasteStep`]. - /// This doesn't refresh the UI until Paste is called. - CutStep(DynamicIndex), - /// Insert a step at the specified index and set references back to the correct step. - /// This refreshes the UI. - PasteStep(usize, InstructionConfiguration), - /// Move a step from the index to a position offset (param 3) from a new index (param 2). - MoveStep(DynamicIndex, DynamicIndex, isize), - /// The [`InstructionConfiguration`] has changed for the step indicated by the [`DynamicIndex`]. - /// This does not refresh the UI. - ConfigUpdate(DynamicIndex, InstructionConfiguration), - /// The metadata has been updated and the action should be updated to reflect that - MetadataUpdated(metadata_component::MetadataOutput), - /// Set parameters - SetParameters(Vec<(String, ParameterKind)>), - /// Remove references to the provided index, or reduce any higher than. - ParamIndexRemoved(usize), - /// Swap references to the indexes provided - ParamIndexesSwapped(usize, usize), - /// Change the run condition of a step - ChangeRunCondition(DynamicIndex, InstructionParameterSource), - /// Set the outputs of an action - SetOutputs(Vec<(String, ParameterKind, InstructionParameterSource)>), - /// Set the commend on a step - SetComment(DynamicIndex, String), -} -#[derive(Clone, Debug)] -pub enum ActionOutputs { - /// Inform other parts that actions may have changed, reload them! - ReloadActions, -} - -#[derive(Debug)] -pub struct ActionsModel { - action_map: Arc, - engine_list: Arc, - - open_action: Option, - open_path: Option, - needs_saving: bool, - header: Rc>, - metadata: Controller, - parameters: Controller, - outputs: Controller, - live_instructions_list: FactoryVecDeque, -} - -impl ActionsModel { - /// Get an [`Rc`] clone of the header controller - pub fn header_controller_rc(&self) -> Rc> { - self.header.clone() - } - - /// Create the absolute barebones of a message dialog, allowing for custom button and response mapping. - fn create_message_dialog_skeleton(&self, title: S, message: S) -> adw::MessageDialog - where - S: AsRef, - { - adw::MessageDialog::builder() - .transient_for(&self.header.widget().toplevel_window().expect( - "ActionsModel::create_message_dialog cannot be called until the header is attached", - )) - .title(title.as_ref()) - .heading(title.as_ref()) - .body(message.as_ref()) - .modal(true) - .build() - } - - /// Create a message dialog attached to the toplevel window. This includes default implementations of an 'OK' button. - fn create_message_dialog(&self, title: S, message: S) -> adw::MessageDialog - where - S: AsRef, - { - let dialog = self.create_message_dialog_skeleton(title, message); - dialog.add_response("ok", &lang::lookup("ok")); - dialog.set_default_response(Some("ok")); - dialog.set_close_response("ok"); - dialog - } - - /// Just open a brand new action - fn new_action(&mut self) { - self.open_path = None; - self.needs_saving = true; - self.open_action = Some(Action::default()); - self.header - .emit(header::ActionsHeaderInput::ChangeActionOpen( - self.open_action.is_some(), - )); - self.metadata - .emit(metadata_component::MetadataInput::ChangeAction( - Action::default(), - )); - self.parameters - .emit(params::ActionParamsInput::ChangeAction(Action::default())); - self.outputs - .emit(outputs::ActionOutputsInput::ChangeAction(Action::default())); - } - - /// Open an action. This does not ask to save first. - fn open_action(&mut self, file: PathBuf) -> Result<(), SaveOrOpenActionError> { - let data = &fs::read_to_string(&file).map_err(SaveOrOpenActionError::IoError)?; - - let versioned_file: VersionedFile = - ron::from_str(data).map_err(SaveOrOpenActionError::ParsingError)?; - if versioned_file.version() != 1 { - return Err(SaveOrOpenActionError::ActionNotVersionCompatible); - } - - let mut action: Action = - ron::from_str(data).map_err(SaveOrOpenActionError::ParsingError)?; - if action.version() != 1 { - return Err(SaveOrOpenActionError::ActionNotVersionCompatible); - } - for (step, ic) in action.instructions.iter_mut().enumerate() { - if self - .engine_list - .get_engine_by_instruction_id(&ic.instruction_id) - .is_none() - { - return Err(SaveOrOpenActionError::MissingInstruction( - step, - ic.instruction_id.clone(), - )); - } - } - - self.open_action = Some(action.clone()); - self.header - .emit(header::ActionsHeaderInput::ChangeActionOpen( - self.open_action.is_some(), - )); - self.metadata - .emit(metadata_component::MetadataInput::ChangeAction( - action.clone(), - )); - self.parameters - .emit(params::ActionParamsInput::ChangeAction(action.clone())); - self.outputs - .emit(outputs::ActionOutputsInput::ChangeAction(action)); - self.open_path = Some(file); - self.needs_saving = false; - log::debug!("New action open."); - log::debug!("Action: {:?}", self.open_action); - Ok(()) - } - - /// Ask the user if they want to save this file. If they response yes, this will also trigger the save function. - /// This function will only ask the user if needed, otherwise it will emit immediately. - fn prompt_to_save(&self, sender: &relm4::Sender, then: ActionInputs) { - if self.needs_saving { - let question = self.create_message_dialog_skeleton( - lang::lookup("action-save-before"), - lang::lookup("action-save-before-message"), - ); - question.add_response("discard", &lang::lookup("discard")); - question.add_response("save", &lang::lookup("save")); - question.set_response_appearance("discard", adw::ResponseAppearance::Destructive); - question.set_default_response(Some("save")); - question.set_close_response("discard"); - let sender_c = sender.clone(); - let then_c = then.clone(); - question.connect_response(Some("save"), move |_, _| { - sender_c.emit(ActionInputs::_SaveActionThen(Box::new(then_c.clone()))); - }); - let sender_c = sender.clone(); - question.connect_response(Some("discard"), move |_, _| { - sender_c.emit(then.clone()); - }); - question.set_visible(true); - } else { - sender.emit(then); - } - } - - /// Ask the user where to save the flow, or just save if that's good enough - fn ask_where_to_save( - &mut self, - sender: &relm4::Sender, - transient_for: &impl IsA, - always_ask_where: bool, - then: ActionInputs, - ) { - if always_ask_where || self.open_path.is_none() { - // Ask where - let dialog = gtk::FileDialog::builder() - .modal(true) - .title(lang::lookup("action-header-save")) - .initial_folder(>k::gio::File::for_path( - std::env::var("TA_ACTION_DIR").unwrap_or("./actions".to_string()), - )) - .filters(&file_filters::filter_list(vec![ - file_filters::actions(), - file_filters::all(), - ])) - .build(); - - let sender_c = sender.clone(); - dialog.save( - Some(transient_for), - Some(&relm4::gtk::gio::Cancellable::new()), - move |res| { - if let Ok(file) = res { - let path = file.path().unwrap(); - sender_c.emit(ActionInputs::__SaveActionThen(path, Box::new(then.clone()))); - } - }, - ); - } else { - sender.emit(ActionInputs::__SaveActionThen( - self.open_path.clone().unwrap(), - Box::new(then), - )); - } - } - - /// Just save the action to disk with the current `open_path` as the destination - fn save_action(&mut self) -> Result<(), SaveOrOpenActionError> { - let save_path = self.open_path.as_ref().unwrap(); - let data = ron::to_string(self.open_action.as_ref().unwrap()) - .map_err(SaveOrOpenActionError::SerializingError)?; - fs::write(save_path, data).map_err(SaveOrOpenActionError::IoError)?; - self.needs_saving = false; - Ok(()) - } - - /// Close this action without checking first - fn close_action(&mut self) { - self.open_action = None; - self.open_path = None; - self.needs_saving = false; - self.live_instructions_list.guard().clear(); - self.header - .emit(header::ActionsHeaderInput::ChangeActionOpen( - self.open_action.is_some(), - )); - } -} - -#[relm4::component(pub)] -impl Component for ActionsModel { - type Init = (Arc, Arc); - type Input = ActionInputs; - type Output = ActionOutputs; - type CommandOutput = (); - - view! { - #[root] - toast_target = adw::ToastOverlay { - gtk::ScrolledWindow { - set_vexpand: true, - set_hscrollbar_policy: gtk::PolicyType::Never, - - if model.open_action.is_none() { - adw::StatusPage { - set_title: &lang::lookup("nothing-open"), - set_description: Some(&lang::lookup("action-nothing-open-description")), - set_icon_name: Some(relm4_icons::icon_name::LIGHTBULB), - set_vexpand: true, - } - } else { - gtk::Box { - set_orientation: gtk::Orientation::Vertical, - set_margin_all: 10, - set_spacing: 10, - - model.metadata.widget(), - - gtk::Separator { - set_orientation: gtk::Orientation::Horizontal, - }, - - model.parameters.widget(), - - gtk::Separator { - set_orientation: gtk::Orientation::Horizontal, - }, - - #[local_ref] - live_instructions_list -> gtk::Box { - set_orientation: gtk::Orientation::Vertical, - set_spacing: 5, - }, - - gtk::Separator { - set_orientation: gtk::Orientation::Horizontal, - }, - - model.outputs.widget(), - } - } - }, - }, - } - - fn init( - init: Self::Init, - root: &Self::Root, - sender: ComponentSender, - ) -> ComponentParts { - let header = Rc::new( - header::ActionsHeader::builder() - .launch((init.1.clone(), init.0.clone())) - .forward(sender.input_sender(), |msg| match msg { - header::ActionsHeaderOutput::NewAction => ActionInputs::NewAction, - header::ActionsHeaderOutput::OpenAction => ActionInputs::OpenAction, - header::ActionsHeaderOutput::SaveAction => ActionInputs::SaveAction, - header::ActionsHeaderOutput::SaveAsAction => ActionInputs::SaveAsAction, - header::ActionsHeaderOutput::CloseAction => ActionInputs::CloseAction, - header::ActionsHeaderOutput::AddStep(step) => ActionInputs::AddStep(step), - }), - ); - - let model = ActionsModel { - action_map: init.0, - engine_list: init.1, - open_action: None, - open_path: None, - needs_saving: false, - header, - live_instructions_list: FactoryVecDeque::new( - gtk::Box::default(), - sender.input_sender(), - ), - metadata: metadata_component::Metadata::builder() - .launch(()) - .forward(sender.input_sender(), |msg| { - ActionInputs::MetadataUpdated(msg) - }), - parameters: params::ActionParams::builder().launch(()).forward( - sender.input_sender(), - |msg| match msg { - params::ActionParamsOutput::IndexRemoved(idx) => { - ActionInputs::ParamIndexRemoved(idx) - } - params::ActionParamsOutput::IndexesSwapped(a, b) => { - ActionInputs::ParamIndexesSwapped(a, b) - } - params::ActionParamsOutput::SetParameters(new_params) => { - ActionInputs::SetParameters(new_params) - } - }, - ), - outputs: outputs::ActionOutputs::builder().launch(()).forward( - sender.input_sender(), - |msg| match msg { - outputs::ActionOutputsOutput::IndexRemoved(idx) => { - ActionInputs::ParamIndexRemoved(idx) - } - outputs::ActionOutputsOutput::IndexesSwapped(a, b) => { - ActionInputs::ParamIndexesSwapped(a, b) - } - outputs::ActionOutputsOutput::SetOutputs(new_outputs) => { - ActionInputs::SetOutputs(new_outputs) - } - }, - ), - }; - - // Trigger update actions from model - sender.input(ActionInputs::UpdateStepsFromModel); - - let live_instructions_list = model.live_instructions_list.widget(); - let widgets = view_output!(); - - ComponentParts { model, widgets } - } - - fn update_with_view( - &mut self, - widgets: &mut Self::Widgets, - message: Self::Input, - sender: ComponentSender, - root: &Self::Root, - ) { - match message { - ActionInputs::NoOp => (), - - ActionInputs::MetadataUpdated(meta) => { - if let Some(action) = self.open_action.as_mut() { - if let Some(new_name) = meta.new_name { - action.friendly_name = new_name; - } - if let Some(new_group) = meta.new_group { - action.group = new_group; - } - if let Some(new_author) = meta.new_author { - action.author = new_author; - } - if let Some(new_description) = meta.new_description { - action.description = new_description; - } - if let Some(new_visible) = meta.new_visible { - action.visible = new_visible; - } - } - } - - ActionInputs::SetComment(step, new_comment) => { - if let Some(action) = self.open_action.as_mut() { - action.instructions[step.current_index()].comment = new_comment; - } - } - - ActionInputs::ChangeRunCondition(step, new_condition) => { - if let Some(action) = self.open_action.as_mut() { - action.instructions[step.current_index()].run_if = new_condition; - } - } - - ActionInputs::SetParameters(new_params) => { - if let Some(action) = self.open_action.as_mut() { - action.parameters = new_params; - sender.input(ActionInputs::UpdateStepsFromModel); - } - } - ActionInputs::SetOutputs(new_outputs) => { - if let Some(action) = self.open_action.as_mut() { - action.outputs = new_outputs; - sender.input(ActionInputs::UpdateStepsFromModel); - } - } - ActionInputs::ParamIndexRemoved(idx) => { - if let Some(action) = self.open_action.as_mut() { - for ic in action.instructions.iter_mut() { - for (_, src) in ic.parameter_sources.iter_mut() { - if let InstructionParameterSource::FromParameter(n) = src { - match idx.cmp(n) { - Ordering::Equal => *src = InstructionParameterSource::Literal, - Ordering::Less => *n -= 1, - _ => (), - } - } - } - } - sender.input(ActionInputs::UpdateStepsFromModel); - } - } - ActionInputs::ParamIndexesSwapped(a, b) => { - if let Some(action) = self.open_action.as_mut() { - for ic in action.instructions.iter_mut() { - for (_, src) in ic.parameter_sources.iter_mut() { - if let InstructionParameterSource::FromParameter(n) = src { - if *n == a { - *n = b; - } else if *n == b { - *n = a; - } - } - } - } - sender.input(ActionInputs::UpdateStepsFromModel); - } - } - - ActionInputs::ActionsMapChanged(new_map) => { - self.action_map = new_map.clone(); - self.header - .emit(header::ActionsHeaderInput::ActionsMapChanged(new_map)); - } - ActionInputs::ConfigUpdate(step, new_config) => { - // unwrap rationale: config updates can't happen if nothing is open - let action = self.open_action.as_mut().unwrap(); - action.instructions[step.current_index()] = new_config; - } - ActionInputs::NewAction => { - self.prompt_to_save(sender.input_sender(), ActionInputs::_NewAction); - } - ActionInputs::_NewAction => { - self.new_action(); - } - ActionInputs::OpenAction => { - self.prompt_to_save(sender.input_sender(), ActionInputs::_OpenAction); - } - ActionInputs::_OpenAction => { - let dialog = gtk::FileDialog::builder() - .modal(true) - .title(lang::lookup("action-header-open")) - .filters(&file_filters::filter_list(vec![ - file_filters::actions(), - file_filters::all(), - ])) - .initial_folder(>k::gio::File::for_path( - std::env::var("TA_ACTION_DIR").unwrap_or("./actions".to_string()), - )) - .build(); - - let sender_c = sender.clone(); - dialog.open( - Some(&root.toplevel_window().unwrap()), - Some(&relm4::gtk::gio::Cancellable::new()), - move |res| { - if let Ok(file) = res { - let path = file.path().unwrap(); - sender_c.input(ActionInputs::__OpenAction(path)); - } - }, - ); - } - ActionInputs::__OpenAction(path) => { - match self.open_action(path) { - Ok(_) => { - // Reload UI - sender.input(ActionInputs::UpdateStepsFromModel); - } - Err(e) => { - // Show error dialog - self.create_message_dialog( - lang::lookup("action-error-opening"), - e.to_string(), - ) - .set_visible(true); - } - } - } - ActionInputs::SaveAction => { - if self.open_action.is_some() { - // unwrap rationale: this cannot be triggered if not attached to a window - self.ask_where_to_save( - sender.input_sender(), - &root.toplevel_window().unwrap(), - false, - ActionInputs::NoOp, - ); - } - } - ActionInputs::SaveAsAction => { - if self.open_action.is_some() { - // unwrap rationale: this cannot be triggered if not attached to a window - self.ask_where_to_save( - sender.input_sender(), - &root.toplevel_window().unwrap(), - true, - ActionInputs::NoOp, - ); - } - } - ActionInputs::_SaveActionThen(then) => { - // unwrap rationale: this cannot be triggered if not attached to a window - self.ask_where_to_save( - sender.input_sender(), - &root.toplevel_window().unwrap(), - false, - *then, - ); - } - ActionInputs::__SaveActionThen(path, then) => { - self.open_path = Some(path.with_extension("taaction")); - if let Err(e) = self.save_action() { - self.create_message_dialog(lang::lookup("action-error-saving"), e.to_string()) - .set_visible(true); - } else { - widgets - .toast_target - .add_toast(adw::Toast::new(&lang::lookup("action-saved"))); - sender.input_sender().emit(*then); - } - let _ = sender.output(ActionOutputs::ReloadActions); - } - ActionInputs::CloseAction => { - self.prompt_to_save(sender.input_sender(), ActionInputs::_CloseAction); - } - ActionInputs::_CloseAction => { - self.close_action(); - } - - ActionInputs::AddStep(step_id) => { - if self.open_action.is_none() { - self.new_action(); - } - - // unwrap rationale: we've just guaranteed a flow is open - let action = self.open_action.as_mut().unwrap(); - // unwrap rationale: the header can't ask to add an action that doesn't exist - action.instructions.push(InstructionConfiguration::from( - self.engine_list.get_instruction_by_id(&step_id).unwrap(), - )); - // Trigger UI steps refresh - sender.input(ActionInputs::UpdateStepsFromModel); - } - - ActionInputs::UpdateStepsFromModel => { - let mut live_list = self.live_instructions_list.guard(); - live_list.clear(); - if let Some(action) = &self.open_action { - let mut possible_outputs = vec![]; - // Populate possible outputs with parameters - for (idx, (name, kind)) in action.parameters.iter().enumerate() { - possible_outputs.push(( - lang::lookup_with_args("source-from-param", { - let mut map = HashMap::new(); - map.insert("param", name.clone().into()); - map - }), - *kind, - InstructionParameterSource::FromParameter(idx), - )); - } - - for (step, config) in action.instructions.iter().enumerate() { - live_list.push_back( - instruction_component::InstructionComponentInitialiser { - possible_outputs: possible_outputs.clone(), - config: config.clone(), - instruction: self - .engine_list - .get_instruction_by_id(&config.instruction_id) - .unwrap(), // rationale: we have already checked the actions are here when the file is opened - }, - ); - // add possible outputs to list AFTER processing this step - // unwrap rationale: actions are check to exist prior to opening. - for (output_id, (name, kind)) in self - .engine_list - .get_instruction_by_id(&config.instruction_id) - .unwrap() - .outputs() - .iter() - { - possible_outputs.push(( - lang::lookup_with_args("source-from-step", { - let mut map = HashMap::new(); - map.insert("step", (step + 1).into()); - map.insert("name", name.clone().into()); - map - }), - *kind, - InstructionParameterSource::FromOutput(step, output_id.clone()), - )); - } - } - - self.outputs - .emit(outputs::ActionOutputsInput::SetPossibleSources( - possible_outputs, - )); - } - } - - ActionInputs::RemoveStep(step_idx) => { - let idx = step_idx.current_index(); - let action = self.open_action.as_mut().unwrap(); - - // This is needed as sometimes, if a menu item lines up above the delete step button, - // they can both be simultaneously triggered. - if idx >= action.instructions.len() { - log::warn!("Skipped running RemoveStep as the index was invalid."); - return; - } - - log::info!("Deleting step {}", idx + 1); - - action.instructions.remove(idx); - - // Remove references to step and renumber references above step to one less than they were - for step in action.instructions.iter_mut() { - for (_step_idx, source) in step.parameter_sources.iter_mut() { - if let InstructionParameterSource::FromOutput(from_step, _output_idx) = - source - { - match (*from_step).cmp(&idx) { - std::cmp::Ordering::Equal => { - *source = InstructionParameterSource::Literal - } - std::cmp::Ordering::Greater => *from_step -= 1, - _ => (), - } - } - } - } - - // Trigger UI steps refresh - sender.input(ActionInputs::UpdateStepsFromModel); - } - ActionInputs::CutStep(step_idx) => { - let idx = step_idx.current_index(); - let action = self.open_action.as_mut().unwrap(); - log::info!("Cut step {}", idx + 1); - - // This is needed as sometimes, if a menu item lines up above a button that triggers this, - // they can both be simultaneously triggered. - if idx >= action.instructions.len() { - log::warn!("Skipped running CutStep as the index was invalid."); - return; - } - - action.instructions.remove(idx); - - // Remove references to step and renumber references above step to one less than they were - for step in action.instructions.iter_mut() { - for (_step_idx, source) in step.parameter_sources.iter_mut() { - if let InstructionParameterSource::FromOutput(from_step, _output_idx) = - source - { - match (*from_step).cmp(&idx) { - std::cmp::Ordering::Equal => *from_step = usize::MAX, - std::cmp::Ordering::Greater => *from_step -= 1, - _ => (), - } - } - } - } - } - ActionInputs::PasteStep(idx, config) => { - let action = self.open_action.as_mut().unwrap(); - let idx = idx.max(0).min(action.instructions.len()); - log::info!("Pasting step to {}", idx + 1); - action.instructions.insert(idx, config); - - // Remove references to step and renumber references above step to one less than they were - for (step_idx, step) in action.instructions.iter_mut().enumerate() { - for (_param_idx, source) in step.parameter_sources.iter_mut() { - if let InstructionParameterSource::FromOutput(from_step, _output_idx) = - source - { - if *from_step == usize::MAX { - if step_idx < idx { - // can't refer to it anymore - *source = InstructionParameterSource::Literal; - } else { - *from_step = idx; - } - } else if *from_step >= idx { - *from_step += 1; - } - } - } - } - - // Trigger UI steps refresh - sender.input(ActionInputs::UpdateStepsFromModel); - } - ActionInputs::MoveStep(from, to, offset) => { - let current_from = from.current_index(); - let step = self.open_action.as_ref().unwrap().instructions[current_from].clone(); - sender.input(ActionInputs::CutStep(from)); - let mut to = (to.current_index() as isize + offset).max(0) as usize; - if to > current_from && to > 0 { - to -= 1; - } - sender.input(ActionInputs::PasteStep(to, step)); - } - } - self.update_view(widgets, sender); - } -} +use std::{cmp::Ordering, collections::HashMap, fs, path::PathBuf, rc::Rc, sync::Arc}; + +use adw::prelude::*; +use relm4::{ + adw, factory::FactoryVecDeque, gtk, prelude::DynamicIndex, Component, ComponentController, + ComponentParts, ComponentSender, Controller, RelmWidgetExt, +}; +use testangel::{ + action_loader::ActionMap, + ipc::EngineList, + types::{Action, InstructionConfiguration, InstructionParameterSource, VersionedFile}, +}; +use testangel_ipc::prelude::ParameterKind; + +use super::{file_filters, lang}; + +pub mod header; +mod instruction_component; +mod metadata_component; +mod outputs; +mod params; + +pub enum SaveOrOpenActionError { + IoError(std::io::Error), + ParsingError(ron::error::SpannedError), + SerializingError(ron::Error), + ActionNotVersionCompatible, + MissingInstruction(usize, String), +} + +impl ToString for SaveOrOpenActionError { + fn to_string(&self) -> String { + match self { + Self::IoError(e) => lang::lookup_with_args("action-save-open-error-io-error", { + let mut map = HashMap::new(); + map.insert("error", e.to_string().into()); + map + }), + Self::ParsingError(e) => { + lang::lookup_with_args("action-save-open-error-parsing-error", { + let mut map = HashMap::new(); + map.insert("error", e.to_string().into()); + map + }) + } + Self::SerializingError(e) => { + lang::lookup_with_args("action-save-open-error-serializing-error", { + let mut map = HashMap::new(); + map.insert("error", e.to_string().into()); + map + }) + } + Self::ActionNotVersionCompatible => { + lang::lookup("action-save-open-error-action-not-version-compatible") + } + Self::MissingInstruction(step, e) => { + lang::lookup_with_args("action-save-open-error-missing-instruction", { + let mut map = HashMap::new(); + map.insert("step", (step + 1).into()); + map.insert("error", e.to_string().into()); + map + }) + } + } + } +} + +#[derive(Clone, Debug)] +pub enum ActionInputs { + /// Do nothing + NoOp, + /// The map of actions has changed and should be updated + ActionsMapChanged(Arc), + /// Create a new action + NewAction, + /// Actually create the new action + _NewAction, + /// Prompt the user to open an action. This will ask to save first if needed. + OpenAction, + /// Actually show the user the open file dialog + _OpenAction, + /// Actually open an action after the user has finished selecting + __OpenAction(PathBuf), + /// Save the action, prompting if needed to set file path + SaveAction, + /// Save the action as a new file, always prompting for a file path + SaveAsAction, + /// Ask where to save if needed, then save + _SaveActionThen(Box), + /// Actually write the action to disk, then emit then input + __SaveActionThen(PathBuf, Box), + /// Close the action, prompting if needing to save first + CloseAction, + /// Actually close the action + _CloseAction, + /// Add the step with the ID provided + AddStep(String), + /// Update the UI steps from the open action. This will clear first and overwrite any changes! + UpdateStepsFromModel, + /// Remove the step with the provided index, resetting all references to it. + RemoveStep(DynamicIndex), + /// Remove the step with the provided index, but change references to it to a temporary value (`usize::MAX`) + /// that can be set again with [`ActionInputs::PasteStep`]. + /// This doesn't refresh the UI until Paste is called. + CutStep(DynamicIndex), + /// Insert a step at the specified index and set references back to the correct step. + /// This refreshes the UI. + PasteStep(usize, InstructionConfiguration), + /// Move a step from the index to a position offset (param 3) from a new index (param 2). + MoveStep(DynamicIndex, DynamicIndex, isize), + /// The [`InstructionConfiguration`] has changed for the step indicated by the [`DynamicIndex`]. + /// This does not refresh the UI. + ConfigUpdate(DynamicIndex, InstructionConfiguration), + /// The metadata has been updated and the action should be updated to reflect that + MetadataUpdated(metadata_component::MetadataOutput), + /// Set parameters + SetParameters(Vec<(String, ParameterKind)>), + /// Remove references to the provided index, or reduce any higher than. + ParamIndexRemoved(usize), + /// Swap references to the indexes provided + ParamIndexesSwapped(usize, usize), + /// Change the run condition of a step + ChangeRunCondition(DynamicIndex, InstructionParameterSource), + /// Set the outputs of an action + SetOutputs(Vec<(String, ParameterKind, InstructionParameterSource)>), + /// Set the commend on a step + SetComment(DynamicIndex, String), +} +#[derive(Clone, Debug)] +pub enum ActionOutputs { + /// Inform other parts that actions may have changed, reload them! + ReloadActions, +} + +#[derive(Debug)] +pub struct ActionsModel { + action_map: Arc, + engine_list: Arc, + + open_action: Option, + open_path: Option, + needs_saving: bool, + header: Rc>, + metadata: Controller, + parameters: Controller, + outputs: Controller, + live_instructions_list: FactoryVecDeque, +} + +impl ActionsModel { + /// Get an [`Rc`] clone of the header controller + pub fn header_controller_rc(&self) -> Rc> { + self.header.clone() + } + + /// Create the absolute barebones of a message dialog, allowing for custom button and response mapping. + fn create_message_dialog_skeleton(&self, title: S, message: S) -> adw::MessageDialog + where + S: AsRef, + { + adw::MessageDialog::builder() + .transient_for(&self.header.widget().toplevel_window().expect( + "ActionsModel::create_message_dialog cannot be called until the header is attached", + )) + .title(title.as_ref()) + .heading(title.as_ref()) + .body(message.as_ref()) + .modal(true) + .build() + } + + /// Create a message dialog attached to the toplevel window. This includes default implementations of an 'OK' button. + fn create_message_dialog(&self, title: S, message: S) -> adw::MessageDialog + where + S: AsRef, + { + let dialog = self.create_message_dialog_skeleton(title, message); + dialog.add_response("ok", &lang::lookup("ok")); + dialog.set_default_response(Some("ok")); + dialog.set_close_response("ok"); + dialog + } + + /// Just open a brand new action + fn new_action(&mut self) { + self.open_path = None; + self.needs_saving = true; + self.open_action = Some(Action::default()); + self.header + .emit(header::ActionsHeaderInput::ChangeActionOpen( + self.open_action.is_some(), + )); + self.metadata + .emit(metadata_component::MetadataInput::ChangeAction( + Action::default(), + )); + self.parameters + .emit(params::ActionParamsInput::ChangeAction(Action::default())); + self.outputs + .emit(outputs::ActionOutputsInput::ChangeAction(Action::default())); + } + + /// Open an action. This does not ask to save first. + fn open_action(&mut self, file: PathBuf) -> Result<(), SaveOrOpenActionError> { + let data = &fs::read_to_string(&file).map_err(SaveOrOpenActionError::IoError)?; + + let versioned_file: VersionedFile = + ron::from_str(data).map_err(SaveOrOpenActionError::ParsingError)?; + if versioned_file.version() != 1 { + return Err(SaveOrOpenActionError::ActionNotVersionCompatible); + } + + let mut action: Action = + ron::from_str(data).map_err(SaveOrOpenActionError::ParsingError)?; + if action.version() != 1 { + return Err(SaveOrOpenActionError::ActionNotVersionCompatible); + } + for (step, ic) in action.instructions.iter_mut().enumerate() { + if self + .engine_list + .get_engine_by_instruction_id(&ic.instruction_id) + .is_none() + { + return Err(SaveOrOpenActionError::MissingInstruction( + step, + ic.instruction_id.clone(), + )); + } + } + + self.open_action = Some(action.clone()); + self.header + .emit(header::ActionsHeaderInput::ChangeActionOpen( + self.open_action.is_some(), + )); + self.metadata + .emit(metadata_component::MetadataInput::ChangeAction( + action.clone(), + )); + self.parameters + .emit(params::ActionParamsInput::ChangeAction(action.clone())); + self.outputs + .emit(outputs::ActionOutputsInput::ChangeAction(action)); + self.open_path = Some(file); + self.needs_saving = false; + log::debug!("New action open."); + log::debug!("Action: {:?}", self.open_action); + Ok(()) + } + + /// Ask the user if they want to save this file. If they response yes, this will also trigger the save function. + /// This function will only ask the user if needed, otherwise it will emit immediately. + fn prompt_to_save(&self, sender: &relm4::Sender, then: ActionInputs) { + if self.needs_saving { + let question = self.create_message_dialog_skeleton( + lang::lookup("action-save-before"), + lang::lookup("action-save-before-message"), + ); + question.add_response("discard", &lang::lookup("discard")); + question.add_response("save", &lang::lookup("save")); + question.set_response_appearance("discard", adw::ResponseAppearance::Destructive); + question.set_default_response(Some("save")); + question.set_close_response("discard"); + let sender_c = sender.clone(); + let then_c = then.clone(); + question.connect_response(Some("save"), move |_, _| { + sender_c.emit(ActionInputs::_SaveActionThen(Box::new(then_c.clone()))); + }); + let sender_c = sender.clone(); + question.connect_response(Some("discard"), move |_, _| { + sender_c.emit(then.clone()); + }); + question.set_visible(true); + } else { + sender.emit(then); + } + } + + /// Ask the user where to save the flow, or just save if that's good enough + fn ask_where_to_save( + &mut self, + sender: &relm4::Sender, + transient_for: &impl IsA, + always_ask_where: bool, + then: ActionInputs, + ) { + if always_ask_where || self.open_path.is_none() { + // Ask where + let dialog = gtk::FileDialog::builder() + .modal(true) + .title(lang::lookup("action-header-save")) + .initial_folder(>k::gio::File::for_path( + std::env::var("TA_ACTION_DIR").unwrap_or("./actions".to_string()), + )) + .filters(&file_filters::filter_list(vec![ + file_filters::actions(), + file_filters::all(), + ])) + .build(); + + let sender_c = sender.clone(); + dialog.save( + Some(transient_for), + Some(&relm4::gtk::gio::Cancellable::new()), + move |res| { + if let Ok(file) = res { + let path = file.path().unwrap(); + sender_c.emit(ActionInputs::__SaveActionThen(path, Box::new(then.clone()))); + } + }, + ); + } else { + sender.emit(ActionInputs::__SaveActionThen( + self.open_path.clone().unwrap(), + Box::new(then), + )); + } + } + + /// Just save the action to disk with the current `open_path` as the destination + fn save_action(&mut self) -> Result<(), SaveOrOpenActionError> { + let save_path = self.open_path.as_ref().unwrap(); + let data = ron::to_string(self.open_action.as_ref().unwrap()) + .map_err(SaveOrOpenActionError::SerializingError)?; + fs::write(save_path, data).map_err(SaveOrOpenActionError::IoError)?; + self.needs_saving = false; + Ok(()) + } + + /// Close this action without checking first + fn close_action(&mut self) { + self.open_action = None; + self.open_path = None; + self.needs_saving = false; + self.live_instructions_list.guard().clear(); + self.header + .emit(header::ActionsHeaderInput::ChangeActionOpen( + self.open_action.is_some(), + )); + } +} + +#[relm4::component(pub)] +impl Component for ActionsModel { + type Init = (Arc, Arc); + type Input = ActionInputs; + type Output = ActionOutputs; + type CommandOutput = (); + + view! { + #[root] + toast_target = adw::ToastOverlay { + gtk::ScrolledWindow { + set_vexpand: true, + set_hscrollbar_policy: gtk::PolicyType::Never, + + if model.open_action.is_none() { + adw::StatusPage { + set_title: &lang::lookup("nothing-open"), + set_description: Some(&lang::lookup("action-nothing-open-description")), + set_icon_name: Some(relm4_icons::icon_name::LIGHTBULB), + set_vexpand: true, + } + } else { + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_margin_all: 10, + set_spacing: 10, + + model.metadata.widget(), + + gtk::Separator { + set_orientation: gtk::Orientation::Horizontal, + }, + + model.parameters.widget(), + + gtk::Separator { + set_orientation: gtk::Orientation::Horizontal, + }, + + #[local_ref] + live_instructions_list -> gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 5, + }, + + gtk::Separator { + set_orientation: gtk::Orientation::Horizontal, + }, + + model.outputs.widget(), + } + } + }, + }, + } + + fn init( + init: Self::Init, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let header = Rc::new( + header::ActionsHeader::builder() + .launch((init.1.clone(), init.0.clone())) + .forward(sender.input_sender(), |msg| match msg { + header::ActionsHeaderOutput::NewAction => ActionInputs::NewAction, + header::ActionsHeaderOutput::OpenAction => ActionInputs::OpenAction, + header::ActionsHeaderOutput::SaveAction => ActionInputs::SaveAction, + header::ActionsHeaderOutput::SaveAsAction => ActionInputs::SaveAsAction, + header::ActionsHeaderOutput::CloseAction => ActionInputs::CloseAction, + header::ActionsHeaderOutput::AddStep(step) => ActionInputs::AddStep(step), + }), + ); + + let model = ActionsModel { + action_map: init.0, + engine_list: init.1, + open_action: None, + open_path: None, + needs_saving: false, + header, + live_instructions_list: FactoryVecDeque::new( + gtk::Box::default(), + sender.input_sender(), + ), + metadata: metadata_component::Metadata::builder() + .launch(()) + .forward(sender.input_sender(), |msg| { + ActionInputs::MetadataUpdated(msg) + }), + parameters: params::ActionParams::builder().launch(()).forward( + sender.input_sender(), + |msg| match msg { + params::ActionParamsOutput::IndexRemoved(idx) => { + ActionInputs::ParamIndexRemoved(idx) + } + params::ActionParamsOutput::IndexesSwapped(a, b) => { + ActionInputs::ParamIndexesSwapped(a, b) + } + params::ActionParamsOutput::SetParameters(new_params) => { + ActionInputs::SetParameters(new_params) + } + }, + ), + outputs: outputs::ActionOutputs::builder().launch(()).forward( + sender.input_sender(), + |msg| match msg { + outputs::ActionOutputsOutput::IndexRemoved(idx) => { + ActionInputs::ParamIndexRemoved(idx) + } + outputs::ActionOutputsOutput::IndexesSwapped(a, b) => { + ActionInputs::ParamIndexesSwapped(a, b) + } + outputs::ActionOutputsOutput::SetOutputs(new_outputs) => { + ActionInputs::SetOutputs(new_outputs) + } + }, + ), + }; + + // Trigger update actions from model + sender.input(ActionInputs::UpdateStepsFromModel); + + let live_instructions_list = model.live_instructions_list.widget(); + let widgets = view_output!(); + + ComponentParts { model, widgets } + } + + fn update_with_view( + &mut self, + widgets: &mut Self::Widgets, + message: Self::Input, + sender: ComponentSender, + root: &Self::Root, + ) { + match message { + ActionInputs::NoOp => (), + + ActionInputs::MetadataUpdated(meta) => { + if let Some(action) = self.open_action.as_mut() { + if let Some(new_name) = meta.new_name { + action.friendly_name = new_name; + } + if let Some(new_group) = meta.new_group { + action.group = new_group; + } + if let Some(new_author) = meta.new_author { + action.author = new_author; + } + if let Some(new_description) = meta.new_description { + action.description = new_description; + } + if let Some(new_visible) = meta.new_visible { + action.visible = new_visible; + } + self.needs_saving = true; + } + } + + ActionInputs::SetComment(step, new_comment) => { + if let Some(action) = self.open_action.as_mut() { + action.instructions[step.current_index()].comment = new_comment; + self.needs_saving = true; + } + } + + ActionInputs::ChangeRunCondition(step, new_condition) => { + if let Some(action) = self.open_action.as_mut() { + action.instructions[step.current_index()].run_if = new_condition; + self.needs_saving = true; + } + } + + ActionInputs::SetParameters(new_params) => { + if let Some(action) = self.open_action.as_mut() { + action.parameters = new_params; + self.needs_saving = true; + sender.input(ActionInputs::UpdateStepsFromModel); + } + } + ActionInputs::SetOutputs(new_outputs) => { + if let Some(action) = self.open_action.as_mut() { + action.outputs = new_outputs; + self.needs_saving = true; + sender.input(ActionInputs::UpdateStepsFromModel); + } + } + ActionInputs::ParamIndexRemoved(idx) => { + if let Some(action) = self.open_action.as_mut() { + for ic in action.instructions.iter_mut() { + for (_, src) in ic.parameter_sources.iter_mut() { + if let InstructionParameterSource::FromParameter(n) = src { + match idx.cmp(n) { + Ordering::Equal => *src = InstructionParameterSource::Literal, + Ordering::Less => *n -= 1, + _ => (), + } + } + } + } + self.needs_saving = true; + sender.input(ActionInputs::UpdateStepsFromModel); + } + } + ActionInputs::ParamIndexesSwapped(a, b) => { + if let Some(action) = self.open_action.as_mut() { + for ic in action.instructions.iter_mut() { + for (_, src) in ic.parameter_sources.iter_mut() { + if let InstructionParameterSource::FromParameter(n) = src { + if *n == a { + *n = b; + } else if *n == b { + *n = a; + } + } + } + } + self.needs_saving = true; + sender.input(ActionInputs::UpdateStepsFromModel); + } + } + + ActionInputs::ActionsMapChanged(new_map) => { + self.action_map = new_map.clone(); + self.header + .emit(header::ActionsHeaderInput::ActionsMapChanged(new_map)); + } + ActionInputs::ConfigUpdate(step, new_config) => { + // unwrap rationale: config updates can't happen if nothing is open + if let Some(action) = self.open_action.as_mut() { + self.needs_saving = true; + action.instructions[step.current_index()] = new_config; + } + } + ActionInputs::NewAction => { + self.prompt_to_save(sender.input_sender(), ActionInputs::_NewAction); + } + ActionInputs::_NewAction => { + self.new_action(); + } + ActionInputs::OpenAction => { + self.prompt_to_save(sender.input_sender(), ActionInputs::_OpenAction); + } + ActionInputs::_OpenAction => { + let dialog = gtk::FileDialog::builder() + .modal(true) + .title(lang::lookup("action-header-open")) + .filters(&file_filters::filter_list(vec![ + file_filters::actions(), + file_filters::all(), + ])) + .initial_folder(>k::gio::File::for_path( + std::env::var("TA_ACTION_DIR").unwrap_or("./actions".to_string()), + )) + .build(); + + let sender_c = sender.clone(); + dialog.open( + Some(&root.toplevel_window().unwrap()), + Some(&relm4::gtk::gio::Cancellable::new()), + move |res| { + if let Ok(file) = res { + let path = file.path().unwrap(); + sender_c.input(ActionInputs::__OpenAction(path)); + } + }, + ); + } + ActionInputs::__OpenAction(path) => { + match self.open_action(path) { + Ok(_) => { + // Reload UI + sender.input(ActionInputs::UpdateStepsFromModel); + } + Err(e) => { + // Show error dialog + self.create_message_dialog( + lang::lookup("action-error-opening"), + e.to_string(), + ) + .set_visible(true); + } + } + } + ActionInputs::SaveAction => { + if self.open_action.is_some() { + // unwrap rationale: this cannot be triggered if not attached to a window + self.ask_where_to_save( + sender.input_sender(), + &root.toplevel_window().unwrap(), + false, + ActionInputs::NoOp, + ); + } + } + ActionInputs::SaveAsAction => { + if self.open_action.is_some() { + // unwrap rationale: this cannot be triggered if not attached to a window + self.ask_where_to_save( + sender.input_sender(), + &root.toplevel_window().unwrap(), + true, + ActionInputs::NoOp, + ); + } + } + ActionInputs::_SaveActionThen(then) => { + // unwrap rationale: this cannot be triggered if not attached to a window + self.ask_where_to_save( + sender.input_sender(), + &root.toplevel_window().unwrap(), + false, + *then, + ); + } + ActionInputs::__SaveActionThen(path, then) => { + self.open_path = Some(path.with_extension("taaction")); + if let Err(e) = self.save_action() { + self.create_message_dialog(lang::lookup("action-error-saving"), e.to_string()) + .set_visible(true); + } else { + widgets + .toast_target + .add_toast(adw::Toast::new(&lang::lookup("action-saved"))); + sender.input_sender().emit(*then); + } + let _ = sender.output(ActionOutputs::ReloadActions); + } + ActionInputs::CloseAction => { + self.prompt_to_save(sender.input_sender(), ActionInputs::_CloseAction); + } + ActionInputs::_CloseAction => { + self.close_action(); + } + + ActionInputs::AddStep(step_id) => { + if self.open_action.is_none() { + self.new_action(); + } + + // unwrap rationale: we've just guaranteed a flow is open + let action = self.open_action.as_mut().unwrap(); + // unwrap rationale: the header can't ask to add an action that doesn't exist + action.instructions.push(InstructionConfiguration::from( + self.engine_list.get_instruction_by_id(&step_id).unwrap(), + )); + self.needs_saving = true; + // Trigger UI steps refresh + sender.input(ActionInputs::UpdateStepsFromModel); + } + + ActionInputs::UpdateStepsFromModel => { + let mut live_list = self.live_instructions_list.guard(); + live_list.clear(); + if let Some(action) = &self.open_action { + let mut possible_outputs = vec![]; + // Populate possible outputs with parameters + for (idx, (name, kind)) in action.parameters.iter().enumerate() { + possible_outputs.push(( + lang::lookup_with_args("source-from-param", { + let mut map = HashMap::new(); + map.insert("param", name.clone().into()); + map + }), + *kind, + InstructionParameterSource::FromParameter(idx), + )); + } + + for (step, config) in action.instructions.iter().enumerate() { + live_list.push_back( + instruction_component::InstructionComponentInitialiser { + possible_outputs: possible_outputs.clone(), + config: config.clone(), + instruction: self + .engine_list + .get_instruction_by_id(&config.instruction_id) + .unwrap(), // rationale: we have already checked the actions are here when the file is opened + }, + ); + // add possible outputs to list AFTER processing this step + // unwrap rationale: actions are check to exist prior to opening. + for (output_id, (name, kind)) in self + .engine_list + .get_instruction_by_id(&config.instruction_id) + .unwrap() + .outputs() + .iter() + { + possible_outputs.push(( + lang::lookup_with_args("source-from-step", { + let mut map = HashMap::new(); + map.insert("step", (step + 1).into()); + map.insert("name", name.clone().into()); + map + }), + *kind, + InstructionParameterSource::FromOutput(step, output_id.clone()), + )); + } + } + + self.outputs + .emit(outputs::ActionOutputsInput::SetPossibleSources( + possible_outputs, + )); + } + } + + ActionInputs::RemoveStep(step_idx) => { + let idx = step_idx.current_index(); + let action = self.open_action.as_mut().unwrap(); + + // This is needed as sometimes, if a menu item lines up above the delete step button, + // they can both be simultaneously triggered. + if idx >= action.instructions.len() { + log::warn!("Skipped running RemoveStep as the index was invalid."); + return; + } + + log::info!("Deleting step {}", idx + 1); + + action.instructions.remove(idx); + + // Remove references to step and renumber references above step to one less than they were + for step in action.instructions.iter_mut() { + for (_step_idx, source) in step.parameter_sources.iter_mut() { + if let InstructionParameterSource::FromOutput(from_step, _output_idx) = + source + { + match (*from_step).cmp(&idx) { + std::cmp::Ordering::Equal => { + *source = InstructionParameterSource::Literal + } + std::cmp::Ordering::Greater => *from_step -= 1, + _ => (), + } + } + } + } + + self.needs_saving = true; + + // Trigger UI steps refresh + sender.input(ActionInputs::UpdateStepsFromModel); + } + ActionInputs::CutStep(step_idx) => { + let idx = step_idx.current_index(); + let action = self.open_action.as_mut().unwrap(); + log::info!("Cut step {}", idx + 1); + + // This is needed as sometimes, if a menu item lines up above a button that triggers this, + // they can both be simultaneously triggered. + if idx >= action.instructions.len() { + log::warn!("Skipped running CutStep as the index was invalid."); + return; + } + + action.instructions.remove(idx); + + self.needs_saving = true; + + // Remove references to step and renumber references above step to one less than they were + for step in action.instructions.iter_mut() { + for (_param_idx, source) in step.parameter_sources.iter_mut() { + if let InstructionParameterSource::FromOutput(from_step, _output_idx) = + source + { + match (*from_step).cmp(&idx) { + std::cmp::Ordering::Equal => *from_step = usize::MAX, + std::cmp::Ordering::Greater => *from_step -= 1, + _ => (), + } + } + } + } + } + ActionInputs::PasteStep(idx, config) => { + let action = self.open_action.as_mut().unwrap(); + let idx = idx.max(0).min(action.instructions.len()); + log::info!("Pasting step to {}", idx + 1); + action.instructions.insert(idx, config); + + // Remove references to step and renumber references above step to one less than they were + for (step_idx, step) in action.instructions.iter_mut().enumerate() { + for (_param_idx, source) in step.parameter_sources.iter_mut() { + if let InstructionParameterSource::FromOutput(from_step, _output_idx) = + source + { + if *from_step == usize::MAX { + if step_idx < idx { + // can't refer to it anymore + *source = InstructionParameterSource::Literal; + } else { + *from_step = idx; + } + } else if *from_step >= idx { + *from_step += 1; + } + } + } + } + + self.needs_saving = true; + + // Trigger UI steps refresh + sender.input(ActionInputs::UpdateStepsFromModel); + } + ActionInputs::MoveStep(from, to, offset) => { + let current_from = from.current_index(); + let step = self.open_action.as_ref().unwrap().instructions[current_from].clone(); + sender.input(ActionInputs::CutStep(from)); + let mut to = (to.current_index() as isize + offset).max(0) as usize; + if to > current_from && to > 0 { + to -= 1; + } + sender.input(ActionInputs::PasteStep(to, step)); + } + } + self.update_view(widgets, sender); + } +} diff --git a/testangel/src/next_ui/actions/outputs.rs b/testangel/src/ui/actions/outputs.rs similarity index 97% rename from testangel/src/next_ui/actions/outputs.rs rename to testangel/src/ui/actions/outputs.rs index 6b73b82..7d51882 100644 --- a/testangel/src/next_ui/actions/outputs.rs +++ b/testangel/src/ui/actions/outputs.rs @@ -9,7 +9,7 @@ use relm4::{ use testangel::types::{Action, InstructionParameterSource}; use testangel_ipc::prelude::ParameterKind; -use crate::next_ui::lang; +use crate::ui::lang; #[derive(Debug)] pub enum ActionOutputsInput { diff --git a/testangel/src/next_ui/actions/params.rs b/testangel/src/ui/actions/params.rs similarity index 96% rename from testangel/src/next_ui/actions/params.rs rename to testangel/src/ui/actions/params.rs index b056122..efe3344 100644 --- a/testangel/src/next_ui/actions/params.rs +++ b/testangel/src/ui/actions/params.rs @@ -9,7 +9,7 @@ use relm4::{ use testangel::types::Action; use testangel_ipc::prelude::ParameterKind; -use crate::next_ui::lang; +use crate::ui::lang; #[derive(Debug)] pub enum ActionParamsInput { diff --git a/testangel/src/next_ui/components/add_step_factory.rs b/testangel/src/ui/components/add_step_factory.rs similarity index 100% rename from testangel/src/next_ui/components/add_step_factory.rs rename to testangel/src/ui/components/add_step_factory.rs diff --git a/testangel/src/next_ui/components/literal_input.rs b/testangel/src/ui/components/literal_input.rs similarity index 96% rename from testangel/src/next_ui/components/literal_input.rs rename to testangel/src/ui/components/literal_input.rs index e42f88a..cc9c912 100644 --- a/testangel/src/next_ui/components/literal_input.rs +++ b/testangel/src/ui/components/literal_input.rs @@ -2,7 +2,7 @@ use adw::prelude::*; use relm4::{adw, gtk, SimpleComponent}; use testangel_ipc::prelude::{ParameterKind, ParameterValue}; -use crate::next_ui::lang; +use crate::ui::lang; #[allow(dead_code)] #[derive(Debug)] diff --git a/testangel/src/next_ui/components/mod.rs b/testangel/src/ui/components/mod.rs similarity index 100% rename from testangel/src/next_ui/components/mod.rs rename to testangel/src/ui/components/mod.rs diff --git a/testangel/src/next_ui/components/variable_row.rs b/testangel/src/ui/components/variable_row.rs similarity index 96% rename from testangel/src/next_ui/components/variable_row.rs rename to testangel/src/ui/components/variable_row.rs index 01a6a49..6d53c45 100644 --- a/testangel/src/next_ui/components/variable_row.rs +++ b/testangel/src/ui/components/variable_row.rs @@ -1,274 +1,274 @@ -use std::marker::PhantomData; -use std::{collections::HashMap, fmt::Debug}; - -use adw::prelude::*; -use relm4::{ - adw, factory::FactoryVecDeque, gtk, prelude::FactoryComponent, Component, ComponentController, - Controller, FactorySender, -}; -use testangel_ipc::prelude::{ParameterKind, ParameterValue}; - -use crate::next_ui::{ - components::literal_input::{LiteralInput, LiteralInputOutput}, - lang, -}; - -#[derive(Debug)] -pub struct VariableRow -where - PS: Debug + Clone + 'static, - I: VariableRowParentInput, -{ - idx: T, - name: String, - kind: ParameterKind, - source: PS, - value: ParameterValue, - - literal_input: Controller, - potential_sources_raw: Vec<(String, PS)>, - potential_sources: FactoryVecDeque>, - _input_marker: PhantomData, -} - -pub struct VariableRowInit -where - PS: ParameterSourceTrait + Debug + std::fmt::Display + PartialEq + Clone + 'static, -{ - pub index: T, - pub name: String, - pub kind: ParameterKind, - pub current_source: PS, - pub current_value: ParameterValue, - pub potential_sources: Vec<(String, PS)>, -} - -pub trait VariableRowParentInput { - /// Replace the value of the source with the index `idx` - fn new_source_for(idx: T, new_source: PS) -> Self; - /// Replace the value of the variable with the index `idx` - fn new_value_for(idx: T, new_value: ParameterValue) -> Self; -} - -pub trait ParameterSourceTrait { - fn literal() -> Self; -} - -impl + ToString + Clone + Debug, T, I: VariableRowParentInput> - VariableRow -{ - fn get_nice_name_for(&self, source: &PS) -> String { - for (name, src) in &self.potential_sources_raw { - if *src == *source { - return name.clone(); - } - } - - source.to_string() - } -} - -#[derive(Debug)] -pub enum VariableRowInput { - SourceSelected(PS), - ChangeValue(ParameterValue), -} - -#[derive(Debug)] -pub enum VariableRowOutput { - NewSourceFor(T, PS), - NewValueFor(T, ParameterValue), -} - -#[relm4::factory(pub)] -impl FactoryComponent for VariableRow -where - PS: ParameterSourceTrait + Debug + std::fmt::Display + PartialEq + Clone + 'static, - I: Debug + VariableRowParentInput + 'static, - T: Clone + Debug + 'static, -{ - type Init = VariableRowInit; - type Input = VariableRowInput; - type Output = VariableRowOutput; - type CommandOutput = (); - type ParentWidget = adw::PreferencesGroup; - type ParentInput = I; - - view! { - adw::ActionRow { - set_title: &self.name, - #[watch] - set_subtitle: &if self.source == PS::literal() { - lang::lookup_with_args( - "variable-row-subtitle-with-value", - { - let mut map = HashMap::new(); - map.insert("kind", self.kind.to_string().into()); - map.insert("source", self.source.to_string().into()); - map.insert("value", self.value.to_string().into()); - map - } - ) - } else { - lang::lookup_with_args( - "variable-row-subtitle", - { - let mut map = HashMap::new(); - map.insert("kind", self.kind.to_string().into()); - map.insert("source", self.get_nice_name_for(&self.source).into()); - map - } - ) - }, - - add_suffix = >k::Box { - set_spacing: 15, - set_orientation: gtk::Orientation::Horizontal, - - adw::Bin { - #[watch] - set_visible: self.source == PS::literal(), - self.literal_input.widget(), - }, - - gtk::MenuButton { - set_icon_name: relm4_icons::icon_name::EDIT, - set_tooltip_text: Some(&lang::lookup("variable-row-edit-param")), - set_css_classes: &["flat"], - set_direction: gtk::ArrowType::Left, - - #[wrap(Some)] - set_popover = >k::Popover { - gtk::ScrolledWindow { - set_hscrollbar_policy: gtk::PolicyType::Never, - set_min_content_height: 150, - - #[local_ref] - potential_sources -> gtk::Box { - set_spacing: 5, - set_orientation: gtk::Orientation::Vertical, - }, - } - }, - }, - }, - } - } - - fn init_model( - init: Self::Init, - _index: &Self::Index, - sender: relm4::FactorySender, - ) -> Self { - let mut potential_sources = - FactoryVecDeque::new(gtk::Box::default(), sender.input_sender()); - { - // populate sources - let mut potential_sources = potential_sources.guard(); - for (label, source) in init.potential_sources.clone() { - potential_sources.push_back((label, source)); - } - } - - let literal_input = LiteralInput::builder() - .launch(init.current_value.clone()) - .forward(sender.input_sender(), |msg| match msg { - LiteralInputOutput::ValueChanged(new_value) => { - VariableRowInput::ChangeValue(new_value) - } - }); - - Self { - idx: init.index, - name: init.name, - kind: init.kind, - source: init.current_source, - value: init.current_value, - literal_input, - potential_sources_raw: init.potential_sources, - potential_sources, - _input_marker: PhantomData, - } - } - - fn init_widgets( - &mut self, - _index: &Self::Index, - root: &Self::Root, - _returned_widget: &::ReturnedWidget, - _sender: FactorySender, - ) -> Self::Widgets { - let potential_sources = self.potential_sources.widget(); - let widgets = view_output!(); - widgets - } - - fn update(&mut self, message: Self::Input, sender: FactorySender) { - match message { - VariableRowInput::SourceSelected(new_source) => { - self.source = new_source.clone(); - sender.output(VariableRowOutput::NewSourceFor( - self.idx.clone(), - new_source, - )); - } - VariableRowInput::ChangeValue(new_value) => { - self.value = new_value.clone(); - sender.output(VariableRowOutput::NewValueFor(self.idx.clone(), new_value)); - } - } - } - - fn forward_to_parent(output: Self::Output) -> Option { - match output { - VariableRowOutput::NewSourceFor(idx, source) => Some(I::new_source_for(idx, source)), - VariableRowOutput::NewValueFor(idx, value) => Some(I::new_value_for(idx, value)), - } - } -} - -#[derive(Debug)] -struct SourceSearchResult { - label: String, - source: PS, -} - -#[derive(Debug)] -enum SourceSearchResultInput { - Select, -} - -#[relm4::factory] -impl FactoryComponent for SourceSearchResult { - type Init = (String, PS); - type Input = SourceSearchResultInput; - type Output = PS; - type CommandOutput = (); - type ParentWidget = gtk::Box; - type ParentInput = VariableRowInput; - - view! { - root = gtk::Button::builder().css_classes(["flat"]).build() { - set_label: &self.label, - - connect_clicked => SourceSearchResultInput::Select, - } - } - - fn init_model(init: Self::Init, _index: &Self::Index, _sender: FactorySender) -> Self { - Self { - label: init.0, - source: init.1, - } - } - - fn update(&mut self, message: Self::Input, sender: FactorySender) { - match message { - SourceSearchResultInput::Select => sender.output(self.source.clone()), - } - } - - fn forward_to_parent(output: Self::Output) -> Option { - Some(VariableRowInput::SourceSelected(output)) - } -} +use std::marker::PhantomData; +use std::{collections::HashMap, fmt::Debug}; + +use adw::prelude::*; +use relm4::{ + adw, factory::FactoryVecDeque, gtk, prelude::FactoryComponent, Component, ComponentController, + Controller, FactorySender, +}; +use testangel_ipc::prelude::{ParameterKind, ParameterValue}; + +use crate::ui::{ + components::literal_input::{LiteralInput, LiteralInputOutput}, + lang, +}; + +#[derive(Debug)] +pub struct VariableRow +where + PS: Debug + Clone + 'static, + I: VariableRowParentInput, +{ + idx: T, + name: String, + kind: ParameterKind, + source: PS, + value: ParameterValue, + + literal_input: Controller, + potential_sources_raw: Vec<(String, PS)>, + potential_sources: FactoryVecDeque>, + _input_marker: PhantomData, +} + +pub struct VariableRowInit +where + PS: ParameterSourceTrait + Debug + std::fmt::Display + PartialEq + Clone + 'static, +{ + pub index: T, + pub name: String, + pub kind: ParameterKind, + pub current_source: PS, + pub current_value: ParameterValue, + pub potential_sources: Vec<(String, PS)>, +} + +pub trait VariableRowParentInput { + /// Replace the value of the source with the index `idx` + fn new_source_for(idx: T, new_source: PS) -> Self; + /// Replace the value of the variable with the index `idx` + fn new_value_for(idx: T, new_value: ParameterValue) -> Self; +} + +pub trait ParameterSourceTrait { + fn literal() -> Self; +} + +impl + ToString + Clone + Debug, T, I: VariableRowParentInput> + VariableRow +{ + fn get_nice_name_for(&self, source: &PS) -> String { + for (name, src) in &self.potential_sources_raw { + if *src == *source { + return name.clone(); + } + } + + source.to_string() + } +} + +#[derive(Debug)] +pub enum VariableRowInput { + SourceSelected(PS), + ChangeValue(ParameterValue), +} + +#[derive(Debug)] +pub enum VariableRowOutput { + NewSourceFor(T, PS), + NewValueFor(T, ParameterValue), +} + +#[relm4::factory(pub)] +impl FactoryComponent for VariableRow +where + PS: ParameterSourceTrait + Debug + std::fmt::Display + PartialEq + Clone + 'static, + I: Debug + VariableRowParentInput + 'static, + T: Clone + Debug + 'static, +{ + type Init = VariableRowInit; + type Input = VariableRowInput; + type Output = VariableRowOutput; + type CommandOutput = (); + type ParentWidget = adw::PreferencesGroup; + type ParentInput = I; + + view! { + adw::ActionRow { + set_title: &self.name, + #[watch] + set_subtitle: &if self.source == PS::literal() { + lang::lookup_with_args( + "variable-row-subtitle-with-value", + { + let mut map = HashMap::new(); + map.insert("kind", self.kind.to_string().into()); + map.insert("source", self.source.to_string().into()); + map.insert("value", self.value.to_string().into()); + map + } + ) + } else { + lang::lookup_with_args( + "variable-row-subtitle", + { + let mut map = HashMap::new(); + map.insert("kind", self.kind.to_string().into()); + map.insert("source", self.get_nice_name_for(&self.source).into()); + map + } + ) + }, + + add_suffix = >k::Box { + set_spacing: 15, + set_orientation: gtk::Orientation::Horizontal, + + adw::Bin { + #[watch] + set_visible: self.source == PS::literal(), + self.literal_input.widget(), + }, + + gtk::MenuButton { + set_icon_name: relm4_icons::icon_name::EDIT, + set_tooltip_text: Some(&lang::lookup("variable-row-edit-param")), + set_css_classes: &["flat"], + set_direction: gtk::ArrowType::Left, + + #[wrap(Some)] + set_popover = >k::Popover { + gtk::ScrolledWindow { + set_hscrollbar_policy: gtk::PolicyType::Never, + set_min_content_height: 150, + + #[local_ref] + potential_sources -> gtk::Box { + set_spacing: 5, + set_orientation: gtk::Orientation::Vertical, + }, + } + }, + }, + }, + } + } + + fn init_model( + init: Self::Init, + _index: &Self::Index, + sender: relm4::FactorySender, + ) -> Self { + let mut potential_sources = + FactoryVecDeque::new(gtk::Box::default(), sender.input_sender()); + { + // populate sources + let mut potential_sources = potential_sources.guard(); + for (label, source) in init.potential_sources.clone() { + potential_sources.push_back((label, source)); + } + } + + let literal_input = LiteralInput::builder() + .launch(init.current_value.clone()) + .forward(sender.input_sender(), |msg| match msg { + LiteralInputOutput::ValueChanged(new_value) => { + VariableRowInput::ChangeValue(new_value) + } + }); + + Self { + idx: init.index, + name: init.name, + kind: init.kind, + source: init.current_source, + value: init.current_value, + literal_input, + potential_sources_raw: init.potential_sources, + potential_sources, + _input_marker: PhantomData, + } + } + + fn init_widgets( + &mut self, + _index: &Self::Index, + root: &Self::Root, + _returned_widget: &::ReturnedWidget, + _sender: FactorySender, + ) -> Self::Widgets { + let potential_sources = self.potential_sources.widget(); + let widgets = view_output!(); + widgets + } + + fn update(&mut self, message: Self::Input, sender: FactorySender) { + match message { + VariableRowInput::SourceSelected(new_source) => { + self.source = new_source.clone(); + sender.output(VariableRowOutput::NewSourceFor( + self.idx.clone(), + new_source, + )); + } + VariableRowInput::ChangeValue(new_value) => { + self.value = new_value.clone(); + sender.output(VariableRowOutput::NewValueFor(self.idx.clone(), new_value)); + } + } + } + + fn forward_to_parent(output: Self::Output) -> Option { + match output { + VariableRowOutput::NewSourceFor(idx, source) => Some(I::new_source_for(idx, source)), + VariableRowOutput::NewValueFor(idx, value) => Some(I::new_value_for(idx, value)), + } + } +} + +#[derive(Debug)] +struct SourceSearchResult { + label: String, + source: PS, +} + +#[derive(Debug)] +enum SourceSearchResultInput { + Select, +} + +#[relm4::factory] +impl FactoryComponent for SourceSearchResult { + type Init = (String, PS); + type Input = SourceSearchResultInput; + type Output = PS; + type CommandOutput = (); + type ParentWidget = gtk::Box; + type ParentInput = VariableRowInput; + + view! { + root = gtk::Button::builder().css_classes(["flat"]).build() { + set_label: &self.label, + + connect_clicked => SourceSearchResultInput::Select, + } + } + + fn init_model(init: Self::Init, _index: &Self::Index, _sender: FactorySender) -> Self { + Self { + label: init.0, + source: init.1, + } + } + + fn update(&mut self, message: Self::Input, sender: FactorySender) { + match message { + SourceSearchResultInput::Select => sender.output(self.source.clone()), + } + } + + fn forward_to_parent(output: Self::Output) -> Option { + Some(VariableRowInput::SourceSelected(output)) + } +} diff --git a/testangel/src/next_ui/file_filters.rs b/testangel/src/ui/file_filters.rs similarity index 100% rename from testangel/src/next_ui/file_filters.rs rename to testangel/src/ui/file_filters.rs diff --git a/testangel/src/ui/flow_editor.rs b/testangel/src/ui/flow_editor.rs deleted file mode 100644 index 7d98809..0000000 --- a/testangel/src/ui/flow_editor.rs +++ /dev/null @@ -1,730 +0,0 @@ -use std::{env, fmt, fs, path::PathBuf, sync::Arc}; - -use iced::{ - theme, - widget::{ - column, combo_box, row, Button, Checkbox, Column, ComboBox, Container, Scrollable, Space, - Text, TextInput, - }, - Length, -}; -use testangel::{ - action_loader::ActionMap, - types::{Action, ActionConfiguration, ActionParameterSource, AutomationFlow, VersionedFile}, -}; -use testangel_ipc::prelude::{ParameterKind, ParameterValue}; - -use super::UiComponent; - -#[derive(Clone, Debug)] -pub enum FlowEditorMessage { - RunFlow, - WriteFileToDisk(PathBuf, SaveFlowThen), - SaveFlow(SaveFlowThen), - SaveAsFlow(SaveFlowThen), - DoPostSaveActions(SaveFlowThen), - CloseFlow, - - StepCreate(AvailableAction), - StepParameterSourceChange(usize, usize, ActionParameterSource), - StepParameterValueChange(usize, usize, String), - StepMoveUp(usize), - StepMoveDown(usize), - StepDelete(usize), -} - -#[derive(Clone, Debug)] -pub enum SaveFlowThen { - DoNothing, - Close, -} - -#[derive(Clone, Debug)] -pub enum FlowEditorMessageOut { - RunFlow(AutomationFlow), -} - -pub enum SaveOrOpenFlowError { - IoError(std::io::Error), - ParsingError(ron::error::SpannedError), - SerializingError(ron::Error), - FlowNotVersionCompatible, - MissingAction(String), -} - -impl fmt::Display for SaveOrOpenFlowError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::IoError(e) => write!(f, "I/O Error: {e}"), - Self::ParsingError(e) => write!(f, "Parsing Error: {e}"), - Self::SerializingError(e) => write!(f, "Serializing error: {e}"), - Self::FlowNotVersionCompatible => write!( - f, - "This flow is not compatible with this version of TestAngel." - ), - Self::MissingAction(action_id) => write!( - f, - "The flow contains an action ({action_id}) which could not be loaded. This may be because that action refers to an instruction that is not available." - ), - } - } -} - -#[derive(Clone, Debug)] -pub struct AvailableAction { - /// The friendly name of this action. - friendly_name: String, - /// The action this is based on. - base_action: Action, -} - -impl fmt::Display for AvailableAction { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.friendly_name) - } -} - -#[derive(Clone, Debug)] -pub struct ComboActionParameterSource { - /// The friendly label of this source - friendly_label: String, - /// The actual source held by this entry - source: ActionParameterSource, -} - -impl fmt::Display for ComboActionParameterSource { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.friendly_label) - } -} - -#[allow(clippy::from_over_into)] -impl Into for ComboActionParameterSource { - fn into(self) -> ActionParameterSource { - self.source - } -} - -pub struct FlowEditor { - actions_list: Arc, - output_list: Vec<(isize, ParameterKind, ActionParameterSource)>, - add_action_combo: combo_box::State, - - // a vector of steps>> - parameter_source_combo: Vec>>, - currently_open: Option, - current_path: Option, - needs_saving: bool, -} - -impl Default for FlowEditor { - fn default() -> Self { - Self { - actions_list: Arc::new(ActionMap::default()), - output_list: vec![], - add_action_combo: combo_box::State::new(Vec::new()), - parameter_source_combo: vec![], - currently_open: None, - current_path: None, - needs_saving: false, - } - } -} - -impl FlowEditor { - /// Initialise a new FlowEditor with the provided [`ActionMap`]. - pub(crate) fn new(actions_list: Arc) -> Self { - let mut available_actions = vec![]; - for (group_name, actions) in actions_list.get_by_group() { - for action in actions { - if action.visible { - available_actions.push(AvailableAction { - friendly_name: format!("{group_name}: {}", action.friendly_name), - base_action: action.clone(), - }); - } - } - } - - Self { - actions_list, - add_action_combo: combo_box::State::new(available_actions), - ..Default::default() - } - } - - pub(crate) fn update_action_map(&mut self, actions_list: Arc) { - self.actions_list = actions_list.clone(); - let mut available_actions = vec![]; - let show_anyway = env::var("TA_SHOW_HIDDEN_ACTIONS") - .unwrap_or("no".to_string()) - .eq_ignore_ascii_case("yes"); - for (group_name, actions) in actions_list.get_by_group() { - for action in actions { - if action.visible || show_anyway { - available_actions.push(AvailableAction { - friendly_name: format!( - "{group_name}: {}{}", - action.friendly_name, - if !action.visible { " (Hidden)" } else { "" } - ), - base_action: action.clone(), - }); - } - } - } - available_actions.sort_by(|a, b| a.friendly_name.cmp(&b.friendly_name)); - self.add_action_combo = combo_box::State::new(available_actions); - } - - /// Create a new flow and open it - pub(crate) fn new_flow(&mut self) { - self.currently_open = Some(AutomationFlow::default()); - self.current_path = None; - self.needs_saving = true; - } - - /// Open a flow - pub(crate) fn open_flow(&mut self, file: PathBuf) -> Result, SaveOrOpenFlowError> { - let data = &fs::read_to_string(&file).map_err(SaveOrOpenFlowError::IoError)?; - - let versioned_file: VersionedFile = - ron::from_str(data).map_err(SaveOrOpenFlowError::ParsingError)?; - if versioned_file.version() != 1 { - return Err(SaveOrOpenFlowError::FlowNotVersionCompatible); - } - - let mut flow: AutomationFlow = - ron::from_str(data).map_err(SaveOrOpenFlowError::ParsingError)?; - if flow.version() != 1 { - return Err(SaveOrOpenFlowError::FlowNotVersionCompatible); - } - let mut steps_reset = vec![]; - for (step, ac) in flow.actions.iter_mut().enumerate() { - // Check for missing action - match self.actions_list.get_action_by_id(&ac.action_id) { - None => return Err(SaveOrOpenFlowError::MissingAction(ac.action_id.clone())), - Some(action) => { - // Check that action parameters haven't changed. If they have, reset values. - if ac.update(action) { - steps_reset.push(step + 1); - } - } - } - } - self.currently_open = Some(flow); - self.current_path = Some(file); - self.needs_saving = false; - self.update_outputs(); - Ok(steps_reset) - } - - /// Offer to save if it is needed with default error handling - fn offer_to_save_default_error_handling( - &mut self, - then: SaveFlowThen, - ) -> iced::Command { - if self.needs_saving { - iced::Command::perform( - rfd::AsyncMessageDialog::new() - .set_level(rfd::MessageLevel::Info) - .set_title("Do you want to save this flow?") - .set_description("This flow has been modified. Do you want to save it?") - .set_buttons(rfd::MessageButtons::YesNo) - .show(), - |wants_to_save| { - if wants_to_save == rfd::MessageDialogResult::Yes { - super::AppMessage::FlowEditor(FlowEditorMessage::SaveFlow(then)) - } else { - super::AppMessage::FlowEditor(FlowEditorMessage::DoPostSaveActions(then)) - } - }, - ) - } else { - self.do_then(then) - } - } - - fn do_then(&mut self, then: SaveFlowThen) -> iced::Command { - match then { - SaveFlowThen::DoNothing => iced::Command::none(), - SaveFlowThen::Close => { - self.close_flow(); - iced::Command::perform(async {}, |_| super::AppMessage::CloseEditor) - } - } - } - - fn write_to_disk(&mut self) -> Result<(), SaveOrOpenFlowError> { - // Save - let save_path = self.current_path.as_ref().unwrap(); - let data = ron::to_string(self.currently_open.as_ref().unwrap()) - .map_err(SaveOrOpenFlowError::SerializingError)?; - fs::write(save_path, data).map_err(SaveOrOpenFlowError::IoError)?; - self.needs_saving = false; - Ok(()) - } - - /// Save the currently opened flow - fn save_flow( - &mut self, - always_prompt_where: bool, - then: SaveFlowThen, - ) -> iced::Command { - self.needs_saving = false; - if always_prompt_where || self.current_path.is_none() { - // Populate save path - return iced::Command::perform( - rfd::AsyncFileDialog::new() - .add_filter("TestAngel Flows", &["taflow"]) - .set_title("Save Flow") - .set_directory(env::var("TA_FLOW_DIR").unwrap_or(".".to_owned())) - .save_file(), - |f| { - if let Some(file) = f { - return super::AppMessage::FlowEditor(FlowEditorMessage::WriteFileToDisk( - file.path().to_path_buf(), - then, - )); - } - super::AppMessage::NoOp - }, - ); - } - - if let Err(e) = self.write_to_disk() { - return iced::Command::perform( - rfd::AsyncMessageDialog::new() - .set_title("Failed to save") - .set_description(format!("Failed to save file: {e}")) - .show(), - |_| super::AppMessage::NoOp, - ); - } - - self.do_then(then) - } - - /// Close the currently opened flow - fn close_flow(&mut self) { - self.currently_open = None; - self.current_path = None; - self.needs_saving = false; - } - - /// Hint that the open flow has been modified. - fn modified(&mut self) { - self.needs_saving = true; - } - - fn ui_action_inputs( - &self, - step_idx: usize, - action_config: ActionConfiguration, - action: Action, - ) -> iced::Element<'_, FlowEditorMessage> { - action - .parameters - .iter() - .enumerate() - .fold(Column::new().spacing(4), |col, (id, (name, kind))| { - let param_source = &action_config.parameter_sources[&id]; - let param_value = &action_config.parameter_values[&id]; - - let literal_input: iced::Element<'_, _> = match param_source { - ActionParameterSource::Literal => match kind { - ParameterKind::Boolean => { - Checkbox::new("Value", param_value.value_bool(), move |new_val| { - FlowEditorMessage::StepParameterValueChange( - step_idx, - id, - if new_val { "yes" } else { "no" }.to_string(), - ) - }) - .into() - } - ParameterKind::Integer => TextInput::new( - "Literal number", - &if param_value.value_i32() == i32::MIN { - String::new() - } else { - param_value.to_string() - }, - ) - .on_input(move |new_val| { - if new_val.trim().is_empty() { - FlowEditorMessage::StepParameterValueChange( - step_idx, - id, - i32::MIN.to_string(), - ) - } else { - FlowEditorMessage::StepParameterValueChange(step_idx, id, new_val) - } - }) - .width(250) - .into(), - ParameterKind::Decimal => TextInput::new( - "Literal decimal", - &if param_value.value_f32() == f32::MIN { - String::new() - } else { - param_value.to_string() - }, - ) - .on_input(move |new_val| { - if new_val.trim().is_empty() { - FlowEditorMessage::StepParameterValueChange( - step_idx, - id, - f32::MIN.to_string(), - ) - } else { - FlowEditorMessage::StepParameterValueChange(step_idx, id, new_val) - } - }) - .width(250) - .into(), - _ => TextInput::new("Literal value", ¶m_value.to_string()) - .on_input(move |new_val| { - FlowEditorMessage::StepParameterValueChange(step_idx, id, new_val) - }) - .width(250) - .into(), - }, - _ => Space::new(0, 0).into(), - }; - - col.push( - row![ - Text::new(format!("{name} ({kind}) use")), - ComboBox::new( - &self.parameter_source_combo[step_idx][id], - "Source", - Some(&ComboActionParameterSource { - friendly_label: self.friendly_source_string(param_source), - source: param_source.clone(), - }), - move |src: ComboActionParameterSource| { - FlowEditorMessage::StepParameterSourceChange( - step_idx, - id, - src.into(), - ) - } - ), - literal_input, - ] - .spacing(4) - .align_items(iced::Alignment::Center), - ) - }) - .into() - } - - fn ui_steps(&self) -> iced::Element<'_, FlowEditorMessage> { - let flow = self.currently_open.as_ref(); - if flow.is_none() { - return Text::new("No flow loaded.").into(); - } - let flow = flow.unwrap(); - - flow.actions - .iter() - .enumerate() - .fold(Column::new().spacing(4), |col, (idx, action_config)| { - let action = self - .actions_list - .get_action_by_id(&action_config.action_id) - .unwrap(); - let mut outputs_text = String::new(); - for (name, kind, _) in &action.outputs { - outputs_text.push_str(&format!("{name}: {kind}\n")); - } - outputs_text = outputs_text.trim_end().to_string(); - col.push( - Container::new( - column![ - Text::new(format!("Step {}: {}", idx + 1, action.friendly_name)), - row![ - Button::new("×").on_press(FlowEditorMessage::StepDelete(idx)), - Button::new("ÊŒ").on_press_maybe(if idx == 0 { - None - } else { - Some(FlowEditorMessage::StepMoveUp(idx)) - }), - Button::new("v").on_press_maybe( - if (idx + 1) == flow.actions.len() { - None - } else { - Some(FlowEditorMessage::StepMoveDown(idx)) - } - ), - Text::new(action.description.clone()), - ] - .spacing(4), - Space::with_height(4), - Text::new("Inputs").size(18), - self.ui_action_inputs(idx, action_config.clone(), action.clone()), - Space::with_height(4), - Text::new("Outputs").size(18), - Text::new(outputs_text), - ] - .spacing(4), - ) - .padding(8) - .width(Length::Fill) - .style(theme::Container::Box), - ) - }) - .into() - } - - /// Update the possible outputs - fn update_outputs(&mut self) { - self.output_list.clear(); - self.parameter_source_combo.clear(); - - if let Some(flow) = &self.currently_open { - for (step, action_config) in flow.actions.iter().enumerate() { - let action = self - .actions_list - .get_action_by_id(&action_config.action_id) - .unwrap(); - - // Build parameter source list - let mut source_opts = vec![]; - for (_, param_kind) in &action.parameters { - let mut sources = vec![ComboActionParameterSource { - friendly_label: self - .friendly_source_string(&ActionParameterSource::Literal), - source: ActionParameterSource::Literal, - }]; - - for (_step, kind, source) in &self.output_list { - if kind == param_kind { - sources.push(ComboActionParameterSource { - friendly_label: self.friendly_source_string(source), - source: source.clone(), - }); - } - } - - source_opts.push(combo_box::State::new(sources)); - } - self.parameter_source_combo.push(source_opts); - - // Determine possible outputs from this step - for (id, (_name, kind, _src)) in action.outputs.iter().enumerate() { - self.output_list.push(( - step as isize, - *kind, - ActionParameterSource::FromOutput(step, id), - )); - } - } - } - } - - /// Convert an [`InstructionParameterSource`] to a friendly String by fetching names of parameters - /// and results from the currently opened action. - fn friendly_source_string(&self, source: &ActionParameterSource) -> String { - if let Some(flow) = &self.currently_open { - return match source { - ActionParameterSource::Literal => "Literal value".to_owned(), - ActionParameterSource::FromOutput(step, id) => { - let ac = &flow.actions[*step]; - let instruction = self.actions_list.get_action_by_id(&ac.action_id).unwrap(); - format!("Step {} Output: {}", step + 1, instruction.outputs[*id].0) - } - }; - } - source.to_string() - } -} - -impl UiComponent for FlowEditor { - type Message = FlowEditorMessage; - type MessageOut = FlowEditorMessageOut; - - fn title(&self) -> Option<&str> { - Some("Flow Editor") - } - - fn view(&self) -> iced::Element<'_, Self::Message> { - Scrollable::new( - Container::new( - column![ - // Toolbar - row![ - Button::new("Run Flow").on_press(FlowEditorMessage::RunFlow), - Button::new("Save") - .on_press(FlowEditorMessage::SaveFlow(SaveFlowThen::DoNothing)), - Button::new("Save as") - .on_press(FlowEditorMessage::SaveAsFlow(SaveFlowThen::DoNothing)), - Button::new("Close Flow").on_press(FlowEditorMessage::CloseFlow), - ] - .spacing(8), - // Actions - self.ui_steps(), - ComboBox::new( - &self.add_action_combo, - "+ Add a step...", - None, - FlowEditorMessage::StepCreate - ), - ] - .spacing(8), - ) - .padding(16), - ) - .into() - } - - fn update( - &mut self, - message: Self::Message, - ) -> ( - Option, - Option>, - ) { - match message { - FlowEditorMessage::RunFlow => { - return ( - Some(FlowEditorMessageOut::RunFlow( - self.currently_open.as_ref().unwrap().clone(), - )), - None, - ); - } - FlowEditorMessage::WriteFileToDisk(path, then) => { - self.current_path = Some(path.with_extension("taflow")); - - if let Err(e) = self.write_to_disk() { - return ( - None, - Some(iced::Command::perform( - rfd::AsyncMessageDialog::new() - .set_title("Failed to save") - .set_description(format!("Failed to save file: {e}")) - .show(), - |_| super::AppMessage::NoOp, - )), - ); - } - - return (None, Some(self.do_then(then))); - } - FlowEditorMessage::DoPostSaveActions(then) => { - return (None, Some(self.do_then(then))); - } - FlowEditorMessage::SaveFlow(then) => { - return (None, Some(self.save_flow(false, then))); - } - FlowEditorMessage::SaveAsFlow(then) => { - return (None, Some(self.save_flow(true, then))); - } - FlowEditorMessage::CloseFlow => { - return ( - None, - Some(self.offer_to_save_default_error_handling(SaveFlowThen::Close)), - ); - } - - FlowEditorMessage::StepCreate(action) => { - self.currently_open - .as_mut() - .unwrap() - .actions - .push(ActionConfiguration::from(action.base_action)); - self.modified(); - self.add_action_combo.unfocus(); - } - FlowEditorMessage::StepParameterSourceChange(idx, id, new_source) => { - self.currently_open.as_mut().unwrap().actions[idx] - .parameter_sources - .entry(id) - .and_modify(|v| { - *v = new_source; - }); - self.modified(); - } - FlowEditorMessage::StepParameterValueChange(idx, id, new_value) => { - self.currently_open.as_mut().unwrap().actions[idx] - .parameter_values - .entry(id) - .and_modify(|v| match v { - ParameterValue::String(v) => *v = new_value, - ParameterValue::Integer(v) => *v = new_value.parse().unwrap_or(*v), - ParameterValue::Decimal(v) => *v = new_value.parse().unwrap_or(*v), - ParameterValue::Boolean(v) => *v = new_value.to_ascii_lowercase() == "yes", - }); - self.modified(); - } - FlowEditorMessage::StepMoveUp(idx) => { - let flow = self.currently_open.as_mut().unwrap(); - let steps = &mut flow.actions; - let val = steps.remove(idx); - steps.insert((idx - 1).max(0), val); - - // Swap idx and (idx - 1) - for action_config in flow.actions.iter_mut() { - for (_, src) in action_config.parameter_sources.iter_mut() { - if let ActionParameterSource::FromOutput(p_idx, _) = src { - if *p_idx == idx { - *p_idx = idx - 1; - } else if *p_idx == (idx - 1) { - *p_idx = idx; - } - } - } - } - - self.modified(); - } - FlowEditorMessage::StepMoveDown(idx) => { - let flow = self.currently_open.as_mut().unwrap(); - let steps = &mut flow.actions; - let val = steps.remove(idx); - steps.insert((idx + 1).min(steps.len()), val); - - // Swap idx and (idx + 1) - for action_config in flow.actions.iter_mut() { - for (_, src) in action_config.parameter_sources.iter_mut() { - if let ActionParameterSource::FromOutput(p_idx, _) = src { - if *p_idx == idx { - *p_idx = idx + 1; - } else if *p_idx == (idx + 1) { - *p_idx = idx; - } - } - } - } - - self.modified(); - } - FlowEditorMessage::StepDelete(idx) => { - let flow = self.currently_open.as_mut().unwrap(); - flow.actions.remove(idx); - - // Reset instruction parameters that referred to idx to Literal - // Renumber all items after idx to (idx - 1). - for action_config in flow.actions.iter_mut() { - for src in action_config.parameter_sources.values_mut() { - if let ActionParameterSource::FromOutput(p_idx, _) = src { - match (*p_idx).cmp(&idx) { - std::cmp::Ordering::Equal => *src = ActionParameterSource::Literal, - std::cmp::Ordering::Greater => *p_idx -= 1, - _ => (), - } - } - } - } - - self.modified(); - } - }; - self.update_outputs(); - (None, None) - } -} diff --git a/testangel/src/ui/flow_running.rs b/testangel/src/ui/flow_running.rs deleted file mode 100644 index 1b89cee..0000000 --- a/testangel/src/ui/flow_running.rs +++ /dev/null @@ -1,169 +0,0 @@ -use std::{ - collections::HashMap, - path::PathBuf, - sync::Arc, - thread::{self, JoinHandle}, - time::Duration, -}; - -use iced::widget::{Container, Text}; -use testangel::{ - action_loader::ActionMap, ipc::EngineList, report_generation, types::AutomationFlow, -}; -use testangel_ipc::prelude::{Evidence, EvidenceContent, ParameterValue}; - -use super::UiComponent; - -#[derive(Clone, Debug)] -pub enum FlowRunningMessage { - Tick, - Save(Option, Vec), -} - -#[derive(Clone, Debug)] -pub enum FlowRunningMessageOut { - BackToEditor, - SaveFlowReport(Vec), -} - -#[derive(Default)] -pub struct FlowRunning { - actions_list: Arc, - engines_list: Arc, - - thread: Option>>>, - is_saving: bool, -} - -impl FlowRunning { - pub fn new(actions_list: Arc, engines_list: Arc) -> Self { - Self { - actions_list, - engines_list, - thread: None, - is_saving: false, - } - } - - pub(crate) fn start_flow(&mut self, flow: AutomationFlow) { - self.is_saving = false; - let actions_list = self.actions_list.clone(); - let engines_list = self.engines_list.clone(); - self.thread = Some(thread::spawn(move || { - let mut outputs: Vec> = Vec::new(); - let mut evidence = Vec::new(); - - for engine in engines_list.inner() { - if engine.reset_state().is_err() { - evidence.push(Evidence { - label: String::from("WARNING: State Warning"), - content: EvidenceContent::Textual(String::from("For this test execution, the state couldn't be correctly reset. Some results may not be accurate.")) - }); - } - } - - for (step, action_config) in flow.actions.iter().enumerate() { - let mut proceed = true; - match action_config.execute( - actions_list.clone(), - engines_list.clone(), - outputs.clone(), - ) { - Ok((output, ev)) => { - outputs.push(output); - evidence = [evidence, ev].concat(); - } - Err(e) => { - rfd::MessageDialog::new() - .set_level(rfd::MessageLevel::Error) - .set_title("Failed to execute") - .set_description(&format!( - "Failed to execute flow at step {}: {e}", - step + 1 - )) - .show(); - proceed = false; - } - } - if !proceed { - return None; - } - } - - Some(evidence) - })); - } - - pub(crate) fn update_action_map(&mut self, actions_list: Arc) { - self.actions_list = actions_list; - } -} - -impl UiComponent for FlowRunning { - type Message = FlowRunningMessage; - type MessageOut = FlowRunningMessageOut; - - fn title(&self) -> Option<&str> { - Some("Flow Running") - } - - fn subscription(&self) -> iced::Subscription { - iced::time::every(Duration::from_millis(500)).map(|_| FlowRunningMessage::Tick) - } - - fn update( - &mut self, - message: Self::Message, - ) -> ( - Option, - Option>, - ) { - match message { - FlowRunningMessage::Tick => { - if let Some(thread) = &self.thread { - if thread.is_finished() { - self.is_saving = true; - if let Some(evidence) = self.thread.take().unwrap().join().unwrap() { - return (Some(FlowRunningMessageOut::SaveFlowReport(evidence)), None); - } - return (Some(FlowRunningMessageOut::BackToEditor), None); - } - } - } - - FlowRunningMessage::Save(to, evidence) => { - if let Some(path) = to { - if let Err(e) = - report_generation::save_report(path.with_extension("pdf"), evidence) - { - return ( - None, - Some(iced::Command::perform( - rfd::AsyncMessageDialog::new() - .set_title("Failed") - .set_description(format!("Failed to generate report: {e}")) - .set_level(rfd::MessageLevel::Error) - .show(), - |_| super::AppMessage::NoOp, - )), - ); - } else if let Err(e) = opener::open(path.with_extension("pdf")) { - log::warn!("Failed to open evidence: {e}"); - } - } - return (Some(FlowRunningMessageOut::BackToEditor), None); - } - } - (None, None) - } - - fn view(&self) -> iced::Element<'_, Self::Message> { - Container::new(Text::new(if self.is_saving { - "Saving report..." - } else { - "Flow running..." - })) - .padding(32) - .into() - } -} diff --git a/testangel/src/next_ui/flows/action_component.rs b/testangel/src/ui/flows/action_component.rs similarity index 97% rename from testangel/src/next_ui/flows/action_component.rs rename to testangel/src/ui/flows/action_component.rs index e1c5c92..831cb67 100644 --- a/testangel/src/next_ui/flows/action_component.rs +++ b/testangel/src/ui/flows/action_component.rs @@ -11,7 +11,7 @@ use relm4::{ use testangel::types::{Action, ActionConfiguration, ActionParameterSource}; use testangel_ipc::prelude::{ParameterKind, ParameterValue}; -use crate::next_ui::{ +use crate::ui::{ components::variable_row::{ ParameterSourceTrait, VariableRow, VariableRowInit, VariableRowParentInput, }, diff --git a/testangel/src/next_ui/flows/execution_dialog.rs b/testangel/src/ui/flows/execution_dialog.rs similarity index 96% rename from testangel/src/next_ui/flows/execution_dialog.rs rename to testangel/src/ui/flows/execution_dialog.rs index 48b81e6..fc7afe8 100644 --- a/testangel/src/next_ui/flows/execution_dialog.rs +++ b/testangel/src/ui/flows/execution_dialog.rs @@ -1,226 +1,226 @@ -use std::{collections::HashMap, sync::Arc}; - -use adw::prelude::*; -use relm4::{adw, gtk, Component, ComponentParts, RelmWidgetExt}; -use testangel::{ - action_loader::ActionMap, - ipc::EngineList, - report_generation::{self, ReportGenerationError}, - types::{AutomationFlow, FlowError}, -}; -use testangel_ipc::prelude::{Evidence, EvidenceContent, ParameterValue}; - -use crate::next_ui::{file_filters, lang}; - -#[derive(Debug)] -pub enum ExecutionDialogCommandOutput { - /// Execution completed with the resulting evidence - Complete(Vec), - - /// Execution failed at the given step and for the given reason - Failed(usize, FlowError), -} - -#[derive(Debug)] -pub struct ExecutionDialogInit { - pub flow: AutomationFlow, - pub engine_list: Arc, - pub action_map: Arc, -} - -#[derive(Debug)] -pub enum ExecutionDialogInput { - Close, - FailedToGenerateReport(ReportGenerationError), -} - -#[derive(Debug)] -pub struct ExecutionDialog; - -impl ExecutionDialog { - /// Create the absolute barebones of a message dialog, allowing for custom button and response mapping. - fn create_message_dialog(&self, title: S, message: S) -> adw::MessageDialog - where - S: AsRef, - { - adw::MessageDialog::builder() - .title(title.as_ref()) - .heading(title.as_ref()) - .body(message.as_ref()) - .modal(true) - .build() - } -} - -#[relm4::component(pub)] -impl Component for ExecutionDialog { - type Init = ExecutionDialogInit; - type Input = ExecutionDialogInput; - type Output = (); - type CommandOutput = ExecutionDialogCommandOutput; - - view! { - #[root] - adw::Window { - set_modal: true, - set_resizable: false, - - gtk::Box { - set_orientation: gtk::Orientation::Vertical, - set_spacing: 5, - set_margin_all: 50, - - gtk::Spinner { - set_spinning: true, - }, - gtk::Label { - set_label: &lang::lookup("flow-execution-running"), - }, - }, - }, - } - - fn init( - init: Self::Init, - root: &Self::Root, - sender: relm4::ComponentSender, - ) -> relm4::ComponentParts { - let model = ExecutionDialog; - let widgets = view_output!(); - let flow = init.flow; - let engine_list = init.engine_list.clone(); - let action_map = init.action_map.clone(); - - sender.spawn_oneshot_command(move || { - let mut outputs: Vec> = Vec::new(); - let mut evidence = Vec::new(); - - for engine in engine_list.inner() { - if engine.reset_state().is_err() { - evidence.push(Evidence { - label: String::from("WARNING: State Warning"), - content: EvidenceContent::Textual(String::from("For this test execution, the state couldn't be correctly reset. Some results may not be accurate.")) - }); - } - } - - for (step, action_config) in flow.actions.iter().enumerate() { - match action_config.execute( - action_map.clone(), - engine_list.clone(), - outputs.clone(), - ) { - Ok((output, ev)) => { - outputs.push(output); - evidence = [evidence, ev].concat(); - } - Err(e) => { - return ExecutionDialogCommandOutput::Failed(step + 1, e); - } - } - } - - ExecutionDialogCommandOutput::Complete(evidence) - }); - - ComponentParts { model, widgets } - } - - fn update( - &mut self, - message: Self::Input, - sender: relm4::ComponentSender, - root: &Self::Root, - ) { - match message { - ExecutionDialogInput::Close => root.destroy(), - ExecutionDialogInput::FailedToGenerateReport(reason) => { - let dialog = self.create_message_dialog( - lang::lookup("report-failed"), - lang::lookup_with_args("report-failed-message", { - let mut map = HashMap::new(); - map.insert("reason", reason.to_string().into()); - map - }), - ); - dialog.set_transient_for(Some(root)); - dialog.add_response("ok", &lang::lookup("ok")); - dialog.set_default_response(Some("ok")); - let sender_c = sender.clone(); - dialog.connect_response(None, move |dlg, _response| { - sender_c.input(ExecutionDialogInput::Close); - dlg.close(); - }); - dialog.set_visible(true); - } - } - } - - fn update_cmd( - &mut self, - message: Self::CommandOutput, - sender: relm4::ComponentSender, - root: &Self::Root, - ) { - match message { - ExecutionDialogCommandOutput::Complete(evidence) => { - log::info!("Execution complete."); - - // Present save dialog - let dialog = gtk::FileDialog::builder() - .modal(true) - .title(lang::lookup("report-save-title")) - .initial_name(lang::lookup("report-default-name")) - .filters(&file_filters::filter_list(vec![ - file_filters::pdfs(), - file_filters::all(), - ])) - .build(); - - let sender_c = sender.clone(); - dialog.save( - Some(root), - Some(&relm4::gtk::gio::Cancellable::new()), - move |res| { - if let Ok(file) = res { - let path = file.path().unwrap(); - if let Err(e) = report_generation::save_report( - path.with_extension("pdf"), - evidence.clone(), - ) { - // Failed to generate report - sender_c.input(ExecutionDialogInput::FailedToGenerateReport(e)); - return; - } else if let Err(e) = opener::open(path.with_extension("pdf")) { - log::warn!("Failed to open evidence: {e}"); - } - } - sender_c.input(ExecutionDialogInput::Close); - }, - ); - } - - ExecutionDialogCommandOutput::Failed(step, reason) => { - log::warn!("Execution failed"); - let dialog = self.create_message_dialog( - lang::lookup("flow-execution-failed"), - lang::lookup_with_args("flow-execution-failed-message", { - let mut map = HashMap::new(); - map.insert("step", step.into()); - map.insert("reason", reason.to_string().into()); - map - }), - ); - dialog.set_transient_for(Some(root)); - dialog.add_response("ok", &lang::lookup("ok")); - dialog.set_default_response(Some("ok")); - let sender_c = sender.clone(); - dialog.connect_response(None, move |dlg, _response| { - sender_c.input(ExecutionDialogInput::Close); - dlg.close(); - }); - dialog.set_visible(true); - } - } - } -} +use std::{collections::HashMap, sync::Arc}; + +use adw::prelude::*; +use relm4::{adw, gtk, Component, ComponentParts, RelmWidgetExt}; +use testangel::{ + action_loader::ActionMap, + ipc::EngineList, + report_generation::{self, ReportGenerationError}, + types::{AutomationFlow, FlowError}, +}; +use testangel_ipc::prelude::{Evidence, EvidenceContent, ParameterValue}; + +use crate::ui::{file_filters, lang}; + +#[derive(Debug)] +pub enum ExecutionDialogCommandOutput { + /// Execution completed with the resulting evidence + Complete(Vec), + + /// Execution failed at the given step and for the given reason + Failed(usize, FlowError), +} + +#[derive(Debug)] +pub struct ExecutionDialogInit { + pub flow: AutomationFlow, + pub engine_list: Arc, + pub action_map: Arc, +} + +#[derive(Debug)] +pub enum ExecutionDialogInput { + Close, + FailedToGenerateReport(ReportGenerationError), +} + +#[derive(Debug)] +pub struct ExecutionDialog; + +impl ExecutionDialog { + /// Create the absolute barebones of a message dialog, allowing for custom button and response mapping. + fn create_message_dialog(&self, title: S, message: S) -> adw::MessageDialog + where + S: AsRef, + { + adw::MessageDialog::builder() + .title(title.as_ref()) + .heading(title.as_ref()) + .body(message.as_ref()) + .modal(true) + .build() + } +} + +#[relm4::component(pub)] +impl Component for ExecutionDialog { + type Init = ExecutionDialogInit; + type Input = ExecutionDialogInput; + type Output = (); + type CommandOutput = ExecutionDialogCommandOutput; + + view! { + #[root] + adw::Window { + set_modal: true, + set_resizable: false, + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 5, + set_margin_all: 50, + + gtk::Spinner { + set_spinning: true, + }, + gtk::Label { + set_label: &lang::lookup("flow-execution-running"), + }, + }, + }, + } + + fn init( + init: Self::Init, + root: &Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + let model = ExecutionDialog; + let widgets = view_output!(); + let flow = init.flow; + let engine_list = init.engine_list.clone(); + let action_map = init.action_map.clone(); + + sender.spawn_oneshot_command(move || { + let mut outputs: Vec> = Vec::new(); + let mut evidence = Vec::new(); + + for engine in engine_list.inner() { + if engine.reset_state().is_err() { + evidence.push(Evidence { + label: String::from("WARNING: State Warning"), + content: EvidenceContent::Textual(String::from("For this test execution, the state couldn't be correctly reset. Some results may not be accurate.")) + }); + } + } + + for (step, action_config) in flow.actions.iter().enumerate() { + match action_config.execute( + action_map.clone(), + engine_list.clone(), + outputs.clone(), + ) { + Ok((output, ev)) => { + outputs.push(output); + evidence = [evidence, ev].concat(); + } + Err(e) => { + return ExecutionDialogCommandOutput::Failed(step + 1, e); + } + } + } + + ExecutionDialogCommandOutput::Complete(evidence) + }); + + ComponentParts { model, widgets } + } + + fn update( + &mut self, + message: Self::Input, + sender: relm4::ComponentSender, + root: &Self::Root, + ) { + match message { + ExecutionDialogInput::Close => root.destroy(), + ExecutionDialogInput::FailedToGenerateReport(reason) => { + let dialog = self.create_message_dialog( + lang::lookup("report-failed"), + lang::lookup_with_args("report-failed-message", { + let mut map = HashMap::new(); + map.insert("reason", reason.to_string().into()); + map + }), + ); + dialog.set_transient_for(Some(root)); + dialog.add_response("ok", &lang::lookup("ok")); + dialog.set_default_response(Some("ok")); + let sender_c = sender.clone(); + dialog.connect_response(None, move |dlg, _response| { + sender_c.input(ExecutionDialogInput::Close); + dlg.close(); + }); + dialog.set_visible(true); + } + } + } + + fn update_cmd( + &mut self, + message: Self::CommandOutput, + sender: relm4::ComponentSender, + root: &Self::Root, + ) { + match message { + ExecutionDialogCommandOutput::Complete(evidence) => { + log::info!("Execution complete."); + + // Present save dialog + let dialog = gtk::FileDialog::builder() + .modal(true) + .title(lang::lookup("report-save-title")) + .initial_name(lang::lookup("report-default-name")) + .filters(&file_filters::filter_list(vec![ + file_filters::pdfs(), + file_filters::all(), + ])) + .build(); + + let sender_c = sender.clone(); + dialog.save( + Some(root), + Some(&relm4::gtk::gio::Cancellable::new()), + move |res| { + if let Ok(file) = res { + let path = file.path().unwrap(); + if let Err(e) = report_generation::save_report( + path.with_extension("pdf"), + evidence.clone(), + ) { + // Failed to generate report + sender_c.input(ExecutionDialogInput::FailedToGenerateReport(e)); + return; + } else if let Err(e) = opener::open(path.with_extension("pdf")) { + log::warn!("Failed to open evidence: {e}"); + } + } + sender_c.input(ExecutionDialogInput::Close); + }, + ); + } + + ExecutionDialogCommandOutput::Failed(step, reason) => { + log::warn!("Execution failed"); + let dialog = self.create_message_dialog( + lang::lookup("flow-execution-failed"), + lang::lookup_with_args("flow-execution-failed-message", { + let mut map = HashMap::new(); + map.insert("step", step.into()); + map.insert("reason", reason.to_string().into()); + map + }), + ); + dialog.set_transient_for(Some(root)); + dialog.add_response("ok", &lang::lookup("ok")); + dialog.set_default_response(Some("ok")); + let sender_c = sender.clone(); + dialog.connect_response(None, move |dlg, _response| { + sender_c.input(ExecutionDialogInput::Close); + dlg.close(); + }); + dialog.set_visible(true); + } + } + } +} diff --git a/testangel/src/next_ui/flows/header.rs b/testangel/src/ui/flows/header.rs similarity index 90% rename from testangel/src/next_ui/flows/header.rs rename to testangel/src/ui/flows/header.rs index 2796b2b..5dad166 100644 --- a/testangel/src/next_ui/flows/header.rs +++ b/testangel/src/ui/flows/header.rs @@ -1,324 +1,312 @@ -use std::sync::Arc; - -use adw::prelude::*; -use relm4::{ - actions::{AccelsPlus, RelmAction, RelmActionGroup}, - adw, - factory::FactoryVecDeque, - gtk, Component, ComponentController, ComponentParts, ComponentSender, RelmWidgetExt, -}; -use testangel::{action_loader::ActionMap, ipc::EngineList}; - -use crate::next_ui::{ - components::add_step_factory::{AddStepInit, AddStepResult, AddStepTrait}, - lang, -}; - -#[derive(Debug)] -pub struct FlowsHeader { - engine_list: Arc, - action_map: Arc, - add_button: gtk::MenuButton, - flow_open: bool, - search_results: FactoryVecDeque>, -} - -#[derive(Debug)] -pub enum FlowsHeaderOutput { - NewFlow, - OpenFlow, - SaveFlow, - SaveAsFlow, - CloseFlow, - RunFlow, - AddStep(String), -} - -#[derive(Debug)] -pub enum FlowsHeaderInput { - OpenAboutDialog, - ActionsMapChanged(Arc), - /// Add the step with the action ID given - AddStep(String), - /// Trigger a search for the steps provided - SearchForSteps(String), - /// Add the top search result to the flow. - AddTopSearchResult, - /// Inform the header bar if a flow is open or not. - ChangeFlowOpen(bool), -} - -impl AddStepTrait for FlowsHeaderInput { - fn add_step(value: String) -> Self { - Self::AddStep(value) - } -} - -#[relm4::component(pub)] -impl Component for FlowsHeader { - type Init = (Arc, Arc); - type Input = FlowsHeaderInput; - type Output = FlowsHeaderOutput; - type CommandOutput = (); - - view! { - #[root] - #[name = "start"] - gtk::Box { - set_spacing: 5, - - #[local_ref] - add_button -> gtk::MenuButton { - set_icon_name: relm4_icons::icon_name::PLUS, - set_tooltip: &lang::lookup("flow-header-add"), - - #[wrap(Some)] - #[name = "menu_popover"] - set_popover = >k::Popover { - gtk::Box { - set_spacing: 2, - set_orientation: gtk::Orientation::Vertical, - - gtk::SearchEntry { - set_max_width_chars: 20, - - connect_activate[sender] => move |_| { - sender.input(FlowsHeaderInput::AddTopSearchResult); - }, - - connect_search_changed[sender] => move |slf| { - let query = slf.text().to_string(); - sender.input(FlowsHeaderInput::SearchForSteps(query)); - }, - }, - - gtk::ScrolledWindow { - set_hscrollbar_policy: gtk::PolicyType::Never, - set_min_content_height: 150, - - #[local_ref] - results_box -> gtk::Box { - set_spacing: 2, - set_orientation: gtk::Orientation::Vertical, - }, - }, - }, - }, - }, - gtk::Button { - set_icon_name: relm4_icons::icon_name::PLAY, - set_tooltip: &lang::lookup("flow-header-run"), - #[watch] - set_sensitive: model.flow_open, - connect_clicked[sender] => move |_| { - // unwrap rationale: receivers will never be dropped - sender.output(FlowsHeaderOutput::RunFlow).unwrap(); - }, - }, - }, - - #[name = "end"] - gtk::Box { - set_spacing: 5, - - gtk::MenuButton { - set_icon_name: relm4_icons::icon_name::MENU, - set_tooltip: &lang::lookup("flow-header-more"), - set_direction: gtk::ArrowType::Down, - - #[wrap(Some)] - set_popover = >k::PopoverMenu::from_model(Some(&flows_menu)) { - set_position: gtk::PositionType::Bottom, - }, - }, - }, - } - - menu! { - flows_menu: { - &lang::lookup("flow-header-new") => FlowsNewAction, - &lang::lookup("flow-header-open") => FlowsOpenAction, - &lang::lookup("flow-header-save") => FlowsSaveAction, - &lang::lookup("flow-header-save-as") => FlowsSaveAsAction, - &lang::lookup("flow-header-close") => FlowsCloseAction, - section! { - &lang::lookup("flow-header-about") => FlowsAboutAction, - } - } - } - - fn init( - init: Self::Init, - root: &Self::Root, - sender: ComponentSender, - ) -> ComponentParts { - let model = FlowsHeader { - engine_list: init.0, - action_map: init.1, - flow_open: false, - add_button: gtk::MenuButton::default(), - search_results: FactoryVecDeque::new(gtk::Box::default(), sender.input_sender()), - }; - // Reset search results - sender.input(FlowsHeaderInput::SearchForSteps(String::new())); - - let results_box = model.search_results.widget(); - let add_button = &model.add_button; - let widgets = view_output!(); - - let sender_c = sender.clone(); - let new_action: RelmAction = RelmAction::new_stateless(move |_| { - // unwrap rationale: receiver will never be disconnected - sender_c.output(FlowsHeaderOutput::NewFlow).unwrap(); - }); - relm4::main_application().set_accelerators_for_action::(&["N"]); - - let sender_c = sender.clone(); - let open_action: RelmAction = RelmAction::new_stateless(move |_| { - // unwrap rationale: receiver will never be disconnected - sender_c.output(FlowsHeaderOutput::OpenFlow).unwrap(); - }); - relm4::main_application().set_accelerators_for_action::(&["O"]); - - let sender_c = sender.clone(); - let save_action: RelmAction = RelmAction::new_stateless(move |_| { - // unwrap rationale: receiver will never be disconnected - sender_c.output(FlowsHeaderOutput::SaveFlow).unwrap(); - }); - relm4::main_application().set_accelerators_for_action::(&["S"]); - - let sender_c = sender.clone(); - let save_as_action: RelmAction = RelmAction::new_stateless(move |_| { - // unwrap rationale: receiver will never be disconnected - sender_c.output(FlowsHeaderOutput::SaveAsFlow).unwrap(); - }); - relm4::main_application() - .set_accelerators_for_action::(&["S"]); - - let sender_c = sender.clone(); - let close_action: RelmAction = RelmAction::new_stateless(move |_| { - // unwrap rationale: receiver will never be disconnected - sender_c.output(FlowsHeaderOutput::CloseFlow).unwrap(); - }); - relm4::main_application().set_accelerators_for_action::(&["W"]); - - let sender_c = sender.clone(); - let about_action: RelmAction = RelmAction::new_stateless(move |_| { - sender_c.input(FlowsHeaderInput::OpenAboutDialog); - }); - relm4::main_application().set_accelerators_for_action::(&["A"]); - - let mut group = RelmActionGroup::::new(); - group.add_action(new_action); - group.add_action(open_action); - group.add_action(save_action); - group.add_action(save_as_action); - group.add_action(close_action); - group.add_action(about_action); - group.register_for_widget(&widgets.end); - - ComponentParts { model, widgets } - } - - fn update_with_view( - &mut self, - widgets: &mut Self::Widgets, - message: Self::Input, - sender: ComponentSender, - root: &Self::Root, - ) { - match message { - FlowsHeaderInput::ChangeFlowOpen(now) => { - self.flow_open = now; - } - FlowsHeaderInput::OpenAboutDialog => { - crate::next_ui::about::AppAbout::builder() - .transient_for(root) - .launch((self.engine_list.clone(), self.action_map.clone())) - .widget() - .set_visible(true); - } - FlowsHeaderInput::ActionsMapChanged(new_map) => { - self.action_map = new_map; - } - FlowsHeaderInput::AddStep(step_id) => { - // close popover - self.add_button.popdown(); - // unwrap rationale: the receiver will never be disconnected - sender.output(FlowsHeaderOutput::AddStep(step_id)).unwrap(); - } - FlowsHeaderInput::AddTopSearchResult => { - if let Some(result) = self.search_results.get(0) { - widgets.menu_popover.popdown(); - let id = result.value(); - // unwrap rationale: the receiver will never be disconnected - sender.output(FlowsHeaderOutput::AddStep(id)).unwrap(); - } - } - FlowsHeaderInput::SearchForSteps(query) => { - let mut results = self.search_results.guard(); - results.clear(); - - let show_hidden = std::env::var("TA_SHOW_HIDDEN_ACTIONS") - .unwrap_or("no".to_string()) - .eq_ignore_ascii_case("yes"); - // Collect results - if query.is_empty() { - // List all alphabetically - let mut unsorted_results = vec![]; - for (group, actions) in self.action_map.get_by_group() { - for action in actions { - if action.visible || show_hidden { - unsorted_results - .push((format!("{group}: {}", action.friendly_name), action)); - } - } - } - - // Sort - unsorted_results.sort_by(|(a, _a), (b, _b)| a.cmp(b)); - for (_, a) in unsorted_results { - results.push_back(AddStepInit { - label: format!("{}: {}", a.group, a.friendly_name), - value: a.id, - }); - } - } else { - let mut unsorted_results = vec![]; - use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; - let matcher = SkimMatcherV2::default(); - for (group, actions) in self.action_map.get_by_group() { - for action in actions { - if action.visible || show_hidden { - if let Some(score) = matcher.fuzzy_match( - &format!("{group}: {}", action.friendly_name), - &query, - ) { - unsorted_results.push((score, action)); - } - } - } - } - - // Sort - unsorted_results.sort_by(|(a, _a), (b, _b)| a.cmp(b)); - for (_, a) in unsorted_results { - results.push_back(AddStepInit { - label: format!("{}: {}", a.group, a.friendly_name), - value: a.id, - }); - } - } - } - } - self.update_view(widgets, sender); - } -} - -relm4::new_action_group!(FlowsActionGroup, "flows"); -relm4::new_stateless_action!(FlowsNewAction, FlowsActionGroup, "new"); -relm4::new_stateless_action!(FlowsOpenAction, FlowsActionGroup, "open"); -relm4::new_stateless_action!(FlowsSaveAction, FlowsActionGroup, "save"); -relm4::new_stateless_action!(FlowsSaveAsAction, FlowsActionGroup, "save-as"); -relm4::new_stateless_action!(FlowsCloseAction, FlowsActionGroup, "close"); -relm4::new_stateless_action!(FlowsAboutAction, FlowsActionGroup, "about"); +use std::sync::Arc; + +use adw::prelude::*; +use relm4::{ + actions::{AccelsPlus, RelmAction, RelmActionGroup}, + adw, + factory::FactoryVecDeque, + gtk, Component, ComponentParts, ComponentSender, RelmWidgetExt, +}; +use testangel::action_loader::ActionMap; + +use crate::ui::{ + components::add_step_factory::{AddStepInit, AddStepResult, AddStepTrait}, + lang, +}; + +#[derive(Debug)] +pub struct FlowsHeader { + action_map: Arc, + add_button: gtk::MenuButton, + flow_open: bool, + search_results: FactoryVecDeque>, +} + +#[derive(Debug)] +pub enum FlowsHeaderOutput { + NewFlow, + OpenFlow, + SaveFlow, + SaveAsFlow, + CloseFlow, + RunFlow, + AddStep(String), +} + +impl std::fmt::Debug for FlowsActionGroup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "FlowsActionGroup") + } +} + +#[derive(Debug)] +pub enum FlowsHeaderInput { + ActionsMapChanged(Arc), + /// Add the step with the action ID given + AddStep(String), + /// Trigger a search for the steps provided + SearchForSteps(String), + /// Add the top search result to the flow. + AddTopSearchResult, + /// Inform the header bar if a flow is open or not. + ChangeFlowOpen(bool), +} + +impl AddStepTrait for FlowsHeaderInput { + fn add_step(value: String) -> Self { + Self::AddStep(value) + } +} + +#[relm4::component(pub)] +impl Component for FlowsHeader { + type Init = Arc; + type Input = FlowsHeaderInput; + type Output = FlowsHeaderOutput; + type CommandOutput = (); + + view! { + #[root] + #[name = "start"] + gtk::Box { + set_spacing: 5, + + #[local_ref] + add_button -> gtk::MenuButton { + set_icon_name: relm4_icons::icon_name::PLUS, + set_tooltip: &lang::lookup("flow-header-add"), + + #[wrap(Some)] + #[name = "menu_popover"] + set_popover = >k::Popover { + gtk::Box { + set_spacing: 2, + set_orientation: gtk::Orientation::Vertical, + + gtk::SearchEntry { + set_max_width_chars: 20, + + connect_activate[sender] => move |_| { + sender.input(FlowsHeaderInput::AddTopSearchResult); + }, + + connect_search_changed[sender] => move |slf| { + let query = slf.text().to_string(); + sender.input(FlowsHeaderInput::SearchForSteps(query)); + }, + }, + + gtk::ScrolledWindow { + set_hscrollbar_policy: gtk::PolicyType::Never, + set_min_content_height: 150, + + #[local_ref] + results_box -> gtk::Box { + set_spacing: 2, + set_orientation: gtk::Orientation::Vertical, + }, + }, + }, + }, + }, + gtk::Button { + set_icon_name: relm4_icons::icon_name::PLAY, + set_tooltip: &lang::lookup("flow-header-run"), + #[watch] + set_sensitive: model.flow_open, + connect_clicked[sender] => move |_| { + // unwrap rationale: receivers will never be dropped + sender.output(FlowsHeaderOutput::RunFlow).unwrap(); + }, + }, + }, + + #[name = "end"] + gtk::Box { + set_spacing: 5, + + gtk::MenuButton { + set_icon_name: relm4_icons::icon_name::MENU, + set_tooltip: &lang::lookup("flow-header-more"), + set_direction: gtk::ArrowType::Down, + + #[wrap(Some)] + set_popover = >k::PopoverMenu::from_model(Some(&flows_menu)) { + set_position: gtk::PositionType::Bottom, + }, + }, + }, + } + + menu! { + flows_menu: { + &lang::lookup("flow-header-new") => FlowsNewAction, + &lang::lookup("flow-header-open") => FlowsOpenAction, + &lang::lookup("flow-header-save") => FlowsSaveAction, + &lang::lookup("flow-header-save-as") => FlowsSaveAsAction, + &lang::lookup("flow-header-close") => FlowsCloseAction, + section! { + &lang::lookup("header-about") => crate::ui::header_bar::GeneralAboutAction, + } + } + } + + fn init( + init: Self::Init, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let model = FlowsHeader { + action_map: init, + flow_open: false, + add_button: gtk::MenuButton::default(), + search_results: FactoryVecDeque::new(gtk::Box::default(), sender.input_sender()), + }; + // Reset search results + sender.input(FlowsHeaderInput::SearchForSteps(String::new())); + + let results_box = model.search_results.widget(); + let add_button = &model.add_button; + let widgets = view_output!(); + + let sender_c = sender.clone(); + let new_action: RelmAction = RelmAction::new_stateless(move |_| { + // unwrap rationale: receiver will never be disconnected + sender_c.output(FlowsHeaderOutput::NewFlow).unwrap(); + }); + relm4::main_application().set_accelerators_for_action::(&["N"]); + + let sender_c = sender.clone(); + let open_action: RelmAction = RelmAction::new_stateless(move |_| { + // unwrap rationale: receiver will never be disconnected + sender_c.output(FlowsHeaderOutput::OpenFlow).unwrap(); + }); + relm4::main_application().set_accelerators_for_action::(&["O"]); + + let sender_c = sender.clone(); + let save_action: RelmAction = RelmAction::new_stateless(move |_| { + // unwrap rationale: receiver will never be disconnected + sender_c.output(FlowsHeaderOutput::SaveFlow).unwrap(); + }); + relm4::main_application().set_accelerators_for_action::(&["S"]); + + let sender_c = sender.clone(); + let save_as_action: RelmAction = RelmAction::new_stateless(move |_| { + // unwrap rationale: receiver will never be disconnected + sender_c.output(FlowsHeaderOutput::SaveAsFlow).unwrap(); + }); + relm4::main_application() + .set_accelerators_for_action::(&["S"]); + + let sender_c = sender.clone(); + let close_action: RelmAction = RelmAction::new_stateless(move |_| { + // unwrap rationale: receiver will never be disconnected + sender_c.output(FlowsHeaderOutput::CloseFlow).unwrap(); + }); + relm4::main_application().set_accelerators_for_action::(&["W"]); + + let mut group = RelmActionGroup::::new(); + group.add_action(new_action); + group.add_action(open_action); + group.add_action(save_action); + group.add_action(save_as_action); + group.add_action(close_action); + group.register_for_widget(&widgets.end); + + ComponentParts { model, widgets } + } + + fn update_with_view( + &mut self, + widgets: &mut Self::Widgets, + message: Self::Input, + sender: ComponentSender, + _root: &Self::Root, + ) { + match message { + FlowsHeaderInput::ChangeFlowOpen(now) => { + self.flow_open = now; + } + FlowsHeaderInput::ActionsMapChanged(new_map) => { + self.action_map = new_map; + } + FlowsHeaderInput::AddStep(step_id) => { + // close popover + self.add_button.popdown(); + // unwrap rationale: the receiver will never be disconnected + sender.output(FlowsHeaderOutput::AddStep(step_id)).unwrap(); + } + FlowsHeaderInput::AddTopSearchResult => { + if let Some(result) = self.search_results.get(0) { + widgets.menu_popover.popdown(); + let id = result.value(); + // unwrap rationale: the receiver will never be disconnected + sender.output(FlowsHeaderOutput::AddStep(id)).unwrap(); + } + } + FlowsHeaderInput::SearchForSteps(query) => { + let mut results = self.search_results.guard(); + results.clear(); + + let show_hidden = std::env::var("TA_SHOW_HIDDEN_ACTIONS") + .unwrap_or("no".to_string()) + .eq_ignore_ascii_case("yes"); + // Collect results + if query.is_empty() { + // List all alphabetically + let mut unsorted_results = vec![]; + for (group, actions) in self.action_map.get_by_group() { + for action in actions { + if action.visible || show_hidden { + unsorted_results + .push((format!("{group}: {}", action.friendly_name), action)); + } + } + } + + // Sort + unsorted_results.sort_by(|(a, _a), (b, _b)| a.cmp(b)); + for (_, a) in unsorted_results { + results.push_back(AddStepInit { + label: format!("{}: {}", a.group, a.friendly_name), + value: a.id, + }); + } + } else { + let mut unsorted_results = vec![]; + use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; + let matcher = SkimMatcherV2::default(); + for (group, actions) in self.action_map.get_by_group() { + for action in actions { + if action.visible || show_hidden { + if let Some(score) = matcher.fuzzy_match( + &format!("{group}: {}", action.friendly_name), + &query, + ) { + unsorted_results.push((score, action)); + } + } + } + } + + // Sort + unsorted_results.sort_by(|(a, _a), (b, _b)| a.cmp(b)); + for (_, a) in unsorted_results { + results.push_back(AddStepInit { + label: format!("{}: {}", a.group, a.friendly_name), + value: a.id, + }); + } + } + } + } + self.update_view(widgets, sender); + } +} + +relm4::new_action_group!(pub FlowsActionGroup, "flows"); +relm4::new_stateless_action!(FlowsNewAction, FlowsActionGroup, "new"); +relm4::new_stateless_action!(FlowsOpenAction, FlowsActionGroup, "open"); +relm4::new_stateless_action!(FlowsSaveAction, FlowsActionGroup, "save"); +relm4::new_stateless_action!(FlowsSaveAsAction, FlowsActionGroup, "save-as"); +relm4::new_stateless_action!(FlowsCloseAction, FlowsActionGroup, "close"); diff --git a/testangel/src/next_ui/flows/mod.rs b/testangel/src/ui/flows/mod.rs similarity index 96% rename from testangel/src/next_ui/flows/mod.rs rename to testangel/src/ui/flows/mod.rs index 04801cf..51e2337 100644 --- a/testangel/src/next_ui/flows/mod.rs +++ b/testangel/src/ui/flows/mod.rs @@ -348,7 +348,7 @@ impl Component for FlowsModel { ) -> ComponentParts { let header = Rc::new( header::FlowsHeader::builder() - .launch((init.1.clone(), init.0.clone())) + .launch(init.0.clone()) .forward(sender.input_sender(), |msg| match msg { header::FlowsHeaderOutput::NewFlow => FlowInputs::NewFlow, header::FlowsHeaderOutput::OpenFlow => FlowInputs::OpenFlow, @@ -399,6 +399,7 @@ impl Component for FlowsModel { // unwrap rationale: config updates can't happen if nothing is open let flow = self.open_flow.as_mut().unwrap(); flow.actions[step.current_index()] = new_config; + self.needs_saving = true; } FlowInputs::NewFlow => { self.prompt_to_save(sender.input_sender(), FlowInputs::_NewFlow); @@ -545,6 +546,7 @@ impl Component for FlowsModel { flow.actions.push(ActionConfiguration::from( self.action_map.get_action_by_id(&step_id).unwrap(), )); + self.needs_saving = true; // Trigger UI steps refresh sender.input(FlowInputs::UpdateStepsFromModel); } @@ -615,6 +617,8 @@ impl Component for FlowsModel { } } + self.needs_saving = true; + // Trigger UI steps refresh sender.input(FlowInputs::UpdateStepsFromModel); } @@ -634,7 +638,7 @@ impl Component for FlowsModel { // Remove references to step and renumber references above step to one less than they were for step in flow.actions.iter_mut() { - for (_step_idx, source) in step.parameter_sources.iter_mut() { + for (_param_idx, source) in step.parameter_sources.iter_mut() { if let ActionParameterSource::FromOutput(from_step, _output_idx) = source { match (*from_step).cmp(&idx) { std::cmp::Ordering::Equal => *from_step = usize::MAX, @@ -644,6 +648,8 @@ impl Component for FlowsModel { } } } + + self.needs_saving = true; } FlowInputs::PasteStep(idx, config) => { let flow = self.open_flow.as_mut().unwrap(); @@ -669,6 +675,8 @@ impl Component for FlowsModel { } } + self.needs_saving = true; + // Trigger UI steps refresh sender.input(FlowInputs::UpdateStepsFromModel); } diff --git a/testangel/src/ui/get_started.rs b/testangel/src/ui/get_started.rs deleted file mode 100644 index 98b2024..0000000 --- a/testangel/src/ui/get_started.rs +++ /dev/null @@ -1,107 +0,0 @@ -use std::env; - -use iced::widget::{column, row, Button, Container, Rule, Space, Text}; - -use super::UiComponent; - -#[derive(Clone, Debug)] -pub enum GetStartedMessage { - NewFlow, - OpenFlow, - NewAction, - OpenAction, -} - -pub struct GetStarted { - is_latest: bool, - hide_action_editor: bool, - local_help_contact: Option, -} -impl GetStarted { - pub fn set_is_latest(&mut self, is_latest: bool) { - self.is_latest = is_latest; - } -} - -impl Default for GetStarted { - fn default() -> Self { - Self { - is_latest: true, - hide_action_editor: !env::var("TA_HIDE_ACTION_EDITOR") - .unwrap_or("no".to_string()) - .eq_ignore_ascii_case("no"), - local_help_contact: env::var("TA_LOCAL_SUPPORT_CONTACT") - .map(Some) - .unwrap_or(None), - } - } -} - -impl UiComponent for GetStarted { - type Message = GetStartedMessage; - type MessageOut = GetStartedMessage; - - fn title(&self) -> Option<&str> { - None - } - - fn view(&self) -> iced::Element<'_, Self::Message> { - let ae: iced::Element<'_, Self::Message> = if self.hide_action_editor { - Space::new(0, 0).into() - } else { - column![ - Text::new("\nDesign Actions").size(24), - Text::new("Actions are environment specific, granular but high level pieces of automation. They bring together generic low level instructions."), - row![ - Button::new("Create new action") - .on_press(GetStartedMessage::NewAction), - Button::new("Open existing action") - .on_press(GetStartedMessage::OpenAction), - ].spacing(8), - ].spacing(4).into() - }; - - let help: iced::Element<'_, Self::Message> = - Text::new(if let Some(contact) = &self.local_help_contact { - format!("For help, please contact: {contact}") - } else { - String::from("Repository: https://github.com/lilopkins/testangel") - }) - .into(); - - Container::new( - column![ - Text::new("Get Started with TestAngel").size(32), - Rule::horizontal(2), - - Text::new("\nDesign Flows").size(24), - Text::new("Flows allow you to automate project-specific tasks and tests. They bring together high level actions into a complete flow."), - row![ - Button::new("Create new flow") - .on_press(GetStartedMessage::NewFlow), - Button::new("Open existing flow") - .on_press(GetStartedMessage::OpenFlow), - ].spacing(8), - - ae, - - Space::with_height(64), - - help, - Text::new(format!("TestAngel v{}{}", env!("CARGO_PKG_VERSION"), if self.is_latest { "" } else { " - New version available!" })).size(10), - ].spacing(4) - ) - .padding([32, 32, 32, 32]) - .into() - } - - fn update( - &mut self, - message: Self::Message, - ) -> ( - Option, - Option>, - ) { - (Some(message), None) - } -} diff --git a/testangel/src/next_ui/header_bar.rs b/testangel/src/ui/header_bar.rs similarity index 60% rename from testangel/src/next_ui/header_bar.rs rename to testangel/src/ui/header_bar.rs index b05831d..f1378ec 100644 --- a/testangel/src/next_ui/header_bar.rs +++ b/testangel/src/ui/header_bar.rs @@ -1,24 +1,41 @@ -use std::rc::Rc; +use std::{rc::Rc, sync::Arc}; use gtk::prelude::*; use relm4::{ + actions::{AccelsPlus, RelmAction, RelmActionGroup}, adw, gtk, Component, ComponentController, ComponentParts, ComponentSender, Controller, RelmIterChildrenExt, }; +use testangel::{action_loader::ActionMap, ipc::EngineList}; use super::{actions::header::ActionsHeader, flows::header::FlowsHeader}; #[derive(Debug)] pub enum HeaderBarInput { ChangedView(String), + OpenAboutDialog, + ActionsMapChanged(Arc), +} + +#[derive(Debug)] +pub enum HeaderBarOutput { + AttachActionGroup(RelmActionGroup), } #[derive(Debug)] pub struct HeaderBarModel { + engine_list: Arc, + action_map: Arc, action_header_rc: Rc>, flow_header_rc: Rc>, } +impl std::fmt::Debug for GeneralActionGroup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "GeneralActionGroup") + } +} + impl HeaderBarModel { fn swap_content(&mut self, swap_target: >k::Box, new_content: >k::Box) { for child in swap_target.iter_children() { @@ -34,14 +51,16 @@ impl Component for HeaderBarModel { Rc>, Rc>, Rc, + Arc, + Arc, ); type Input = HeaderBarInput; - type Output = (); + type Output = HeaderBarOutput; type CommandOutput = (); view! { #[root] - adw::HeaderBar { + root = adw::HeaderBar { #[name = "start_box"] pack_start = >k::Box, @@ -60,16 +79,28 @@ impl Component for HeaderBarModel { fn init( init: Self::Init, root: &Self::Root, - _sender: ComponentSender, + sender: ComponentSender, ) -> ComponentParts { let model = HeaderBarModel { action_header_rc: init.0, flow_header_rc: init.1, + engine_list: init.3, + action_map: init.4, }; let stack = &*init.2; let widgets = view_output!(); + let sender_c = sender.clone(); + let about_action: RelmAction = RelmAction::new_stateless(move |_| { + sender_c.input(HeaderBarInput::OpenAboutDialog); + }); + relm4::main_application() + .set_accelerators_for_action::(&["A"]); + let mut group = RelmActionGroup::::new(); + group.add_action(about_action); + let _ = sender.output(HeaderBarOutput::AttachActionGroup(group)); + ComponentParts { model, widgets } } @@ -78,9 +109,17 @@ impl Component for HeaderBarModel { widgets: &mut Self::Widgets, message: Self::Input, sender: ComponentSender, - _root: &Self::Root, + root: &Self::Root, ) { match message { + HeaderBarInput::ActionsMapChanged(new_map) => self.action_map = new_map, + HeaderBarInput::OpenAboutDialog => { + crate::ui::about::AppAbout::builder() + .transient_for(root) + .launch((self.engine_list.clone(), self.action_map.clone())) + .widget() + .set_visible(true); + } HeaderBarInput::ChangedView(new_view) => { if new_view == "flows" { let rc_clone = self.flow_header_rc.clone(); @@ -99,3 +138,6 @@ impl Component for HeaderBarModel { self.update_view(widgets, sender); } } + +relm4::new_action_group!(pub GeneralActionGroup, "general"); +relm4::new_stateless_action!(pub GeneralAboutAction, GeneralActionGroup, "about"); diff --git a/testangel/src/next_ui/lang.rs b/testangel/src/ui/lang.rs similarity index 100% rename from testangel/src/next_ui/lang.rs rename to testangel/src/ui/lang.rs diff --git a/testangel/src/ui/mod.rs b/testangel/src/ui/mod.rs index f0a284a..54f31ac 100644 --- a/testangel/src/ui/mod.rs +++ b/testangel/src/ui/mod.rs @@ -1,366 +1,208 @@ -use std::{env, fmt::Debug, path::PathBuf, sync::Arc}; - -use iced::{ - executor, - settings::Settings, - window::{self, icon}, - Application, Command, Element, Event, Subscription, Theme, -}; -use testangel::{ipc::EngineList, *}; - -mod action_editor; -mod action_running; -mod flow_editor; -mod flow_running; -mod get_started; - -pub(crate) fn initialise_ui() { - let mut settings = Settings::default(); - settings.window.icon = Some( - icon::from_file_data(include_bytes!("../../../icon.png"), None).expect("icon was invalid!"), - ); - settings.exit_on_close_request = false; - settings.window.min_size = Some((800, 600)); - #[cfg(target_os = "linux")] - { - settings.window.platform_specific.application_id = String::from("TestAngel"); - } - App::run(settings).expect("Couldn't open UI"); -} - -#[derive(Default)] -pub struct App { - engine_list: Arc, - - state: State, - action_editor: action_editor::ActionEditor, - action_running: action_running::ActionRunning, - flow_editor: flow_editor::FlowEditor, - flow_running: flow_running::FlowRunning, - get_started: get_started::GetStarted, -} - -#[derive(Debug, Clone)] -pub enum AppMessage { - Event(iced::Event), - ActionEditor(action_editor::ActionEditorMessage), - ActionRunning(action_running::ActionRunningMessage), - FlowEditor(flow_editor::FlowEditorMessage), - FlowRunning(flow_running::FlowRunningMessage), - GetStarted(get_started::GetStartedMessage), - OpenAction(Option), - OpenFlow(Option), - CloseEditor, - NoOp, - UpdateIsLatest(bool), -} - -#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] -enum State { - #[default] - GetStarted, - AutomationFlowEditor, - AutomationFlowRunning, - ActionEditor, - ActionRunning, -} - -impl App { - fn update_action_list(&mut self) { - let actions_rc = Arc::new(action_loader::get_actions(self.engine_list.clone())); - self.flow_editor.update_action_map(actions_rc.clone()); - self.flow_running.update_action_map(actions_rc); - } -} - -impl Application for App { - type Message = AppMessage; - type Flags = (); - type Executor = executor::Default; - type Theme = Theme; - - fn new(_flags: ()) -> (Self, Command) { - let engines_rc = Arc::new(ipc::get_engines()); - let actions_rc = Arc::new(action_loader::get_actions(engines_rc.clone())); - ( - Self { - engine_list: engines_rc.clone(), - action_editor: action_editor::ActionEditor::new(engines_rc.clone()), - action_running: action_running::ActionRunning::new(engines_rc.clone()), - flow_editor: flow_editor::FlowEditor::new(actions_rc.clone()), - flow_running: flow_running::FlowRunning::new(actions_rc, engines_rc), - ..Default::default() - }, - Command::perform(version::check_is_latest(), AppMessage::UpdateIsLatest), - ) - } - - fn title(&self) -> String { - let sub_title = match self.state { - State::GetStarted => self.get_started.title(), - State::ActionEditor => self.action_editor.title(), - State::AutomationFlowEditor => self.flow_editor.title(), - State::AutomationFlowRunning => self.flow_running.title(), - State::ActionRunning => self.action_editor.title(), - }; - let separator = if sub_title.is_some() { " :: " } else { "" }; - let sub_title = sub_title.unwrap_or_default(); - format!("TestAngel{separator}{sub_title}") - } - - fn subscription(&self) -> Subscription { - Subscription::batch(vec![ - match self.state { - State::GetStarted => self.get_started.subscription().map(AppMessage::GetStarted), - State::ActionEditor => self - .action_editor - .subscription() - .map(AppMessage::ActionEditor), - State::ActionRunning => self - .action_running - .subscription() - .map(AppMessage::ActionRunning), - State::AutomationFlowEditor => { - self.flow_editor.subscription().map(AppMessage::FlowEditor) - } - State::AutomationFlowRunning => self - .flow_running - .subscription() - .map(AppMessage::FlowRunning), - }, - iced::subscription::events().map(AppMessage::Event), - ]) - } - - fn update(&mut self, message: Self::Message) -> Command { - match message { - AppMessage::NoOp => (), - AppMessage::UpdateIsLatest(is_latest) => { - self.get_started.set_is_latest(is_latest); - } - AppMessage::Event(event) => { - if let Event::Window(window::Event::CloseRequested) = event { - std::process::exit(0); - } - } - AppMessage::ActionEditor(msg) => { - let (msg_out, cmd) = self.action_editor.update(msg); - if let Some(msg_out) = msg_out { - match msg_out { - action_editor::ActionEditorMessageOut::RunAction(action) => { - self.action_running.set_action(action); - self.state = State::ActionRunning; - } - } - } - if let Some(cmd) = cmd { - return cmd; - } - } - AppMessage::ActionRunning(msg) => { - let (msg_out, cmd) = self.action_running.update(msg); - if let Some(msg_out) = msg_out { - match msg_out { - action_running::ActionRunningMessageOut::BackToEditor => { - self.state = State::ActionEditor; - } - action_running::ActionRunningMessageOut::SaveActionReport(evidence) => { - return Command::perform( - rfd::AsyncFileDialog::new() - .add_filter("Portable Document Format", &["pdf"]) - .set_file_name("report.pdf") - .set_title("Save Report") - .set_directory(env::current_dir().expect("Failed to get cwd")) - .save_file(), - |f| { - AppMessage::ActionRunning( - action_running::ActionRunningMessage::Save( - f.map(|f| f.path().to_path_buf()), - evidence, - ), - ) - }, - ); - } - } - } - if let Some(cmd) = cmd { - return cmd; - } - } - AppMessage::FlowEditor(msg) => { - let (msg_out, cmd) = self.flow_editor.update(msg); - if let Some(msg_out) = msg_out { - match msg_out { - flow_editor::FlowEditorMessageOut::RunFlow(flow) => { - self.state = State::AutomationFlowRunning; - self.flow_running.start_flow(flow); - } - } - } - if let Some(cmd) = cmd { - return cmd; - } - } - AppMessage::CloseEditor => { - self.state = State::GetStarted; - } - AppMessage::FlowRunning(msg) => { - let (msg_out, cmd) = self.flow_running.update(msg); - if let Some(msg_out) = msg_out { - match msg_out { - flow_running::FlowRunningMessageOut::BackToEditor => { - self.state = State::AutomationFlowEditor; - } - flow_running::FlowRunningMessageOut::SaveFlowReport(evidence) => { - return Command::perform( - rfd::AsyncFileDialog::new() - .add_filter("Portable Document Format", &["pdf"]) - .set_file_name("report.pdf") - .set_title("Save Report") - .set_directory(env::current_dir().expect("Failed to get cwd")) - .save_file(), - |f| { - AppMessage::FlowRunning(flow_running::FlowRunningMessage::Save( - f.map(|f| f.path().to_path_buf()), - evidence, - )) - }, - ); - } - } - } - if let Some(cmd) = cmd { - return cmd; - } - } - AppMessage::GetStarted(msg) => { - let (msg_out, cmd) = self.get_started.update(msg); - if let Some(msg_out) = msg_out { - match msg_out { - get_started::GetStartedMessage::NewAction => { - self.state = State::ActionEditor; - self.action_editor.new_action(); - } - get_started::GetStartedMessage::NewFlow => { - self.state = State::AutomationFlowEditor; - self.update_action_list(); - self.flow_editor.new_flow(); - } - get_started::GetStartedMessage::OpenAction => { - return Command::perform( - rfd::AsyncFileDialog::new() - .add_filter("TestAngel Actions", &["taaction"]) - .set_title("Open Action") - .set_directory( - env::var("TA_ACTION_DIR").unwrap_or("./actions".to_owned()), - ) - .pick_file(), - |ret| AppMessage::OpenAction(ret.map(|f| f.path().to_path_buf())), - ); - } - get_started::GetStartedMessage::OpenFlow => { - return Command::perform( - rfd::AsyncFileDialog::new() - .add_filter("TestAngel Flows", &["taflow"]) - .set_title("Open Flow") - .set_directory( - env::var("TA_FLOW_DIR").unwrap_or(".".to_owned()), - ) - .pick_file(), - |ret| AppMessage::OpenFlow(ret.map(|f| f.path().to_path_buf())), - ); - } - } - } - if let Some(cmd) = cmd { - return cmd; - } - } - AppMessage::OpenAction(maybe_file) => { - if let Some(file) = maybe_file { - match self.action_editor.open_action(file) { - Ok(_) => self.state = State::ActionEditor, - Err(e) => { - return Command::perform( - rfd::AsyncMessageDialog::new() - .set_level(rfd::MessageLevel::Error) - .set_title("Failed to open action") - .set_description(format!("{e}")) - .set_buttons(rfd::MessageButtons::Ok) - .show(), - |_| AppMessage::NoOp, - ) - } - } - } - } - AppMessage::OpenFlow(maybe_file) => { - if let Some(file) = maybe_file { - self.update_action_list(); - match self.flow_editor.open_flow(file) { - Ok(changed) => { - self.state = State::AutomationFlowEditor; - if !changed.is_empty() { - return Command::perform(rfd::AsyncMessageDialog::new() - .set_level(rfd::MessageLevel::Warning) - .set_title("Action has changed") - .set_buttons(rfd::MessageButtons::Ok) - .set_description(format!( - "The parameters in steps {} have changed so it has been reset.", - changed.iter().map(|step| step.to_string()).collect::>().join(",") - )) - .show(), |_| AppMessage::NoOp); - } - } - Err(e) => { - return Command::perform( - rfd::AsyncMessageDialog::new() - .set_level(rfd::MessageLevel::Error) - .set_title("Failed to open flow") - .set_description(format!("{e}")) - .set_buttons(rfd::MessageButtons::Ok) - .show(), - |_| AppMessage::NoOp, - ) - } - } - } - } - } - Command::none() - } - - fn view(&self) -> Element<'_, Self::Message> { - // Render content - let content: Element<'_, AppMessage> = match self.state { - State::GetStarted => self.get_started.view().map(AppMessage::GetStarted), - State::ActionEditor => self.action_editor.view().map(AppMessage::ActionEditor), - State::ActionRunning => self.action_running.view().map(AppMessage::ActionRunning), - State::AutomationFlowEditor => self.flow_editor.view().map(AppMessage::FlowEditor), - State::AutomationFlowRunning => self.flow_running.view().map(AppMessage::FlowRunning), - }; - - content - } -} - -trait UiComponent { - type Message: Debug + Send; - type MessageOut: Debug + Send; - - fn title(&self) -> Option<&str>; - - /// Handle a message. - fn update( - &mut self, - message: Self::Message, - ) -> (Option, Option>); - - fn subscription(&self) -> Subscription { - Subscription::none() - } - - /// Render the central panel UI. - fn view(&self) -> Element<'_, Self::Message>; -} +use std::{rc::Rc, sync::Arc}; + +use gtk::prelude::*; +use relm4::{ + actions::RelmActionGroup, adw, gtk, Component, ComponentController, ComponentParts, Controller, + RelmApp, +}; +use testangel::{ + action_loader::{self, ActionMap}, + ipc::{self, EngineList}, +}; + +use self::header_bar::HeaderBarInput; + +mod about; +mod actions; +mod components; +mod file_filters; +mod flows; +mod header_bar; +pub(crate) mod lang; + +/// Initialise and open the UI. +pub fn initialise_ui() { + log::info!("Starting Next UI..."); + let app = RelmApp::new("lilopkins.testangel"); + relm4_icons::initialize_icons(); + initialise_icons(); + + let engines = Arc::new(ipc::get_engines()); + let actions = Arc::new(action_loader::get_actions(engines.clone())); + app.run::(AppInit { engines, actions }); +} + +fn initialise_icons() { + relm4::gtk::gio::resources_register_include!("icons.gresource").unwrap(); + log::info!("Loaded icon bundle."); + + let display = relm4::gtk::gdk::Display::default().unwrap(); + let theme = gtk::IconTheme::for_display(&display); + theme.add_resource_path("/uk/hpkns/testangel/icons"); +} + +pub struct AppInit { + engines: Arc, + actions: Arc, +} + +#[derive(Debug)] +enum AppInput { + NoOp, + /// The view has changed and should be read from visible_child_name, then components updated as needed. + ChangedView(Option), + /// The actions might have changed and should be reloaded + ReloadActionsMap, + /// Attach the action group to the window + AttachGeneralActionGroup(RelmActionGroup), +} + +#[derive(Debug)] +struct AppModel { + stack: Rc, + header: Controller, + + flows: Controller, + actions: Controller, + + engines_list: Arc, + actions_map: Arc, +} + +#[relm4::component] +impl Component for AppModel { + type Init = AppInit; + type Input = AppInput; + type Output = (); + type CommandOutput = (); + + view! { + main_window = adw::Window { + set_title: Some(&lang::lookup("app-name")), + set_default_width: 800, + set_default_height: 600, + set_icon_name: Some("testangel"), + + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 0, + + model.header.widget(), + + #[local_ref] + stack -> adw::ViewStack { + connect_visible_child_name_notify[sender] => move |st| { + sender.input(AppInput::ChangedView(st.visible_child_name().map(|s| s.into()))); + }, + }, + } + } + } + + fn init( + init: Self::Init, + root: &Self::Root, + sender: relm4::ComponentSender, + ) -> relm4::ComponentParts { + // Initialise the sub-components (pages) + let flows = flows::FlowsModel::builder() + .launch((init.actions.clone(), init.engines.clone())) + .forward(sender.input_sender(), |_msg| AppInput::NoOp); + let actions = actions::ActionsModel::builder() + .launch((init.actions.clone(), init.engines.clone())) + .forward(sender.input_sender(), |msg| match msg { + actions::ActionOutputs::ReloadActions => AppInput::ReloadActionsMap, + }); + + let stack = Rc::new(adw::ViewStack::new()); + gtk::Window::set_default_icon_name("testangel"); + + // Initialise the headerbar + let header = header_bar::HeaderBarModel::builder() + .launch(( + actions.model().header_controller_rc(), + flows.model().header_controller_rc(), + stack.clone(), + init.engines.clone(), + init.actions.clone(), + )) + .forward(sender.input_sender(), |msg| match msg { + header_bar::HeaderBarOutput::AttachActionGroup(group) => { + AppInput::AttachGeneralActionGroup(group) + } + }); + + // Build model + let model = AppModel { + actions_map: init.actions, + engines_list: init.engines, + stack, + header, + flows, + actions, + }; + + // Render window parts + let stack = &*model.stack; + + // Add pages + stack.add_titled_with_icon( + model.flows.widget(), + Some("flows"), + &lang::lookup("tab-flows"), + relm4_icons::icon_name::PAPYRUS_VERTICAL, + ); + if !std::env::var("TA_HIDE_ACTION_EDITOR") + .unwrap_or("no".to_string()) + .eq_ignore_ascii_case("yes") + { + stack.add_titled_with_icon( + model.actions.widget(), + Some("actions"), + &lang::lookup("tab-actions"), + relm4_icons::icon_name::PUZZLE_PIECE, + ); + } + + let widgets = view_output!(); + log::debug!("Initialised model: {model:?}"); + + // Trigger initial header bar update + sender.input(AppInput::ChangedView( + stack.visible_child_name().map(|s| s.into()), + )); + + ComponentParts { model, widgets } + } + + fn update( + &mut self, + message: Self::Input, + _sender: relm4::ComponentSender, + root: &Self::Root, + ) { + match message { + AppInput::NoOp => (), + AppInput::AttachGeneralActionGroup(group) => { + group.register_for_widget(root); + } + AppInput::ChangedView(new_view) => { + self.header + .emit(HeaderBarInput::ChangedView(new_view.unwrap_or_default())); + } + AppInput::ReloadActionsMap => { + self.actions_map = Arc::new(action_loader::get_actions(self.engines_list.clone())); + self.flows.emit(flows::FlowInputs::ActionsMapChanged( + self.actions_map.clone(), + )); + self.actions.emit(actions::ActionInputs::ActionsMapChanged( + self.actions_map.clone(), + )); + self.header + .emit(header_bar::HeaderBarInput::ActionsMapChanged( + self.actions_map.clone(), + )) + } + } + } +}