diff --git a/Cargo.lock b/Cargo.lock index 902d3663..a1dd320c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,110 @@ dependencies = [ "uuid", ] +[[package]] +name = "accesskit_atspi_common" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8c61bee90b42a772d39d06a740207dc71a4e780004ace1db8d99fb1baaa954" +dependencies = [ + "accesskit", + "accesskit_consumer 0.36.0", + "atspi-common", + "phf", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_consumer" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e0d7e25d06f4dc21d1774d67146e9e80d6789216cbd4d1e88185b0095dba60" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_consumer" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" +dependencies = [ + "accesskit", + "accesskit_consumer 0.38.0", + "hashbrown 0.16.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b016ca8db0ea0ea2ceff29a9d6240391492d960716aa471967c00e8cc8cb197c" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" +dependencies = [ + "accesskit", + "accesskit_consumer 0.35.0", + "hashbrown 0.16.1", + "static_assertions", + "windows", + "windows-core", +] + +[[package]] +name = "accesskit_winit" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + [[package]] name = "adler2" version = "2.0.1" @@ -24,6 +128,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -62,6 +167,46 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.2", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.21", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -74,6 +219,26 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -91,6 +256,12 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + [[package]] name = "as-slice" version = "0.2.1" @@ -100,6 +271,189 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.5", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.5", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.5", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -121,7 +475,7 @@ dependencies = [ "num-traits", "pastey", "rayon", - "thiserror", + "thiserror 2.0.21", "v_frame", "y4m", ] @@ -155,7 +509,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-set" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" +dependencies = [ + "bit-vec 0.9.1", ] [[package]] @@ -164,6 +527,18 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.2" @@ -179,6 +554,37 @@ dependencies = [ "no_std_io2", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "brotli-decompressor" version = "5.0.3" @@ -227,6 +633,63 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.2", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.13.2", + "polling", + "rustix 1.1.5", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.4", + "rustix 1.1.5", + "wayland-backend", + "wayland-client", +] + [[package]] name = "cc" version = "1.5.1" @@ -245,6 +708,41 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + [[package]] name = "color" version = "0.3.3" @@ -260,6 +758,65 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + [[package]] name = "crc32fast" version = "1.5.2" @@ -300,15 +857,105 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.4", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + [[package]] name = "ecolor" version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0656a7530b9e2841ee7a5f983965bdd9840c32f4012f759cfd44b912cced6b34" dependencies = [ + "bytemuck", "emath", ] +[[package]] +name = "eframe" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ad41fac6e3149abb4866e93df37dbd33baa95db525c3368c944584e95a5dc2" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-wgpu", + "egui-winit", + "egui_glow", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "pollster 1.0.1", + "profiling", + "raw-window-handle", + "static_assertions", + "wasm-bindgen", + "web-sys", + "web-time", + "wgpu", + "windows-sys 0.61.2", + "winit", +] + [[package]] name = "egui" version = "0.36.2" @@ -317,7 +964,7 @@ checksum = "dc938cc27cd911415e1e4d151bc56ff9df063d8db081374e87aeb805ea74b2ba" dependencies = [ "accesskit", "ahash", - "bitflags", + "bitflags 2.13.2", "emath", "epaint", "itertools 0.15.0", @@ -329,6 +976,61 @@ dependencies = [ "web-sys", ] +[[package]] +name = "egui-wgpu" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cef1f6d1bba40b09a4d0d62cb822b81030922cfbf65c5373398c3a3caafc5bd" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "profiling", + "thiserror 2.0.21", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98466000559d66ba4db786a30448b01483946add4188e7f10f522a27b4fb89cc" +dependencies = [ + "accesskit_winit", + "arboard", + "bytemuck", + "egui", + "log", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", + "profiling", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "606ed4167daf341e5753d703285c4c1d562324b2c07aa8f405cd7d066fe58f3b" +dependencies = [ + "bytemuck", + "egui", + "glow", + "log", + "profiling", + "winit", +] + [[package]] name = "either" version = "1.18.0" @@ -340,6 +1042,36 @@ name = "emath" version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52c8d4141cf0f60fb2f7aefaf352d3ffa5171ef0ab09731c64391801e95e48f1" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "epaint" @@ -348,6 +1080,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0ec0308dc23130fc1a7d4623a8534c2ed069c6dc84e258ee4f3de99615aa1a3" dependencies = [ "ahash", + "bytemuck", "ecolor", "emath", "epaint_default_fonts", @@ -391,6 +1124,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -398,9 +1137,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + [[package]] name = "euclid" version = "0.22.14" @@ -410,6 +1155,26 @@ dependencies = [ "num-traits", ] +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.5.0" @@ -475,12 +1240,69 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "futures-core" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "futures-task" version = "0.3.34" @@ -494,11 +1316,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", + "futures-macro", "futures-task", "pin-project-lite", "slab", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.5", + "windows-link", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -532,6 +1365,17 @@ dependencies = [ "weezl", ] +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + [[package]] name = "glifo" version = "0.2.0" @@ -540,7 +1384,7 @@ checksum = "ed4a1bb24121291d27230c1b1b44e07d6a9b28cefdb32fe1581dfb84e14f940a" dependencies = [ "bytemuck", "foldhash", - "hashbrown", + "hashbrown 0.17.1", "log", "peniko", "skrifa", @@ -548,6 +1392,98 @@ dependencies = [ "vello_common", ] +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.13.2", + "cfg_aliases", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.21", + "windows", +] + [[package]] name = "guillotiere" version = "0.7.0" @@ -565,6 +1501,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "num-traits", "zerocopy", ] @@ -574,12 +1511,23 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" dependencies = [ - "bitflags", + "bitflags 2.13.2", "bytemuck", "read-fonts", "smallvec", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -589,6 +1537,18 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "image" version = "0.25.10" @@ -624,6 +1584,16 @@ version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e44b0a4eaa4c82f441d50a963f2d5f05a787240aeee097597033e72accfd22f" +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -659,6 +1629,64 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + [[package]] name = "jobserver" version = "0.1.35" @@ -871,6 +1899,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "kurbo" version = "0.13.1" @@ -899,6 +1944,49 @@ dependencies = [ "cc", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "bitflags 2.13.2", + "libc", + "plain", + "redox_syscall 0.9.4", +] + +[[package]] +name = "lightcraft" +version = "0.1.0" +dependencies = [ + "eframe", + "egui", + "lightcraft-codecs", + "lightcraft-engine", + "lightcraft-raster", + "lightcraft-ui-egui", + "log", + "rfd", + "serde_json", +] + [[package]] name = "lightcraft-catalog" version = "0.1.0" @@ -907,7 +1995,7 @@ dependencies = [ "proptest", "serde", "serde_json", - "thiserror", + "thiserror 2.0.21", ] [[package]] @@ -929,7 +2017,7 @@ dependencies = [ "ravif", "rayon", "serde", - "thiserror", + "thiserror 2.0.21", "tiff", "zune-core", "zune-jpeg", @@ -960,16 +2048,19 @@ name = "lightcraft-engine" version = "0.1.0" dependencies = [ "lightcraft-catalog", + "lightcraft-codecs", "lightcraft-color", "lightcraft-develop", "lightcraft-geom", + "lightcraft-meta", "lightcraft-pipeline", "lightcraft-raster", + "lightcraft-raw", "lightcraft-scenes", "log", "serde", "serde_json", - "thiserror", + "thiserror 2.0.21", ] [[package]] @@ -987,7 +2078,7 @@ dependencies = [ "lightcraft-geom", "lightcraft-tiff", "proptest", - "quick-xml", + "quick-xml 0.38.4", "serde", "serde_json", ] @@ -1031,7 +2122,7 @@ dependencies = [ "proptest", "rayon", "serde", - "thiserror", + "thiserror 2.0.21", ] [[package]] @@ -1051,7 +2142,7 @@ version = "0.1.0" dependencies = [ "proptest", "serde", - "thiserror", + "thiserror 2.0.21", ] [[package]] @@ -1077,12 +2168,24 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -1122,6 +2225,24 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1162,6 +2283,74 @@ dependencies = [ "pxfm", ] +[[package]] +name = "naga" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" +dependencies = [ + "arrayvec", + "bit-set 0.10.0", + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.17.1", + "indexmap", + "libm", + "log", + "naga-types", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.21", + "unicode-ident", +] + +[[package]] +name = "naga-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49" +dependencies = [ + "hashbrown 0.17.1", + "indexmap", + "rustc-hash 1.1.0", + "thiserror 2.0.21", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[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.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -1246,6 +2435,340 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", ] [[package]] @@ -1254,6 +2777,41 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1272,7 +2830,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -1302,25 +2860,143 @@ dependencies = [ "smallvec", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "png" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags", + "bitflags 2.13.2", "crc32fast", "fdeflate", "flate2", "miniz_oxide 0.8.9", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.5", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "pollster" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6355899e1c9462875b6757c79f3caa011a1fdae12bbb1a2e72dd1f234f8336" + [[package]] name = "polycool" version = "0.4.0" @@ -1330,6 +3006,21 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1339,6 +3030,21 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -1373,9 +3079,9 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ - "bit-set", - "bit-vec", - "bitflags", + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags 2.13.2", "num-traits", "rand", "rand_chacha", @@ -1413,6 +3119,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.47" @@ -1472,6 +3187,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + [[package]] name = "rav1e" version = "0.8.1" @@ -1502,7 +3223,7 @@ dependencies = [ "rand", "rand_chacha", "simd_helpers", - "thiserror", + "thiserror 2.0.21", "v_frame", "wasm-bindgen", ] @@ -1521,6 +3242,24 @@ dependencies = [ "rgb", ] +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + [[package]] name = "rayon" version = "1.12.0" @@ -1552,13 +3291,31 @@ dependencies = [ "once_cell", ] +[[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 = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.2", +] + +[[package]] +name = "redox_syscall" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" +dependencies = [ + "bitflags 2.13.2", ] [[package]] @@ -1567,23 +3324,90 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "rfd" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" +dependencies = [ + "block2 0.6.2", + "dispatch2", + "js-sys", + "libc", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "percent-encoding", + "pollster 0.4.0", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "web-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rgb" version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ - "bitflags", + "bitflags 2.13.2", "errno", "libc", - "linux-raw-sys", - "windows-sys", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] @@ -1604,6 +3428,21 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[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" @@ -1616,6 +3455,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.229" @@ -1659,18 +3504,49 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simd_helpers" version = "0.1.0" @@ -1680,6 +3556,18 @@ dependencies = [ "quote", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + [[package]] name = "skrifa" version = "0.44.0" @@ -1696,18 +3584,114 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.13.2", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.13.2", + "calloop 0.14.4", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.5", + "thiserror 2.0.21", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.13.2", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "syn" version = "2.0.119" @@ -1739,8 +3723,26 @@ dependencies = [ "fastrand", "getrandom 0.4.3", "once_cell", - "rustix", - "windows-sys", + "rustix 1.1.5", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", ] [[package]] @@ -1749,7 +3751,18 @@ version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -1777,16 +3790,68 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + [[package]] name = "tracing" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -1796,6 +3861,26 @@ dependencies = [ "once_cell", ] +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.3", +] + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "unarray" version = "0.1.4" @@ -1820,11 +3905,22 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "uuid" version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] [[package]] name = "v_frame" @@ -1849,7 +3945,7 @@ dependencies = [ "log", "peniko", "smallvec", - "thiserror", + "thiserror 2.0.21", ] [[package]] @@ -1860,7 +3956,7 @@ checksum = "ac7349e1f55f6b801c7c277958df4ea53e7f20f21e8014910ad888b2ecda93ea" dependencies = [ "bytemuck", "glifo", - "hashbrown", + "hashbrown 0.17.1", "vello_common", ] @@ -1879,6 +3975,16 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasip2" version = "1.0.4+wasi-0.2.12" @@ -1901,6 +4007,17 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.129" @@ -1933,6 +4050,141 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.5", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix 1.1.5", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.13.2", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.5", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml 0.41.0", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.106" @@ -1943,18 +4195,342 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "weezl" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "wgpu" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" +dependencies = [ + "arrayvec", + "bitflags 2.13.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "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 = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" +dependencies = [ + "arrayvec", + "bit-set 0.10.0", + "bit-vec 0.9.1", + "bitflags 2.13.2", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "indexmap", + "log", + "naga", + "naga-types", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.21", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-wasm" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d260ca2ca8adc654f513a869bf3cac0b47f5b8d3f87803d6942015f8de597aae" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set 0.10.0", + "bitflags 2.13.2", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "hashbrown 0.17.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "naga", + "naga-types", + "ndk-sys", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "static_assertions", + "thiserror 2.0.21", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge", + "wgpu-types", + "windows", + "windows-core", + "windows-result", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "js-sys", + "log", + "naga-types", + "raw-window-handle", + "static_assertions", + "web-sys", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1964,12 +4540,273 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.13.2", + "block2 0.5.1", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "smithay-client-toolkit 0.19.2", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[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.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.5", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.2", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + [[package]] name = "xtask" version = "0.1.0" @@ -1983,6 +4820,112 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.5", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.6", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" +dependencies = [ + "serde", + "winnow", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + [[package]] name = "zerocopy" version = "0.8.59" @@ -2029,3 +4972,44 @@ checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ "zune-core", ] + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.6", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.6", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml index 00eca32c..1e47d213 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["crates/*", "xtask"] +members = ["crates/*", "apps/*", "xtask"] [workspace.package] version = "0.1.0" diff --git a/README.md b/README.md index de204a3b..38d076f1 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ cargo xtask ci # fmt, clippy, tests, lay See [ROADMAP.md](ROADMAP.md) for milestones and estimates. -## The Craft family +## Crafting Apps Clean-room, pure-Rust creative tools from the same workshop — native on macOS, Windows and Linux, and in the browser. diff --git a/apps/lightcraft/Cargo.toml b/apps/lightcraft/Cargo.toml new file mode 100644 index 00000000..96d7e8ab --- /dev/null +++ b/apps/lightcraft/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "lightcraft" +version.workspace = true +edition.workspace = true +license.workspace = true +publish = false +default-run = "lightcraft" + +[lints] +workspace = true + +[dependencies] +lightcraft-engine = { workspace = true } +lightcraft-ui-egui = { workspace = true } +lightcraft-codecs = { workspace = true } +lightcraft-raster = { workspace = true } +eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11"] } +egui = { workspace = true } +serde_json = { workspace = true } +log = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +rfd = { workspace = true } diff --git a/apps/lightcraft/src/control_server.rs b/apps/lightcraft/src/control_server.rs new file mode 100644 index 00000000..faa28baf --- /dev/null +++ b/apps/lightcraft/src/control_server.rs @@ -0,0 +1,62 @@ +//! Loopback JSON-lines control server: one request per line, one reply per line. +//! This is the transport the MCP server (`lightcraft-cli mcp --connect`) wraps. + +use std::io::{BufRead, BufReader, Write}; +use std::net::{TcpListener, TcpStream}; +use std::sync::mpsc::{Receiver, Sender, channel}; +use std::time::Duration; + +use lightcraft_ui_egui::ControlRequest; +use serde_json::{Value, json}; + +pub fn start(port: u16, ctx: egui::Context) -> Receiver { + let (tx, rx) = channel::(); + let listener = match TcpListener::bind(("127.0.0.1", port)) { + Ok(l) => l, + Err(e) => { + eprintln!("lightcraft: control server failed to bind 127.0.0.1:{port}: {e}"); + return rx; + } + }; + eprintln!("lightcraft: control server listening on 127.0.0.1:{port}"); + std::thread::spawn(move || { + for stream in listener.incoming().flatten() { + let tx = tx.clone(); + let ctx = ctx.clone(); + std::thread::spawn(move || serve(stream, tx, ctx)); + } + }); + rx +} + +fn serve(stream: TcpStream, tx: Sender, ctx: egui::Context) { + let Ok(read) = stream.try_clone() else { return }; + let mut out = stream; + for line in BufReader::new(read).lines() { + let Ok(line) = line else { break }; + if line.trim().is_empty() { + continue; + } + let reply = match serde_json::from_str::(&line) { + Ok(msg) => { + let id = msg.get("id").cloned().unwrap_or(Value::Null); + let method = msg.get("method").and_then(Value::as_str).unwrap_or("").to_string(); + let params = msg.get("params").cloned().unwrap_or(json!({})); + let (req, rrx) = ControlRequest::new(method, params); + if tx.send(req).is_err() { + break; + } + ctx.request_repaint(); + let mut r = rrx.recv_timeout(Duration::from_secs(60)).unwrap_or_else(|_| json!({"ok": false, "error": "timeout"})); + if let Some(o) = r.as_object_mut() { + o.insert("id".into(), id); + } + r + } + Err(e) => json!({"ok": false, "error": format!("bad JSON: {e}")}), + }; + if writeln!(out, "{reply}").is_err() { + break; + } + } +} diff --git a/apps/lightcraft/src/main.rs b/apps/lightcraft/src/main.rs new file mode 100644 index 00000000..c98ceaa6 --- /dev/null +++ b/apps/lightcraft/src/main.rs @@ -0,0 +1,152 @@ +//! LightCraft desktop app. +//! +//! Usage: `lightcraft [--control ] [--demo] [files or folders…]` +//! +//! `--control ` (or `LIGHTCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server: +//! `{"id":1,"method":"ui.inspect","params":{}}` → `{"id":1,"ok":true,"result":…}`. +//! See `lightcraft_ui_egui::control` for the methods. + +mod control_server; + +use lightcraft_engine::Session; +use lightcraft_ui_egui::{LightcraftApp, Services, UiState}; + +struct App(LightcraftApp); + +impl eframe::App for App { + fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + self.0.logic(ctx); + } + fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) { + self.0.raw_input_hook(raw); + } + fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { + self.0.ui(ui); + } + fn on_exit(&mut self) { + save_prefs(&self.0); + } +} + +fn config_dir() -> Option { + if cfg!(target_os = "macos") { + std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join("Library/Application Support/LightCraft")) + } else if cfg!(windows) { + std::env::var_os("APPDATA").map(|a| std::path::PathBuf::from(a).join("LightCraft")) + } else { + std::env::var_os("XDG_CONFIG_HOME") + .map(std::path::PathBuf::from) + .or_else(|| std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config"))) + .map(|c| c.join("lightcraft")) + } +} + +fn load_prefs(app: &mut LightcraftApp) { + if std::env::var_os("LIGHTCRAFT_NO_PREFS").is_some() { + return; + } + if let Some(p) = config_dir().map(|d| d.join("ui.json")) + && let Ok(bytes) = std::fs::read(&p) + && let Ok(ui) = serde_json::from_slice::(&bytes) + { + app.ui = ui.sanitized(); + } +} + +fn save_prefs(app: &LightcraftApp) { + if std::env::var_os("LIGHTCRAFT_NO_PREFS").is_some() { + return; + } + if let Some(d) = config_dir() { + let _ = std::fs::create_dir_all(&d); + if let Ok(bytes) = serde_json::to_vec_pretty(&app.ui) { + let _ = std::fs::write(d.join("ui.json"), bytes); + } + } +} + +fn services() -> Services { + Services { + pick_files: Some(Box::new(|| { + rfd::FileDialog::new() + .add_filter("Photos", &["jpg", "jpeg", "png", "tif", "tiff", "webp", "dng", "cr2", "nef", "arw", "psd", "jxl", "gif", "bmp"]) + .pick_files() + .unwrap_or_default() + .into_iter() + .map(|p| p.to_string_lossy().to_string()) + .collect() + })), + write: Some(Box::new(|p: &str, b: &[u8]| std::fs::write(p, b).map_err(|e| e.to_string()))), + png: Some(Box::new(|img: &lightcraft_raster::Rgba8| { + lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(img), &lightcraft_codecs::EncodeMeta::default()).unwrap_or_default() + })), + } +} + +/// Expand folders (recursively) into photo files. +fn collect(path: &std::path::Path, out: &mut Vec) { + if path.is_dir() { + if let Ok(rd) = std::fs::read_dir(path) { + let mut v: Vec<_> = rd.flatten().map(|e| e.path()).collect(); + v.sort(); + for p in v { + collect(&p, out); + } + } + } else if path.extension().is_some_and(|e| { + matches!( + e.to_string_lossy().to_lowercase().as_str(), + "jpg" | "jpeg" | "png" | "tif" | "tiff" | "webp" | "dng" | "cr2" | "nef" | "arw" | "psd" | "jxl" | "gif" | "bmp" + ) + }) { + out.push(path.to_string_lossy().to_string()); + } +} + +fn main() -> eframe::Result { + let mut control_port: Option = std::env::var("LIGHTCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok()); + let mut files = Vec::new(); + let mut demo = true; + let mut args = std::env::args().skip(1); + while let Some(a) = args.next() { + match a.as_str() { + "--control" => control_port = args.next().and_then(|p| p.parse().ok()), + "--no-demo" => demo = false, + "--version" => { + println!("lightcraft {}", env!("CARGO_PKG_VERSION")); + return Ok(()); + } + _ => collect(std::path::Path::new(&a), &mut files), + } + } + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_title("LightCraft") + .with_inner_size([1600.0, 1000.0]) + .with_min_inner_size([900.0, 560.0]) + .with_drag_and_drop(true) + .with_fullsize_content_view(true) + .with_titlebar_shown(false) + .with_title_shown(false), + ..Default::default() + }; + eframe::run_native( + "LightCraft", + options, + Box::new(move |cc| { + let session = if demo && files.is_empty() { Session::with_demo() } else { Session::new() }.with_fs(); + let mut app = LightcraftApp::new(session, services()); + load_prefs(&mut app); + app.integrated_titlebar = cfg!(target_os = "macos"); + if let Some(port) = control_port { + let rx = control_server::start(port, cc.egui_ctx.clone()); + app = app.with_control(rx); + } + if !files.is_empty() { + let _ = app.run("library.import", serde_json::json!({"paths": files})); + app.ui.view = lightcraft_ui_egui::state::ViewMode::PhotoGrid; + } + Ok(Box::new(App(app))) + }), + ) +} diff --git a/crates/catalog/src/model.rs b/crates/catalog/src/model.rs index f43287ac..b3c0d4fa 100644 --- a/crates/catalog/src/model.rs +++ b/crates/catalog/src/model.rs @@ -140,6 +140,9 @@ pub struct Photo { /// Video duration in seconds. #[serde(default)] pub duration: Option, + /// Raw files: the camera's as-shot white balance (Kelvin, tint). + #[serde(default)] + pub as_shot_wb: Option<(f64, f64)>, } impl Photo { @@ -165,6 +168,7 @@ impl Photo { history: Vec::new(), deleted: false, duration: None, + as_shot_wb: None, } } pub fn is_edited(&self) -> bool { diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 54736a8e..6015ab24 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -16,6 +16,9 @@ lightcraft-develop = { workspace = true } lightcraft-scenes = { workspace = true } lightcraft-pipeline = { workspace = true } lightcraft-catalog = { workspace = true } +lightcraft-codecs = { workspace = true } +lightcraft-raw = { workspace = true } +lightcraft-meta = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/crates/engine/src/files.rs b/crates/engine/src/files.rs new file mode 100644 index 00000000..0e79c9c9 --- /dev/null +++ b/crates/engine/src/files.rs @@ -0,0 +1,136 @@ +//! Real files: probing (dimensions + metadata for import) and loading (decode → linear Rec.2020, +//! oriented, downscaled to the requested level). Standard formats go through `lightcraft-codecs`, +//! camera raws through `lightcraft-raw` (demosaic + DNG colour model). + +use std::sync::Arc; + +use lightcraft_catalog::{MediaKind, Meta}; +use lightcraft_color::cct::xy_to_temp_tint; +use lightcraft_geom::Orientation; +use lightcraft_pipeline::SourceInfo; +use lightcraft_raster::Rgb32f; +use lightcraft_raster::resample::{Filter, fit}; + +use crate::media::{FileLoader, FileProbe, ProbeInfo}; + +fn meta_of(m: &lightcraft_meta::Metadata) -> (Meta, Option) { + let shutter = m.exposure_time.map(|t| if t >= 1.0 { format!("{t:.0}") } else { format!("1/{:.0}", 1.0 / t) }).unwrap_or_default(); + let camera = [m.make.clone().unwrap_or_default(), m.model.clone().unwrap_or_default()].join(" ").trim().to_string(); + let meta = Meta { + camera, + lens: m.lens_model.clone().unwrap_or_default(), + focal_mm: m.focal_length.map(|f| f as f32), + aperture: m.f_number.map(|f| f as f32), + shutter, + iso: m.iso, + location: String::new(), + gps: m.gps.as_ref().map(|g| (g.latitude, g.longitude)), + title: m.title.clone().unwrap_or_default(), + caption: m.caption.clone().unwrap_or_default(), + copyright: m.copyright.clone().unwrap_or_default(), + creator: m.artist.clone().unwrap_or_default(), + keywords: m.keywords.clone(), + }; + (meta, m.capture_time.as_ref().map(|d| d.to_iso())) +} + +fn ext_upper(name: &str) -> String { + std::path::Path::new(name).extension().map(|e| e.to_string_lossy().to_uppercase()).unwrap_or_default() +} + +/// Probe a file's bytes: kind, dimensions (oriented), metadata. +pub fn probe_bytes(name: &str, bytes: &[u8]) -> Result { + let m = lightcraft_meta::extract(bytes); + let (meta, captured) = meta_of(&m); + if lightcraft_raw::probe(bytes).is_some() { + let raw = lightcraft_raw::decode(bytes).map_err(|e| e.to_string())?; + let (mut w, mut h) = (raw.crop.width.max(1) as u32, raw.crop.height.max(1) as u32); + if w <= 1 || h <= 1 { + (w, h) = (raw.active_area.width as u32, raw.active_area.height as u32); + } + if raw.orientation.swaps_axes() { + std::mem::swap(&mut w, &mut h); + } + let (t, tint) = xy_to_temp_tint(lightcraft_raw::color::as_shot_white_xy(&raw)); + let as_shot_wb = Some((t.round(), tint.round())); + return Ok(ProbeInfo { + width: w, + height: h, + format: ext_upper(name), + kind: MediaKind::Raw, + file_size: bytes.len() as u64, + captured, + meta, + as_shot_wb, + }); + } + let fmt = lightcraft_codecs::sniff(bytes).ok_or("unrecognized file format")?; + if !fmt.can_decode() { + return Err(format!("{fmt:?} files are not supported yet")); + } + let d = lightcraft_codecs::decode(bytes, lightcraft_codecs::DecodeOptions::fit(64, 64)).map_err(|e| e.to_string())?; + let o = Orientation::from_exif(d.orientation); + let (mut w, mut h) = (d.source_width, d.source_height); + if o.swaps_axes() { + std::mem::swap(&mut w, &mut h); + } + let format = match ext_upper(name).as_str() { + "JPG" | "JPEG" => "JPEG".to_string(), + "" => format!("{fmt:?}").to_uppercase(), + e => e.to_string(), + }; + Ok(ProbeInfo { width: w, height: h, format, kind: MediaKind::Image, file_size: bytes.len() as u64, captured, meta, as_shot_wb: None }) +} + +/// Decode a file into a linear Rec.2020 image no larger than `max_edge`, oriented. +pub fn load_bytes(bytes: &[u8], max_edge: usize) -> Result<(Rgb32f, SourceInfo), String> { + if lightcraft_raw::probe(bytes).is_some() { + let raw = lightcraft_raw::decode(bytes).map_err(|e| e.to_string())?; + let method = if max_edge <= 600 { lightcraft_raw::Method::Bilinear } else { lightcraft_raw::Method::Ahd }; + let mut img = raw.develop(method).map_err(|e| e.to_string())?; + let xy = lightcraft_raw::color::as_shot_white_xy(&raw); + let t = lightcraft_raw::color::camera_transform(&raw, xy); + lightcraft_raw::highlight::reconstruct(&mut img, t.wb, 0.99); + let m = t.matrix.to_f32(); + let gain = 2f32.powf(t.baseline_exposure as f32); + let wb = t.wb; + let img = img.map(|p| { + let c = [p[0] * wb[0], p[1] * wb[1], p[2] * wb[2]]; + [ + ((m[0][0] * c[0] + m[0][1] * c[1] + m[0][2] * c[2]) * gain).max(0.0), + ((m[1][0] * c[0] + m[1][1] * c[1] + m[1][2] * c[2]) * gain).max(0.0), + ((m[2][0] * c[0] + m[2][1] * c[1] + m[2][2] * c[2]) * gain).max(0.0), + ] + }); + let img = fit(&img, max_edge, max_edge, Filter::Box).oriented(raw.orientation); + let (temp, tint) = xy_to_temp_tint(xy); + return Ok((img, SourceInfo { raw: true, as_shot_temp: temp.round(), as_shot_tint: tint.round() })); + } + let d = lightcraft_codecs::decode(bytes, lightcraft_codecs::DecodeOptions::fit(max_edge as u32, max_edge as u32)).map_err(|e| e.to_string())?; + let img = d.to_working(); + let img = if img.width.max(img.height) > max_edge { fit(&img, max_edge, max_edge, Filter::Mitchell) } else { img }; + Ok((img.oriented(Orientation::from_exif(d.orientation)), SourceInfo::default())) +} + +/// Filesystem-backed hooks (native). On the web the host installs bytes-based hooks instead. +pub fn fs_hooks() -> (FileLoader, FileProbe) { + let loader: FileLoader = Arc::new(|path: &str, max_edge: usize| { + let bytes = std::fs::read(path).map_err(|e| format!("{path}: {e}"))?; + load_bytes(&bytes, max_edge) + }); + let probe: FileProbe = Arc::new(|path: &str| { + let bytes = std::fs::read(path).map_err(|e| format!("{path}: {e}"))?; + probe_bytes(path, &bytes) + }); + (loader, probe) +} + +impl crate::Session { + /// Install the filesystem file hooks (desktop, CLI, MCP headless). + pub fn with_fs(mut self) -> Self { + let (l, p) = fs_hooks(); + self.media.file_loader = Some(l); + self.media.file_probe = Some(p); + self + } +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index bfaff662..13ff7733 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -12,6 +12,7 @@ pub mod cmd; pub mod demo; +pub mod files; pub mod media; pub mod presets; mod view; diff --git a/crates/engine/src/media.rs b/crates/engine/src/media.rs index 5a2078b4..d34fd4c3 100644 --- a/crates/engine/src/media.rs +++ b/crates/engine/src/media.rs @@ -200,6 +200,7 @@ pub struct ProbeInfo { pub file_size: u64, pub captured: Option, pub meta: lightcraft_catalog::Meta, + pub as_shot_wb: Option<(f64, f64)>, } pub type FileProbe = Arc Result + Send + Sync>; @@ -234,6 +235,10 @@ pub fn import_paths(s: &mut crate::Session, paths: &[String]) -> crate::Result, +} + +impl ControlRequest { + pub fn new(method: impl Into, params: Value) -> (Self, std::sync::mpsc::Receiver) { + let (tx, rx) = std::sync::mpsc::channel(); + (Self { method: method.into(), params, reply: tx }, rx) + } +} + +pub enum Outcome { + Done(Value), + Screenshot { path: Option }, +} + +fn ok(v: Value) -> Outcome { + Outcome::Done(json!({"ok": true, "result": v})) +} +fn err(e: impl std::fmt::Display) -> Outcome { + Outcome::Done(json!({"ok": false, "error": e.to_string()})) +} +fn wrap(r: Result) -> Outcome { + match r { + Ok(v) => ok(v), + Err(e) => err(e), + } +} + +pub fn all_commands(app: &LightcraftApp) -> Value { + let mut v: Vec = app.session.commands().into_iter().map(|c| serde_json::to_value(c).unwrap_or_default()).collect(); + for (id, label, sc, menu) in crate::menus::UI_COMMANDS { + v.push(json!({"id": id, "label": label, "shortcut": sc, "menu": [menu], "enabled": crate::menus::ui_enabled(app, id), "ui": true})); + } + Value::Array(v) +} + +fn rect_json(r: egui::Rect) -> Value { + json!([r.left(), r.top(), r.width(), r.height()]) +} + +pub fn inspect(app: &LightcraftApp, ctx: &egui::Context) -> Value { + let r = ctx.content_rect(); + json!({ + "ui": serde_json::to_value(&app.ui).unwrap_or_default(), + "window": [r.width(), r.height()], + "pixelsPerPoint": ctx.pixels_per_point(), + "canvasRect": app.canvas_rect.map(rect_json), + "imageRect": app.image_rect.map(rect_json), + "active": app.session.active().map(|p| p.0), + "selection": app.session.selection.ids.iter().map(|p| p.0).collect::>(), + "activeMask": app.session.active_mask, + "widgetCount": app.widgets.len(), + "perf": {"frameMs": app.perf.frame_ms, "fps": app.perf.fps, "lastRenderMs": app.renderer.last_main_ms, "renderQueue": app.renderer.queued(), "rendersDone": app.renderer.completed}, + "status": app.ui.status, + }) +} + +fn modifiers(p: &Value) -> egui::Modifiers { + let b = |n: &str| p.get(n).and_then(Value::as_bool).unwrap_or(false); + egui::Modifiers { alt: b("alt"), ctrl: b("ctrl"), shift: b("shift"), mac_cmd: b("cmd") && cfg!(target_os = "macos"), command: b("cmd") } +} + +fn key_from(name: &str) -> Option { + egui::Key::from_name(name).or(match name.to_ascii_lowercase().as_str() { + "enter" | "return" => Some(egui::Key::Enter), + "esc" | "escape" => Some(egui::Key::Escape), + "delete" | "backspace" => Some(egui::Key::Backspace), + "left" => Some(egui::Key::ArrowLeft), + "right" => Some(egui::Key::ArrowRight), + "up" => Some(egui::Key::ArrowUp), + "down" => Some(egui::Key::ArrowDown), + "space" => Some(egui::Key::Space), + "tab" => Some(egui::Key::Tab), + "\\" | "backslash" => Some(egui::Key::Backslash), + "/" | "slash" => Some(egui::Key::Slash), + _ => None, + }) +} + +fn push_drag(app: &mut LightcraftApp, a: egui::Pos2, b: egui::Pos2, steps: u64, m: egui::Modifiers) { + app.synthetic.push(egui::Event::PointerMoved(a)); + app.synthetic.push(egui::Event::PointerButton { pos: a, button: egui::PointerButton::Primary, pressed: true, modifiers: m }); + for i in 1..=steps.max(1) { + let t = i as f32 / steps.max(1) as f32; + app.synthetic.push(egui::Event::PointerMoved(a + (b - a) * t)); + } + app.synthetic.push(egui::Event::PointerButton { pos: b, button: egui::PointerButton::Primary, pressed: false, modifiers: m }); +} + +fn widget_rect(app: &LightcraftApp, id: &str) -> Option { + app.widgets.iter().rev().find(|(w, _)| w == id).map(|(_, r)| *r) +} + +pub fn handle(app: &mut LightcraftApp, ctx: &egui::Context, req: &ControlRequest) -> Outcome { + let p = &req.params; + let s = |k: &str| p.get(k).and_then(Value::as_str); + let f = |k: &str| p.get(k).and_then(Value::as_f64); + match req.method.as_str() { + "engine.execute" | "ui.menu.invoke" | "command" => { + let Some(id) = s("command").or(s("id")) else { return err("missing `command`") }; + let params = p.get("params").cloned().filter(|v| !v.is_null()).unwrap_or(json!({})); + let r = app.run(id, params); + ctx.request_repaint(); + wrap(r) + } + "engine.commands" => ok(all_commands(app)), + "ui.menu.list" => ok(serde_json::to_value(crate::menus::menu_entries(app)).unwrap_or_default()), + "ui.inspect" => ok(inspect(app, ctx)), + "ui.widgets" => { + let filter = s("filter").unwrap_or(""); + ok(Value::Array( + app.widgets.iter().filter(|(id, _)| id.contains(filter)).map(|(id, r)| json!({"id": id, "rect": rect_json(*r)})).collect(), + )) + } + "ui.clickWidget" | "ui.dragWidget" => { + let Some(id) = s("id") else { return err("missing `id`") }; + let Some(r) = widget_rect(app, id) else { return err(format!("no widget `{id}` on screen (see ui.widgets)")) }; + let m = modifiers(p); + // optional relative position inside the widget (0..1) + let at = egui::pos2(r.left() + r.width() * f("fx").unwrap_or(0.5) as f32, r.top() + r.height() * f("fy").unwrap_or(0.5) as f32); + if req.method == "ui.clickWidget" { + let n = p.get("count").and_then(Value::as_u64).unwrap_or(1); + app.synthetic.push(egui::Event::PointerMoved(at)); + for _ in 0..n { + app.synthetic.push(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: true, modifiers: m }); + app.synthetic.push(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: false, modifiers: m }); + } + } else { + let to = egui::pos2( + f("toX").map(|v| v as f32).unwrap_or(at.x + f("dx").unwrap_or(0.0) as f32), + f("toY").map(|v| v as f32).unwrap_or(at.y + f("dy").unwrap_or(0.0) as f32), + ); + push_drag(app, at, to, p.get("steps").and_then(Value::as_u64).unwrap_or(10), m); + } + ctx.request_repaint(); + ok(json!({"rect": rect_json(r)})) + } + "ui.move" => { + app.synthetic.push(egui::Event::PointerMoved(egui::pos2(f("x").unwrap_or(0.0) as f32, f("y").unwrap_or(0.0) as f32))); + ctx.request_repaint(); + ok(Value::Null) + } + "ui.click" | "ui.drag" => { + let a = egui::pos2(f("x").unwrap_or(0.0) as f32, f("y").unwrap_or(0.0) as f32); + let m = modifiers(p); + let button = if s("button") == Some("right") { egui::PointerButton::Secondary } else { egui::PointerButton::Primary }; + if req.method == "ui.drag" { + let b = egui::pos2(f("toX").unwrap_or(0.0) as f32, f("toY").unwrap_or(0.0) as f32); + push_drag(app, a, b, p.get("steps").and_then(Value::as_u64).unwrap_or(10), m); + } else { + app.synthetic.push(egui::Event::PointerMoved(a)); + for _ in 0..p.get("count").and_then(Value::as_u64).unwrap_or(1) { + app.synthetic.push(egui::Event::PointerButton { pos: a, button, pressed: true, modifiers: m }); + app.synthetic.push(egui::Event::PointerButton { pos: a, button, pressed: false, modifiers: m }); + } + } + ctx.request_repaint(); + ok(Value::Null) + } + "ui.pointer" => { + // events in normalized image coordinates of the displayed photo frame + let Some(img) = app.image_rect else { return err("no image on screen (switch to Detail view)") }; + let Some(events) = p.get("events").and_then(Value::as_array) else { return err("missing `events`") }; + let m = modifiers(p); + for e in events { + let x = e.get("x").and_then(Value::as_f64).unwrap_or(0.5) as f32; + let y = e.get("y").and_then(Value::as_f64).unwrap_or(0.5) as f32; + let pos = egui::pos2(img.left() + x * img.width(), img.top() + y * img.height()); + match e.get("kind").and_then(Value::as_str).unwrap_or("move") { + "down" => { + app.synthetic.push(egui::Event::PointerMoved(pos)); + app.synthetic.push(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, modifiers: m }); + } + "up" => { + app.synthetic.push(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers: m }) + } + _ => app.synthetic.push(egui::Event::PointerMoved(pos)), + } + } + ctx.request_repaint(); + ok(json!({"events": events.len()})) + } + "ui.key" => { + let Some(k) = s("key").and_then(key_from) else { return err("unknown or missing `key`") }; + let m = modifiers(p); + app.synthetic.push(egui::Event::Key { key: k, physical_key: None, pressed: true, repeat: false, modifiers: m }); + app.synthetic.push(egui::Event::Key { key: k, physical_key: None, pressed: false, repeat: false, modifiers: m }); + ctx.request_repaint(); + ok(Value::Null) + } + "ui.text" => { + app.synthetic.push(egui::Event::Text(s("text").unwrap_or("").to_string())); + ctx.request_repaint(); + ok(Value::Null) + } + "ui.scroll" => { + app.synthetic.push(egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Point, + delta: egui::vec2(f("dx").unwrap_or(0.0) as f32, f("dy").unwrap_or(0.0) as f32), + phase: egui::TouchPhase::Move, + modifiers: modifiers(p), + }); + ctx.request_repaint(); + ok(Value::Null) + } + "ui.set" => { + let mut v = serde_json::to_value(&app.ui).unwrap_or_default(); + lightcraft_develop::presets::deep_merge(&mut v, p); + match serde_json::from_value::(v) { + Ok(mut u) => { + u.toast = app.ui.toast.clone(); + u.dialog = app.ui.dialog.clone(); + u.status = app.ui.status.clone(); + app.ui = u; + ctx.request_repaint(); + ok(serde_json::to_value(&app.ui).unwrap_or_default()) + } + Err(e) => err(e), + } + } + "ui.dialog.confirm" => match app.ui.dialog.take() { + Some(d) => wrap(crate::panels::dialogs::confirm_dialog(app, &d)), + None => err("no dialog open"), + }, + "ui.dialog.cancel" => { + app.ui.dialog = None; + ok(Value::Null) + } + "ui.resize" => { + let w = f("width").unwrap_or(1600.0) as f32; + let h = f("height").unwrap_or(1000.0) as f32; + ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(egui::vec2(w, h))); + ok(Value::Null) + } + "ui.screenshot" => { + ctx.request_repaint(); + Outcome::Screenshot { path: s("path").map(str::to_string) } + } + "ui.render" => { + let id = p.get("id").and_then(Value::as_u64).map(lightcraft_catalog::PhotoId).or(app.session.active()); + let Some(id) = id else { return err("no photo") }; + let size = p.get("size").and_then(Value::as_u64).unwrap_or(1600) as usize; + match app.session.render_now(id, size, size) { + Ok(r) => match (s("path"), app.services.png.as_ref()) { + (Some(path), Some(png)) => { + let bytes = png(&r.image); + match app.services.write.as_mut() { + Some(w) => wrap(w(path, &bytes).map(|_| json!({"path": path, "width": r.image.width, "height": r.image.height}))), + None => err("no writer"), + } + } + _ => ok(json!({"width": r.image.width, "height": r.image.height, "histogram": r.histogram})), + }, + Err(e) => err(e), + } + } + "app.quit" => { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + ok(Value::Null) + } + other => err(format!("unknown method `{other}`")), + } +} + +/// Export the active photo (UI command `app.export`): renders at `longEdge` and writes a PNG/JPEG via services. +pub fn export_active(app: &mut LightcraftApp, p: &Value) -> Result { + let id = app.session.active().ok_or("no photo selected")?; + let size = p.get("longEdge").and_then(Value::as_u64).unwrap_or(3000) as usize; + let r = app.session.render_now(id, size, size)?; + let name = app.session.catalog.photo(id).map(|p| p.file_name.clone()).unwrap_or_else(|| "export".into()); + let stem = name.rsplit_once('.').map(|(a, _)| a.to_string()).unwrap_or(name); + let path = p.get("path").and_then(Value::as_str).map(str::to_string).unwrap_or_else(|| format!("{stem}-lightcraft.png")); + let png = app.services.png.as_ref().ok_or("no encoder")?; + let bytes = png(&r.image); + let w = app.services.write.as_mut().ok_or("no writer")?; + w(&path, &bytes)?; + Ok(json!({"path": path, "width": r.image.width, "height": r.image.height})) +} + +pub fn save_screenshot(app: &mut LightcraftApp, image: &egui::ColorImage, path: Option<&str>) -> Value { + let [w, h] = image.size; + let Some(path) = path else { + return json!({"ok": true, "result": {"width": w, "height": h}}); + }; + let rgba: Vec = image.pixels.iter().flat_map(|c| c.to_array()).collect(); + let Some(img) = lightcraft_raster::Rgba8::from_bytes(w, h, &rgba) else { + return json!({"ok": false, "error": "bad screenshot buffer"}); + }; + let Some(png) = app.services.png.as_ref() else { return json!({"ok": false, "error": "no encoder"}) }; + let bytes = png(&img); + match app.services.write.as_mut() { + Some(wr) => match wr(path, &bytes) { + Ok(()) => json!({"ok": true, "result": {"path": path, "width": w, "height": h}}), + Err(e) => json!({"ok": false, "error": e}), + }, + None => json!({"ok": false, "error": "no writer configured"}), + } +} diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index df98e137..a8656c6f 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -381,7 +381,14 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, color: Color32) { Invert => { pen.circle(10.0, 10.0, 7.0); pen.p.add(Shape::convex_polygon( - (0..=16).map(|i| pen.pt(10.0 + 7.0 * ((90.0 + i as f32 * 180.0 / 16.0f32).to_radians()).cos(), 10.0 + 7.0 * ((90.0 + i as f32 * 180.0 / 16.0f32).to_radians()).sin())).collect(), + (0..=16) + .map(|i| { + pen.pt( + 10.0 + 7.0 * ((90.0 + i as f32 * 180.0 / 16.0f32).to_radians()).cos(), + 10.0 + 7.0 * ((90.0 + i as f32 * 180.0 / 16.0f32).to_radians()).sin(), + ) + }) + .collect(), color, Stroke::NONE, )); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 9be3efe4..c83e7c5f 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -208,7 +208,8 @@ impl LightcraftApp { } #[cfg(not(target_arch = "wasm32"))] { - let dropped: Vec = ctx.input(|i| i.raw.dropped_files.iter().filter_map(|f| f.path.as_ref().map(|p| p.to_string_lossy().to_string())).collect()); + let dropped: Vec = + ctx.input(|i| i.raw.dropped_files.iter().map(|f| f.path().to_string_lossy().to_string()).filter(|p| !p.is_empty()).collect()); if !dropped.is_empty() { let _ = self.run("library.import", serde_json::json!({"paths": dropped})); } @@ -227,12 +228,6 @@ impl LightcraftApp { if let Some(egui::Event::PointerMoved(p) | egui::Event::PointerButton { pos: p, .. }) = self.synthetic.first() { raw.events.push(egui::Event::PointerMoved(*p)); } - if let Some(m) = self.synthetic.iter().take(n).find_map(|e| match e { - egui::Event::Key { modifiers, .. } | egui::Event::PointerButton { modifiers, .. } => Some(*modifiers), - _ => None, - }) { - raw.modifiers = m; - } raw.events.extend(self.synthetic.drain(..n)); } @@ -282,3 +277,8 @@ pub fn now_ms() -> f64 { 0.0 } } + +/// Whether the settings render in black & white. +pub fn is_bw(d: &lightcraft_develop::DevelopSettings) -> bool { + d.treatment == lightcraft_develop::Treatment::Bw || d.profile.id == "lc.mono" +} diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs new file mode 100644 index 00000000..042f4eab --- /dev/null +++ b/crates/ui-egui/src/menus.rs @@ -0,0 +1,347 @@ +//! UI-level commands (views, panels, zoom, tools, dialogs) and the menu model shared by the native +//! menu bar, the shortcut handler and the control channel. + +use serde::Serialize; +use serde_json::{Value, json}; + +use crate::LightcraftApp; +use crate::state::{BeforeAfter, Dialog, RightPanel, ViewMode, Zoom}; + +/// (id, label, shortcut, menu path) +pub type UiCommand = (&'static str, &'static str, Option<&'static str>, &'static str); + +pub const UI_COMMANDS: &[UiCommand] = &[ + ("view.photoGrid", "Photo Grid", Some("G"), "View"), + ("view.squareGrid", "Square Grid", Some("Shift+G"), "View"), + ("view.detail", "Detail", Some("D"), "View"), + ("view.filmstrip", "Filmstrip", Some("/"), "View"), + ("view.leftPanel", "My Photos Panel", Some("Cmd+Shift+L"), "View"), + ("view.beforeAfter", "Compare Before and After", Some("Y"), "View"), + ("view.beforeAfterSplit", "Before/After Split", Some("Shift+Y"), "View"), + ("view.showOriginal", "Show Original", Some("\\"), "View"), + ("view.zoomFit", "Zoom to Fit", Some("Cmd+0"), "View"), + ("view.zoom100", "Zoom 100%", Some("Cmd+1"), "View"), + ("view.zoomToggle", "Toggle Zoom", Some("Z"), "View"), + ("view.zoomIn", "Zoom In", Some("Cmd+="), "View"), + ("view.zoomOut", "Zoom Out", Some("Cmd+-"), "View"), + ("view.clipping", "Show Clipping", Some("J"), "View"), + ("view.histogram", "Histogram", Some("Cmd+Shift+H"), "View"), + ("view.maskOverlay", "Show Mask Overlay", Some("O"), "View"), + ("view.cropOverlay", "Cycle Crop Overlay", Some("Shift+O"), "View"), + ("view.back", "Back to Grid", Some("Escape"), "View"), + ("view.filterBar", "Filter", None, "View"), + ("panel.edit", "Edit", Some("E"), "View"), + ("panel.crop", "Crop & Rotate", Some("C"), "View"), + ("panel.remove", "Remove", Some("H"), "View"), + ("panel.masking", "Masking", Some("M"), "View"), + ("panel.redeye", "Red Eye", None, "View"), + ("panel.presets", "Presets", Some("Shift+P"), "View"), + ("panel.info", "Info", Some("I"), "View"), + ("panel.keywords", "Keywords", Some("K"), "View"), + ("panel.versions", "Versions", Some("Shift+V"), "View"), + ("panel.activity", "History", Some("Shift+Y"), "View"), + ("panel.close", "Close Panel", None, "View"), + ("section.light", "Light", Some("Cmd+Alt+1"), "View"), + ("section.color", "Color", Some("Cmd+Alt+2"), "View"), + ("section.effects", "Effects", Some("Cmd+Alt+3"), "View"), + ("section.detail", "Detail", Some("Cmd+Alt+4"), "View"), + ("section.optics", "Optics", Some("Cmd+Alt+5"), "View"), + ("tool.brush", "Brush", Some("B"), "View"), + ("tool.linear", "Linear Gradient", Some("L"), "View"), + ("tool.radial", "Radial Gradient", Some("R"), "View"), + ("tool.wbPicker", "White Balance Selector", Some("W"), "View"), + ("tool.none", "No Tool", None, "View"), + ("dialog.newAlbum", "New Album…", Some("Cmd+N"), "File"), + ("dialog.createPreset", "Create Preset…", Some("Cmd+Shift+P"), "Photo"), + ("dialog.copySettings", "Choose Edit Settings to Copy…", Some("Cmd+Shift+C"), "Photo"), + ("dialog.export", "Export…", Some("Cmd+Shift+E"), "File"), + ("file.addPhotos", "Add Photos…", Some("Cmd+Shift+I"), "File"), + ("app.about", "About LightCraft", None, "LightCraft"), + ("app.shortcuts", "Keyboard Shortcuts", Some("Cmd+/"), "Help"), + ("app.export", "Export Now", None, ""), +]; + +fn panel(app: &mut LightcraftApp, ctx: &egui::Context, p: RightPanel, name: &str) { + if app.ui.right == p { + app.ui.right = RightPanel::None; + app.toast(ctx, format!("{name} Off")); + } else { + app.ui.right = p; + app.toast(ctx, format!("{name} On")); + if p.is_edit_tool() && !matches!(app.ui.view, ViewMode::Detail) { + app.ui.view = ViewMode::Detail; + } + } + if p != RightPanel::Masking && app.ui.tool != "wbPicker" { + app.ui.tool.clear(); + } + let _ = app.session.end_interaction(); +} + +/// Handle UI commands; `None` means "not a UI command — send it to the engine". +pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option> { + let ctx = egui::Context::default(); + let r: Result = match id { + "view.photoGrid" => { + app.ui.view = ViewMode::PhotoGrid; + Ok(Value::Null) + } + "view.squareGrid" => { + app.ui.view = ViewMode::SquareGrid; + Ok(Value::Null) + } + "view.detail" => { + app.ui.view = ViewMode::Detail; + Ok(Value::Null) + } + "view.back" => { + if app.ui.dialog.is_some() { + app.ui.dialog = None; + } else if !app.ui.tool.is_empty() { + app.ui.tool.clear(); + } else if app.ui.view == ViewMode::Detail { + app.ui.view = ViewMode::PhotoGrid; + } + Ok(Value::Null) + } + "view.filmstrip" => { + app.ui.filmstrip = !app.ui.filmstrip; + Ok(Value::Null) + } + "view.leftPanel" => { + app.ui.left_panel = !app.ui.left_panel; + Ok(Value::Null) + } + "view.beforeAfter" => { + app.ui.before_after = if app.ui.before_after == BeforeAfter::SideBySide { BeforeAfter::Off } else { BeforeAfter::SideBySide }; + Ok(Value::Null) + } + "view.beforeAfterSplit" => { + app.ui.before_after = if app.ui.before_after == BeforeAfter::Split { BeforeAfter::Off } else { BeforeAfter::Split }; + Ok(Value::Null) + } + "view.showOriginal" => { + app.ui.before_after = if app.ui.before_after == BeforeAfter::Original { BeforeAfter::Off } else { BeforeAfter::Original }; + Ok(Value::Null) + } + "view.zoomFit" => { + app.ui.zoom = Zoom::Fit; + Ok(Value::Null) + } + "view.zoom100" => { + app.ui.zoom = Zoom::Percent(100); + Ok(Value::Null) + } + "view.zoomToggle" => { + app.ui.zoom = if app.ui.zoom == Zoom::Fit { Zoom::Percent(100) } else { Zoom::Fit }; + Ok(Value::Null) + } + "view.zoomIn" | "view.zoomOut" => { + let steps = [25u32, 50, 100, 200, 400, 800]; + let cur = match app.ui.zoom { + Zoom::Percent(p) => p, + _ => 25, + }; + let next = if id == "view.zoomIn" { + steps.iter().find(|s| **s > cur).copied().unwrap_or(800) + } else { + steps.iter().rev().find(|s| **s < cur).copied().unwrap_or(0) + }; + app.ui.zoom = if next == 0 { Zoom::Fit } else { Zoom::Percent(next) }; + Ok(Value::Null) + } + "view.clipping" => { + app.ui.show_clipping = !app.ui.show_clipping; + Ok(Value::Null) + } + "view.histogram" => { + app.ui.histogram = !app.ui.histogram; + Ok(Value::Null) + } + "view.maskOverlay" => { + app.ui.mask_overlay = !app.ui.mask_overlay; + Ok(Value::Null) + } + "view.cropOverlay" => { + use crate::state::CropOverlay::*; + app.ui.crop_overlay = match app.ui.crop_overlay { + Thirds => Grid, + Grid => Golden, + Golden => Diagonal, + Diagonal => None, + None => Thirds, + }; + Ok(Value::Null) + } + "view.filterBar" => { + app.ui.left_panel = true; + Ok(Value::Null) + } + "panel.edit" => { + panel(app, &ctx, RightPanel::Edit, "Edit"); + Ok(Value::Null) + } + "panel.crop" => { + panel(app, &ctx, RightPanel::Crop, "Crop, Rotate, Geometry"); + Ok(Value::Null) + } + "panel.remove" => { + panel(app, &ctx, RightPanel::Remove, "Remove"); + if app.ui.right == RightPanel::Remove && app.ui.tool.is_empty() { + app.ui.tool = "remove".into(); + } + Ok(Value::Null) + } + "panel.masking" => { + panel(app, &ctx, RightPanel::Masking, "Masking"); + Ok(Value::Null) + } + "panel.redeye" => { + panel(app, &ctx, RightPanel::RedEye, "Red Eye"); + Ok(Value::Null) + } + "panel.info" => { + panel(app, &ctx, RightPanel::Info, "Info"); + Ok(Value::Null) + } + "panel.keywords" => { + panel(app, &ctx, RightPanel::Keywords, "Keywords"); + Ok(Value::Null) + } + "panel.versions" => { + panel(app, &ctx, RightPanel::Versions, "Versions"); + Ok(Value::Null) + } + "panel.activity" => { + panel(app, &ctx, RightPanel::Activity, "History"); + Ok(Value::Null) + } + "panel.presets" => { + app.ui.presets = !app.ui.presets; + if app.ui.presets && app.ui.view != ViewMode::Detail { + app.ui.view = ViewMode::Detail; + } + Ok(Value::Null) + } + "panel.close" => { + app.ui.right = RightPanel::None; + app.ui.presets = false; + Ok(Value::Null) + } + s if s.starts_with("section.") => { + let sec = &s["section.".len()..]; + if app.ui.right != RightPanel::Edit { + app.ui.right = RightPanel::Edit; + } + app.ui.toggle_section(sec); + Ok(Value::Null) + } + s if s.starts_with("tool.") => { + let tool = &s["tool.".len()..]; + match tool { + "none" => app.ui.tool.clear(), + "brush" => { + app.ui.right = RightPanel::Masking; + app.ui.view = ViewMode::Detail; + app.ui.tool = "brush".into(); + } + "linear" | "radial" => { + app.ui.right = RightPanel::Masking; + app.ui.view = ViewMode::Detail; + app.ui.tool = tool.into(); + return Some(app.session.execute("mask.add", &json!({"kind": tool})).map_err(|e| e.to_string())); + } + "wbPicker" => { + app.ui.right = RightPanel::Edit; + app.ui.view = ViewMode::Detail; + app.ui.tool = "wbPicker".into(); + } + other => return Some(Err(format!("unknown tool `{other}`"))), + } + Ok(Value::Null) + } + "dialog.newAlbum" => { + app.ui.dialog = Some(Dialog::NewAlbum { name: p.get("name").and_then(Value::as_str).unwrap_or("").into(), folder: false }); + Ok(Value::Null) + } + "dialog.createPreset" => { + app.ui.dialog = Some(Dialog::CreatePreset { name: String::new(), group: "User Presets".into() }); + Ok(Value::Null) + } + "dialog.copySettings" => { + let groups = + app.session.copy_groups.iter().filter_map(|g| serde_json::to_value(g).ok().and_then(|v| v.as_str().map(str::to_string))).collect(); + app.ui.dialog = Some(Dialog::CopySettings { groups }); + Ok(Value::Null) + } + "dialog.export" => { + app.ui.dialog = Some(Dialog::Export { format: "jpeg".into(), quality: 90, long_edge: 3000 }); + Ok(Value::Null) + } + "app.about" => { + app.ui.dialog = Some(Dialog::About); + Ok(Value::Null) + } + "app.shortcuts" => { + app.ui.dialog = Some(Dialog::Shortcuts); + Ok(Value::Null) + } + "file.addPhotos" => { + let paths = match p.get("paths").and_then(Value::as_array) { + Some(a) => a.iter().filter_map(Value::as_str).map(str::to_string).collect(), + None => app.services.pick_files.as_mut().map(|f| f()).unwrap_or_default(), + }; + if paths.is_empty() { + return Some(Ok(Value::Null)); + } + return Some(app.session.execute("library.import", &json!({"paths": paths})).map_err(|e| e.to_string())); + } + "app.export" => crate::control::export_active(app, p), + _ => return None, + }; + Some(r) +} + +pub fn ui_enabled(app: &LightcraftApp, id: &str) -> bool { + match id { + s if s.starts_with("panel.") || s.starts_with("tool.") || s.starts_with("section.") => app.session.active().is_some() || s == "panel.close", + "app.export" | "dialog.export" | "dialog.createPreset" | "dialog.copySettings" => app.session.active().is_some(), + _ => true, + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct MenuEntry { + pub id: String, + pub label: String, + pub menu: Vec, + pub shortcut: Option, + pub enabled: bool, +} + +/// The flattened menu model (UI commands + engine commands with menu paths). +pub fn menu_entries(app: &LightcraftApp) -> Vec { + let mut v: Vec = UI_COMMANDS + .iter() + .filter(|c| !c.3.is_empty()) + .map(|(id, label, sc, m)| MenuEntry { + id: id.to_string(), + label: label.to_string(), + menu: vec![m.to_string()], + shortcut: sc.map(str::to_string), + enabled: ui_enabled(app, id), + }) + .collect(); + for c in app.session.commands() { + if !c.menu.is_empty() { + v.push(MenuEntry { + id: c.id.into(), + label: c.label.into(), + menu: c.menu.iter().map(|s| s.to_string()).collect(), + shortcut: c.shortcut.map(str::to_string), + enabled: c.enabled, + }); + } + } + v +} diff --git a/crates/ui-egui/src/panels/bottombar.rs b/crates/ui-egui/src/panels/bottombar.rs index 60bf389a..eb508ff2 100644 --- a/crates/ui-egui/src/panels/bottombar.rs +++ b/crates/ui-egui/src/panels/bottombar.rs @@ -40,7 +40,8 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let active = app.session.active().and_then(|id| app.session.catalog.photo(id).cloned()); let pill = Rect::from_center_size(pos2(full.center().x - 60.0, full.center().y), vec2(196.0, 30.0)); ui.painter().rect_filled(pill, 15.0, t.canvas); - let mut pill_ui = ui.new_child(egui::UiBuilder::new().max_rect(pill.shrink2(vec2(10.0, 4.0))).layout(egui::Layout::left_to_right(egui::Align::Center))); + let mut pill_ui = + ui.new_child(egui::UiBuilder::new().max_rect(pill.shrink2(vec2(10.0, 4.0))).layout(egui::Layout::left_to_right(egui::Align::Center))); pill_ui.spacing_mut().item_spacing.x = 3.0; let rating = active.as_ref().map(|p| p.rating).unwrap_or(0); if let Some(r) = stars(&mut pill_ui, "bottom", rating, 19.0) { @@ -48,7 +49,9 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { } pill_ui.add_space(6.0); let flag = active.as_ref().map(|p| p.flag).unwrap_or_default(); - for (id, icon, f) in [("unflag", Icon::Circle, Flag::None), ("pick", Icon::FlagPick, Flag::Pick), ("reject", Icon::FlagReject, Flag::Reject)] { + for (id, icon, f) in + [("unflag", Icon::Circle, Flag::None), ("pick", Icon::FlagPick, Flag::Pick), ("reject", Icon::FlagReject, Flag::Reject)] + { let (r, resp) = pill_ui.allocate_exact_size(vec2(22.0, 22.0), Sense::click()); register(pill_ui.ctx(), format!("flag:{id}"), r); let on = flag == f && f != Flag::None; @@ -71,7 +74,13 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let cresp = ui.interact(copy_r, egui::Id::new("copy-settings"), Sense::click()); register(ui.ctx(), "button:copySettings", copy_r); ui.painter().rect_filled(copy_r, 15.0, if cresp.hovered() { t.hover } else { t.canvas }); - ui.painter().text(copy_r.center(), Align2::CENTER_CENTER, label, t.font(13.0), if active.is_some() { t.text_label } else { t.text_disabled }); + ui.painter().text( + copy_r.center(), + Align2::CENTER_CENTER, + label, + t.font(13.0), + if active.is_some() { t.text_label } else { t.text_disabled }, + ); if cresp.clicked() && active.is_some() { let _ = if has_clip { app.run("develop.paste", json!({})) } else { app.run("develop.copy", json!({})) }; let msg = if has_clip { "Settings pasted" } else { "Edit settings copied" }; @@ -87,10 +96,24 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { } // right side - let mut child = ui.new_child(egui::UiBuilder::new().max_rect(Rect::from_min_max(pos2(full.right() - 360.0, full.top()), full.right_bottom())).layout(egui::Layout::right_to_left(egui::Align::Center))); + let mut child = ui.new_child( + egui::UiBuilder::new() + .max_rect(Rect::from_min_max(pos2(full.right() - 360.0, full.top()), full.right_bottom())) + .layout(egui::Layout::right_to_left(egui::Align::Center)), + ); child.spacing_mut().item_spacing.x = 6.0; if matches!(app.ui.view, ViewMode::Detail | ViewMode::Compare) { - if icon_button(&mut child, "beforeAfter", Icon::BeforeAfter, vec2(30.0, 30.0), app.ui.before_after != BeforeAfter::Off, true, "Before/After (Y)").clicked() { + if icon_button( + &mut child, + "beforeAfter", + Icon::BeforeAfter, + vec2(30.0, 30.0), + app.ui.before_after != BeforeAfter::Off, + true, + "Before/After (Y)", + ) + .clicked() + { let _ = app.run("view.beforeAfter", json!({})); } if icon_button(&mut child, "filmstrip", Icon::Filmstrip, vec2(30.0, 30.0), app.ui.filmstrip, true, "Filmstrip (/)").clicked() { @@ -102,10 +125,16 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { Zoom::Fill => "Fill".to_string(), Zoom::Percent(p) => format!("{p}%"), }; - let zr = child.add(egui::Button::new(egui::RichText::new(format!("{zoom_label} ⌄")).font(t.font(13.0)).color(t.text_label)).frame(false)); - register(child.ctx(), "button:zoom", zr.rect); + let zr = crate::widgets::dropdown(&mut child, "zoom", &zoom_label, t.font(13.0), t.text_label); egui::Popup::menu(&zr).show(|ui| { - for (label, z) in [("Fit", Zoom::Fit), ("Fill", Zoom::Fill), ("50%", Zoom::Percent(50)), ("100%", Zoom::Percent(100)), ("200%", Zoom::Percent(200)), ("400%", Zoom::Percent(400))] { + for (label, z) in [ + ("Fit", Zoom::Fit), + ("Fill", Zoom::Fill), + ("50%", Zoom::Percent(50)), + ("100%", Zoom::Percent(100)), + ("200%", Zoom::Percent(200)), + ("400%", Zoom::Percent(400)), + ] { if ui.selectable_label(app.ui.zoom == z, label).clicked() { app.ui.zoom = z; app.ui.pan = (0.5, 0.5); diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index a5a459bc..9c6d2046 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -106,7 +106,10 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let split = app.ui.before_after == BeforeAfter::SideBySide; let areas: Vec = if split { let half = area.width() / 2.0 - 6.0; - vec![Rect::from_min_size(area.min, vec2(half, area.height())), Rect::from_min_size(pos2(area.center().x + 6.0, area.top()), vec2(half, area.height()))] + vec![ + Rect::from_min_size(area.min, vec2(half, area.height())), + Rect::from_min_size(pos2(area.center().x + 6.0, area.top()), vec2(half, area.height())), + ] } else { vec![area] }; @@ -122,10 +125,10 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { app.renderer.request(Slot::Main, job, 100); } let show_before = app.ui.before_after == BeforeAfter::Original || ui.input(|i| i.key_down(egui::Key::Backslash)); - if split || show_before || app.ui.before_after == BeforeAfter::Split { - if let Some(job) = app.session.render_job(id, rw.max(8), rh.max(8), true, !crop_tool) { - app.renderer.request(Slot::Before, job, 90); - } + if (split || show_before || app.ui.before_after == BeforeAfter::Split) + && let Some(job) = app.session.render_job(id, rw.max(8), rh.max(8), true, !crop_tool) + { + app.renderer.request(Slot::Before, job, 90); } let p = ui.painter_at(canvas); let draw = |slot: Slot, r: Rect| { @@ -187,7 +190,16 @@ fn clipping_overlay(app: &LightcraftApp, p: &egui::Painter, r: Rect) { } } -fn general_interaction(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Response, map: &CanvasMap, img: Rect, canvas: Rect, native: [usize; 2], aspect: f32) { +fn general_interaction( + app: &mut LightcraftApp, + ui: &mut egui::Ui, + resp: &egui::Response, + map: &CanvasMap, + img: Rect, + canvas: Rect, + native: [usize; 2], + aspect: f32, +) { if app.ui.tool == "wbPicker" { ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair); if resp.clicked() @@ -260,9 +272,7 @@ fn crop_overlay(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Respons p.line_segment([lerp(pts[0], pts[3], f), lerp(pts[1], pts[2], f)], guide); } // handles: 0..3 corners, 4..7 edges (top, right, bottom, left) - let handles: Vec = (0..8) - .map(|i| if i < 4 { pts[i] } else { lerp(pts[i - 4], pts[(i - 3) % 4], 0.5) }) - .collect(); + let handles: Vec = (0..8).map(|i| if i < 4 { pts[i] } else { lerp(pts[i - 4], pts[(i - 3) % 4], 0.5) }).collect(); for (i, h) in handles.iter().enumerate() { register(ui.ctx(), format!("cropHandle:{i}"), Rect::from_center_size(*h, vec2(14.0, 14.0))); let s = if i < 4 { 12.0 } else { 9.0 }; @@ -402,7 +412,10 @@ fn mask_overlay(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Respons if sel && app.ui.mask_overlay { p.add(egui::Shape::convex_polygon(pts.clone(), red, Stroke::NONE)); } - p.add(egui::Shape::closed_line(pts, Stroke::new(if sel { 1.5 } else { 1.0 }, Color32::from_white_alpha(if sel { 230 } else { 120 })))); + p.add(egui::Shape::closed_line( + pts, + Stroke::new(if sel { 1.5 } else { 1.0 }, Color32::from_white_alpha(if sel { 230 } else { 120 })), + )); pin(p, c0, sel); if sel { register(ui.ctx(), format!("maskPin:{}", m.id), Rect::from_center_size(c0, vec2(14.0, 14.0))); @@ -626,7 +639,13 @@ fn filmstrip(app: &mut LightcraftApp, ui: &mut egui::Ui, r: Rect) { } if resp.clicked() { let m = ui.input(|i| i.modifiers); - let mode = if m.command { "toggle" } else if m.shift { "range" } else { "replace" }; + let mode = if m.command { + "toggle" + } else if m.shift { + "range" + } else { + "replace" + }; let _ = app.run("library.select", json!({"ids": [id.0], "mode": mode})); } } diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index d0fcacc4..c27aef66 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -24,89 +24,92 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { Dialog::About => "About LightCraft", Dialog::Shortcuts => "Keyboard Shortcuts", }; - egui::Window::new(title).collapsible(false).resizable(false).anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]).default_width(380.0).show(ctx, |ui| { - ui.spacing_mut().item_spacing.y = 8.0; - match &mut dlg { - Dialog::NewAlbum { name, .. } | Dialog::RenameAlbum { name, .. } => { - let r = ui.add(egui::TextEdit::singleline(name).hint_text("Name").desired_width(f32::INFINITY)); - r.request_focus(); - if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { - confirm = true; + egui::Window::new(title).collapsible(false).resizable(false).anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]).default_width(380.0).show( + ctx, + |ui| { + ui.spacing_mut().item_spacing.y = 8.0; + match &mut dlg { + Dialog::NewAlbum { name, .. } | Dialog::RenameAlbum { name, .. } => { + let r = ui.add(egui::TextEdit::singleline(name).hint_text("Name").desired_width(f32::INFINITY)); + r.request_focus(); + if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { + confirm = true; + } } - } - Dialog::CreatePreset { name, group } => { - ui.add(egui::TextEdit::singleline(name).hint_text("Preset name").desired_width(f32::INFINITY)); - ui.add(egui::TextEdit::singleline(group).hint_text("Group").desired_width(f32::INFINITY)); - ui.label(egui::RichText::new("Includes the current settings except crop, masks and remove.").color(t.text_dim)); - } - Dialog::CopySettings { groups } => { - ui.columns(2, |cols| { - for (i, g) in SettingsGroup::ALL.iter().enumerate() { - let key = serde_json::to_value(g).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default(); - let mut on = groups.contains(&key); - if cols[i % 2].checkbox(&mut on, g.label()).changed() { - if on { - groups.push(key); - } else { - groups.retain(|x| *x != key); - } - } - } - }); - } - Dialog::Export { format, quality, long_edge } => { - ui.horizontal(|ui| { - ui.label("Format"); - for f in ["jpeg", "png", "tiff"] { - ui.selectable_value(format, f.to_string(), f.to_uppercase()); - } - }); - ui.add(egui::Slider::new(quality, 10..=100).text("Quality")); - ui.add(egui::Slider::new(long_edge, 512..=8000).text("Long edge (px)")); - } - Dialog::About => { - ui.label(egui::RichText::new("LightCraft").font(t.semibold(20.0)).color(t.text)); - ui.label(format!("Version {} — a clean-room, pure-Rust photo library and raw developer.", env!("CARGO_PKG_VERSION"))); - ui.label("MIT OR Apache-2.0. Fonts: Source Sans 3 (OFL). Icons: original."); - } - Dialog::Shortcuts => { - egui::ScrollArea::vertical().max_height(420.0).show(ui, |ui| { - egui::Grid::new("shortcuts").striped(true).show(ui, |ui| { - for (id, label, sc, _) in crate::menus::UI_COMMANDS { - if let Some(sc) = sc { - ui.label(*label); - ui.label(*sc); - ui.label(egui::RichText::new(*id).color(t.text_dim)); - ui.end_row(); - } - } - for c in lightcraft_engine::command_specs() { - if let Some(sc) = c.shortcut { - ui.label(c.label); - ui.label(sc); - ui.label(egui::RichText::new(c.id).color(t.text_dim)); - ui.end_row(); + Dialog::CreatePreset { name, group } => { + ui.add(egui::TextEdit::singleline(name).hint_text("Preset name").desired_width(f32::INFINITY)); + ui.add(egui::TextEdit::singleline(group).hint_text("Group").desired_width(f32::INFINITY)); + ui.label(egui::RichText::new("Includes the current settings except crop, masks and remove.").color(t.text_dim)); + } + Dialog::CopySettings { groups } => { + ui.columns(2, |cols| { + for (i, g) in SettingsGroup::ALL.iter().enumerate() { + let key = serde_json::to_value(g).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default(); + let mut on = groups.contains(&key); + if cols[i % 2].checkbox(&mut on, g.label()).changed() { + if on { + groups.push(key); + } else { + groups.retain(|x| *x != key); + } } } }); - }); - } - } - ui.add_space(4.0); - ui.horizontal(|ui| { - let informational = matches!(dlg, Dialog::About | Dialog::Shortcuts); - if !informational && ui.button("Cancel").clicked() { - close = true; - } - if ui.button(if informational { "Close" } else { "OK" }).clicked() { - if informational { - close = true; - } else { - confirm = true; + } + Dialog::Export { format, quality, long_edge } => { + ui.horizontal(|ui| { + ui.label("Format"); + for f in ["jpeg", "png", "tiff"] { + ui.selectable_value(format, f.to_string(), f.to_uppercase()); + } + }); + ui.add(egui::Slider::new(quality, 10..=100).text("Quality")); + ui.add(egui::Slider::new(long_edge, 512..=8000).text("Long edge (px)")); + } + Dialog::About => { + ui.label(egui::RichText::new("LightCraft").font(t.semibold(20.0)).color(t.text)); + ui.label(format!("Version {} — a clean-room, pure-Rust photo library and raw developer.", env!("CARGO_PKG_VERSION"))); + ui.label("MIT OR Apache-2.0. Fonts: Source Sans 3 (OFL). Icons: original."); + } + Dialog::Shortcuts => { + egui::ScrollArea::vertical().max_height(420.0).show(ui, |ui| { + egui::Grid::new("shortcuts").striped(true).show(ui, |ui| { + for (id, label, sc, _) in crate::menus::UI_COMMANDS { + if let Some(sc) = sc { + ui.label(*label); + ui.label(*sc); + ui.label(egui::RichText::new(*id).color(t.text_dim)); + ui.end_row(); + } + } + for c in lightcraft_engine::command_specs() { + if let Some(sc) = c.shortcut { + ui.label(c.label); + ui.label(sc); + ui.label(egui::RichText::new(c.id).color(t.text_dim)); + ui.end_row(); + } + } + }); + }); } } - }); - }); + ui.add_space(4.0); + ui.horizontal(|ui| { + let informational = matches!(dlg, Dialog::About | Dialog::Shortcuts); + if !informational && ui.button("Cancel").clicked() { + close = true; + } + if ui.button(if informational { "Close" } else { "OK" }).clicked() { + if informational { + close = true; + } else { + confirm = true; + } + } + }); + }, + ); if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { close = true; } @@ -122,7 +125,9 @@ pub fn confirm_dialog(app: &mut LightcraftApp, dlg: &Dialog) -> Result app.run("album.create", json!({"name": name, "folder": folder, "addSelected": !folder})), Dialog::RenameAlbum { id, name } => app.run("album.rename", json!({"id": id, "name": name})), - Dialog::CreatePreset { name, group } => app.run("preset.create", json!({"name": if name.is_empty() { "My Preset" } else { name }, "group": group})), + Dialog::CreatePreset { name, group } => { + app.run("preset.create", json!({"name": if name.is_empty() { "My Preset" } else { name }, "group": group})) + } Dialog::CopySettings { groups } => app.run("develop.copy", json!({"groups": groups})), Dialog::Export { format, quality, long_edge } => app.run("app.export", json!({"format": format, "quality": quality, "longEdge": long_edge})), Dialog::About | Dialog::Shortcuts => Ok(serde_json::Value::Null), diff --git a/crates/ui-egui/src/panels/edit.rs b/crates/ui-egui/src/panels/edit.rs index f308e3eb..065f6b92 100644 --- a/crates/ui-egui/src/panels/edit.rs +++ b/crates/ui-egui/src/panels/edit.rs @@ -40,8 +40,28 @@ fn control(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings, id: } /// Relative temperature scale for rendered (non-raw) files: −100..100 ↔ Kelvin via mired shift. -const REL_TEMP: ControlSpec = ControlSpec { id: "wb.tempRel", label: "Temp", section: Section::Color, min: -100.0, max: 100.0, default: 0.0, step: 1.0, decimals: 0, track: Track::Temp }; -const REL_TINT: ControlSpec = ControlSpec { id: "wb.tintRel", label: "Tint", section: Section::Color, min: -100.0, max: 100.0, default: 0.0, step: 1.0, decimals: 0, track: Track::Tint }; +const REL_TEMP: ControlSpec = ControlSpec { + id: "wb.tempRel", + label: "Temp", + section: Section::Color, + min: -100.0, + max: 100.0, + default: 0.0, + step: 1.0, + decimals: 0, + track: Track::Temp, +}; +const REL_TINT: ControlSpec = ControlSpec { + id: "wb.tintRel", + label: "Tint", + section: Section::Color, + min: -100.0, + max: 100.0, + default: 0.0, + step: 1.0, + decimals: 0, + track: Track::Tint, +}; fn k_to_rel(k: f64) -> f64 { ((1e6 / 6500.0 - 1e6 / k) / 0.8).clamp(-100.0, 100.0) @@ -85,8 +105,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { ui.horizontal(|ui| { ui.label(egui::RichText::new("Profile").font(t.font(13.0)).color(t.text_dim)); let name = lightcraft_engine::presets::PROFILES.iter().find(|p| p.id == d.profile.id).map(|p| p.name).unwrap_or("Color"); - let r = ui.add(egui::Button::new(egui::RichText::new(format!("{name} ⌄")).font(t.font(15.0)).color(t.text_label)).frame(false)); - register(ui.ctx(), "button:profile", r.rect); + let r = crate::widgets::dropdown(ui, "profile", name, t.font(15.0), t.text_label); egui::Popup::menu(&r).show(|ui| { for p in lightcraft_engine::presets::PROFILES { if ui.selectable_label(p.id == d.profile.id, p.name).clicked() { @@ -117,8 +136,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 2, bottom: 2 }).show(ui, |ui| { ui.horizontal(|ui| { ui.label(egui::RichText::new("White Balance").font(t.font(13.0)).color(t.text_dim)); - let r = ui.add(egui::Button::new(egui::RichText::new(format!("{} ⌄", d.wb.mode.label())).font(t.font(14.0)).color(t.text_label)).frame(false)); - register(ui.ctx(), "button:wbMode", r.rect); + let r = crate::widgets::dropdown(ui, "wbMode", d.wb.mode.label(), t.font(14.0), t.text_label); egui::Popup::menu(&r).show(|ui| { for m in WbMode::ALL { if m == WbMode::Custom { @@ -132,7 +150,9 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { }); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let active = app.ui.tool == "wbPicker"; - if crate::widgets::icon_button(ui, "wbPicker", Icon::Picker, vec2(28.0, 28.0), active, true, "White Balance Selector (W)").clicked() { + if crate::widgets::icon_button(ui, "wbPicker", Icon::Picker, vec2(28.0, 28.0), active, true, "White Balance Selector (W)") + .clicked() + { app.ui.tool = if active { String::new() } else { "wbPicker".into() }; } }); @@ -186,7 +206,8 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { control(app, ui, d, c, c == "detail.sharpenAmount" || d.detail.sharpen_amount > 0.0); } sub_title(ui, "Noise Reduction"); - for c in ["detail.nrLuminance", "detail.nrDetail", "detail.nrContrast", "detail.nrColor", "detail.nrColorDetail", "detail.nrColorSmoothness"] { + for c in ["detail.nrLuminance", "detail.nrDetail", "detail.nrContrast", "detail.nrColor", "detail.nrColorDetail", "detail.nrColorSmoothness"] + { control(app, ui, d, c, true); } ui.add_space(8.0); @@ -216,7 +237,14 @@ fn sub_title(ui: &mut egui::Ui, title: &str) { ui.painter().text(pos2(r.left() + 24.0, r.center().y + 4.0), Align2::LEFT_CENTER, title, t.semibold(13.0), t.text_label); } -fn section(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings, id: &str, title: &str, body: impl FnOnce(&mut LightcraftApp, &mut egui::Ui, &DevelopSettings)) { +fn section( + app: &mut LightcraftApp, + ui: &mut egui::Ui, + d: &DevelopSettings, + id: &str, + title: &str, + body: impl FnOnce(&mut LightcraftApp, &mut egui::Ui, &DevelopSettings), +) { let open = app.ui.section_open(id); let (resp, toggled) = section_header(ui, id, title, open, Some(d.section_enabled(id))); if let Some(on) = toggled { @@ -308,7 +336,13 @@ fn curve_editor(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, d: &Dev // channel selector egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 6, bottom: 4 }).show(ui, |ui| { ui.horizontal(|ui| { - for (ch, c) in [("parametric", Color32::from_gray(200)), ("master", Color32::WHITE), ("red", hex("#dd3333")), ("green", hex("#33bb55")), ("blue", hex("#3377ee"))] { + for (ch, c) in [ + ("parametric", Color32::from_gray(200)), + ("master", Color32::WHITE), + ("red", hex("#dd3333")), + ("green", hex("#33bb55")), + ("blue", hex("#3377ee")), + ] { let (r, resp) = ui.allocate_exact_size(vec2(24.0, 24.0), Sense::click()); register(ui.ctx(), format!("curveChannel:{ch}"), r); let sel = app.ui.curve_channel == ch; @@ -340,8 +374,14 @@ fn curve_editor(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, d: &Dev p.rect_filled(r, 2.0, t.canvas); for i in 1..4 { let f = i as f32 / 4.0; - p.line_segment([pos2(r.left() + r.width() * f, r.top()), pos2(r.left() + r.width() * f, r.bottom())], Stroke::new(1.0, Color32::from_gray(52))); - p.line_segment([pos2(r.left(), r.top() + r.height() * f), pos2(r.right(), r.top() + r.height() * f)], Stroke::new(1.0, Color32::from_gray(52))); + p.line_segment( + [pos2(r.left() + r.width() * f, r.top()), pos2(r.left() + r.width() * f, r.bottom())], + Stroke::new(1.0, Color32::from_gray(52)), + ); + p.line_segment( + [pos2(r.left(), r.top() + r.height() * f), pos2(r.right(), r.top() + r.height() * f)], + Stroke::new(1.0, Color32::from_gray(52)), + ); } p.line_segment([r.left_bottom(), r.right_top()], Stroke::new(1.0, Color32::from_gray(70))); let to_screen = |x: f64, y: f64| pos2(r.left() + x as f32 * r.width(), r.bottom() - y as f32 * r.height()); @@ -349,7 +389,12 @@ fn curve_editor(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, d: &Dev if ch == "parametric" { // draw the effective parametric curve by sampling the pipeline's region curve approximation let c = &d.curve; - let regions = [(0.0, c.split_shadows / 100.0, c.shadows), (c.split_shadows / 100.0, c.split_mid / 100.0, c.darks), (c.split_mid / 100.0, c.split_highlights / 100.0, c.lights), (c.split_highlights / 100.0, 1.0, c.highlights)]; + let regions = [ + (0.0, c.split_shadows / 100.0, c.shadows), + (c.split_shadows / 100.0, c.split_mid / 100.0, c.darks), + (c.split_mid / 100.0, c.split_highlights / 100.0, c.lights), + (c.split_highlights / 100.0, 1.0, c.highlights), + ]; let pts: Vec = (0..=64) .map(|i| { let x = i as f64 / 64.0; diff --git a/crates/ui-egui/src/panels/left.rs b/crates/ui-egui/src/panels/left.rs index 761f5466..3a4bc495 100644 --- a/crates/ui-egui/src/panels/left.rs +++ b/crates/ui-egui/src/panels/left.rs @@ -10,7 +10,16 @@ use crate::icons::{Icon, paint}; use crate::theme::Tokens; use crate::widgets::{icon_button, register}; -fn row(app: &mut LightcraftApp, ui: &mut egui::Ui, id: &str, icon: Icon, label: &str, count: Option, selected: bool, indent: f32) -> egui::Response { +fn row( + app: &mut LightcraftApp, + ui: &mut egui::Ui, + id: &str, + icon: Icon, + label: &str, + count: Option, + selected: bool, + indent: f32, +) -> egui::Response { let t = Tokens::get(ui.ctx()); let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 29.0), Sense::click()); register(ui.ctx(), format!("source:{id}"), r); @@ -20,8 +29,19 @@ fn row(app: &mut LightcraftApp, ui: &mut egui::Ui, id: &str, icon: Icon, label: } else if resp.hovered() { ui.painter().rect_filled(inner, 4.0, t.hover.gamma_multiply(0.6)); } - paint(ui.painter(), Rect::from_min_size(pos2(r.left() + 18.0 + indent, r.center().y - 8.0), vec2(16.0, 16.0)), icon, if selected { t.text } else { t.icon }); - ui.painter().text(pos2(r.left() + 42.0 + indent, r.center().y), Align2::LEFT_CENTER, label, t.font(13.5), if selected { t.text } else { t.text_label }); + paint( + ui.painter(), + Rect::from_min_size(pos2(r.left() + 18.0 + indent, r.center().y - 8.0), vec2(16.0, 16.0)), + icon, + if selected { t.text } else { t.icon }, + ); + ui.painter().text( + pos2(r.left() + 42.0 + indent, r.center().y), + Align2::LEFT_CENTER, + label, + t.font(13.5), + if selected { t.text } else { t.text_label }, + ); if let Some(n) = count { ui.painter().text(pos2(r.right() - 18.0, r.center().y), Align2::RIGHT_CENTER, n.to_string(), t.font(12.5), t.text_dim); } @@ -58,7 +78,11 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { // Albums header let (ar, _) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::hover()); ui.painter().text(pos2(ar.left() + 18.0, ar.center().y), Align2::LEFT_CENTER, "Albums", t.semibold(13.5), t.text_label); - let mut hdr = ui.new_child(egui::UiBuilder::new().max_rect(Rect::from_min_max(pos2(ar.right() - 50.0, ar.top()), ar.right_bottom())).layout(egui::Layout::right_to_left(egui::Align::Center))); + let mut hdr = ui.new_child( + egui::UiBuilder::new() + .max_rect(Rect::from_min_max(pos2(ar.right() - 50.0, ar.top()), ar.right_bottom())) + .layout(egui::Layout::right_to_left(egui::Align::Center)), + ); let plus = icon_button(&mut hdr, "albumNew", Icon::Plus, vec2(26.0, 26.0), false, true, "Create Album"); egui::Popup::menu(&plus).show(|ui| { if ui.button("Create Album…").clicked() { @@ -82,7 +106,9 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { } } ui.add_space(10.0); - if row(app, ui, "recentlyDeleted", Icon::Trash, "Recently Deleted", Some(deleted), src == LibrarySource::RecentlyDeleted, 0.0).clicked() { + if row(app, ui, "recentlyDeleted", Icon::Trash, "Recently Deleted", Some(deleted), src == LibrarySource::RecentlyDeleted, 0.0) + .clicked() + { let _ = app.run("library.source", json!({"kind": "recentlyDeleted"})); } }); diff --git a/crates/ui-egui/src/panels/masking.rs b/crates/ui-egui/src/panels/masking.rs index 03018efb..eea553e8 100644 --- a/crates/ui-egui/src/panels/masking.rs +++ b/crates/ui-egui/src/panels/masking.rs @@ -118,10 +118,26 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { let sel = app.session.active_mask == Some(m.id); let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 30.0), Sense::click()); register(ui.ctx(), format!("mask:{}", m.id), r); - ui.painter().rect_filled(r, 4.0, if sel { t.tool_active } else if resp.hovered() { t.hover.gamma_multiply(0.7) } else { t.chrome }); + ui.painter().rect_filled( + r, + 4.0, + if sel { + t.tool_active + } else if resp.hovered() { + t.hover.gamma_multiply(0.7) + } else { + t.chrome + }, + ); let icon = m.components.first().map(|c| kind_label(&c.shape).1).unwrap_or(Icon::Mask); paint(ui.painter(), Rect::from_min_size(r.min + vec2(8.0, 7.0), vec2(16.0, 16.0)), icon, t.text_label); - ui.painter().text(pos2(r.left() + 32.0, r.center().y), Align2::LEFT_CENTER, &m.name, t.font(13.0), if m.visible { t.text } else { t.text_disabled }); + ui.painter().text( + pos2(r.left() + 32.0, r.center().y), + Align2::LEFT_CENTER, + &m.name, + t.font(13.0), + if m.visible { t.text } else { t.text_disabled }, + ); if resp.clicked() { let _ = app.run("mask.select", json!({"id": m.id})); } @@ -169,7 +185,17 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { let out = slider(ui, s, v, true, None); apply_slider_out(app, s, out, |app, v| app.run("mask.adjust", json!({"values": {s.id: v}}))); } - let amt = ControlSpec { id: "amount", label: "Amount", section: Section::Light, min: 0.0, max: 200.0, default: 100.0, step: 1.0, decimals: 0, track: Track::Plain }; + let amt = ControlSpec { + id: "amount", + label: "Amount", + section: Section::Light, + min: 0.0, + max: 200.0, + default: 100.0, + step: 1.0, + decimals: 0, + track: Track::Plain, + }; let out = slider(ui, &amt, m.adjust.amount, true, None); apply_slider_out(app, &amt, out, |app, v| app.run("mask.adjust", json!({"values": {"amount": v}}))); ui.add_space(30.0); @@ -177,7 +203,14 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { } fn component_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, op: &str) { - for (kind, label) in [("brush", "Brush"), ("linear", "Linear Gradient"), ("radial", "Radial Gradient"), ("sky", "Sky"), ("subject", "Subject"), ("luminanceRange", "Luminance Range")] { + for (kind, label) in [ + ("brush", "Brush"), + ("linear", "Linear Gradient"), + ("radial", "Radial Gradient"), + ("sky", "Sky"), + ("subject", "Subject"), + ("luminanceRange", "Luminance Range"), + ] { if ui.button(label).clicked() { if kind == "brush" { app.ui.tool = "brush".into(); diff --git a/crates/ui-egui/src/panels/presets.rs b/crates/ui-egui/src/panels/presets.rs index 2c0039a7..6d891cc5 100644 --- a/crates/ui-egui/src/panels/presets.rs +++ b/crates/ui-egui/src/panels/presets.rs @@ -22,8 +22,13 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { ui.spacing_mut().item_spacing.y = 0.0; let (hr, _) = ui.allocate_exact_size(vec2(ui.available_width(), 46.0), Sense::hover()); ui.painter().text(pos2(hr.left() + 24.0, hr.center().y + 2.0), Align2::LEFT_CENTER, "Presets", t.semibold(15.0), t.text); - let mut hdr = ui.new_child(egui::UiBuilder::new().max_rect(Rect::from_min_max(pos2(hr.right() - 80.0, hr.top()), hr.right_bottom())).layout(egui::Layout::right_to_left(egui::Align::Center))); - if icon_button(&mut hdr, "presetCreate", Icon::Plus, vec2(28.0, 28.0), false, app.session.active().is_some(), "Create Preset…").clicked() { + let mut hdr = ui.new_child( + egui::UiBuilder::new() + .max_rect(Rect::from_min_max(pos2(hr.right() - 80.0, hr.top()), hr.right_bottom())) + .layout(egui::Layout::right_to_left(egui::Align::Center)), + ); + if icon_button(&mut hdr, "presetCreate", Icon::Plus, vec2(28.0, 28.0), false, app.session.active().is_some(), "Create Preset…").clicked() + { app.ui.dialog = Some(crate::state::Dialog::CreatePreset { name: String::new(), group: "User Presets".into() }); } divider(ui); @@ -31,7 +36,17 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let amt_id = egui::Id::new("preset-amount"); let last: Option<(String, f64)> = ui.data(|d| d.get_temp(amt_id)); if let Some((pid, amount)) = last.clone() { - let spec = ControlSpec { id: "presetAmount", label: "Amount", section: Section::Profile, min: 0.0, max: 200.0, default: 100.0, step: 1.0, decimals: 0, track: Track::Plain }; + let spec = ControlSpec { + id: "presetAmount", + label: "Amount", + section: Section::Profile, + min: 0.0, + max: 200.0, + default: 100.0, + step: 1.0, + decimals: 0, + track: Track::Plain, + }; let out = slider(ui, &spec, amount, true, None); if let Some(v) = out.value { // re-apply from the pre-preset state: undo last preset step then apply with the new amount @@ -51,7 +66,12 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let open: bool = ui.data(|d| d.get_temp(open_id)).unwrap_or(true); let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 32.0), Sense::click()); register(ui.ctx(), format!("presetGroup:{g}"), r); - paint(ui.painter(), Rect::from_center_size(pos2(r.left() + 22.0, r.center().y), vec2(12.0, 12.0)), if open { Icon::ChevronDown } else { Icon::ChevronRight }, t.text_label); + paint( + ui.painter(), + Rect::from_center_size(pos2(r.left() + 22.0, r.center().y), vec2(12.0, 12.0)), + if open { Icon::ChevronDown } else { Icon::ChevronRight }, + t.text_label, + ); ui.painter().text(pos2(r.left() + 36.0, r.center().y), Align2::LEFT_CENTER, &g, t.semibold(13.0), t.text_label); if resp.clicked() { ui.data_mut(|d| d.insert_temp(open_id, !open)); @@ -70,7 +90,12 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { } ui.painter().text(pos2(r.left() + 40.0, r.center().y), Align2::LEFT_CENTER, &name, t.font(13.0), t.text_label); if fav { - paint(ui.painter(), Rect::from_center_size(pos2(r.right() - 20.0, r.center().y), vec2(12.0, 12.0)), Icon::StarFilled, t.star); + paint( + ui.painter(), + Rect::from_center_size(pos2(r.right() - 20.0, r.center().y), vec2(12.0, 12.0)), + Icon::StarFilled, + t.star, + ); } if resp.clicked() { let _ = app.run("preset.apply", json!({"id": pid, "amount": 100})); diff --git a/crates/ui-egui/src/panels/right.rs b/crates/ui-egui/src/panels/right.rs index e024cb46..59d45397 100644 --- a/crates/ui-egui/src/panels/right.rs +++ b/crates/ui-egui/src/panels/right.rs @@ -88,10 +88,10 @@ fn crop(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { }); ui.add_space(6.0); ui.horizontal(|ui| { - if text_button(ui, "cropRotateLeft", "⟲ 90°", false).clicked() { + if text_button(ui, "cropRotateLeft", "Rotate Left", false).clicked() { let _ = app.run("photo.rotateLeft", json!({})); } - if text_button(ui, "cropRotateRight", "⟳ 90°", false).clicked() { + if text_button(ui, "cropRotateRight", "Rotate Right", false).clicked() { let _ = app.run("photo.rotateRight", json!({})); } if text_button(ui, "cropFlipH", "Flip H", d.crop.flip_h).clicked() { @@ -119,7 +119,12 @@ fn crop(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { padded(ui, |ui| { ui.horizontal(|ui| { ui.label("Overlay"); - for (label, o) in [("Thirds", crate::state::CropOverlay::Thirds), ("Grid", crate::state::CropOverlay::Grid), ("Golden", crate::state::CropOverlay::Golden), ("None", crate::state::CropOverlay::None)] { + for (label, o) in [ + ("Thirds", crate::state::CropOverlay::Thirds), + ("Grid", crate::state::CropOverlay::Grid), + ("Golden", crate::state::CropOverlay::Golden), + ("None", crate::state::CropOverlay::None), + ] { if ui.selectable_label(app.ui.crop_overlay == o, label).clicked() { app.ui.crop_overlay = o; } @@ -244,7 +249,7 @@ fn keywords(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { ui.add_space(8.0); ui.horizontal_wrapped(|ui| { for k in &p.meta.keywords { - let r = ui.add(egui::Button::new(egui::RichText::new(format!("{k} ✕")).color(t.text_label)).corner_radius(10.0)); + let r = ui.add(egui::Button::new(egui::RichText::new(format!("{k} ×")).color(t.text_label)).corner_radius(10.0)); if r.clicked() { let _ = app.run("photo.setMeta", json!({"removeKeywords": [k]})); } @@ -255,7 +260,9 @@ fn keywords(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { let all = app.session.catalog.keywords(); ui.horizontal_wrapped(|ui| { for (k, n) in all { - if !p.meta.keywords.contains(&k) && ui.add(egui::Button::new(egui::RichText::new(format!("{k} {n}")).color(t.text_dim)).frame(false)).clicked() { + if !p.meta.keywords.contains(&k) + && ui.add(egui::Button::new(egui::RichText::new(format!("{k} {n}")).color(t.text_dim)).frame(false)).clicked() + { let _ = app.run("photo.setMeta", json!({"addKeywords": [k]})); } } @@ -276,7 +283,7 @@ fn versions(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { if ui.button(&v.name).on_hover_text(&v.created).clicked() { let _ = app.run("version.restore", json!({"index": i})); } - if ui.small_button("✕").clicked() { + if ui.small_button("×").clicked() { let _ = app.run("version.delete", json!({"index": i})); } }); diff --git a/crates/ui-egui/src/panels/strip.rs b/crates/ui-egui/src/panels/strip.rs index 6d4626ae..9b7a1dde 100644 --- a/crates/ui-egui/src/panels/strip.rs +++ b/crates/ui-egui/src/panels/strip.rs @@ -14,7 +14,12 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { egui::Panel::right("tool_strip") .exact_size(t.strip_w) .resizable(false) - .frame(egui::Frame::NONE.fill(t.chrome).inner_margin(egui::Margin { left: 0, right: 0, top: 8, bottom: 8 }).stroke(egui::Stroke::new(1.0, t.divider))) + .frame( + egui::Frame::NONE + .fill(t.chrome) + .inner_margin(egui::Margin { left: 0, right: 0, top: 8, bottom: 8 }) + .stroke(egui::Stroke::new(1.0, t.divider)), + ) .show(ui, |ui| { let has_photo = app.session.active().is_some(); ui.vertical_centered(|ui| { @@ -50,7 +55,10 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { }); // bottom icons let bottom = ui.max_rect().bottom(); - let mut child = ui.new_child(egui::UiBuilder::new().max_rect(egui::Rect::from_min_max(egui::pos2(ui.max_rect().left(), bottom - 96.0), ui.max_rect().right_bottom()))); + let mut child = ui.new_child( + egui::UiBuilder::new() + .max_rect(egui::Rect::from_min_max(egui::pos2(ui.max_rect().left(), bottom - 96.0), ui.max_rect().right_bottom())), + ); child.vertical_centered(|ui| { let sz = vec2(t.strip_w, 40.0); if icon_button(ui, "keywords", Icon::Tag, sz, app.ui.right == RightPanel::Keywords, has_photo, "Keywords (K)").clicked() { diff --git a/crates/ui-egui/src/panels/topbar.rs b/crates/ui-egui/src/panels/topbar.rs index c6317e89..7429a05d 100644 --- a/crates/ui-egui/src/panels/topbar.rs +++ b/crates/ui-egui/src/panels/topbar.rs @@ -32,9 +32,16 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let sr = Rect::from_center_size(pos2(full.center().x, full.center().y), vec2(sw, 28.0)); let id = egui::Id::new("search-field"); let focused = ui.memory(|m| m.has_focus(id)); - ui.painter().rect(sr, 4.0, if focused { t.canvas } else { t.field }, Stroke::new(1.0, if focused { t.accent } else { t.field_border }), StrokeKind::Inside); + ui.painter().rect( + sr, + 4.0, + if focused { t.canvas } else { t.field }, + Stroke::new(1.0, if focused { t.accent } else { t.field_border }), + StrokeKind::Inside, + ); register(ui.ctx(), "field:search", sr); - let mut child = ui.new_child(egui::UiBuilder::new().max_rect(sr.shrink2(vec2(10.0, 4.0))).layout(egui::Layout::left_to_right(egui::Align::Center))); + let mut child = + ui.new_child(egui::UiBuilder::new().max_rect(sr.shrink2(vec2(10.0, 4.0))).layout(egui::Layout::left_to_right(egui::Align::Center))); let empty = app.ui.search.is_empty(); if empty && !focused { let g = child.painter().layout_no_wrap("Search Photos".into(), t.font(13.5), t.text_dim); @@ -44,7 +51,12 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { child.painter().galley(pos2(x0 + 24.0, sr.center().y - g.size().y / 2.0), g, t.text_dim); } let resp = child.add( - egui::TextEdit::singleline(&mut app.ui.search).id(id).frame(egui::Frame::NONE).desired_width(sr.width() - 20.0).font(t.font(13.5)).text_color(t.text), + egui::TextEdit::singleline(&mut app.ui.search) + .id(id) + .frame(egui::Frame::NONE) + .desired_width(sr.width() - 20.0) + .font(t.font(13.5)) + .text_color(t.text), ); if resp.changed() { let q = app.ui.search.clone(); @@ -55,7 +67,18 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let fresp = ui.interact(fr, egui::Id::new("filter-btn"), Sense::click()); register(ui.ctx(), "icon:filter", fr); let filtering = app.session.filter != Default::default(); - paint(ui.painter(), fr.shrink(6.0), Icon::Filter, if filtering { t.accent } else if fresp.hovered() { t.text } else { t.icon }); + paint( + ui.painter(), + fr.shrink(6.0), + Icon::Filter, + if filtering { + t.accent + } else if fresp.hovered() { + t.text + } else { + t.icon + }, + ); if fresp.clicked() { let _ = app.run("view.filterBar", json!({})); } diff --git a/crates/ui-egui/src/render.rs b/crates/ui-egui/src/render.rs index 0e19f4d9..5babdeb0 100644 --- a/crates/ui-egui/src/render.rs +++ b/crates/ui-egui/src/render.rs @@ -170,7 +170,10 @@ impl Renderer { } None => { let tex = ctx.load_texture(name, color, egui::TextureOptions::LINEAR); - self.textures.insert(slot, Tex { key: r.key, photo: r.photo, tex, size: [img.width, img.height], histogram: Some(rendered.histogram), ms }); + self.textures.insert( + slot, + Tex { key: r.key, photo: r.photo, tex, size: [img.width, img.height], histogram: Some(rendered.histogram), ms }, + ); } } if slot == Slot::Main { diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs new file mode 100644 index 00000000..af53a4bd --- /dev/null +++ b/crates/ui-egui/src/shortcuts.rs @@ -0,0 +1,136 @@ +//! Keyboard shortcuts: parse `Cmd+Shift+X` style strings and dispatch UI and engine commands. + +use egui::{Key, Modifiers}; +use serde_json::json; + +use crate::LightcraftApp; + +pub fn parse(s: &str) -> Option<(Modifiers, Key)> { + let mut m = Modifiers::NONE; + let mut key = None; + for part in s.split('+') { + match part { + "Cmd" => m.command = true, + "Shift" => m.shift = true, + "Alt" => m.alt = true, + "Ctrl" => m.ctrl = true, + k => { + key = Key::from_name(k).or(match k { + "Right" => Some(Key::ArrowRight), + "Left" => Some(Key::ArrowLeft), + "Up" => Some(Key::ArrowUp), + "Down" => Some(Key::ArrowDown), + "Delete" => Some(Key::Backspace), + "\\" => Some(Key::Backslash), + "/" => Some(Key::Slash), + "=" => Some(Key::Equals), + "-" => Some(Key::Minus), + "[" => Some(Key::OpenBracket), + "]" => Some(Key::CloseBracket), + _ => None, + }) + } + } + } + key.map(|k| (m, k)) +} + +fn matches(i: &egui::InputState, m: Modifiers, k: Key) -> bool { + i.events.iter().any(|e| match e { + egui::Event::Key { key, pressed: true, modifiers, .. } => { + *key == k && modifiers.command == m.command && modifiers.shift == m.shift && modifiers.alt == m.alt + } + _ => false, + }) +} + +pub fn handle(app: &mut LightcraftApp, ctx: &egui::Context) { + // don't steal keys from text fields + if ctx.egui_wants_keyboard_input() { + return; + } + let mut fire: Vec = Vec::new(); + ctx.input(|i| { + for (id, _, sc, _) in crate::menus::UI_COMMANDS { + if let Some((m, k)) = sc.and_then(parse) + && matches(i, m, k) + { + fire.push(id.to_string()); + } + } + for c in lightcraft_engine::command_specs() { + if let Some((m, k)) = c.shortcut.and_then(parse) + && matches(i, m, k) + { + fire.push(c.id.to_string()); + } + } + // rating 0-5, colour labels 6-9 (with Shift: and advance) + for (n, key) in [Key::Num0, Key::Num1, Key::Num2, Key::Num3, Key::Num4, Key::Num5].iter().enumerate() { + if matches(i, Modifiers::NONE, *key) { + fire.push(format!("rate:{n}:0")); + } + if matches(i, Modifiers::SHIFT, *key) { + fire.push(format!("rate:{n}:1")); + } + } + for (label, key) in [("red", Key::Num6), ("yellow", Key::Num7), ("green", Key::Num8), ("blue", Key::Num9)] { + if matches(i, Modifiers::NONE, key) { + fire.push(format!("label:{label}")); + } + } + }); + for f in fire { + if let Some(rest) = f.strip_prefix("rate:") { + let (n, adv) = rest.split_once(':').unwrap_or(("0", "0")); + let _ = app.run("photo.rate", json!({"rating": n.parse::().unwrap_or(0), "advance": adv == "1"})); + let label = if n == "0" { "Rating cleared".to_string() } else { format!("Rated {}", "★".repeat(n.parse().unwrap_or(0))) }; + app.toast(ctx, label); + } else if let Some(l) = f.strip_prefix("label:") { + let _ = app.run("photo.label", json!({"label": l})); + } else { + // X is both reject (library) and swap crop aspect (crop tool) + if f == "photo.reject" && app.ui.right == crate::state::RightPanel::Crop { + let _ = app.run("crop.rotateAspect", json!({})); + continue; + } + if f == "crop.rotateAspect" && app.ui.right != crate::state::RightPanel::Crop { + continue; + } + let _ = app.run(&f, json!({})); + match f.as_str() { + "photo.pick" => app.toast(ctx, "Flagged as Pick"), + "photo.reject" => app.toast(ctx, "Flagged as Reject"), + "photo.unflag" => app.toast(ctx, "Unflagged"), + "edit.undo" => app.toast(ctx, "Undo"), + "edit.redo" => app.toast(ctx, "Redo"), + _ => {} + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_shortcuts() { + let (m, k) = parse("Cmd+Shift+Z").unwrap(); + assert!(m.command && m.shift && !m.alt); + assert_eq!(k, Key::Z); + assert_eq!(parse("Right").unwrap().1, Key::ArrowRight); + assert_eq!(parse("\\").unwrap().1, Key::Backslash); + // every declared shortcut parses + for (id, _, sc, _) in crate::menus::UI_COMMANDS { + if let Some(sc) = sc { + assert!(parse(sc).is_some(), "{id}: {sc}"); + } + } + for c in lightcraft_engine::command_specs() { + if let Some(sc) = c.shortcut { + assert!(parse(sc).is_some(), "{}: {sc}", c.id); + } + } + } +} diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index eab24f28..4d888905 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -108,7 +108,9 @@ impl Tokens { pub fn install_fonts(ctx: &egui::Context) { let mut fonts = FontDefinitions::default(); fonts.font_data.insert("SourceSans3".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf")))); - fonts.font_data.insert("SourceSans3-Semibold".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/SourceSans3-Semibold.ttf")))); + fonts + .font_data + .insert("SourceSans3-Semibold".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/SourceSans3-Semibold.ttf")))); let fallback: Vec = fonts.families.get(&FontFamily::Proportional).cloned().unwrap_or_default(); let mut prop = vec!["SourceSans3".to_string()]; prop.extend(fallback.clone()); @@ -136,7 +138,13 @@ pub fn apply(ctx: &egui::Context) { v.popup_shadow = egui::epaint::Shadow { offset: [0, 4], blur: 16, spread: 0, color: Color32::from_black_alpha(140) }; v.window_shadow = egui::epaint::Shadow { offset: [0, 8], blur: 30, spread: 0, color: Color32::from_black_alpha(160) }; let w = &mut v.widgets; - for (wv, fill) in [(&mut w.noninteractive, t.chrome), (&mut w.inactive, t.button), (&mut w.hovered, t.hover), (&mut w.active, t.pressed), (&mut w.open, t.hover)] { + for (wv, fill) in [ + (&mut w.noninteractive, t.chrome), + (&mut w.inactive, t.button), + (&mut w.hovered, t.hover), + (&mut w.active, t.pressed), + (&mut w.open, t.hover), + ] { wv.bg_fill = fill; wv.weak_bg_fill = fill; wv.corner_radius = CornerRadius::same(4); diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 04ffae84..be4fa561 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -50,7 +50,13 @@ fn track_stops(track: &Track) -> Option> { let prev = hex(BAND_COLORS[(i + 7) % 8]); let next = hex(BAND_COLORS[(i + 1) % 8]); let me = hex(BAND_COLORS[i]); - vec![prev.gamma_multiply(0.7), lerp(prev, me, 0.5).gamma_multiply(0.7), me.gamma_multiply(0.7), lerp(me, next, 0.5).gamma_multiply(0.7), next.gamma_multiply(0.7)] + vec![ + prev.gamma_multiply(0.7), + lerp(prev, me, 0.5).gamma_multiply(0.7), + me.gamma_multiply(0.7), + lerp(me, next, 0.5).gamma_multiply(0.7), + next.gamma_multiply(0.7), + ] } Track::Sat { band } => vec![hex("#454343"), lerp(hex("#454343"), hex(BAND_COLORS[*band as usize]), 0.6), hex(BAND_COLORS[*band as usize])], Track::Lum { band } => { @@ -231,7 +237,12 @@ pub fn flyout_row(ui: &mut Ui, id: &str, title: &str, icon: Icon, open: bool) -> p.rect_filled(tile, 3.0, t.chrome.gamma_multiply(1.15)); paint(p, tile.shrink(3.0), icon, t.text_label); p.text(pos2(tile.right() + 10.0, r.center().y), Align2::LEFT_CENTER, title, t.semibold(13.0), t.text_label); - paint(p, Rect::from_center_size(pos2(r.right() - 14.0, r.center().y), vec2(12.0, 12.0)), if open { Icon::ChevronDown } else { Icon::TriangleLeft }, t.text_label); + paint( + p, + Rect::from_center_size(pos2(r.right() - 14.0, r.center().y), vec2(12.0, 12.0)), + if open { Icon::ChevronDown } else { Icon::TriangleLeft }, + t.text_label, + ); resp } @@ -306,3 +317,16 @@ pub fn stars(ui: &mut Ui, id: &str, rating: u8, size: f32) -> Option { let _ = hover_n; out } + +/// A borderless dropdown button: `text` followed by a painted chevron (no font glyph needed). +pub fn dropdown(ui: &mut Ui, id: &str, text: &str, font: egui::FontId, color: Color32) -> Response { + let t = Tokens::get(ui.ctx()); + let galley = ui.painter().layout_no_wrap(text.to_string(), font, color); + let size = vec2(galley.size().x + 20.0, galley.size().y.max(20.0)); + let (r, resp) = ui.allocate_exact_size(size, Sense::click()); + register(ui.ctx(), format!("dropdown:{id}"), r); + let c = if resp.hovered() { t.text } else { color }; + ui.painter().galley(pos2(r.left(), r.center().y - galley.size().y / 2.0), galley, c); + paint(ui.painter(), Rect::from_center_size(pos2(r.right() - 7.0, r.center().y + 1.0), vec2(12.0, 12.0)), Icon::ChevronDown, c); + resp +}