diff --git a/Cargo.lock b/Cargo.lock index 6115027bade..b7565faf93b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,7 +30,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "bytes", "futures-core", "futures-sink", @@ -52,12 +52,12 @@ dependencies = [ "actix-service", "actix-utils", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.13.0", "bytes", "bytestring", "derive_more 2.0.1", "encoding_rs", - "foldhash", + "foldhash 0.1.5", "futures-core", "http 0.2.12", "httparse", @@ -111,7 +111,7 @@ dependencies = [ "actix-utils", "futures-core", "futures-util", - "mio 1.0.4", + "mio 1.2.1", "socket2 0.5.10", "tokio", "tracing", @@ -155,7 +155,7 @@ dependencies = [ "cfg-if", "derive_more 2.0.1", "encoding_rs", - "foldhash", + "foldhash 0.1.5", "futures-core", "futures-util", "impl-more", @@ -251,7 +251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.9.4", + "bitflags 2.13.0", "cfg-if", "libc", ] @@ -273,7 +273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" dependencies = [ "android-properties", - "bitflags 2.9.4", + "bitflags 2.13.0", "cc", "jni 0.22.4", "libc", @@ -372,9 +372,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arbitrary" @@ -394,11 +394,11 @@ dependencies = [ "clipboard-win", "image 0.25.8", "log", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", "windows-sys 0.60.2", @@ -454,14 +454,10 @@ dependencies = [ "futures-channel", "futures-util", "rand 0.9.2", - "raw-window-handle", "serde", "serde_repr", "tokio", "url", - "wayland-backend", - "wayland-client", - "wayland-protocols", "zbus", ] @@ -840,7 +836,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -863,7 +859,7 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -881,7 +877,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -902,7 +898,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "annotate-snippets", - "bitflags 2.9.4", + "bitflags 2.13.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -943,11 +939,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -982,11 +978,11 @@ dependencies = [ [[package]] name = "block2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.2", + "objc2 0.6.4", ] [[package]] @@ -1104,7 +1100,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "cairo-sys-rs", "glib", "libc", @@ -1129,7 +1125,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "log", "polling", "rustix 0.38.44", @@ -1374,8 +1370,8 @@ name = "cap-cursor-info" version = "0.0.0" dependencies = [ "hex", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "serde", "sha2", "specta", @@ -1402,6 +1398,7 @@ dependencies = [ "async-stream", "axum", "base64 0.22.1", + "block2 0.6.2", "bytemuck", "bytes", "cap-audio", @@ -1425,8 +1422,6 @@ dependencies = [ "cidre", "clipboard-rs", "cocoa", - "core-foundation 0.10.1", - "core-graphics 0.24.0", "cpal 0.15.3 (git+https://github.com/CapSoftware/cpal?rev=6013cb5f8bd3)", "device_query", "dirs 6.0.0", @@ -1446,9 +1441,12 @@ dependencies = [ "md5", "nix 0.29.0", "objc", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-application-services", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-web-kit", "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", @@ -1487,6 +1485,7 @@ dependencies = [ "tauri-plugin-opener", "tauri-plugin-os", "tauri-plugin-positioner", + "tauri-plugin-prevent-default", "tauri-plugin-process", "tauri-plugin-sentry", "tauri-plugin-shell", @@ -1820,8 +1819,8 @@ dependencies = [ "libc", "libproc", "objc", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "parking_lot", "pipewire", "relative-path", @@ -1875,7 +1874,7 @@ dependencies = [ "image 0.25.8", "log", "metal 0.31.0", - "objc2 0.6.2", + "objc2 0.6.4", "pretty_assertions", "rayon", "reactive_graph", @@ -2226,9 +2225,9 @@ checksum = "afede46921767868c5c7f8f55202bdd8bec0bab6bc9605174200f45924f93c62" dependencies = [ "clipboard-win", "image 0.25.8", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", "windows 0.59.0", "x11rb", ] @@ -2258,7 +2257,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "block", "cocoa-foundation", "core-foundation 0.10.1", @@ -2274,7 +2273,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "block", "core-foundation 0.10.1", "core-graphics-types 0.2.0", @@ -2475,7 +2474,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "core-foundation 0.10.1", "core-graphics-types 0.2.0", "foreign-types 0.5.0", @@ -2488,7 +2487,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "core-foundation 0.10.1", "core-graphics-types 0.2.0", "foreign-types 0.5.0", @@ -2512,7 +2511,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "core-foundation 0.10.1", "libc", ] @@ -2588,7 +2587,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "fontdb 0.16.2", "log", "rangemap", @@ -2784,6 +2783,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + [[package]] name = "cssparser-macros" version = "0.6.1" @@ -2796,12 +2808,12 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.9" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ - "quote", - "syn 2.0.106", + "ctor-proc-macro", + "dtor 0.3.0", ] [[package]] @@ -2810,9 +2822,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf" dependencies = [ - "dtor", + "dtor 0.8.1", ] +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + [[package]] name = "cursor-icon" version = "1.2.0" @@ -2881,6 +2899,17 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.0", +] + [[package]] name = "debugid" version = "0.8.0" @@ -3105,10 +3134,10 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", + "bitflags 2.13.0", + "block2 0.6.2", "libc", - "objc2 0.6.2", + "objc2 0.6.4", ] [[package]] @@ -3172,6 +3201,21 @@ dependencies = [ "litrs", ] +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.0", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -3217,7 +3261,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "bytemuck", "drm-ffi", "drm-fourcc", @@ -3265,12 +3309,27 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + [[package]] name = "dtor" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dunce" version = "1.0.5" @@ -3524,7 +3583,7 @@ name = "ffmpeg-next" version = "7.1.0" source = "git+https://github.com/CapSoftware/rust-ffmpeg?rev=49db1fede112#49db1fede1123029670728bd6e3fcbb07b0f5716" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "ffmpeg-sys-next", "libc", ] @@ -3630,6 +3689,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "font-types" version = "0.9.0" @@ -3751,9 +3816,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -3766,9 +3831,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -3776,15 +3841,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -3804,9 +3869,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -3823,9 +3888,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -3834,21 +3899,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -3858,7 +3923,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -4141,7 +4205,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "futures-channel", "futures-core", "futures-executor", @@ -4196,8 +4260,8 @@ checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7" dependencies = [ "crossbeam-channel", "keyboard-types", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "once_cell", "serde", "thiserror 2.0.16", @@ -4268,7 +4332,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "gpu-alloc-types", ] @@ -4278,7 +4342,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", ] [[package]] @@ -4299,7 +4363,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -4310,7 +4374,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", ] [[package]] @@ -4440,7 +4504,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -4513,10 +4577,20 @@ checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.14.1", "match_token", ] +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever 0.38.0", +] + [[package]] name = "http" version = "0.2.12" @@ -4703,7 +4777,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.5.10", "system-configuration 0.6.1", "tokio", "tower-service", @@ -4737,9 +4811,9 @@ dependencies = [ [[package]] name = "ico" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", "png 0.17.16", @@ -4998,7 +5072,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "crossterm", "dyn-clone", "fuzzy-matcher", @@ -5041,33 +5115,12 @@ dependencies = [ "leaky-cow", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-docker" version = "0.2.0" @@ -5120,6 +5173,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -5241,11 +5303,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.80" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -5306,7 +5369,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "serde", "unicode-segmentation", ] @@ -5343,10 +5406,10 @@ version = "0.8.8-speedreader" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ - "cssparser", - "html5ever", + "cssparser 0.29.6", + "html5ever 0.29.1", "indexmap 2.11.4", - "selectors", + "selectors 0.24.0", ] [[package]] @@ -5436,9 +5499,18 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.175" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] [[package]] name = "libfuzzer-sys" @@ -5503,7 +5575,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "libc", "redox_syscall 0.5.17", ] @@ -5514,7 +5586,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2909f3be29d674e7f10604aff18d1bbe1bb03c4cd61c8a8ba19c0b1d162f7d4e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "cc", "cookie-factory", "libc", @@ -5671,8 +5743,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "119c8490084af61b44c9eda9d626475847a186737c0378c85e32d77c33a01cd4" dependencies = [ "cc", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", "time", ] @@ -5729,9 +5801,20 @@ dependencies = [ "log", "phf 0.11.3", "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", - "tendril", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.0", + "web_atoms", ] [[package]] @@ -5822,7 +5905,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "block", "core-graphics-types 0.1.3", "foreign-types 0.5.0", @@ -5837,7 +5920,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "block", "core-graphics-types 0.1.3", "foreign-types 0.5.0", @@ -5868,7 +5951,7 @@ version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c4d14bcca0fd3ed165a03000480aaa364c6860c34e900cb2dafdf3b95340e77" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "debugid", "num-derive", "num-traits", @@ -5883,7 +5966,7 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abcd9c8a1e6e1e9d56ce3627851f39a17ea83e17c96bc510f29d7e43d78a7d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "byteorder", "cfg-if", "crash-context", @@ -5973,14 +6056,14 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -6031,23 +6114,23 @@ dependencies = [ [[package]] name = "muda" -version = "0.17.1" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" dependencies = [ "crossbeam-channel", "dpi", "gtk", "keyboard-types", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "once_cell", - "png 0.17.16", + "png 0.18.0", "serde", "thiserror 2.0.16", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -6058,7 +6141,7 @@ checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.9.4", + "bitflags 2.13.0", "cfg_aliases 0.2.1", "codespan-reporting", "half", @@ -6093,10 +6176,10 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -6143,7 +6226,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "jni-sys 0.3.0", "log", "ndk-sys 0.5.0+25.2.9519653", @@ -6157,7 +6240,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "jni-sys 0.3.0", "log", "ndk-sys 0.6.0+11769913", @@ -6211,7 +6294,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "cfg-if", "cfg_aliases 0.1.1", "libc", @@ -6223,7 +6306,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -6235,7 +6318,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -6420,17 +6503,6 @@ dependencies = [ "objc_exception", ] -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - [[package]] name = "objc-sys" version = "0.3.5" @@ -6449,9 +6521,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -6463,7 +6535,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -6475,21 +6547,38 @@ dependencies = [ [[package]] name = "objc2-app-kit" -version = "0.3.1" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-cloud-kit 0.3.2", + "objc2-core-data 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-application-services" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +checksum = "69282c2b5bc58fba07cb9de2113619532eb551e98efe3d8d695509ef45fbd53b" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", + "bitflags 2.13.0", "libc", - "objc2 0.6.2", - "objc2-cloud-kit 0.3.1", - "objc2-core-data 0.3.1", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", - "objc2-core-image 0.3.1", - "objc2-foundation 0.3.1", - "objc2-quartz-core 0.3.1", + "objc2-core-services", + "objc2-foundation 0.3.2", ] [[package]] @@ -6498,18 +6587,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e085a2e16c61dadbad7a808fc9d5b5f8472b1b825b53d529c9f64ccac78e722" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", + "bitflags 2.13.0", + "block2 0.6.2", "dispatch2", - "objc2 0.6.2", + "objc2 0.6.4", "objc2-avf-audio", "objc2-core-foundation", "objc2-core-graphics", - "objc2-core-image 0.3.1", + "objc2-core-image 0.3.2", "objc2-core-media", "objc2-core-video", - "objc2-foundation 0.3.1", - "objc2-quartz-core 0.3.1", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", ] [[package]] @@ -6518,8 +6607,8 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfc1d11521c211a7ebe17739fc806719da41f56c6b3f949d9861b459188ce910" dependencies = [ - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -6528,22 +6617,22 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", - "objc2-core-location", + "objc2-core-location 0.2.2", "objc2-foundation 0.2.2", ] [[package]] name = "objc2-cloud-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -6564,7 +6653,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82" dependencies = [ "dispatch2", - "objc2 0.6.2", + "objc2 0.6.4", "objc2-core-audio-types", "objc2-core-foundation", ] @@ -6575,8 +6664,8 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", + "bitflags 2.13.0", + "objc2 0.6.4", ] [[package]] @@ -6585,7 +6674,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -6593,35 +6682,37 @@ dependencies = [ [[package]] name = "objc2-core-data" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", + "block2 0.6.2", "dispatch2", - "objc2 0.6.2", + "libc", + "objc2 0.6.4", ] [[package]] name = "objc2-core-graphics" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "dispatch2", - "objc2 0.6.2", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] @@ -6640,12 +6731,12 @@ dependencies = [ [[package]] name = "objc2-core-image" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -6660,29 +6751,63 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-core-media" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0b7afa6822e2fa20dfc88d10186b2432bf8560b5ed73ec9d31efd78277bc878" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "dispatch2", - "objc2 0.6.2", + "objc2 0.6.4", "objc2-core-audio", "objc2-core-audio-types", "objc2-core-foundation", "objc2-core-video", ] +[[package]] +name = "objc2-core-services" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583300ad934cba24ff5292aee751ecc070f7ca6b39a574cc21b7b5e588e06a0b" +dependencies = [ + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-security", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", +] + [[package]] name = "objc2-core-video" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1989c3e76c7e978cab0ba9e6f4961cd00ed14ca21121444cc26877403bfb6303" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", + "bitflags 2.13.0", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", "objc2-io-surface", @@ -6709,7 +6834,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "block2 0.5.1", "dispatch", "libc", @@ -6718,14 +6843,14 @@ dependencies = [ [[package]] name = "objc2-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", + "bitflags 2.13.0", + "block2 0.6.2", "libc", - "objc2 0.6.2", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -6741,22 +6866,22 @@ dependencies = [ [[package]] name = "objc2-io-surface" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", + "bitflags 2.13.0", + "objc2 0.6.4", "objc2-core-foundation", ] [[package]] name = "objc2-javascript-core" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" dependencies = [ - "objc2 0.6.2", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -6778,7 +6903,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -6790,10 +6915,10 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", ] [[package]] @@ -6802,7 +6927,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -6811,23 +6936,24 @@ dependencies = [ [[package]] name = "objc2-quartz-core" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", ] [[package]] name = "objc2-security" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", + "bitflags 2.13.0", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -6847,19 +6973,19 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit 0.2.2", "objc2-core-data 0.2.2", "objc2-core-image 0.2.2", - "objc2-core-location", + "objc2-core-location 0.2.2", "objc2-foundation 0.2.2", "objc2-link-presentation", "objc2-quartz-core 0.2.2", "objc2-symbols", "objc2-uniform-type-identifiers", - "objc2-user-notifications", + "objc2-user-notifications 0.2.2", ] [[package]] @@ -6868,10 +6994,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", + "bitflags 2.13.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-cloud-kit 0.3.2", + "objc2-core-data 0.3.2", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-location 0.3.2", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-user-notifications 0.3.2", ] [[package]] @@ -6891,25 +7025,35 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", - "objc2-core-location", + "objc2-core-location 0.2.2", "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-web-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", + "bitflags 2.13.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "objc2-javascript-core", "objc2-security", ] @@ -6923,15 +7067,6 @@ dependencies = [ "cc", ] -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - [[package]] name = "object" version = "0.36.7" @@ -6991,7 +7126,7 @@ version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "libc", "once_cell", "onig_sys", @@ -7025,7 +7160,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -7051,6 +7186,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-sys" version = "0.9.109" @@ -7250,8 +7391,8 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" dependencies = [ - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", "objc2-osa-kit", "serde", "serde_json", @@ -7343,6 +7484,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "pathdiff" version = "0.2.3" @@ -7417,10 +7564,20 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.3", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + [[package]] name = "phf_codegen" version = "0.8.0" @@ -7441,6 +7598,16 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -7471,6 +7638,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + [[package]] name = "phf_macros" version = "0.10.0" @@ -7487,12 +7664,12 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator 0.13.1", + "phf_shared 0.13.1", "proc-macro2", "quote", "syn 2.0.106", @@ -7526,8 +7703,17 @@ dependencies = [ ] [[package]] -name = "pico-args" -version = "0.5.0" +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.1", +] + +[[package]] +name = "pico-args" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" @@ -7580,7 +7766,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8585aba8a52ad74ccc633b8e293c1dc4277976bd5d510b925533f34fd6685f38" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "libc", "libspa", "libspa-sys", @@ -7643,7 +7829,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "crc32fast", "fdeflate", "flate2", @@ -7838,7 +8024,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "hex", ] @@ -7955,7 +8141,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls 0.23.31", - "socket2 0.6.0", + "socket2 0.5.10", "thiserror 2.0.16", "tokio", "tracing", @@ -7992,7 +8178,7 @@ dependencies = [ "cfg_aliases 0.2.1", "libc", "once_cell", - "socket2 0.6.0", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -8301,7 +8487,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", ] [[package]] @@ -8488,11 +8674,50 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.4.2", "web-sys", "webpki-roots 1.0.2", ] +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.7.0", + "hyper-rustls 0.27.7", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls 0.23.31", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper 1.0.2", + "tokio", + "tokio-rustls 0.26.3", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", + "web-sys", +] + [[package]] name = "resize" version = "0.8.8" @@ -8531,27 +8756,26 @@ dependencies = [ [[package]] name = "rfd" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" dependencies = [ - "ashpd", - "block2 0.6.1", + "block2 0.6.2", "dispatch2", "glib-sys", "gobject-sys", "gtk-sys", "js-sys", "log", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -8665,7 +8889,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -8678,7 +8902,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -8711,6 +8935,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -8739,6 +8975,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls 0.23.31", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.6", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -8772,7 +9035,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "bytemuck", "libm", "smallvec", @@ -8789,7 +9052,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "bytemuck", "core_maths", "log", @@ -8874,9 +9137,9 @@ dependencies = [ "clap", "futures", "inquire", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", "scap-targets", "tokio", "tracing", @@ -8980,7 +9243,7 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c62ae379564f834110c5b020cc731a7c14b8ec4879b4367579a467a0a704c21" dependencies = [ - "block2 0.6.1", + "block2 0.6.2", "core-foundation 0.10.1", "core-graphics 0.25.0", "core-media-rs", @@ -9039,13 +9302,26 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", "security-framework-sys", ] +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework-sys" version = "2.15.0" @@ -9063,14 +9339,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", - "cssparser", + "cssparser 0.29.6", "derive_more 0.99.20", "fxhash", "log", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", - "servo_arc", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser 0.36.0", + "derive_more 2.0.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash 2.1.1", + "servo_arc 0.4.3", "smallvec", ] @@ -9219,7 +9514,7 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fac841c7050aa73fc2bec8f7d8e9cb1159af0b3095757b99820823f3e54e5080" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "sentry-backtrace", "sentry-core", "tracing-core", @@ -9245,9 +9540,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -9267,18 +9562,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -9425,6 +9720,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha1" version = "0.10.6" @@ -9589,7 +9893,7 @@ version = "0.86.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e372258f52414e04de007326fa497581617c9fa872a3225dca5e42212723c426" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "lazy_static", "skia-bindings", ] @@ -9642,7 +9946,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -9682,12 +9986,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -9821,7 +10125,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", ] [[package]] @@ -9876,6 +10180,18 @@ dependencies = [ "serde", ] +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + [[package]] name = "string_cache_codegen" version = "0.5.4" @@ -9888,6 +10204,18 @@ dependencies = [ "quote", ] +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -9912,6 +10240,15 @@ dependencies = [ "strum_macros 0.27.2", ] +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", +] + [[package]] name = "strum_macros" version = "0.26.4" @@ -9937,6 +10274,18 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "subtle" version = "2.6.1" @@ -10132,7 +10481,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -10185,33 +10534,35 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.3" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", + "bitflags 2.13.0", + "block2 0.6.2", "core-foundation 0.10.1", - "core-graphics 0.24.0", + "core-graphics 0.25.0", "crossbeam-channel", - "dispatch", + "dbus", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", "jni 0.21.1", - "lazy_static", "libc", "log", "ndk 0.9.0", - "ndk-context", "ndk-sys 0.6.0+11769913", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.1", "once_cell", "parking_lot", + "percent-encoding", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", @@ -10257,9 +10608,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tauri" -version = "2.8.5" +version = "2.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" dependencies = [ "anyhow", "bytes", @@ -10279,15 +10630,15 @@ dependencies = [ "log", "mime", "muda", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", "objc2-ui-kit 0.3.1", "objc2-web-kit", "percent-encoding", "plist", "raw-window-handle", - "reqwest 0.12.24", + "reqwest 0.13.4", "serde", "serde_json", "serde_repr", @@ -10303,7 +10654,6 @@ dependencies = [ "tokio", "tray-icon", "url", - "urlpattern", "webkit2gtk", "webview2-com", "window-vibrancy", @@ -10312,9 +10662,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.4.1" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" dependencies = [ "anyhow", "cargo_toml", @@ -10328,15 +10678,14 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.7", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.4.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" dependencies = [ "base64 0.22.1", "brotli", @@ -10361,9 +10710,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.4.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -10375,25 +10724,21 @@ dependencies = [ [[package]] name = "tauri-nspanel" -version = "2.0.1" -source = "git+https://github.com/ahkohd/tauri-nspanel?branch=v2#18ffb9a201fbf6fedfaa382fd4b92315ea30ab1a" +version = "2.1.0" +source = "git+https://github.com/ahkohd/tauri-nspanel?branch=v2.1#a3122e894383aa068ec5365a42994e3ac94ba1b6" dependencies = [ - "bitflags 2.9.4", - "block", - "cocoa", - "core-foundation 0.10.1", - "core-graphics 0.25.0", - "objc", - "objc-foundation", - "objc_id", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "pastey", "tauri", ] [[package]] name = "tauri-plugin" -version = "2.4.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" dependencies = [ "anyhow", "glob", @@ -10402,15 +10747,14 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.7", "walkdir", ] [[package]] name = "tauri-plugin-clipboard-manager" -version = "2.3.0" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adddd9e9275b20e77af3061d100a25a884cced3c4c9ef680bd94dd0f7e26c1ca" +checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf" dependencies = [ "arboard", "log", @@ -10444,9 +10788,9 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" -version = "2.4.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beee42a4002bc695550599b011728d9dfabf82f767f134754ed6655e434824e" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" dependencies = [ "log", "raw-window-handle", @@ -10462,13 +10806,15 @@ dependencies = [ [[package]] name = "tauri-plugin-fs" -version = "2.4.2" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" dependencies = [ "anyhow", "dunce", "glob", + "log", + "objc2-foundation 0.3.2", "percent-encoding", "schemars 0.8.22", "serde", @@ -10478,7 +10824,7 @@ dependencies = [ "tauri-plugin", "tauri-utils", "thiserror 2.0.16", - "toml 0.9.7", + "toml 1.1.0+spec-1.1.0", "url", ] @@ -10562,8 +10908,8 @@ checksum = "786156aa8e89e03d271fbd3fe642207da8e65f3c961baa9e2930f332bf80a1f5" dependencies = [ "dunce", "glob", - "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", "open", "schemars 0.8.22", "serde", @@ -10609,6 +10955,20 @@ dependencies = [ "thiserror 2.0.16", ] +[[package]] +name = "tauri-plugin-prevent-default" +version = "5.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "674bac5c9831f4ca7ae42840768ac968b76353dcbe30bb0aad149c56f0be1389" +dependencies = [ + "bitflags 2.13.0", + "itertools 0.15.0", + "serde", + "strum 0.28.0", + "tauri", + "thiserror 2.0.16", +] + [[package]] name = "tauri-plugin-process" version = "2.3.0" @@ -10690,9 +11050,9 @@ dependencies = [ [[package]] name = "tauri-plugin-updater" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" +checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af" dependencies = [ "base64 0.22.1", "dirs 6.0.0", @@ -10704,7 +11064,8 @@ dependencies = [ "minisign-verify", "osakit", "percent-encoding", - "reqwest 0.12.24", + "reqwest 0.13.4", + "rustls 0.23.31", "semver", "serde", "serde_json", @@ -10726,7 +11087,7 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5f6fe3291bfa609c7e0b0ee3bedac294d94c7018934086ce782c1d0f2a468e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "log", "serde", "serde_json", @@ -10737,16 +11098,16 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.8.0" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" dependencies = [ "cookie", "dpi", "gtk", "http 1.3.1", "jni 0.21.1", - "objc2 0.6.2", + "objc2 0.6.4", "objc2-ui-kit 0.3.1", "objc2-web-kit", "raw-window-handle", @@ -10762,17 +11123,16 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.8.1" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" dependencies = [ "gtk", "http 1.3.1", "jni 0.21.1", "log", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", - "objc2-foundation 0.3.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "once_cell", "percent-encoding", "raw-window-handle", @@ -10817,24 +11177,26 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.7.0" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" dependencies = [ "anyhow", "brotli", "cargo_metadata", - "ctor 0.2.9", + "ctor 0.8.0", + "dom_query", "dunce", "glob", - "html5ever", + "html5ever 0.29.1", "http 1.3.1", "infer", "json-patch", "kuchikiki", "log", "memchr", - "phf 0.11.3", + "phf 0.13.1", + "plist", "proc-macro2", "quote", "regex", @@ -10846,7 +11208,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.16", - "toml 0.9.7", + "toml 1.1.0+spec-1.1.0", "url", "urlpattern", "uuid", @@ -10899,6 +11261,16 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -11126,30 +11498,27 @@ dependencies = [ [[package]] name = "tokio" -version = "1.47.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", - "mio 1.0.4", + "mio 1.2.1", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.6.0", + "socket2 0.6.4", "tokio-macros", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -11393,20 +11762,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "bytes", "futures-util", "http 1.3.1", "http-body 1.0.1", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -11423,9 +11792,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -11447,9 +11816,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -11458,9 +11827,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -11538,24 +11907,24 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.21.1" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" dependencies = [ "crossbeam-channel", "dirs 6.0.0", "libappindicator", "muda", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "once_cell", - "png 0.17.16", + "png 0.18.0", "serde", "thiserror 2.0.16", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -12064,9 +12433,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.103" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -12075,38 +12444,21 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.53" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.103" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -12114,22 +12466,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.103" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.106", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.103" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -12147,6 +12499,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wayland-backend" version = "0.3.11" @@ -12167,7 +12532,7 @@ version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "rustix 1.1.2", "wayland-backend", "wayland-scanner", @@ -12179,7 +12544,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "cursor-icon", "wayland-backend", ] @@ -12201,7 +12566,7 @@ version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -12213,7 +12578,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -12226,7 +12591,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -12258,9 +12623,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.80" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -12276,11 +12641,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + [[package]] name = "webkit2gtk" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" dependencies = [ "bitflags 1.3.2", "cairo-rs", @@ -12302,9 +12679,9 @@ dependencies = [ [[package]] name = "webkit2gtk-sys" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" dependencies = [ "bitflags 1.3.2", "cairo-sys-rs", @@ -12393,7 +12770,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec8fb398f119472be4d80bc3647339f56eb63b2a331f6a3d16e25d8144197dd9" dependencies = [ "arrayvec", - "bitflags 2.9.4", + "bitflags 2.13.0", "cfg_aliases 0.2.1", "document-features", "hashbrown 0.15.5", @@ -12423,7 +12800,7 @@ dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags 2.9.4", + "bitflags 2.13.0", "cfg_aliases 0.2.1", "document-features", "hashbrown 0.15.5", @@ -12480,7 +12857,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags 2.9.4", + "bitflags 2.13.0", "block", "bytemuck", "cfg-if", @@ -12523,7 +12900,7 @@ version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aa49460c2a8ee8edba3fca54325540d904dd85b2e086ada762767e17d06e8bc" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "bytemuck", "js-sys", "log", @@ -12610,10 +12987,10 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.2", - "objc2-app-kit 0.3.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "raw-window-handle", "windows-sys 0.59.0", "windows-version", @@ -13271,7 +13648,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.9.4", + "bitflags 2.13.0", "block2 0.5.1", "bytemuck", "calloop", @@ -13389,7 +13766,7 @@ version = "0.1.0" dependencies = [ "arrayvec", "axum", - "bitflags 2.9.4", + "bitflags 2.13.0", "bytemuck", "chrono", "clang-sys", @@ -13460,30 +13837,29 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "wry" -version = "0.53.3" +version = "0.55.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f0e9642a0d061f6236c54ccae64c2722a7879ad4ec7dff59bd376d446d8e90" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" dependencies = [ "base64 0.22.1", - "block2 0.6.1", + "block2 0.6.2", "cookie", "crossbeam-channel", "dirs 6.0.0", + "dom_query", "dpi", "dunce", "gdkx11", "gtk", - "html5ever", "http 1.3.1", "javascriptcore-rs", "jni 0.21.1", - "kuchikiki", "libc", "ndk 0.9.0", - "objc2 0.6.2", - "objc2-app-kit 0.3.1", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "objc2-ui-kit 0.3.1", "objc2-web-kit", "once_cell", @@ -13567,7 +13943,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.13.0", "dlib", "log", "once_cell", @@ -13873,3 +14249,7 @@ dependencies = [ "syn 2.0.106", "winnow 0.7.13", ] + +[[patch.unused]] +name = "tao" +version = "0.34.3" diff --git a/Cargo.toml b/Cargo.toml index 5c9c2cab521..94a39f418f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,14 +8,14 @@ members = [ ] [workspace.dependencies] -anyhow = { version = "1.0.86" } +anyhow = { version = "1.0.102" } # This includes a currently-unreleased fix that ensures the audio stream is actually # stopped and released on drop on macOS, plus zeroing of WASAPI capture # packets flagged AUDCLNT_BUFFERFLAGS_SILENT (loopback of an idle endpoint # otherwise surfaces unspecified buffer contents as audio). cpal = { git = "https://github.com/CapSoftware/cpal", rev = "6013cb5f8bd3" } ffmpeg = { package = "ffmpeg-next", git = "https://github.com/CapSoftware/rust-ffmpeg", rev = "49db1fede112" } -tokio = { version = "1.39.3", features = [ +tokio = { version = "1.52.2", features = [ "macros", "process", "fs", @@ -24,7 +24,7 @@ tokio = { version = "1.39.3", features = [ "rt-multi-thread", "time", ] } -tauri = { version = "2.5.0", features = ["specta"] } +tauri = { version = "2.11.5", features = ["specta"] } specta = { version = "=2.0.0-rc.20", features = [ "derive", "serde_json", @@ -39,18 +39,18 @@ nokhwa = { git = "https://github.com/CapSoftware/nokhwa", rev = "b9c8079e82e2", "serialize", ] } nokhwa-bindings-macos = { git = "https://github.com/CapSoftware/nokhwa", rev = "b9c8079e82e2" } -wgpu = { version = "25.0.0", features = ["wgpu-core"] } -wgpu-hal = "25.0.0" -wgpu-core = "25.0.0" -flume = "0.11.0" +wgpu = { version = "25.0.2", features = ["wgpu-core"] } +wgpu-hal = "25.0.2" +wgpu-core = "25.0.2" +flume = "0.11.1" thiserror = "1.0" sentry = { version = "0.42.0", features = [ "anyhow", "backtrace", "debug-images", ] } -tracing = "0.1.41" -futures = "0.3.31" +tracing = "0.1.44" +futures = "0.3.32" aho-corasick = "1.1.4" cidre = { git = "https://github.com/CapSoftware/cidre", rev = "bf84b67079a8", features = [ @@ -74,7 +74,7 @@ windows = "0.60.0" windows-core = "0.60" windows-sys = "0.59.0" windows-capture = "1.5.0" -percent-encoding = "2.3.1" +percent-encoding = "2.3.2" sysinfo = "0.32" # TODO: Reenable these: https://github.com/CapSoftware/Cap/issues/859 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 3eb2586b91e..ff0205a5a5b 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -42,11 +42,11 @@ "@solidjs/router": "^0.14.2", "@solidjs/start": "^1.1.3", "@tanstack/solid-query": "^5.51.21", - "@tauri-apps/api": "2.8.0", - "@tauri-apps/plugin-clipboard-manager": "^2.3.0", + "@tauri-apps/api": "2.11.0", + "@tauri-apps/plugin-clipboard-manager": "^2.3.2", "@tauri-apps/plugin-deep-link": "^2.4.1", - "@tauri-apps/plugin-dialog": "2.4.2", - "@tauri-apps/plugin-fs": "2.4.2", + "@tauri-apps/plugin-dialog": "2.7.1", + "@tauri-apps/plugin-fs": "2.5.1", "@tauri-apps/plugin-http": "2.5.2", "@tauri-apps/plugin-notification": "^2.3.0", "@tauri-apps/plugin-opener": "^2.5.0", @@ -54,7 +54,7 @@ "@tauri-apps/plugin-process": "2.3.0", "@tauri-apps/plugin-shell": "^2.3.0", "@tauri-apps/plugin-store": "^2.4.0", - "@tauri-apps/plugin-updater": "2.9.0", + "@tauri-apps/plugin-updater": "2.10.1", "@ts-rest/core": "^3.52.1", "@types/react-tooltip": "^4.2.4", "cva": "npm:class-variance-authority@^0.7.0", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 8ff25909b1d..f99d3ea7c4b 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -2,8 +2,9 @@ name = "cap-desktop" version = "0.5.6" description = "Beautiful screen recordings, owned by you." -authors = ["you"] +authors = ["Richie McIlroy "] edition = "2024" +license = "AGPL-3.0-only" [lints] workspace = true @@ -13,7 +14,7 @@ name = "cap_desktop_lib" crate-type = ["lib", "cdylib", "staticlib"] [build-dependencies] -tauri-build = { version = "2.1.0", features = [] } +tauri-build = { version = "2.6.1", features = [] } [target.'cfg(target_os = "macos")'.build-dependencies] swift-rs = { version = "1.0.6", features = ["build"] } @@ -26,8 +27,8 @@ tauri = { workspace = true, features = [ "image-png", ] } tauri-specta = { version = "=2.0.0-rc.20", features = ["derive", "typescript"] } -tauri-plugin-dialog = "2.2.0" -tauri-plugin-fs = "2.2.0" +tauri-plugin-dialog = "2.7.1" +tauri-plugin-fs = "2.5.1" tauri-plugin-global-shortcut = "2.2.0" tauri-plugin-http = "=2.5.2" tauri-plugin-notification = "2.2.0" @@ -36,13 +37,14 @@ tauri-plugin-process = "2.2.0" tauri-plugin-shell = "2.2.0" tauri-plugin-single-instance = { version = "2.2.0", features = ["deep-link"] } tauri-plugin-store = "2.2.0" -tauri-plugin-updater = "2.9.0" +tauri-plugin-updater = "2.10.1" tauri-plugin-oauth = { git = "https://github.com/FabianLars/tauri-plugin-oauth", branch = "v2" } tauri-plugin-window-state = "2.2.0" tauri-plugin-positioner = "2.2.0" tauri-plugin-deep-link = "2.2.0" -tauri-plugin-clipboard-manager = "2.2.1" +tauri-plugin-clipboard-manager = "2.3.2" tauri-plugin-opener = "2.2.6" +tauri-plugin-prevent-default = "5" serde = { workspace = true } serde_json = "1.0.111" @@ -127,25 +129,27 @@ aho-corasick.workspace = true [target.'cfg(target_os = "macos")'.dependencies] -core-graphics = "0.24.0" -core-foundation = "0.10.0" -objc2-app-kit = { version = "0.3.0", features = [ +objc2 = "0.6.4" +block2= "0.6.2" +objc2-core-foundation = "0.3.2" +objc2-application-services = "0.3.2" +objc2-foundation = { version = "0.3.2", features = [ + "NSNotification", + "NSString", + "NSThread", +] } +objc2-app-kit = { version = "0.3.2", features = [ "NSWindow", "NSResponder", "NSHapticFeedback", "NSWorkspace", "NSRunningApplication", ] } -objc2 = "0.6.0" -objc2-foundation = { version = "0.3.0", features = [ - "NSNotification", - "NSString", - "NSThread", -] } +objc2-web-kit = "0.3.2" cocoa = "0.26.0" objc = "0.2.7" swift-rs = "1.0.6" -tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" } +tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2.1" } cidre = { workspace = true } cap-camera-ffmpeg = { path = "../../../crates/camera-ffmpeg" } diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index 745254f6eb4..944246dc097 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -51,6 +51,7 @@ "core:webview:default", "core:webview:allow-create-webview-window", "core:app:allow-version", + "core:app:allow-set-app-theme", "shell:default", "core:image:default", "dialog:default", diff --git a/apps/desktop/src-tauri/src/automation.rs b/apps/desktop/src-tauri/src/automation.rs index a05f0a030f8..9f94b2092db 100644 --- a/apps/desktop/src-tauri/src/automation.rs +++ b/apps/desktop/src-tauri/src/automation.rs @@ -452,7 +452,7 @@ impl AutomationHost for DesktopAutomationHost { async fn open_editor(&self, ctx: &TriggerContext) -> Result<(), String> { if let Some(image_path) = ctx.image_path.as_ref() { - let _ = crate::windows::ShowCapWindow::ScreenshotEditor { + let _ = crate::windows::CapWindow::ScreenshotEditor { path: image_path.clone(), } .show(&self.app) @@ -465,7 +465,7 @@ impl AutomationHost for DesktopAutomationHost { .as_ref() .ok_or("No project path for editor")?; - let _ = crate::windows::ShowCapWindow::Editor { + let _ = crate::windows::CapWindow::Editor { project_path: path.clone(), } .show(&self.app) diff --git a/apps/desktop/src-tauri/src/camera.rs b/apps/desktop/src-tauri/src/camera.rs index 111d9effeea..e02040313e8 100644 --- a/apps/desktop/src-tauri/src/camera.rs +++ b/apps/desktop/src-tauri/src/camera.rs @@ -507,7 +507,7 @@ fn wait_for_shutdown_signal(runtime: &Runtime, receiver: oneshot::Receiver<()>, // crashes after ~4-5 toggle cycles. // // Pause/Resume hide/show the window while keeping GPU resources alive. -// See the comment in windows.rs ShowCapWindow::Camera for the critical detail +// See the comment in windows.rs CapWindow::Camera for the critical detail // about avoiding order_front_regardless(). #[derive(Clone)] enum ReconfigureEvent { diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index a7b94be3ac3..8240ebdea3e 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use tauri::{AppHandle, Manager, Url}; use tracing::trace; -use crate::{App, ArcLock, recording::StartRecordingInputs, windows::ShowCapWindow}; +use crate::{App, ArcLock, recording::StartRecordingInputs, windows::CapWindow}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] @@ -196,7 +196,7 @@ impl DeepLinkAction { crate::open_project_from_path(Path::new(&project_path), app.clone()) } DeepLinkAction::OpenSettings { page } => { - crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await + crate::show_window(app.clone(), CapWindow::Settings { page }).await } } } diff --git a/apps/desktop/src-tauri/src/display_utils.rs b/apps/desktop/src-tauri/src/display_utils.rs new file mode 100644 index 00000000000..3e579f07e5b --- /dev/null +++ b/apps/desktop/src-tauri/src/display_utils.rs @@ -0,0 +1,376 @@ +use scap_targets::{Display, DisplayId}; +use tauri::{PhysicalPosition, PhysicalSize}; + +// Credits: tauri-plugin-window-state +pub trait MonitorExt { + fn intersects( + &self, + position: PhysicalPosition, + size: PhysicalSize, + scale: f64, + ) -> bool; +} + +impl MonitorExt for Display { + fn intersects( + &self, + position: PhysicalPosition, + size: PhysicalSize, + _scale: f64, + ) -> bool { + #[cfg(target_os = "macos")] + { + let Some(bounds) = self.raw_handle().logical_bounds() else { + return false; + }; + + let left = (bounds.position().x() * _scale) as i32; + let right = left + (bounds.size().width() * _scale) as i32; + let top = (bounds.position().y() * _scale) as i32; + let bottom = top + (bounds.size().height() * _scale) as i32; + + [ + (position.x, position.y), + (position.x + size.width as i32, position.y), + (position.x, position.y + size.height as i32), + ( + position.x + size.width as i32, + position.y + size.height as i32, + ), + ] + .into_iter() + .any(|(x, y)| x >= left && x < right && y >= top && y < bottom) + } + + #[cfg(windows)] + { + let Some(bounds) = self.raw_handle().physical_bounds() else { + return false; + }; + + let left = bounds.position().x() as i32; + let right = left + bounds.size().width() as i32; + let top = bounds.position().y() as i32; + let bottom = top + bounds.size().height() as i32; + + [ + (position.x, position.y), + (position.x + size.width as i32, position.y), + (position.x, position.y + size.height as i32), + ( + position.x + size.width as i32, + position.y + size.height as i32, + ), + ] + .into_iter() + .any(|(x, y)| x >= left && x < right && y >= top && y < bottom) + } + + #[cfg(target_os = "linux")] + { + let Some(bounds) = self.raw_handle().physical_bounds() else { + return false; + }; + + let left = bounds.position().x() as i32; + let right = left + bounds.size().width() as i32; + let top = bounds.position().y() as i32; + let bottom = top + bounds.size().height() as i32; + + [ + (position.x, position.y), + (position.x + size.width as i32, position.y), + (position.x, position.y + size.height as i32), + ( + position.x + size.width as i32, + position.y + size.height as i32, + ), + ] + .into_iter() + .any(|(x, y)| x >= left && x < right && y >= top && y < bottom) + } + } +} + +const DEFAULT_FALLBACK_DISPLAY_WIDTH: f64 = 1920.0; +const DEFAULT_FALLBACK_DISPLAY_HEIGHT: f64 = 1080.0; + +pub struct CursorMonitorInfo { + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, + // On Windows each monitor's "logical" rect is its physical rect divided by + // its own scale, so logical rects of mixed-DPI monitors overlap and tao's + // LogicalPosition conversion (which uses whatever monitor the window + // currently occupies) can land a window on the wrong monitor. Positioning + // must go through this monitor's own scale, as a physical position. + #[cfg(windows)] + pub scale: f64, +} + +impl From<&Display> for CursorMonitorInfo { + fn from(display: &Display) -> Self { + let bounds = display.raw_handle().logical_bounds(); + + #[cfg(windows)] + let scale = bounds + .as_ref() + .map(|b| b.size().width()) + .filter(|width| *width > 0.0) + .and_then(|logical_width| { + display + .physical_size() + .map(|physical| physical.width() / logical_width) + }) + .filter(|scale| scale.is_finite() && *scale > 0.0) + .unwrap_or(1.0); + + let (x, y, width, height) = bounds + .map(|b| { + ( + b.position().x(), + b.position().y(), + b.size().width(), + b.size().height(), + ) + }) + .unwrap_or(( + 0.0, + 0.0, + DEFAULT_FALLBACK_DISPLAY_WIDTH, + DEFAULT_FALLBACK_DISPLAY_HEIGHT, + )); + + Self { + x, + y, + width, + height, + #[cfg(windows)] + scale, + } + } +} + +impl CursorMonitorInfo { + pub fn get() -> Self { + Self::from(&Display::get_containing_cursor().unwrap_or_else(Display::primary)) + } + + /// Converts a global-logical point on this monitor into a `Position` that + /// lands exactly there regardless of which monitor the window currently + /// occupies. Logical on macOS/Linux (a true global space there), physical + /// on Windows. + pub fn position(&self, x: f64, y: f64) -> tauri::Position { + #[cfg(windows)] + return tauri::Position::Physical(tauri::PhysicalPosition::new( + (x * self.scale).round() as i32, + (y * self.scale).round() as i32, + )); + + #[cfg(not(windows))] + tauri::Position::Logical(tauri::LogicalPosition::new(x, y)) + } + + pub fn center_position(&self, window_width: f64, window_height: f64) -> (f64, f64) { + let pos_x = self.x + (self.width - window_width) / 2.0; + let pos_y = self.y + (self.height - window_height) / 2.0; + (pos_x, pos_y) + } + + pub fn bottom_center_position( + &self, + window_width: f64, + window_height: f64, + offset_y: f64, + ) -> (f64, f64) { + let pos_x = self.x + (self.width - window_width) / 2.0; + let pos_y = self.y + self.height - window_height - offset_y; + (pos_x, pos_y) + } + + pub fn from_window(window: &tauri::WebviewWindow) -> Self { + let Ok(window_pos) = window.outer_position() else { + return Self::get(); + }; + + // outer_position is physical. On Windows, resolve the display in + // physical space (per-monitor logical rects overlap in mixed-DPI + // layouts). On macOS, convert to logical points, a true global space. + // On Linux scap reports logical bounds in unscaled physical units, so + // the raw position compares directly. + #[cfg(windows)] + { + let (pos_x, pos_y) = (window_pos.x as f64, window_pos.y as f64); + for display in Display::list() { + if let Some(bounds) = display.raw_handle().physical_bounds() { + let (x, y, width, height) = ( + bounds.position().x(), + bounds.position().y(), + bounds.size().width(), + bounds.size().height(), + ); + + if pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height { + return Self::from(&display); + } + } + } + + Self::get() + } + + #[cfg(target_os = "macos")] + { + let scale = window.scale_factor().unwrap_or(1.0); + let pos = window_pos.to_logical::(scale); + + for display in Display::list() { + if display_contains_logical(&display, pos.x, pos.y) { + return Self::from(&display); + } + } + + Self::get() + } + + #[cfg(target_os = "linux")] + { + let (pos_x, pos_y) = (window_pos.x as f64, window_pos.y as f64); + + for display in Display::list() { + if display_contains_logical(&display, pos_x, pos_y) { + return Self::from(&display); + } + } + + Self::get() + } + } +} + +fn display_contains_logical(display: &Display, pos_x: f64, pos_y: f64) -> bool { + display + .raw_handle() + .logical_bounds() + .map(|bounds| { + let (x, y, width, height) = ( + bounds.position().x(), + bounds.position().y(), + bounds.size().width(), + bounds.size().height(), + ); + + pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height + }) + .unwrap_or(false) +} + +/// Resolves the display a persisted window position belongs to, preferring the +/// display it was saved on. On Windows the saved logical coordinates are only +/// meaningful relative to that display (mixed-DPI logical rects overlap), so +/// restores must convert through its scale rather than the window's current one. +pub fn display_for_saved_position( + pos_x: f64, + pos_y: f64, + display_id: Option<&DisplayId>, +) -> Option { + display_id + .and_then(Display::from_id) + .filter(|display| display_contains_logical(display, pos_x, pos_y)) + .or_else(|| display_containing_logical(pos_x, pos_y)) +} + +/// Converts a global-logical point into a `Position` that lands exactly there, +/// resolving the owning display by containment when the caller doesn't know it. +/// Falls back to a plain logical position when no display contains the point. +pub fn logical_point_position(pos_x: f64, pos_y: f64) -> tauri::Position { + #[cfg(windows)] + if let Some(display) = display_containing_logical(pos_x, pos_y) { + return CursorMonitorInfo::from(&display).position(pos_x, pos_y); + } + + tauri::Position::Logical(tauri::LogicalPosition::new(pos_x, pos_y)) +} + +fn display_containing_logical(pos_x: f64, pos_y: f64) -> Option { + Display::list() + .into_iter() + .find(|display| display_contains_logical(display, pos_x, pos_y)) +} + +pub fn is_position_on_display(display_id: &DisplayId, pos_x: f64, pos_y: f64) -> bool { + Display::from_id(display_id) + .and_then(|display| display.raw_handle().logical_bounds()) + .map(|bounds| { + let (x, y, width, height) = ( + bounds.position().x(), + bounds.position().y(), + bounds.size().width(), + bounds.size().height(), + ); + + pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height + }) + .unwrap_or(false) +} + +pub fn display_name_for_position(pos_x: f64, pos_y: f64) -> Option { + Display::list().into_iter().find_map(|display| { + let bounds = display.raw_handle().logical_bounds()?; + let (x, y, width, height) = ( + bounds.position().x(), + bounds.position().y(), + bounds.size().width(), + bounds.size().height(), + ); + + if pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height { + display.name().filter(|name| !name.trim().is_empty()) + } else { + None + } + }) +} + +pub fn is_position_on_monitor_name(monitor_name: &str, pos_x: f64, pos_y: f64) -> bool { + Display::list().into_iter().any(|display| { + if display.name().as_deref() != Some(monitor_name) { + return false; + } + + display + .raw_handle() + .logical_bounds() + .map(|bounds| { + let (x, y, width, height) = ( + bounds.position().x(), + bounds.position().y(), + bounds.size().width(), + bounds.size().height(), + ); + + pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height + }) + .unwrap_or(false) + }) +} + +pub fn is_position_on_any_screen(pos_x: f64, pos_y: f64) -> bool { + for display in Display::list() { + if let Some(bounds) = display.raw_handle().logical_bounds() { + let (x, y, width, height) = ( + bounds.position().x(), + bounds.position().y(), + bounds.size().width(), + bounds.size().height(), + ); + + if pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height { + return true; + } + } + } + false +} diff --git a/apps/desktop/src-tauri/src/fake_window.rs b/apps/desktop/src-tauri/src/fake_window.rs index 568094ebf1b..53632107664 100644 --- a/apps/desktop/src-tauri/src/fake_window.rs +++ b/apps/desktop/src-tauri/src/fake_window.rs @@ -355,10 +355,9 @@ pub fn spawn_fake_window_listener(app: AppHandle, window: WebviewWindow) { None => true, }; if changed { - let _ = window - .set_position(crate::windows::logical_point_position( - px, py, - )); + let _ = window.set_position( + crate::display_utils::logical_point_position(px, py), + ); last_target_pos = Some((px, py)); } handled = true; @@ -378,8 +377,9 @@ pub fn spawn_fake_window_listener(app: AppHandle, window: WebviewWindow) { && let Some(display) = get_display_by_id(&cursor_display_id) && let Some((pos_x, pos_y)) = calculate_bottom_center_position(&display) { - let _ = - window.set_position(crate::windows::logical_point_position(pos_x, pos_y)); + let _ = window.set_position(crate::display_utils::logical_point_position( + pos_x, pos_y, + )); current_display_id = Some(cursor_display_id); } } diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-tauri/src/general_settings.rs index 313a89dfe7b..df154792112 100644 --- a/apps/desktop/src-tauri/src/general_settings.rs +++ b/apps/desktop/src-tauri/src/general_settings.rs @@ -165,15 +165,13 @@ pub struct GeneralSettingsStore { pub disable_auto_open_links: bool, #[serde(default = "default_true")] pub has_completed_startup: bool, - #[serde(default)] - pub theme: AppTheme, + #[serde(default, alias = "theme")] + pub appearance: Appearance, #[serde(default)] pub commercial_license: Option, #[serde(default)] pub last_version: Option, #[serde(default)] - pub window_transparency: bool, - #[serde(default)] pub post_studio_recording_behaviour: PostStudioRecordingBehaviour, #[serde(default)] pub main_window_recording_start_behaviour: MainWindowRecordingStartBehaviour, @@ -216,8 +214,6 @@ pub struct GeneralSettingsStore { #[serde(default)] pub studio_recording_quality: StudioRecordingQuality, #[serde(default)] - pub main_window_position: Option, - #[serde(default)] pub camera_window_position: Option, #[serde(default)] pub camera_window_positions_by_monitor_name: BTreeMap, @@ -309,10 +305,9 @@ impl Default for GeneralSettingsStore { enable_notifications: true, disable_auto_open_links: false, has_completed_startup: false, - theme: AppTheme::System, + appearance: Appearance::System, commercial_license: None, last_version: None, - window_transparency: false, post_studio_recording_behaviour: PostStudioRecordingBehaviour::OpenEditor, main_window_recording_start_behaviour: MainWindowRecordingStartBehaviour::Close, custom_cursor_capture: cap_recording::DEFAULT_CUSTOM_CURSOR_CAPTURE, @@ -333,7 +328,6 @@ impl Default for GeneralSettingsStore { transcription_hints: default_transcription_hints(), editor_preview_quality: EditorPreviewQuality::Half, studio_recording_quality: default_studio_recording_quality(), - main_window_position: None, camera_window_position: None, camera_window_positions_by_monitor_name: BTreeMap::new(), has_completed_onboarding: false, @@ -349,13 +343,23 @@ impl Default for GeneralSettingsStore { #[derive(Default, Debug, Copy, Clone, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] -pub enum AppTheme { +pub enum Appearance { #[default] System, Light, Dark, } +impl From for Option { + fn from(appearance: Appearance) -> Self { + match appearance { + Appearance::Light => Some(tauri::Theme::Light), + Appearance::Dark => Some(tauri::Theme::Dark), + Appearance::System => None, + } + } +} + impl GeneralSettingsStore { pub fn recordings_dir(app: &AppHandle) -> std::path::PathBuf { let custom = Self::get(app) diff --git a/apps/desktop/src-tauri/src/hotkeys.rs b/apps/desktop/src-tauri/src/hotkeys.rs index 97c9718a2b3..a29aaf0df4e 100644 --- a/apps/desktop/src-tauri/src/hotkeys.rs +++ b/apps/desktop/src-tauri/src/hotkeys.rs @@ -2,7 +2,7 @@ use crate::{ RequestOpenRecordingPicker, RequestStartRecording, recording, recording_settings::{RecordingSettingsStore, RecordingTargetMode}, tray, - windows::ShowCapWindow, + windows::CapWindow, }; use cap_recording::screen_capture::ScreenCaptureTarget; use global_hotkey::HotKeyState; @@ -102,7 +102,7 @@ pub fn init(app: &AppHandle) { if shortcut.key == Code::Comma && shortcut.mods == Modifiers::META { let app = app.clone(); tokio::spawn(async move { - let _ = ShowCapWindow::Settings { page: None }.show(&app).await; + let _ = CapWindow::Settings { page: None }.show(&app).await; }); } @@ -213,7 +213,7 @@ async fn handle_hotkey(app: AppHandle, action: HotkeyAction) -> Result<(), Strin match recording::take_screenshot(app.clone(), target.clone()).await { Ok(path) => { if crate::automation::should_open_screenshot_editor(&app, &target) { - let _ = ShowCapWindow::ScreenshotEditor { path }.show(&app).await; + let _ = CapWindow::ScreenshotEditor { path }.show(&app).await; } Ok(()) } @@ -232,7 +232,7 @@ async fn handle_hotkey(app: AppHandle, action: HotkeyAction) -> Result<(), Strin match recording::take_screenshot(app.clone(), target.clone()).await { Ok(path) => { if crate::automation::should_open_screenshot_editor(&app, &target) { - let _ = ShowCapWindow::ScreenshotEditor { path }.show(&app).await; + let _ = CapWindow::ScreenshotEditor { path }.show(&app).await; } Ok(()) } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 3f85c7c3b18..5fa445e49df 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -15,6 +15,7 @@ mod cli; mod clip_thumbnails; mod crash_sentinel; mod deeplink_actions; +mod display_utils; mod editor_window; mod exit_shutdown; mod export; @@ -27,6 +28,7 @@ mod http_client; mod import; mod logging; mod notifications; +#[cfg(target_os = "macos")] mod panel_manager; mod permissions; mod platform; @@ -120,8 +122,8 @@ use web_api::ManagerExt as WebManagerExt; #[cfg(target_os = "macos")] use windows::hide_overlay; use windows::{ - CapWindowId, EditorRecordingTarget, EditorWindowIds, ScreenshotEditorWindowIds, ShowCapWindow, - set_window_transparent, show_overlay, + CapWindow, CapWindowId, EditorRecordingTarget, EditorWindowIds, ScreenshotEditorWindowIds, + show_overlay, }; use crate::{recording::start_recording, upload::build_video_meta}; @@ -135,10 +137,6 @@ use exit_shutdown::{ }; use futures::FutureExt; use std::panic::AssertUnwindSafe; -#[cfg(target_os = "macos")] -use tauri::menu::{ - AboutMetadata, HELP_SUBMENU_ID, Menu, MenuItem, PredefinedMenuItem, Submenu, WINDOW_SUBMENU_ID, -}; type FinalizingRecordingsMap = std::collections::HashMap, watch::Receiver)>; @@ -360,10 +358,6 @@ const APP_EXIT_STEP_TIMEOUT: Duration = Duration::from_millis(750); const APP_EXIT_CAMERA_SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(1200); const APP_EXIT_TOTAL_TIMEOUT: Duration = Duration::from_secs(3); const APP_EXIT_FORCE_TIMEOUT: Duration = Duration::from_secs(8); -#[cfg(target_os = "macos")] -const APP_MENU_QUIT_ID: &str = "app_quit"; -#[cfg(target_os = "macos")] -static MACOS_NATIVE_TERMINATE_APP: std::sync::OnceLock = std::sync::OnceLock::new(); async fn await_exit_step(name: &'static str, timeout: Duration, fut: F) -> Option where @@ -476,152 +470,6 @@ fn should_show_onboarding(app: &AppHandle) -> bool { || !permissions::do_permissions_check(false).necessary_granted() } -#[cfg(target_os = "macos")] -fn build_macos_app_menu(app_handle: &AppHandle) -> tauri::Result> { - let pkg_info = app_handle.package_info(); - let config = app_handle.config(); - let about_metadata = AboutMetadata { - name: Some(pkg_info.name.clone()), - version: Some(pkg_info.version.to_string()), - copyright: config.bundle.copyright.clone(), - authors: config - .bundle - .publisher - .clone() - .map(|publisher| vec![publisher]), - ..Default::default() - }; - let quit_label = config - .product_name - .as_ref() - .map(|name| format!("Quit {name}")) - .unwrap_or_else(|| "Quit Cap".to_string()); - - let window_menu = Submenu::with_id_and_items( - app_handle, - WINDOW_SUBMENU_ID, - "Window", - true, - &[ - &PredefinedMenuItem::minimize(app_handle, None)?, - &PredefinedMenuItem::maximize(app_handle, None)?, - &PredefinedMenuItem::separator(app_handle)?, - &PredefinedMenuItem::close_window(app_handle, None)?, - ], - )?; - - let help_menu = Submenu::with_id_and_items(app_handle, HELP_SUBMENU_ID, "Help", true, &[])?; - - Menu::with_items( - app_handle, - &[ - &Submenu::with_items( - app_handle, - pkg_info.name.clone(), - true, - &[ - &PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?, - &PredefinedMenuItem::separator(app_handle)?, - &PredefinedMenuItem::services(app_handle, None)?, - &PredefinedMenuItem::separator(app_handle)?, - &PredefinedMenuItem::hide(app_handle, None)?, - &PredefinedMenuItem::hide_others(app_handle, None)?, - &PredefinedMenuItem::separator(app_handle)?, - &MenuItem::with_id( - app_handle, - APP_MENU_QUIT_ID, - quit_label, - true, - Some("Cmd+Q"), - )?, - ], - )?, - &Submenu::with_items( - app_handle, - "File", - true, - &[&PredefinedMenuItem::close_window(app_handle, None)?], - )?, - &Submenu::with_items( - app_handle, - "Edit", - true, - &[ - &PredefinedMenuItem::undo(app_handle, None)?, - &PredefinedMenuItem::redo(app_handle, None)?, - &PredefinedMenuItem::separator(app_handle)?, - &PredefinedMenuItem::cut(app_handle, None)?, - &PredefinedMenuItem::copy(app_handle, None)?, - &PredefinedMenuItem::paste(app_handle, None)?, - &PredefinedMenuItem::select_all(app_handle, None)?, - ], - )?, - &Submenu::with_items( - app_handle, - "View", - true, - &[&PredefinedMenuItem::fullscreen(app_handle, None)?], - )?, - &window_menu, - &help_menu, - ], - ) -} - -#[cfg(target_os = "macos")] -unsafe extern "C" fn macos_application_should_terminate( - _: &objc::runtime::Object, - _: objc::runtime::Sel, - _: cocoa::base::id, -) -> isize { - if let Some(app) = MACOS_NATIVE_TERMINATE_APP.get() { - let app = app.clone(); - tokio::spawn(async move { - request_app_exit(app).await; - }); - } else { - warn!("macOS native termination requested before app exit handler was installed"); - } - - 0 -} - -#[cfg(target_os = "macos")] -fn install_macos_native_terminate_handler(app: &AppHandle) { - use cocoa::base::{id, nil}; - use objc::{class, msg_send, sel, sel_impl}; - - let _ = MACOS_NATIVE_TERMINATE_APP.set(app.clone()); - - unsafe { - let ns_app: id = msg_send![class!(NSApplication), sharedApplication]; - let delegate: id = msg_send![ns_app, delegate]; - - if delegate == nil { - warn!("Unable to install macOS native termination handler without app delegate"); - return; - } - - let delegate_class = (*delegate).class() as *const _ as *mut objc::runtime::Class; - let imp = std::mem::transmute::< - unsafe extern "C" fn(&objc::runtime::Object, objc::runtime::Sel, id) -> isize, - objc::runtime::Imp, - >(macos_application_should_terminate); - let added = objc::runtime::class_addMethod( - delegate_class, - sel!(applicationShouldTerminate:), - imp, - c"q@:@".as_ptr(), - ); - - if added == objc::runtime::YES { - info!("Installed macOS native termination handler"); - } else { - warn!("macOS native termination handler was already installed"); - } - } -} - fn now_millis() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -1263,7 +1111,7 @@ async fn set_camera_input( let show_result = if camera_window_is_visible { Ok(()) } else { - ShowCapWindow::Camera { centered: false } + CapWindow::Camera { centered: false } .show(&app_handle) .await .map(|_| ()) @@ -1354,7 +1202,7 @@ async fn set_camera_input( if !showed_camera_window { showed_camera_window = true; - let show_result = ShowCapWindow::Camera { centered: false } + let show_result = CapWindow::Camera { centered: false } .show(&app_handle) .await; show_result @@ -2355,9 +2203,6 @@ pub struct RecordingDeleted { path: PathBuf, } -#[derive(specta::Type, tauri_specta::Event, Serialize)] -pub struct SetCaptureAreaPending(bool); - #[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)] pub struct NewScreenshotAdded { path: PathBuf, @@ -3215,8 +3060,10 @@ fn close_recordings_overlay_window(app: AppHandle) { if let Ok(panel) = app_for_close.get_webview_panel(&CapWindowId::RecordingsOverlay.label()) { - panel.released_when_closed(false); - panel.close(); + panel.set_released_when_closed(false); + if let Some(window) = panel.to_window() { + let _ = window.close(); + } } }) .ok(); @@ -3587,7 +3434,7 @@ fn screenshot_share_link_for_hash( } async fn upgrade_required_result(app: &AppHandle) -> UploadResult { - let _ = ShowCapWindow::Upgrade.show(app).await; + let _ = CapWindow::Upgrade.show(app).await; UploadResult::UpgradeRequired } @@ -4355,8 +4202,8 @@ async fn editor_delete_project( #[tauri::command] #[specta::specta] #[instrument(skip(app))] -async fn show_window(app: AppHandle, window: ShowCapWindow) -> Result<(), String> { - if matches!(window, ShowCapWindow::Camera { .. }) { +async fn show_window(app: AppHandle, window: CapWindow) -> Result<(), String> { + if matches!(window, CapWindow::Camera { .. }) { let operation_lock = app.state::(); let _operation_guard = operation_lock.lock().await; window.show(&app).await.map_err(|e| e.to_string())?; @@ -4909,8 +4756,6 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { seek_to, get_display_frame_for_cropping, clip_thumbnails::get_clip_thumbnail, - windows::position_traffic_lights, - windows::set_theme, windows::apply_macos_liquid_glass_background, global_message_dialog, show_window, @@ -4920,7 +4765,6 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { list_fails, set_fail, update_auth_plan, - set_window_transparent, get_editor_meta, get_recording_meta_by_path, set_editor_recording_target, @@ -4961,6 +4805,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { recovery::find_incomplete_recordings, recovery::recover_recording, recovery::discard_incomplete_recording, + set_appearance, automation::get_automations, automation::set_automations, automation::test_automation, @@ -4997,7 +4842,6 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { hotkeys::OnEscapePress, upload::UploadProgressEvent, import::VideoImportProgress, - SetCaptureAreaPending, DevicesUpdated, updates::UpdateDownloadProgress, updates::UpdateReady, @@ -5096,7 +4940,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { else { let app = app.clone(); tokio::spawn(async move { - ShowCapWindow::Main { + CapWindow::Main { init_target_mode: None, } .show(&app) @@ -5112,15 +4956,8 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { #[cfg(target_os = "macos")] { builder = builder - .menu(build_macos_app_menu) - .on_menu_event(|app, event| { - if event.id() == APP_MENU_QUIT_ID { - let app = app.clone(); - tokio::spawn(async move { - request_app_exit(app).await; - }); - } - }) + .menu(platform::menu::build_app_menu) + .on_menu_event(platform::menu::on_event) .plugin(tauri_nspanel::init()); } @@ -5139,27 +4976,23 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_prevent_default::debug()) .plugin( tauri_plugin_window_state::Builder::new() .with_state_flags({ use tauri_plugin_window_state::StateFlags; - let mut flags = StateFlags::all(); - flags.remove(StateFlags::VISIBLE); - flags + StateFlags::all() - StateFlags::VISIBLE - StateFlags::DECORATIONS }) .with_denylist(&[ CapWindowId::Onboarding.label().as_str(), - CapWindowId::Main.label().as_str(), - CapWindowId::Settings.label().as_str(), "window-capture-occluder", "target-select-overlay", - CapWindowId::CaptureArea.label().as_str(), CapWindowId::Camera.label().as_str(), CapWindowId::RecordingsOverlay.label().as_str(), CapWindowId::RecordingControls.label().as_str(), CapWindowId::Upgrade.label().as_str(), - "editor", - "screenshot-editor", + #[cfg(target_os = "macos")] + CapWindowId::Main.label().as_str(), ]) .map_label(|label| match label { label if label.starts_with("camera-") => "camera", @@ -5177,6 +5010,9 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { .setup(move |app| { let app = app.handle().clone(); + #[cfg(target_os = "macos")] + platform::menu::init(&app)?; + if let Err(err) = update_project_names::migrate_if_needed(&app) { tracing::error!("Failed to migrate project file names: {}", err); } @@ -5320,8 +5156,6 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { app.manage(AppExitState::default()); app.manage(MainWindowReadyState::default()); app.manage(deeplink_actions::DeepLinkActionExecutor::new(&app)); - #[cfg(target_os = "macos")] - install_macos_native_terminate_handler(&app); spawn_process_memory_sampler(app.clone()); app.manage(Arc::new(RwLock::new( @@ -5381,10 +5215,10 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { async move { if should_show_onboarding(&app) { println!("Showing onboarding"); - let _ = ShowCapWindow::Onboarding.show(&app).await; + let _ = CapWindow::Onboarding.show(&app).await; } else { println!("Showing main window"); - let _ = ShowCapWindow::Main { + let _ = CapWindow::Main { init_target_mode: None, } .show(&app) @@ -5427,7 +5261,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { if let Some(target_mode) = event.target_mode { open_target_picker(&app, target_mode).await; } else { - let _ = ShowCapWindow::Main { + let _ = CapWindow::Main { init_target_mode: None, } .show(&app) @@ -5436,7 +5270,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { }); RequestOpenSettings::listen_any_spawn(&app, async |payload, app| { - let _ = ShowCapWindow::Settings { + let _ = CapWindow::Settings { page: Some(payload.page), } .show(&app) @@ -5815,34 +5649,19 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { #[cfg(not(windows))] let moved_display = display_for_position(logical_pos.x, logical_pos.y); - match window_id { - CapWindowId::Main => { - let display_id = - moved_display.as_ref().map(|display| display.id()); - window_position_persistence::queue_main_position( - app, - general_settings::WindowPosition { - x: logical_pos.x, - y: logical_pos.y, - display_id, - }, - ); - } - CapWindowId::Camera => { - if app - .try_state::() - .is_some_and(|guard| guard.should_ignore()) - { - return; - } - window_position_persistence::queue_camera_position( - app, - logical_pos.x, - logical_pos.y, - moved_display.map(|display| display.id()), - ); + if let CapWindowId::Camera = window_id { + if app + .try_state::() + .is_some_and(|guard| guard.should_ignore()) + { + return; } - _ => {} + window_position_persistence::queue_camera_position( + app, + logical_pos.x, + logical_pos.y, + moved_display.map(|display| display.id()), + ); } } } @@ -5976,7 +5795,7 @@ fn handle_run_event(_handle: &AppHandle, event: tauri::RunEvent) { } else { let handle = _handle.clone(); spawn_on_runtime(async move { - let _ = ShowCapWindow::Main { + let _ = CapWindow::Main { init_target_mode: None, } .show(&handle) @@ -6065,7 +5884,7 @@ where fn show_camera_window_unlocked(app: &AppHandle) { let app = app.clone(); spawn_on_runtime(async move { - let _ = ShowCapWindow::Camera { centered: false }.show(&app).await; + let _ = CapWindow::Camera { centered: false }.show(&app).await; }); } @@ -6150,7 +5969,7 @@ fn restore_camera_window(app: &AppHandle) { return; }; let _operation_guard = operation_lock.lock().await; - let _ = ShowCapWindow::Camera { centered: false }.show(&app).await; + let _ = CapWindow::Camera { centered: false }.show(&app).await; }); } } @@ -6168,7 +5987,7 @@ fn reopen_main_window(app: &AppHandle) { } else { let handle = app.clone(); tokio::spawn(async move { - let _ = ShowCapWindow::Main { + let _ = CapWindow::Main { init_target_mode: None, } .show(&handle) @@ -6621,6 +6440,15 @@ fn format_project_name( ) } +#[tauri::command(async)] +#[specta::specta] +#[instrument(skip(app))] +fn set_appearance(app: AppHandle, appearance: general_settings::Appearance) -> Result<(), String> { + GeneralSettingsStore::update(&app, move |s| s.appearance = appearance)?; + app.set_theme(appearance.into()); + Ok(()) +} + trait EventExt: tauri_specta::Event { fn listen_any_spawn( app: &AppHandle, @@ -6663,7 +6491,7 @@ fn open_importable_from_path(path: &Path, app: AppHandle) -> Result<(), String> tokio::spawn(async move { match import::start_video_import(app.clone(), source_path).await { Ok(project_path) => { - if let Err(err) = (ShowCapWindow::Editor { project_path }).show(&app).await { + if let Err(err) = (CapWindow::Editor { project_path }).show(&app).await { error!("Failed to show imported video editor: {err}"); show_import_error_dialog( &app, @@ -6688,7 +6516,7 @@ fn open_importable_from_path(path: &Path, app: AppHandle) -> Result<(), String> tokio::spawn(async move { match import::start_image_import(app.clone(), source_path).await { Ok(path) => { - if let Err(err) = (ShowCapWindow::ScreenshotEditor { path }).show(&app).await { + if let Err(err) = (CapWindow::ScreenshotEditor { path }).show(&app).await { error!("Failed to show imported image editor: {err}"); show_import_error_dialog( &app, @@ -6724,7 +6552,7 @@ fn open_project_from_path(path: &Path, app: AppHandle) -> Result<(), String> { } let project_path = path.to_path_buf(); - tokio::spawn(async move { ShowCapWindow::Editor { project_path }.show(&app).await }); + tokio::spawn(async move { CapWindow::Editor { project_path }.show(&app).await }); } RecordingMetaInner::Instant(_) => { let mp4_path = path.join("content/output.mp4"); diff --git a/apps/desktop/src-tauri/src/panel_manager.rs b/apps/desktop/src-tauri/src/panel_manager.rs index 68bc7eff07d..ce5ea39e157 100644 --- a/apps/desktop/src-tauri/src/panel_manager.rs +++ b/apps/desktop/src-tauri/src/panel_manager.rs @@ -1,11 +1,20 @@ -#[cfg(target_os = "macos")] +// objc2 traits are brought in by the `tauri_panel!` macro below; avoid +// redundant imports that cause duplicate symbol errors. use std::collections::HashMap; -#[cfg(target_os = "macos")] +use tauri::{Manager, Wry}; +use tauri_nspanel::tauri_panel; use tokio::sync::RwLock; -#[cfg(target_os = "macos")] use tracing::{debug, info, trace, warn}; -#[cfg(target_os = "macos")] +tauri_panel! { + panel!(DefaultPanel {}) + panel!(TargetSelectOverlayPanel { + config: { + can_become_key_window: true, + } + }) +} + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum PanelState { #[default] @@ -15,13 +24,11 @@ pub enum PanelState { Destroying, } -#[cfg(target_os = "macos")] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum PanelWindowType { Camera, } -#[cfg(target_os = "macos")] impl std::fmt::Display for PanelWindowType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -30,13 +37,11 @@ impl std::fmt::Display for PanelWindowType { } } -#[cfg(target_os = "macos")] struct PanelEntry { state: PanelState, operation_id: u64, } -#[cfg(target_os = "macos")] impl Default for PanelEntry { fn default() -> Self { Self { @@ -46,20 +51,17 @@ impl Default for PanelEntry { } } -#[cfg(target_os = "macos")] pub struct PanelManager { panels: RwLock>, operation_counter: std::sync::atomic::AtomicU64, } -#[cfg(target_os = "macos")] impl Default for PanelManager { fn default() -> Self { Self::new() } } -#[cfg(target_os = "macos")] impl PanelManager { pub fn new() -> Self { Self { @@ -179,20 +181,17 @@ impl PanelManager { } } -#[cfg(target_os = "macos")] pub struct PanelOperationGuard { pub operation_id: u64, completed: bool, } -#[cfg(target_os = "macos")] impl PanelOperationGuard { pub fn mark_completed(&mut self) { self.completed = true; } } -#[cfg(target_os = "macos")] pub fn is_window_handle_valid(window: &tauri::WebviewWindow) -> bool { match window.inner_size() { Ok(_) => true, @@ -203,14 +202,9 @@ pub fn is_window_handle_valid(window: &tauri::WebviewWindow) -> bool { } } -#[cfg(target_os = "macos")] -pub type NSPanel = tauri_nspanel::objc_id::Id< - tauri_nspanel::raw_nspanel::RawNSPanel, - tauri_nspanel::objc_id::Shared, ->; - -#[cfg(target_os = "macos")] -pub fn try_to_panel(window: &tauri::WebviewWindow) -> Result { +pub fn try_to_panel( + window: &tauri::WebviewWindow, +) -> Result, PanelConversionError> { use tauri_nspanel::WebviewWindowExt as NSPanelWebviewWindowExt; if !is_window_handle_valid(window) { @@ -218,18 +212,16 @@ pub fn try_to_panel(window: &tauri::WebviewWindow) -> Result() .map_err(|e| PanelConversionError::ConversionFailed(format!("{e:?}"))) } -#[cfg(target_os = "macos")] #[derive(Debug)] pub enum PanelConversionError { InvalidHandle, ConversionFailed(String), } -#[cfg(target_os = "macos")] impl std::fmt::Display for PanelConversionError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -239,5 +231,4 @@ impl std::fmt::Display for PanelConversionError { } } -#[cfg(target_os = "macos")] impl std::error::Error for PanelConversionError {} diff --git a/apps/desktop/src-tauri/src/permissions.rs b/apps/desktop/src-tauri/src/permissions.rs index d8475d42b9e..3c7eea86612 100644 --- a/apps/desktop/src-tauri/src/permissions.rs +++ b/apps/desktop/src-tauri/src/permissions.rs @@ -39,14 +39,6 @@ impl Drop for MacosPanelWindowActivationGuard { } } -#[cfg(target_os = "macos")] -#[link(name = "ApplicationServices", kind = "framework")] -unsafe extern "C" { - fn AXIsProcessTrusted() -> bool; - fn AXIsProcessTrustedWithOptions(options: core_foundation::dictionary::CFDictionaryRef) - -> bool; -} - #[cfg(target_os = "macos")] fn macos_prompt_screen_recording_access() { scap_screencapturekit::request_permission(); @@ -54,18 +46,16 @@ fn macos_prompt_screen_recording_access() { #[cfg(target_os = "macos")] fn macos_prompt_accessibility_access() { - use core_foundation::base::TCFType; - use core_foundation::dictionary::CFDictionary; - use core_foundation::string::CFString; + use objc2_core_foundation::{CFBoolean, CFDictionary, CFString}; - let prompt_key = CFString::new("AXTrustedCheckOptionPrompt"); - let prompt_value = core_foundation::boolean::CFBoolean::true_value(); - - let options = - CFDictionary::from_CFType_pairs(&[(prompt_key.as_CFType(), prompt_value.as_CFType())]); + let options = CFDictionary::from_slices( + &[&*CFString::from_static_str("AXTrustedCheckOptionPrompt")], + &[CFBoolean::new(true)], + ); + // SAFETY: The AXIsProcessTrustedWithOptions function is safe to call with a valid CFDictionaryRef. unsafe { - AXIsProcessTrustedWithOptions(options.as_concrete_TypeRef()); + objc2_application_services::AXIsProcessTrustedWithOptions(Some(options.as_opaque())); } } @@ -137,13 +127,10 @@ fn macos_activate_permission_request(app: &tauri::AppHandle) { macos_focus_permission_window(app); - if let Some(current_app) = unsafe { + if let Some(current_app) = NSRunningApplication::runningApplicationWithProcessIdentifier(std::process::id() as _) - } { - unsafe { - current_app - .activateWithOptions(NSApplicationActivationOptions::ActivateIgnoringOtherApps); - } + { + current_app.activateWithOptions(NSApplicationActivationOptions::ActivateIgnoringOtherApps); } } @@ -272,7 +259,7 @@ fn macos_permission_status(permission: &OSPermission, initial_check: bool) -> OS } } OSPermission::Accessibility => { - if unsafe { AXIsProcessTrusted() } { + if unsafe { objc2_application_services::AXIsProcessTrusted() } { OSPermissionStatus::Granted } else if initial_check { OSPermissionStatus::Empty diff --git a/apps/desktop/src-tauri/src/platform/macos/delegates.rs b/apps/desktop/src-tauri/src/platform/macos/delegates.rs deleted file mode 100644 index 5b6536b7f73..00000000000 --- a/apps/desktop/src-tauri/src/platform/macos/delegates.rs +++ /dev/null @@ -1,510 +0,0 @@ -// TODO(Ilya): Re-write all macos code to use `objc2` crates n -/// -/// Credit to @haasal, @charrondev, Hoppscotch app, Electron, Zed Editor -/// -/// https://github.com/haasal -/// https://gist.github.com/charrondev -/// https://github.com/hoppscotch/hoppscotch -/// https://github.com/clearlysid/tauri-plugin-decorum/ -/// (Issue) https://github.com/tauri-apps/tauri/issues/4789 -/// (Gist) https://gist.github.com/charrondev/43150e940bd2771b1ea88256d491c7a9 -/// (Hoppscotch) https://github.com/hoppscotch/hoppscotch/blob/286fcd2bb08a84f027b10308d1e18da368f95ebf/packages/hoppscotch-selfhost-desktop/src-tauri/src/mac/window.rs -/// (Electron) https://github.com/electron/electron/blob/38512efd25a159ddc64a54c22ef9eb6dd60064ec/shell/browser/native_window_mac.mm#L1454 -/// -use objc::{class, msg_send, sel, sel_impl}; -use tauri::{Emitter, LogicalPosition, Runtime, Window}; - -pub struct UnsafeWindowHandle(pub *mut std::ffi::c_void); -unsafe impl Send for UnsafeWindowHandle {} -unsafe impl Sync for UnsafeWindowHandle {} - -#[derive(Debug)] -struct WindowState { - window: Window, - controls_inset: LogicalPosition, -} - -// TODO: Respect RTL display language -// TODO: Update Height, consider supporting the scenario where the buttons are hidden by the system due to screen sharing of the window -// https://developer.apple.com/documentation/appkit/nsapplication/1428556-userinterfacelayoutdirection?language=objc -pub fn position_window_controls( - ns_window_handle: UnsafeWindowHandle, - inset: &LogicalPosition, -) { - use cocoa::{ - appkit::{NSView, NSWindow, NSWindowButton, NSWindowStyleMask}, - base::id, - foundation::NSRect, - }; - - let ns_window = ns_window_handle.0 as id; - unsafe { - // In native fullscreen the standard window buttons are hosted in a - // system-managed titlebar overlay; resizing their superviews there - // makes AppKit throw an NSException, which aborts the process once it - // unwinds into Rust. The system positions them itself in fullscreen. - if ns_window - .styleMask() - .contains(NSWindowStyleMask::NSFullScreenWindowMask) - { - return; - } - - let close = ns_window.standardWindowButton_(NSWindowButton::NSWindowCloseButton); - let minimize = ns_window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton); - let zoom = ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton); - if close.is_null() || minimize.is_null() || zoom.is_null() { - return; - } - - let title_bar_container_view = close.superview().superview(); - if title_bar_container_view.is_null() { - return; - } - - let close_rect: NSRect = msg_send![close, frame]; - let button_height = close_rect.size.height; - - let title_bar_frame_height = button_height + inset.y; - let mut title_bar_rect = NSView::frame(title_bar_container_view); - title_bar_rect.size.height = title_bar_frame_height; - title_bar_rect.origin.y = NSView::frame(ns_window).size.height - title_bar_frame_height; - let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect]; - - let window_buttons = vec![close, minimize, zoom]; - let space_between = NSView::frame(minimize).origin.x - NSView::frame(close).origin.x; - let vertical_offset = 4.0; // Adjust this value to push buttons down - - for (i, button) in window_buttons.into_iter().enumerate() { - let mut rect: NSRect = NSView::frame(button); - rect.origin.x = inset.x + (i as f64 * space_between); - rect.origin.y = ((title_bar_frame_height - button_height) / 2.0) - vertical_offset; - button.setFrameOrigin(rect.origin); - } - } -} - -pub fn setup(window: Window, controls_inset: LogicalPosition) { - use cocoa::appkit::NSWindow; - use cocoa::base::{BOOL, id}; - use cocoa::foundation::NSUInteger; - use objc::declare::ClassDecl; - use objc::runtime::{Class, Object, Sel}; - use std::ffi::c_void; - - let Ok(ns_win) = window.ns_window() else { - tracing::warn!("Failed to get window handle for delegate setup"); - return; - }; - - // Do the initial positioning - position_window_controls(UnsafeWindowHandle(ns_win), &controls_inset); - - // Ensure they stay in place while resizing the window. - fn with_window_state) -> T, T>( - this: &Object, - func: F, - ) { - let x: *mut c_void = unsafe { *this.get_ivar("app_box") }; - // `app_box` is nulled out in `windowWillClose:`; ignore any late - // callbacks that arrive after that instead of dereferencing null. - if x.is_null() { - return; - } - let ptr = unsafe { &mut *(x as *mut WindowState) }; - func(ptr); - } - - fn suppress_delegate_panic(selector: &'static str, fallback: T, operation: F) -> T - where - F: FnOnce() -> T, - { - match std::panic::catch_unwind(std::panic::AssertUnwindSafe(operation)) { - Ok(value) => value, - Err(_) => { - tracing::error!(selector, "Suppressed panic in macOS window delegate"); - fallback - } - } - } - - unsafe { - let ns_win_id = ns_win as id; - let current_delegate: id = ns_win_id.delegate(); - - extern "C" fn on_window_should_close(this: &Object, _cmd: Sel, sender: id) -> BOOL { - suppress_delegate_panic("windowShouldClose:", cocoa::base::NO, || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - msg_send![super_del, windowShouldClose: sender] - }) - } - extern "C" fn on_window_will_close(this: &Object, _cmd: Sel, notification: id) { - let super_del: id = unsafe { *this.get_ivar("super_delegate") }; - - // Forward to the previous delegate first, but don't let a panic there - // skip the cleanup below (which would bring back the leak). - suppress_delegate_panic("windowWillClose:", (), || unsafe { - let _: () = msg_send![super_del, windowWillClose: notification]; - }); - - suppress_delegate_panic("windowWillClose:cleanup", (), || unsafe { - // Drop the boxed `WindowState` (and the `Window` handle it holds) - // that was leaked via `Box::into_raw` when this delegate was created. - let app_box: *mut c_void = *this.get_ivar("app_box"); - if !app_box.is_null() { - let this_mut = this as *const Object as *mut Object; - (*this_mut).set_ivar("app_box", std::ptr::null_mut::()); - drop(Box::from_raw(app_box as *mut WindowState)); - } - - // Restore the previous delegate before releasing this one, so any - // further delegate callbacks during teardown don't hit a freed object. - let window: id = *this.get_ivar("window"); - let _: () = msg_send![window, setDelegate: super_del]; - - // NSWindow does not retain its delegate, so the reference taken when - // this delegate was created (`new`) is the only owning one. Release - // it now that the window is closing. - let this_id = this as *const Object as id; - let _: () = msg_send![this_id, release]; - }); - } - extern "C" fn on_window_did_resize(this: &Object, _cmd: Sel, notification: id) { - suppress_delegate_panic("windowDidResize:", (), || unsafe { - with_window_state(this, |state: &mut WindowState| { - if let Ok(window_handle) = state.window.ns_window() { - position_window_controls( - UnsafeWindowHandle(window_handle), - &state.controls_inset, - ); - } else { - tracing::warn!("Failed to get handle to NSWindow during resize"); - } - }); - - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidResize: notification]; - }); - } - extern "C" fn on_window_did_move(this: &Object, _cmd: Sel, notification: id) { - suppress_delegate_panic("windowDidMove:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidMove: notification]; - }); - } - extern "C" fn on_window_did_change_backing_properties( - this: &Object, - _cmd: Sel, - notification: id, - ) { - suppress_delegate_panic("windowDidChangeBackingProperties:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidChangeBackingProperties: notification]; - }); - } - extern "C" fn on_window_did_become_key(this: &Object, _cmd: Sel, notification: id) { - suppress_delegate_panic("windowDidBecomeKey:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidBecomeKey: notification]; - }); - } - extern "C" fn on_window_did_resign_key(this: &Object, _cmd: Sel, notification: id) { - suppress_delegate_panic("windowDidResignKey:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidResignKey: notification]; - }); - } - extern "C" fn on_dragging_entered(this: &Object, _cmd: Sel, notification: id) -> BOOL { - suppress_delegate_panic("draggingEntered:", cocoa::base::NO, || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - msg_send![super_del, draggingEntered: notification] - }) - } - extern "C" fn on_prepare_for_drag_operation( - this: &Object, - _cmd: Sel, - notification: id, - ) -> BOOL { - suppress_delegate_panic("prepareForDragOperation:", cocoa::base::NO, || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - msg_send![super_del, prepareForDragOperation: notification] - }) - } - extern "C" fn on_perform_drag_operation(this: &Object, _cmd: Sel, sender: id) -> BOOL { - suppress_delegate_panic("performDragOperation:", cocoa::base::NO, || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - msg_send![super_del, performDragOperation: sender] - }) - } - extern "C" fn on_conclude_drag_operation(this: &Object, _cmd: Sel, notification: id) { - suppress_delegate_panic("concludeDragOperation:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, concludeDragOperation: notification]; - }); - } - extern "C" fn on_dragging_exited(this: &Object, _cmd: Sel, notification: id) { - suppress_delegate_panic("draggingExited:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, draggingExited: notification]; - }); - } - extern "C" fn on_window_will_use_full_screen_presentation_options( - this: &Object, - _cmd: Sel, - window: id, - proposed_options: NSUInteger, - ) -> NSUInteger { - suppress_delegate_panic( - "window:willUseFullScreenPresentationOptions:", - proposed_options, - || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - msg_send![super_del, window: window willUseFullScreenPresentationOptions: proposed_options] - }, - ) - } - extern "C" fn on_window_did_enter_full_screen( - this: &Object, - _cmd: Sel, - notification: id, - ) { - suppress_delegate_panic("windowDidEnterFullScreen:", (), || unsafe { - with_window_state(this, |state: &mut WindowState| { - if let Err(err) = state.window.emit("did-enter-fullscreen", ()) { - tracing::warn!("Failed to emit did-enter-fullscreen: {err}"); - } - }); - - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidEnterFullScreen: notification]; - }); - } - extern "C" fn on_window_will_enter_full_screen( - this: &Object, - _cmd: Sel, - notification: id, - ) { - suppress_delegate_panic("windowWillEnterFullScreen:", (), || unsafe { - with_window_state(this, |state: &mut WindowState| { - if let Err(err) = state.window.emit("will-enter-fullscreen", ()) { - tracing::warn!("Failed to emit will-enter-fullscreen: {err}"); - } - }); - - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowWillEnterFullScreen: notification]; - }); - } - extern "C" fn on_window_did_exit_full_screen( - this: &Object, - _cmd: Sel, - notification: id, - ) { - suppress_delegate_panic("windowDidExitFullScreen:", (), || unsafe { - with_window_state(this, |state: &mut WindowState| { - if let Err(err) = state.window.emit("did-exit-fullscreen", ()) { - tracing::warn!("Failed to emit did-exit-fullscreen: {err}"); - } - - if let Ok(window_handle) = state.window.ns_window() { - position_window_controls( - UnsafeWindowHandle(window_handle), - &state.controls_inset, - ); - } else { - tracing::warn!("Failed to get handle to NSWindow after exiting fullscreen"); - } - }); - - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidExitFullScreen: notification]; - }); - } - extern "C" fn on_window_will_exit_full_screen( - this: &Object, - _cmd: Sel, - notification: id, - ) { - suppress_delegate_panic("windowWillExitFullScreen:", (), || unsafe { - with_window_state(this, |state: &mut WindowState| { - if let Err(err) = state.window.emit("will-exit-fullscreen", ()) { - tracing::warn!("Failed to emit will-exit-fullscreen: {err}"); - } - }); - - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowWillExitFullScreen: notification]; - }); - } - extern "C" fn on_window_did_fail_to_enter_full_screen( - this: &Object, - _cmd: Sel, - window: id, - ) { - suppress_delegate_panic("windowDidFailToEnterFullScreen:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, windowDidFailToEnterFullScreen: window]; - }); - } - extern "C" fn on_effective_appearance_did_change( - this: &Object, - _cmd: Sel, - notification: id, - ) { - suppress_delegate_panic("effectiveAppearanceDidChange:", (), || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![super_del, effectiveAppearanceDidChange: notification]; - }); - } - extern "C" fn on_effective_appearance_did_changed_on_main_thread( - this: &Object, - _cmd: Sel, - notification: id, - ) { - suppress_delegate_panic( - "effectiveAppearanceDidChangedOnMainThread:", - (), - || unsafe { - let super_del: id = *this.get_ivar("super_delegate"); - let _: () = msg_send![ - super_del, - effectiveAppearanceDidChangedOnMainThread: notification - ]; - }, - ); - } - - // Register the delegate class once and reuse it for every window. Previously a brand - // new class was registered (with a randomized name) on every call to `setup`, which - // permanently leaked Objective-C class metadata for the lifetime of the process. - // - // NOTE: `static CLASS` below is a single process-wide instance shared across every - // monomorphization of this function, not one per `R`. `setup` is only ever called - // with `R = tauri::Wry` in this app, so this is fine in practice; if it were ever - // called with a different `R`, the first call's `on_*::` method pointers would - // be baked into the shared class for all `R`. - fn get_or_register_delegate_class() -> &'static Class { - static CLASS: std::sync::OnceLock<&'static Class> = std::sync::OnceLock::new(); - CLASS.get_or_init(|| { - if let Some(existing) = Class::get("CapWindowDelegate") { - return existing; - } - - let mut decl = match ClassDecl::new("CapWindowDelegate", class!(NSObject)) { - Some(decl) => decl, - None => { - return Class::get("CapWindowDelegate") - .expect("CapWindowDelegate should exist if already registered"); - } - }; - - decl.add_ivar::("window"); - decl.add_ivar::<*mut c_void>("app_box"); - decl.add_ivar::("toolbar"); - decl.add_ivar::("super_delegate"); - - unsafe { - decl.add_method( - sel!(windowShouldClose:), - on_window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_method( - sel!(windowWillClose:), - on_window_will_close:: as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidResize:), - on_window_did_resize:: as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidMove:), - on_window_did_move as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidChangeBackingProperties:), - on_window_did_change_backing_properties as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidBecomeKey:), - on_window_did_become_key as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidResignKey:), - on_window_did_resign_key as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(draggingEntered:), - on_dragging_entered as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_method( - sel!(prepareForDragOperation:), - on_prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_method( - sel!(performDragOperation:), - on_perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - decl.add_method( - sel!(concludeDragOperation:), - on_conclude_drag_operation as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(draggingExited:), - on_dragging_exited as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(window:willUseFullScreenPresentationOptions:), - on_window_will_use_full_screen_presentation_options - as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger, - ); - decl.add_method( - sel!(windowDidEnterFullScreen:), - on_window_did_enter_full_screen:: as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowWillEnterFullScreen:), - on_window_will_enter_full_screen:: as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidExitFullScreen:), - on_window_did_exit_full_screen:: as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowWillExitFullScreen:), - on_window_will_exit_full_screen:: as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(windowDidFailToEnterFullScreen:), - on_window_did_fail_to_enter_full_screen as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(effectiveAppearanceDidChange:), - on_effective_appearance_did_change as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(effectiveAppearanceDidChangedOnMainThread:), - on_effective_appearance_did_changed_on_main_thread - as extern "C" fn(&Object, Sel, id), - ); - } - - decl.register() - }) - } - - let app_state = WindowState { - window, - controls_inset, - }; - let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void; - - let delegate_class = get_or_register_delegate_class::(); - let delegate: id = msg_send![delegate_class, new]; - (*delegate).set_ivar("window", ns_win_id); - (*delegate).set_ivar("app_box", app_box); - (*delegate).set_ivar("toolbar", cocoa::base::nil); - (*delegate).set_ivar("super_delegate", current_delegate); - - ns_win_id.setDelegate_(delegate) - } -} diff --git a/apps/desktop/src-tauri/src/platform/macos/menu.rs b/apps/desktop/src-tauri/src/platform/macos/menu.rs new file mode 100644 index 00000000000..7f610f5445e --- /dev/null +++ b/apps/desktop/src-tauri/src/platform/macos/menu.rs @@ -0,0 +1,251 @@ +#![allow(non_snake_case)] + +use objc2::runtime::{AnyObject, Sel}; +use objc2::{MainThreadMarker, sel}; +use objc2_app_kit::NSApplication; +use tauri::AppHandle; +use tauri::menu::{ + HELP_SUBMENU_ID, Menu, MenuItem, PredefinedMenuItem, Submenu, WINDOW_SUBMENU_ID, +}; +use tauri_plugin_opener::OpenerExt; + +use crate::spawn_on_runtime; +use crate::windows::CapWindow; + +const APP_MENU_QUIT_ID: &str = "app_quit"; +static MACOS_NATIVE_TERMINATE_APP: std::sync::OnceLock = std::sync::OnceLock::new(); + +pub fn init(app: &AppHandle) -> tauri::Result<()> { + let _ = MACOS_NATIVE_TERMINATE_APP.set(app.clone()); + + app.run_on_main_thread(|| { + let mtm = MainThreadMarker::new().expect("Running on main"); + let nsapp = NSApplication::sharedApplication(mtm); + + unsafe { setup_terminate_handler(&nsapp) }; + patch_native_app_menu(&nsapp); + }) +} + +fn patch_native_app_menu(nsapp: &NSApplication) { + let Some(main_menu) = nsapp.mainMenu() else { + return; + }; + + // First item in the menu bar is always the app menu on macOS + let Some(app_menu_item) = main_menu.itemAtIndex(0) else { + return; + }; + let Some(app_submenu) = app_menu_item.submenu() else { + return; + }; + + // Index 0: About — replace Tauri's internal action with the real one. + // This is what triggers the system About panel, including the app icon + // display introduced in macOS 15. + if let Some(about_item) = app_submenu.itemAtIndex(0) { + unsafe { + about_item.setAction(Some(sel!(orderFrontStandardAboutPanel:))); + // nil target means the action travels up the responder chain to NSApp + about_item.setTarget(None); + } + } + + // Quit (last item) — native terminate: gets the icon on macOS 15+ + // and routes through applicationShouldTerminate: which we already own + let Some(last) = app_submenu.numberOfItems().checked_sub(1) else { + return; + }; + if let Some(item) = app_submenu.itemAtIndex(last) { + unsafe { + item.setAction(Some(sel!(terminate:))); + item.setTarget(None); // nil = travels to NSApp via responder chain + } + } +} + +unsafe fn setup_terminate_handler(nsapp: &NSApplication) { + let Some(delegate) = nsapp.delegate() else { + tracing::warn!("Unable to install macOS native termination handler without app delegate"); + return; + }; + + let delegate_class = (delegate.as_ref() as &objc2::runtime::AnyObject).class() as *const _ + as *mut objc2::runtime::AnyClass; + + unsafe extern "C-unwind" fn applicationShouldTerminate( + _: &objc2::runtime::AnyObject, + _: objc2::runtime::Sel, + _: *mut objc2_app_kit::NSApplication, + ) -> isize { + match MACOS_NATIVE_TERMINATE_APP.get() { + Some(app) => { + tauri::async_runtime::spawn({ + let app = app.clone(); + async move { + crate::request_app_exit(app).await; + } + }); + } + None => { + tracing::warn!("macOS native termination requested before handler was installed"); + } + }; + 0 // NSTerminateCancel — we handle the actual exit ourselves + } + + let added = unsafe { + objc2::ffi::class_addMethod( + delegate_class, + sel!(applicationShouldTerminate:), + std::mem::transmute::< + unsafe extern "C-unwind" fn( + &AnyObject, + Sel, + *mut objc2_app_kit::NSApplication, + ) -> isize, + unsafe extern "C-unwind" fn(), + >(applicationShouldTerminate), + c"q@:@".as_ptr(), // NSInteger + ) + }; + + if added.as_bool() { + tracing::info!("Installed macOS native termination handler"); + } else { + tracing::warn!("macOS native termination handler was already installed"); + } +} + +pub fn build_app_menu(app: &AppHandle) -> tauri::Result> { + let window_menu = Submenu::with_id_and_items( + app, + WINDOW_SUBMENU_ID, + "Window", + true, + &[ + &PredefinedMenuItem::minimize(app, None)?, + &PredefinedMenuItem::maximize(app, None)?, + &PredefinedMenuItem::bring_all_to_front(app, None)?, + &PredefinedMenuItem::separator(app)?, + ], + )?; + + let help_menu = Submenu::with_id_and_items( + app, + HELP_SUBMENU_ID, + "Help", + true, + &[ + &MenuItem::with_id(app, "help.changelog", "Changelog", true, None::<&str>)?, + &PredefinedMenuItem::separator(app)?, + &MenuItem::with_id(app, "help.dashboard", "Dashboard", true, None::<&str>)?, + &MenuItem::with_id(app, "help.docs", "Documentation", true, None::<&str>)?, + &MenuItem::with_id(app, "help.faq", "FAQ", true, None::<&str>)?, + &MenuItem::with_id(app, "help.self_hosting", "Self-hosting", true, None::<&str>)?, + &MenuItem::with_id(app, "help.help_center", "Help Center", true, None::<&str>)?, + &MenuItem::with_id(app, "help.status", "System Status", true, None::<&str>)?, + &PredefinedMenuItem::separator(app)?, + &MenuItem::with_id( + app, + "help.discord", + "Join Discord Community", + true, + None::<&str>, + )?, + ], + )?; + + Menu::with_items( + app, + &[ + &Submenu::with_items( + app, + app.package_info().name.clone(), + true, + &[ + &PredefinedMenuItem::about(app, None, None)?, + &PredefinedMenuItem::separator(app)?, + // Using this exact name and acceletator combination on macOS 15+ gets the item to have an auto translatable name and an icon + &MenuItem::with_id(app, "settings", "Preferences…", true, Some("Cmd+,"))?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::services(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::hide(app, None)?, + &PredefinedMenuItem::hide_others(app, None)?, + &PredefinedMenuItem::show_all(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::quit(app, None)?, + ], + )?, + &Submenu::with_items( + app, + "File", + true, + &[&PredefinedMenuItem::close_window(app, None)?], + )?, + &Submenu::with_items( + app, + "Edit", + true, + &[ + &PredefinedMenuItem::undo(app, None)?, + &PredefinedMenuItem::redo(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::cut(app, None)?, + &PredefinedMenuItem::copy(app, None)?, + &PredefinedMenuItem::paste(app, None)?, + &PredefinedMenuItem::select_all(app, None)?, + ], + )?, + &Submenu::with_items( + app, + "View", + true, + &[&PredefinedMenuItem::fullscreen(app, None)?], + )?, + &window_menu, + &help_menu, + ], + ) +} + +pub fn on_event(app: &AppHandle, event: tauri::menu::MenuEvent) { + let url = match event.id().as_ref() { + APP_MENU_QUIT_ID => { + let app = app.clone(); + spawn_on_runtime(async move { + crate::request_app_exit(app).await; + }); + return; + } + "settings" => { + let app = app.clone(); + spawn_on_runtime(async move { + let _ = CapWindow::Settings { page: None }.show(&app).await; + }); + return; + } + "help.changelog" => { + let app = app.clone(); + spawn_on_runtime(async move { + let _ = CapWindow::Settings { + page: Some("/changelog".to_string()), + } + .show(&app) + .await; + }); + return; + } + "help.dashboard" => "https://cap.so/dashboard", + "help.docs" => "https://cap.so/docs", + "help.faq" => "https://cap.so/faq", + "help.self_hosting" => "https://cap.so/self-hosting", + "help.help_center" => "https://help.cap.so/", + "help.status" => "https://cap.openstatus.dev/", + "help.discord" => "https://discord.gg/y8gdQ3WRN3", + _ => return, + }; + + let _ = app.opener().open_url(url, None::<&str>); +} diff --git a/apps/desktop/src-tauri/src/platform/macos/mod.rs b/apps/desktop/src-tauri/src/platform/macos/mod.rs index e50f93de056..1c1206ad99a 100644 --- a/apps/desktop/src-tauri/src/platform/macos/mod.rs +++ b/apps/desktop/src-tauri/src/platform/macos/mod.rs @@ -1,58 +1,10 @@ -// use std::ffi::c_void; - -// use cocoa::{ -// base::{id, nil}, -// foundation::NSString, -// }; -// use core_graphics::{ -// base::boolean_t, -// display::{CFDictionaryRef, CGRect}, -// }; -// use objc::{class, msg_send, sel, sel_impl}; - -pub mod delegates; +pub mod menu; mod sc_shareable_content; +use objc2::{MainThreadMarker, msg_send, sel}; +use objc2_app_kit::NSWindow; pub use sc_shareable_content::*; - -pub fn set_window_level(window: tauri::Window, level: objc2_app_kit::NSWindowLevel) { - let c_window = window.clone(); - _ = window.run_on_main_thread(move || unsafe { - let Ok(ns_win) = c_window.ns_window() else { - return; - }; - let ns_win = ns_win as *const objc2_app_kit::NSWindow; - (*ns_win).setLevel(level); - }); -} - -pub fn apply_squircle_corners(window: &tauri::WebviewWindow, radius: f64) { - use cocoa::base::{id, nil}; - use cocoa::foundation::NSString; - use objc::{msg_send, sel, sel_impl}; - - let Ok(ns_win) = window.ns_window() else { - return; - }; - - unsafe { - let ns_win = ns_win as id; - let content_view: id = msg_send![ns_win, contentView]; - - if content_view != nil { - let _: () = msg_send![content_view, setWantsLayer: true]; - - let layer: id = msg_send![content_view, layer]; - if layer != nil { - let _: () = msg_send![layer, setCornerRadius: radius]; - let _: () = msg_send![layer, setMasksToBounds: true]; - - let continuous = NSString::alloc(nil).init_str("continuous"); - let _: () = msg_send![layer, setCornerCurve: continuous]; - } - } - } -} +use tauri::WebviewWindow; const TAURI_VIBRANCY_VIEW_TAG: isize = 91376254; const LIQUID_GLASS_IDENTIFIER: &str = "so.cap.liquid-glass-background"; @@ -493,28 +445,30 @@ pub async fn teardown_all_liquid_glass(app: &tauri::AppHandle) -> Result<(), Str Ok(()) } -// pub fn get_ns_window_number(ns_window: *mut c_void) -> isize { -// let ns_window = ns_window as *const objc2_app_kit::NSWindow; - -// unsafe { (*ns_window).windowNumber() } -// } - -// #[link(name = "CoreGraphics", kind = "framework")] -// unsafe extern "C" { -// pub fn CGRectMakeWithDictionaryRepresentation( -// dict: CFDictionaryRef, -// rect: *mut CGRect, -// ) -> boolean_t; -// } - -// /// Makes the background of the WKWebView layer transparent. -// /// This differs from Tauri's implementation as it does not change the window background which causes performance performance issues and artifacts when shadows are enabled on the window. -// /// Use Tauri's implementation to make the window itself transparent. -// pub fn make_webview_transparent(target: &tauri::WebviewWindow) -> tauri::Result<()> { -// target.with_webview(|webview| unsafe { -// let wkwebview = webview.inner() as id; -// let no: id = msg_send![class!(NSNumber), numberWithBool:0]; -// // [https://developer.apple.com/documentation/webkit/webview/1408486-drawsbackground] -// let _: id = msg_send![wkwebview, setValue:no forKey: NSString::alloc(nil).init_str("drawsBackground")]; -// }) -// } +pub trait WebviewWindowExt { + fn with_nswindow_on_main( + &self, + f: F, + ) -> tauri::Result<()>; +} + +impl WebviewWindowExt for WebviewWindow { + fn with_nswindow_on_main( + &self, + f: F, + ) -> tauri::Result<()> { + self.run_on_main_thread({ + let webview = self.clone(); + move || { + let Ok(ns_window) = webview.ns_window() else { + tracing::error!("NSWindow not ready"); + return; + }; + // SAFETY: Tauri runs this on the main thread + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + let nswindow = unsafe { &*ns_window.cast::() }; + f(mtm, nswindow); + } + }) + } +} diff --git a/apps/desktop/src-tauri/src/platform/mod.rs b/apps/desktop/src-tauri/src/platform/mod.rs index e51d3e709a3..48324c5b969 100644 --- a/apps/desktop/src-tauri/src/platform/mod.rs +++ b/apps/desktop/src-tauri/src/platform/mod.rs @@ -38,7 +38,7 @@ pub fn perform_haptic_feedback( _time: Option, ) -> Result<(), String> { #[cfg(target_os = "macos")] - unsafe { + { use objc2_app_kit::{ NSHapticFeedbackManager, NSHapticFeedbackPattern, NSHapticFeedbackPerformanceTime, NSHapticFeedbackPerformer, diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index f71312bd1eb..43e1ab52380 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -72,7 +72,7 @@ use crate::{ upload::{InstantMultipartUpload, SegmentUploader, compress_image}, web_api::ManagerExt, windows::{ - CapWindowId, EditorRecordingTarget, ShowCapWindow, editor_window_for_path, hide_overlay, + CapWindow, CapWindowId, EditorRecordingTarget, editor_window_for_path, hide_overlay, }, }; @@ -1527,7 +1527,7 @@ pub async fn start_recording( if is_camera_only { let operation_lock = app.state::(); let _operation_guard = operation_lock.lock().await; - if let Err(err) = (ShowCapWindow::Camera { centered: true }).show(&app).await { + if let Err(err) = (CapWindow::Camera { centered: true }).show(&app).await { let error = format!("Failed to show centered camera window: {err}"); state_mtx.write().await.clear_pending_recording(); notify_recording_start_failed(&app, &error); @@ -1734,13 +1734,13 @@ pub async fn start_recording( if let Some(show) = inputs .capture_target .display() - .map(|d| ShowCapWindow::WindowCaptureOccluder { screen_id: d.id() }) + .map(|d| CapWindow::WindowCaptureOccluder { screen_id: d.id() }) { let _ = show.show(&app).await; } } ScreenCaptureTarget::Area { screen, .. } => { - let _ = ShowCapWindow::WindowCaptureOccluder { + let _ = CapWindow::WindowCaptureOccluder { screen_id: screen.clone(), } .show(&app) @@ -1751,7 +1751,7 @@ pub async fn start_recording( let countdown = general_settings.and_then(|v| v.recording_countdown); crate::target_select_overlay::close_target_select_overlay_windows(&app); - let _ = ShowCapWindow::InProgressRecording { + let _ = CapWindow::InProgressRecording { countdown, capture_target: Some(inputs.capture_target.clone()), } @@ -2767,7 +2767,7 @@ pub async fn delete_recording(app: AppHandle, state: MutableState<'_, App>) -> R match settings.post_deletion_behaviour { PostDeletionBehaviour::DoNothing => {} PostDeletionBehaviour::ReopenRecordingWindow => { - let _ = ShowCapWindow::Main { + let _ = CapWindow::Main { init_target_mode: None, } .show(&app) @@ -2815,7 +2815,6 @@ pub async fn take_screenshot( id, CapWindowId::TargetSelectOverlay { .. } | CapWindowId::WindowCaptureOccluder { .. } - | CapWindowId::CaptureArea | CapWindowId::ModeSelect | CapWindowId::RecordingsOverlay ) @@ -3229,7 +3228,7 @@ async fn apply_post_studio_editor_behaviour( default, ) { Some(PostStudioRecordingBehaviour::OpenEditor) => { - let _ = ShowCapWindow::Editor { + let _ = CapWindow::Editor { project_path: recording_dir, } .show(app) @@ -3238,7 +3237,7 @@ async fn apply_post_studio_editor_behaviour( true } Some(PostStudioRecordingBehaviour::ShowOverlay) => { - let _ = ShowCapWindow::RecordingsOverlay.show(app).await; + let _ = CapWindow::RecordingsOverlay.show(app).await; let app = AppHandle::clone(app); tokio::spawn(async move { diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs index f48963b398e..e765779c803 100644 --- a/apps/desktop/src-tauri/src/target_select_overlay.rs +++ b/apps/desktop/src-tauri/src/target_select_overlay.rs @@ -13,7 +13,7 @@ use crate::{ App, ArcLock, general_settings, recording_settings::RecordingTargetMode, window_exclusion::WindowExclusion, - windows::{CapWindowId, ShowCapWindow, hide_overlay, show_overlay}, + windows::{CapWindow, CapWindowId, hide_overlay, show_overlay}, }; use scap_targets::{ Display, DisplayId, Window, WindowId, @@ -120,7 +120,7 @@ pub async fn open_target_select_overlays( state.spawn(display_id, window.clone()); } else if start.elapsed() < Duration::from_secs(1) { - if let Ok(window) = (ShowCapWindow::TargetSelectOverlay { + if let Ok(window) = (CapWindow::TargetSelectOverlay { display_id: display_id.clone(), target_mode, }) @@ -133,7 +133,7 @@ pub async fn open_target_select_overlays( let app_clone = app.clone(); let display_id_clone = display_id.clone(); tokio::spawn(async move { - if let Ok(window) = (ShowCapWindow::TargetSelectOverlay { + if let Ok(window) = (CapWindow::TargetSelectOverlay { display_id: display_id_clone, target_mode, }) @@ -389,12 +389,8 @@ pub async fn focus_window(window_id: WindowId) -> Result<(), String> { .owner_pid() .ok_or("Could not get window owner PID")?; - if let Some(app) = - unsafe { NSRunningApplication::runningApplicationWithProcessIdentifier(pid) } - { - unsafe { - app.activateWithOptions(NSApplicationActivationOptions::ActivateIgnoringOtherApps); - } + if let Some(app) = NSRunningApplication::runningApplicationWithProcessIdentifier(pid) { + app.activateWithOptions(NSApplicationActivationOptions::ActivateIgnoringOtherApps); } } diff --git a/apps/desktop/src-tauri/src/tray.rs b/apps/desktop/src-tauri/src/tray.rs index 4d0d8f7e3e7..7a71dbbef6d 100644 --- a/apps/desktop/src-tauri/src/tray.rs +++ b/apps/desktop/src-tauri/src/tray.rs @@ -2,7 +2,7 @@ use crate::{ NewScreenshotAdded, NewStudioRecordingAdded, RecordingStarted, RecordingStopped, RequestOpenSettings, recording, recording_settings::{RecordingSettingsStore, RecordingTargetMode}, - windows::ShowCapWindow, + windows::CapWindow, }; use cap_recording::RecordingMode; @@ -608,7 +608,7 @@ fn handle_previous_item_click(app: &AppHandle, path_str: &str) { let app = app.clone(); let screenshot_path = path; tokio::spawn(async move { - let _ = ShowCapWindow::ScreenshotEditor { + let _ = CapWindow::ScreenshotEditor { path: screenshot_path, } .show(&app) @@ -630,7 +630,7 @@ fn handle_previous_item_click(app: &AppHandle, path_str: &str) { let app = app.clone(); let project_path = path.clone(); tokio::spawn(async move { - let _ = ShowCapWindow::Editor { project_path }.show(&app).await; + let _ = CapWindow::Editor { project_path }.show(&app).await; }); } RecordingMetaInner::Instant(_) => { @@ -788,7 +788,7 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { Ok(TrayItem::OpenCap) => { let app = app.clone(); tokio::spawn(async move { - let _ = ShowCapWindow::Main { + let _ = CapWindow::Main { init_target_mode: None, } .show(&app) @@ -826,8 +826,7 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { match recording::take_screenshot(app.clone(), target.clone()).await { Ok(path) => { if crate::automation::should_open_screenshot_editor(&app, &target) { - let _ = - ShowCapWindow::ScreenshotEditor { path }.show(&app).await; + let _ = CapWindow::ScreenshotEditor { path }.show(&app).await; } } Err(e) => { @@ -871,8 +870,7 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { if crate::import::is_supported_video_import_path(&path) { match crate::import::start_video_import(app.clone(), path).await { Ok(project_path) => { - let _ = - ShowCapWindow::Editor { project_path }.show(&app).await; + let _ = CapWindow::Editor { project_path }.show(&app).await; } Err(e) => { tracing::error!("Failed to import video: {e}"); @@ -886,9 +884,8 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { } else if crate::import::is_supported_image_import_path(&path) { match crate::import::start_image_import(app.clone(), path).await { Ok(path) => { - let _ = ShowCapWindow::ScreenshotEditor { path } - .show(&app) - .await; + let _ = + CapWindow::ScreenshotEditor { path }.show(&app).await; } Err(e) => { tracing::error!("Failed to import image: {e}"); @@ -925,7 +922,7 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { Ok(TrayItem::OpenSettings) => { let app = app.clone(); tokio::spawn( - async move { ShowCapWindow::Settings { page: None }.show(&app).await }, + async move { CapWindow::Settings { page: None }.show(&app).await }, ); } Ok(TrayItem::UploadLogs) => { @@ -966,7 +963,7 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> { Ok(TrayItem::RequestPermissions) => { let app = app.clone(); tokio::spawn(async move { - let _ = ShowCapWindow::Onboarding.show(&app).await; + let _ = CapWindow::Onboarding.show(&app).await; }); } _ => {} diff --git a/apps/desktop/src-tauri/src/window_position_persistence.rs b/apps/desktop/src-tauri/src/window_position_persistence.rs index 2831a1bb5af..301498b335a 100644 --- a/apps/desktop/src-tauri/src/window_position_persistence.rs +++ b/apps/desktop/src-tauri/src/window_position_persistence.rs @@ -4,11 +4,10 @@ use std::time::Duration; use tauri::{AppHandle, Manager}; use tokio::sync::Notify; -use crate::general_settings::{GeneralSettingsStore, WindowPosition}; +use crate::general_settings::GeneralSettingsStore; #[derive(Default)] struct PendingState { - main: Option, camera_position: Option<(f64, f64, Option)>, } @@ -25,14 +24,6 @@ impl WindowPositionPersistence { }) } - pub fn queue_main(&self, position: WindowPosition) { - { - let mut guard = self.pending.lock().unwrap_or_else(|e| e.into_inner()); - guard.main = Some(position); - } - self.notify.notify_one(); - } - pub fn queue_camera(&self, x: f64, y: f64, display_id: Option) { { let mut guard = self.pending.lock().unwrap_or_else(|e| e.into_inner()); @@ -85,16 +76,13 @@ pub fn install(app: &AppHandle) { }; let pending = persistence.take_pending(); - if pending.main.is_none() && pending.camera_position.is_none() { + if pending.camera_position.is_none() { continue; } let write_app = app_handle.clone(); let write_result = tokio::task::spawn_blocking(move || { GeneralSettingsStore::update(&write_app, |settings| { - if let Some(main) = pending.main { - settings.main_window_position = Some(main); - } if let Some((x, y, display_id)) = pending.camera_position { crate::update_camera_window_position_settings(settings, x, y, display_id); } @@ -113,12 +101,6 @@ pub fn install(app: &AppHandle) { }); } -pub fn queue_main_position(app: &AppHandle, position: WindowPosition) { - if let Some(persistence) = app.try_state::>() { - persistence.queue_main(position); - } -} - pub fn queue_camera_position(app: &AppHandle, x: f64, y: f64, display_id: Option) { if let Some(persistence) = app.try_state::>() { persistence.queue_camera(x, y, display_id); diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs index f5ba8f69d7a..d76af6baf0f 100644 --- a/apps/desktop/src-tauri/src/windows.rs +++ b/apps/desktop/src-tauri/src/windows.rs @@ -17,22 +17,32 @@ use std::{ time::Duration, }; use tauri::{ - AppHandle, LogicalPosition, LogicalSize, Manager, Monitor, PhysicalPosition, PhysicalSize, - WebviewUrl, WebviewWindow, WebviewWindowBuilder, Wry, + AppHandle, Listener, LogicalPosition, LogicalSize, Manager, Monitor, PhysicalPosition, + PhysicalSize, WebviewUrl, WebviewWindow, WebviewWindowBuilder, Wry, }; use tauri_specta::Event; use tokio::sync::RwLock; use tracing::{debug, error, info, instrument, warn}; #[cfg(target_os = "macos")] -use crate::panel_manager::{PanelManager, PanelState, PanelWindowType, is_window_handle_valid}; +use crate::{ + panel_manager::{ + DefaultPanel, PanelManager, PanelState, PanelWindowType, TargetSelectOverlayPanel, + is_window_handle_valid, + }, + platform::WebviewWindowExt, +}; use crate::{ App, ArcLock, CameraWindowCloseGate, CameraWindowPositionGuard, MainWindowReadyState, - NewNotification, RequestSetTargetMode, camera_preview_error_message, + NewNotification, RequestScreenCapturePrewarm, RequestSetTargetMode, + camera_preview_error_message, display_utils, + display_utils::{ + CursorMonitorInfo, MonitorExt, display_for_saved_position, logical_point_position, + }, editor_window::PendingEditorInstances, emit_camera_preview_clear, emit_camera_preview_error, fake_window, - general_settings::{self, AppTheme, GeneralSettingsStore}, + general_settings::{self, Appearance, GeneralSettingsStore}, permissions, recording::{RecordingEvent, RecordingInputKind}, recording_settings::RecordingTargetMode, @@ -43,10 +53,7 @@ use crate::{ use cap_recording::{feeds, sources::screen_capture::ScreenCaptureTarget}; #[cfg(target_os = "macos")] -const DEFAULT_TRAFFIC_LIGHTS_INSET: LogicalPosition = LogicalPosition::new(12.0, 12.0); - -const DEFAULT_FALLBACK_DISPLAY_WIDTH: f64 = 1920.0; -const DEFAULT_FALLBACK_DISPLAY_HEIGHT: f64 = 1080.0; +const DEFAULT_TRAFFIC_LIGHTS_INSET: LogicalPosition = LogicalPosition::new(12.0, 20.0); #[cfg(windows)] const WINDOWS_WEBVIEW2_BROWSER_ARGS: &str = "--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection --autoplay-policy=no-user-gesture-required --disable-vulkan --use-angle=d3d11"; @@ -62,58 +69,6 @@ fn windows_webview2_browser_args() -> String { args } -#[cfg(target_os = "macos")] -fn is_system_dark_mode() -> bool { - use cocoa::base::{id, nil}; - use cocoa::foundation::NSString; - use objc::{class, msg_send, sel, sel_impl}; - - unsafe { - let app: id = msg_send![class!(NSApplication), sharedApplication]; - let appearance: id = msg_send![app, effectiveAppearance]; - if appearance == nil { - return false; - } - let name: id = msg_send![appearance, name]; - if name == nil { - return false; - } - let dark_appearance = NSString::alloc(nil).init_str("NSAppearanceNameDarkAqua"); - let vibrant_dark = NSString::alloc(nil).init_str("NSAppearanceNameVibrantDark"); - let is_dark: bool = msg_send![name, isEqualToString: dark_appearance]; - let is_vibrant_dark: bool = msg_send![name, isEqualToString: vibrant_dark]; - is_dark || is_vibrant_dark - } -} - -#[cfg(target_os = "windows")] -fn is_system_dark_mode() -> bool { - use winreg::RegKey; - use winreg::enums::HKEY_CURRENT_USER; - - let hkcu = RegKey::predef(HKEY_CURRENT_USER); - if let Ok(key) = - hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize") - && let Ok(value) = key.get_value::("AppsUseLightTheme") - { - return value == 0; - } - false -} - -#[cfg(target_os = "linux")] -fn is_system_dark_mode() -> bool { - let output = std::process::Command::new("gsettings") - .args(["get", "org.gnome.desktop.interface", "color-scheme"]) - .output(); - if let Ok(output) = output - && output.status.success() - { - return String::from_utf8_lossy(&output.stdout).contains("dark"); - } - false -} - pub fn hide_overlay(window: &WebviewWindow) { let _ = window.set_ignore_cursor_events(true); let _ = window.hide(); @@ -537,8 +492,10 @@ pub(crate) async fn cleanup_camera_window( use tauri_nspanel::ManagerExt; for label in panel_labels { if let Ok(panel) = app_for_close.get_webview_panel(&label) { - panel.released_when_closed(false); - panel.close(); + panel.set_released_when_closed(false); + if let Some(window) = panel.to_window() { + let _ = window.close(); + } } } let _ = panel_close_tx.send(()); @@ -570,209 +527,6 @@ pub(crate) async fn cleanup_camera_window( !still_exists } -struct CursorMonitorInfo { - x: f64, - y: f64, - width: f64, - height: f64, - // On Windows each monitor's "logical" rect is its physical rect divided by - // its own scale, so logical rects of mixed-DPI monitors overlap and tao's - // LogicalPosition conversion (which uses whatever monitor the window - // currently occupies) can land a window on the wrong monitor. Positioning - // must go through this monitor's own scale, as a physical position. - #[cfg(windows)] - scale: f64, -} - -impl CursorMonitorInfo { - fn get() -> Self { - Self::from_display(&Display::get_containing_cursor().unwrap_or_else(Display::primary)) - } - - fn from_display(display: &Display) -> Self { - let bounds = display.raw_handle().logical_bounds(); - - #[cfg(windows)] - let scale = bounds - .as_ref() - .map(|b| b.size().width()) - .filter(|width| *width > 0.0) - .and_then(|logical_width| { - display - .physical_size() - .map(|physical| physical.width() / logical_width) - }) - .filter(|scale| scale.is_finite() && *scale > 0.0) - .unwrap_or(1.0); - - let (x, y, width, height) = bounds - .map(|b| { - ( - b.position().x(), - b.position().y(), - b.size().width(), - b.size().height(), - ) - }) - .unwrap_or(( - 0.0, - 0.0, - DEFAULT_FALLBACK_DISPLAY_WIDTH, - DEFAULT_FALLBACK_DISPLAY_HEIGHT, - )); - - Self { - x, - y, - width, - height, - #[cfg(windows)] - scale, - } - } - - /// Converts a global-logical point on this monitor into a `Position` that - /// lands exactly there regardless of which monitor the window currently - /// occupies. Logical on macOS/Linux (a true global space there), physical - /// on Windows. - fn position(&self, x: f64, y: f64) -> tauri::Position { - #[cfg(windows)] - return tauri::Position::Physical(tauri::PhysicalPosition::new( - (x * self.scale).round() as i32, - (y * self.scale).round() as i32, - )); - - #[cfg(not(windows))] - tauri::Position::Logical(tauri::LogicalPosition::new(x, y)) - } - - fn center_position(&self, window_width: f64, window_height: f64) -> (f64, f64) { - let pos_x = self.x + (self.width - window_width) / 2.0; - let pos_y = self.y + (self.height - window_height) / 2.0; - (pos_x, pos_y) - } - - fn bottom_center_position( - &self, - window_width: f64, - window_height: f64, - offset_y: f64, - ) -> (f64, f64) { - let pos_x = self.x + (self.width - window_width) / 2.0; - let pos_y = self.y + self.height - window_height - offset_y; - (pos_x, pos_y) - } - - fn from_window(window: &tauri::WebviewWindow) -> Self { - let Ok(window_pos) = window.outer_position() else { - return Self::get(); - }; - - // outer_position is physical. On Windows, resolve the display in - // physical space (per-monitor logical rects overlap in mixed-DPI - // layouts). On macOS, convert to logical points, a true global space. - // On Linux scap reports logical bounds in unscaled physical units, so - // the raw position compares directly. - #[cfg(windows)] - { - let (pos_x, pos_y) = (window_pos.x as f64, window_pos.y as f64); - for display in Display::list() { - if let Some(bounds) = display.raw_handle().physical_bounds() { - let (x, y, width, height) = ( - bounds.position().x(), - bounds.position().y(), - bounds.size().width(), - bounds.size().height(), - ); - - if pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height { - return Self::from_display(&display); - } - } - } - - Self::get() - } - - #[cfg(target_os = "macos")] - { - let scale = window.scale_factor().unwrap_or(1.0); - let pos = window_pos.to_logical::(scale); - - for display in Display::list() { - if display_contains_logical(&display, pos.x, pos.y) { - return Self::from_display(&display); - } - } - - Self::get() - } - - #[cfg(target_os = "linux")] - { - let (pos_x, pos_y) = (window_pos.x as f64, window_pos.y as f64); - - for display in Display::list() { - if display_contains_logical(&display, pos_x, pos_y) { - return Self::from_display(&display); - } - } - - Self::get() - } - } -} - -fn display_contains_logical(display: &Display, pos_x: f64, pos_y: f64) -> bool { - display - .raw_handle() - .logical_bounds() - .map(|bounds| { - let (x, y, width, height) = ( - bounds.position().x(), - bounds.position().y(), - bounds.size().width(), - bounds.size().height(), - ); - - pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height - }) - .unwrap_or(false) -} - -fn display_containing_logical(pos_x: f64, pos_y: f64) -> Option { - Display::list() - .into_iter() - .find(|display| display_contains_logical(display, pos_x, pos_y)) -} - -/// Resolves the display a persisted window position belongs to, preferring the -/// display it was saved on. On Windows the saved logical coordinates are only -/// meaningful relative to that display (mixed-DPI logical rects overlap), so -/// restores must convert through its scale rather than the window's current one. -fn display_for_saved_position( - pos_x: f64, - pos_y: f64, - display_id: Option<&DisplayId>, -) -> Option { - display_id - .and_then(Display::from_id) - .filter(|display| display_contains_logical(display, pos_x, pos_y)) - .or_else(|| display_containing_logical(pos_x, pos_y)) -} - -/// Converts a global-logical point into a `Position` that lands exactly there, -/// resolving the owning display by containment when the caller doesn't know it. -/// Falls back to a plain logical position when no display contains the point. -pub fn logical_point_position(pos_x: f64, pos_y: f64) -> tauri::Position { - #[cfg(windows)] - if let Some(display) = display_containing_logical(pos_x, pos_y) { - return CursorMonitorInfo::from_display(&display).position(pos_x, pos_y); - } - - tauri::Position::Logical(tauri::LogicalPosition::new(pos_x, pos_y)) -} - fn center_camera_window(app: &AppHandle, window: &WebviewWindow) { let camera_state = match app.try_state::>() { Some(state) => state @@ -809,81 +563,6 @@ fn center_camera_window(app: &AppHandle, window: &WebviewWindow) { } } -fn is_position_on_display(display_id: &DisplayId, pos_x: f64, pos_y: f64) -> bool { - Display::from_id(display_id) - .and_then(|display| display.raw_handle().logical_bounds()) - .map(|bounds| { - let (x, y, width, height) = ( - bounds.position().x(), - bounds.position().y(), - bounds.size().width(), - bounds.size().height(), - ); - - pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height - }) - .unwrap_or(false) -} - -fn display_name_for_position(pos_x: f64, pos_y: f64) -> Option { - Display::list().into_iter().find_map(|display| { - let bounds = display.raw_handle().logical_bounds()?; - let (x, y, width, height) = ( - bounds.position().x(), - bounds.position().y(), - bounds.size().width(), - bounds.size().height(), - ); - - if pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height { - display.name().filter(|name| !name.trim().is_empty()) - } else { - None - } - }) -} - -fn is_position_on_monitor_name(monitor_name: &str, pos_x: f64, pos_y: f64) -> bool { - Display::list().into_iter().any(|display| { - if display.name().as_deref() != Some(monitor_name) { - return false; - } - - display - .raw_handle() - .logical_bounds() - .map(|bounds| { - let (x, y, width, height) = ( - bounds.position().x(), - bounds.position().y(), - bounds.size().width(), - bounds.size().height(), - ); - - pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height - }) - .unwrap_or(false) - }) -} - -fn is_position_on_any_screen(pos_x: f64, pos_y: f64) -> bool { - for display in Display::list() { - if let Some(bounds) = display.raw_handle().logical_bounds() { - let (x, y, width, height) = ( - bounds.position().x(), - bounds.position().y(), - bounds.size().width(), - bounds.size().height(), - ); - - if pos_x >= x && pos_x < x + width && pos_y >= y && pos_y < y + height { - return true; - } - } - } - false -} - // Recovers a window that ended up entirely off every connected display (e.g. the // monitor it was on got disconnected), which otherwise leaves it open but unreachable. fn recenter_window_if_offscreen(window: &WebviewWindow) { @@ -908,19 +587,6 @@ fn recenter_window_if_offscreen(window: &WebviewWindow) { let _ = window.set_position(monitor.position(pos_x, pos_y)); } -fn ensure_settings_window_bounds(window: &WebviewWindow) { - const MIN_W: f64 = 780.0; - const MIN_H: f64 = 560.0; - let _ = window.set_min_size(Some(LogicalSize::new(MIN_W, MIN_H))); - if let (Ok(physical), Ok(scale)) = (window.inner_size(), window.scale_factor()) { - let width = physical.width as f64 / scale; - let height = physical.height as f64 / scale; - if width < MIN_W || height < MIN_H { - let _ = window.set_size(LogicalSize::new(width.max(MIN_W), height.max(MIN_H))); - } - } -} - #[derive(Clone, Deserialize, Type)] pub enum CapWindowId { Main, @@ -929,7 +595,6 @@ pub enum CapWindowId { RecordingsOverlay, WindowCaptureOccluder { screen_id: DisplayId }, TargetSelectOverlay { display_id: DisplayId }, - CaptureArea, Camera, RecordingControls, Upgrade, @@ -947,7 +612,6 @@ impl FromStr for CapWindowId { "main" => Self::Main, "settings" => Self::Settings, s if is_camera_window_label(s) => Self::Camera, - "capture-area" => Self::CaptureArea, // legacy identifier "in-progress-recording" => Self::RecordingControls, "recordings-overlay" => Self::RecordingsOverlay, @@ -993,7 +657,6 @@ impl std::fmt::Display for CapWindowId { Self::WindowCaptureOccluder { screen_id } => { write!(f, "window-capture-occluder-{screen_id}") } - Self::CaptureArea => write!(f, "capture-area"), Self::TargetSelectOverlay { display_id } => { write!(f, "target-select-overlay-{display_id}") } @@ -1018,7 +681,6 @@ impl CapWindowId { match self { Self::Settings => "Cap Settings".to_string(), Self::WindowCaptureOccluder { .. } => "Cap Window Capture Occluder".to_string(), - Self::CaptureArea => "Cap Capture Area".to_string(), Self::RecordingControls => "Cap Recording Controls".to_string(), Self::Editor { .. } => "Cap Editor".to_string(), Self::ScreenshotEditor { .. } => "Cap Screenshot Editor".to_string(), @@ -1045,24 +707,6 @@ impl CapWindowId { ) } - pub fn is_transparent(&self) -> bool { - if matches!(self, Self::Settings) { - return cfg!(target_os = "macos"); - } - - matches!( - self, - Self::Main - | Self::Onboarding - | Self::Camera - | Self::WindowCaptureOccluder { .. } - | Self::CaptureArea - | Self::RecordingControls - | Self::RecordingsOverlay - | Self::TargetSelectOverlay { .. } - ) - } - pub fn get(&self, app: &AppHandle) -> Option { if matches!(self, Self::Camera) { return current_camera_window(app); @@ -1076,21 +720,23 @@ impl CapWindowId { pub fn traffic_lights_position(&self) -> Option>> { match self { Self::Editor { .. } | Self::ScreenshotEditor { .. } => { - Some(Some(LogicalPosition::new(20.0, 32.0))) + Some(Some(LogicalPosition::new(20.0, 28.0))) } Self::Camera - | Self::Main - | Self::Onboarding | Self::WindowCaptureOccluder { .. } - | Self::CaptureArea | Self::RecordingsOverlay | Self::RecordingControls | Self::TargetSelectOverlay { .. } => None, - Self::Settings => Some(Some(LogicalPosition::new(22.0, 22.0))), + Self::Settings => Some(Some(LogicalPosition::new(20.0, 28.0))), _ => Some(None), } } + #[cfg(target_os = "macos")] + pub fn frame_autosaves(&self) -> bool { + matches!(self, Self::Main) + } + pub fn min_size(&self) -> Option<(f64, f64)> { Some(match self { Self::Main => (330.0, 395.0), @@ -1104,10 +750,28 @@ impl CapWindowId { _ => return None, }) } + + pub fn resizable(&self) -> bool { + matches!( + self, + Self::Debug | Self::Editor { .. } | Self::ScreenshotEditor { .. } | Self::Settings + ) + } + + pub fn appears_transparent(&self) -> bool { + matches!( + self, + Self::Camera + | Self::RecordingControls + | Self::RecordingsOverlay + | Self::TargetSelectOverlay { .. } + | Self::WindowCaptureOccluder { .. } + ) + } } #[derive(Debug, Clone, Type, Deserialize)] -pub enum ShowCapWindow { +pub enum CapWindow { Main { init_target_mode: Option, }, @@ -1125,9 +789,6 @@ pub enum ShowCapWindow { display_id: DisplayId, target_mode: Option, }, - CaptureArea { - screen_id: DisplayId, - }, Camera { centered: bool, }, @@ -1144,7 +805,7 @@ pub enum ShowCapWindow { Onboarding, } -impl ShowCapWindow { +impl CapWindow { pub async fn show(&self, app: &AppHandle) -> tauri::Result { if let Self::Editor { project_path } = &self { let state = app.state::(); @@ -1523,10 +1184,6 @@ impl ShowCapWindow { window.unminimize().ok(); window.set_focus().ok(); - if let Self::Settings { .. } = self { - ensure_settings_window_bounds(&window); - } - if let Self::Main { init_target_mode } = self { emit_app_event( app, @@ -1553,7 +1210,6 @@ impl ShowCapWindow { return Ok(window); } - let _id = self.id(app); let cursor_monitor = CursorMonitorInfo::get(); let window = match self { @@ -1562,23 +1218,20 @@ impl ShowCapWindow { return Box::pin(Self::Onboarding.show(app)).await; } - let title = CapWindowId::Main.title(); - let should_protect = should_protect_window(app, &title); - #[cfg(target_os = "macos")] let panel_activation_guard = permissions::prepare_macos_panel_window(app); + let title = CapWindowId::Main.title(); + let should_protect = should_protect_window(app, &title); + let window = self .window_builder(app, "/") - .resizable(false) - .maximized(false) .maximizable(false) .minimizable(false) .always_on_top(true) .visible_on_all_workspaces(true) .content_protected(should_protect) .transparent(true) - .visible(false) .initialization_script(format!( " window.__CAP__ = window.__CAP__ ?? {{}}; @@ -1588,91 +1241,45 @@ impl ShowCapWindow { .expect("Failed to serialize initial target mode") )) .build()?; - lock_window_text_scale(&window); - - let saved_position = GeneralSettingsStore::get(app) - .ok() - .flatten() - .and_then(|s| s.main_window_position) - .filter(|pos| is_position_on_any_screen(pos.x, pos.y)); - - let main_position = if let Some(pos) = saved_position { - match display_for_saved_position(pos.x, pos.y, pos.display_id.as_ref()) { - Some(display) => { - CursorMonitorInfo::from_display(&display).position(pos.x, pos.y) - } - None => tauri::Position::Logical(tauri::LogicalPosition::new(pos.x, pos.y)), - } - } else { - let (pos_x, pos_y) = cursor_monitor.center_position(330.0, 395.0); - cursor_monitor.position(pos_x, pos_y) - }; - - #[cfg(target_os = "macos")] - { - app.run_on_main_thread({ - let window = window.clone(); - let app = app.clone(); - let panel_activation_guard = panel_activation_guard; - move || { - let _panel_activation_guard = panel_activation_guard; - use tauri_nspanel::cocoa::appkit::NSWindowCollectionBehavior; - use tauri_nspanel::panel_delegate; - use crate::panel_manager::try_to_panel; - - const MAIN_PANEL_LEVEL: i32 = 100; - - let delegate = panel_delegate!(MainPanelDelegate { - window_did_become_key, - window_did_resign_key - }); - - delegate.set_listener(Box::new(|_delegate_name: String| {})); - - let panel = match try_to_panel(&window) { - Ok(p) => p, - Err(e) => { - tracing::error!("Failed to convert main window to panel: {}", e); - crate::permissions::sync_macos_dock_visibility(&app); - return; - } - }; - - panel.set_collection_behaviour( - NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenPrimary, - ); - - panel.set_delegate(delegate); - panel.set_level(MAIN_PANEL_LEVEL); - - let _ = window.set_position(main_position); + if let Err(e) = window.set_zoom(1.0) { + warn!("Failed to lock Main window zoom to 1.0: {}", e); + } - crate::platform::apply_squircle_corners(&window, 16.0); + #[cfg(target_os = "macos")] + window.run_on_main_thread({ + let window = window.clone(); + let app = app.clone(); + let panel_activation_guard = panel_activation_guard; + move || { + use crate::panel_manager::try_to_panel; + use objc2_app_kit::{NSWindowCollectionBehavior, NSWindowStyleMask}; + use objc2_foundation::ns_string; + use tauri_nspanel::{Panel, tauri_panel}; + + let _panel_activation_guard = panel_activation_guard; + + let panel = match DefaultPanel::from_window(&window) { + Ok(p) => p, + Err(e) => { + tracing::error!("Failed to convert main window to panel: {}", e); + crate::permissions::sync_macos_dock_visibility(&app); + return; + } + }; - crate::permissions::schedule_macos_dock_visibility_sync(&app); - } - }) - .ok(); - } + panel.set_collection_behavior( + NSWindowCollectionBehavior::CanJoinAllSpaces + | NSWindowCollectionBehavior::FullScreenAuxiliary + | NSWindowCollectionBehavior::Primary + | NSWindowCollectionBehavior::FullScreenDisallowsTiling, + ); - #[cfg(not(target_os = "macos"))] - { - let _ = window.set_position(main_position); + panel.set_level(100); - #[cfg(windows)] - { - if let Err(e) = window.set_size(LogicalSize::new(330.0, 395.0)) { - warn!("Failed to set Main window size on Windows: {}", e); - } - if let Err(e) = window.set_position(main_position) { - warn!("Failed to position Main window on Windows: {}", e); - } + crate::permissions::schedule_macos_dock_visibility_sync(&app); } - - window.show().ok(); - } + })?; window } @@ -1718,16 +1325,12 @@ impl ShowCapWindow { app, format!("/target-select-overlay?displayId={display_id}&isHoveredDisplay={is_hovered_display}{target_mode_param}"), ) - .maximized(false) - .resizable(false) .fullscreen(false) - .shadow(false) .content_protected(should_protect) .always_on_top(true) .visible_on_all_workspaces(true) .skip_taskbar(true) .transparent(true) - .visible(false) .initialization_script(format!( "window.__CAP__ = window.__CAP__ ?? {{}}; window.__CAP__.cameraWsPort = {camera_ws_port};" )); @@ -1757,7 +1360,6 @@ impl ShowCapWindow { } let window = window_builder.build()?; - lock_window_text_scale(&window); #[cfg(target_os = "linux")] { @@ -1817,9 +1419,10 @@ impl ShowCapWindow { let panel_activation_guard = panel_activation_guard; move || { let _panel_activation_guard = panel_activation_guard; - use tauri_nspanel::cocoa::appkit::NSWindowCollectionBehavior; - use tauri_nspanel::panel_delegate; - use tauri_nspanel::WebviewWindowExt as NSPanelWebviewWindowExt; + use objc2_app_kit::{ + NSStatusWindowLevel, NSWindowCollectionBehavior, NSWindowStyleMask, + }; + use tauri_nspanel::Panel; #[link(name = "CoreGraphics", kind = "framework")] unsafe extern "C" { @@ -1829,35 +1432,25 @@ impl ShowCapWindow { #[allow(non_upper_case_globals)] const kCGMaximumWindowLevelKey: i32 = 10; - let delegate = panel_delegate!(TargetSelectOverlayPanelDelegate { - window_did_become_key, - window_did_resign_key - }); - - delegate.set_listener(Box::new(|_delegate_name: String| {})); - - let panel = match window.to_panel() { + let panel = match TargetSelectOverlayPanel::from_window(&window) { Ok(p) => p, Err(e) => { - tracing::error!("Failed to convert target select overlay to panel: {:?}", e); + tracing::error!( + "Failed to convert target select overlay to panel: {:?}", + e + ); crate::permissions::sync_macos_dock_visibility(&app); return; } }; - panel.set_collection_behaviour( - NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenPrimary, + panel.set_collection_behavior( + NSWindowCollectionBehavior::FullScreenAuxiliary + | NSWindowCollectionBehavior::CanJoinAllSpaces, ); - panel.set_delegate(delegate); - - #[allow(non_upper_case_globals)] - const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7; - panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel); - - let max_level = unsafe { CGWindowLevelForKey(kCGMaximumWindowLevelKey) }; - panel.set_level(max_level - 1); + panel.set_level((NSStatusWindowLevel + 1) as i64); + panel.set_style_mask(NSWindowStyleMask::NonactivatingPanel); panel.order_front_regardless(); panel.show(); @@ -1882,9 +1475,6 @@ impl ShowCapWindow { format!("/settings/{}", page.clone().unwrap_or_default()), ) .inner_size(782.0, 775.0) - .min_inner_size(780.0, 560.0) - .resizable(true) - .maximized(false) .focused(true); #[cfg(target_os = "macos")] @@ -1892,36 +1482,16 @@ impl ShowCapWindow { builder = builder.transparent(true); } - let window = builder.build()?; - lock_window_text_scale(&window); - - let (pos_x, pos_y) = cursor_monitor.center_position(782.0, 775.0); - let _ = window.set_position(cursor_monitor.position(pos_x, pos_y)); - - #[cfg(windows)] - { - if let Err(e) = window.set_size(LogicalSize::new(782.0, 775.0)) { - warn!("Failed to set Settings window size on Windows: {}", e); - } - if let Err(e) = window.set_position(cursor_monitor.position(pos_x, pos_y)) { - warn!("Failed to position Settings window on Windows: {}", e); - } - } - - ensure_settings_window_bounds(&window); - - window + builder.build()? } Self::Editor { .. } => { - let open_started = std::time::Instant::now(); + let _open_started = std::time::Instant::now(); hide_recording_windows(app, false); release_camera_preview_if_idle(app); - let window = match self + match self .window_builder(app, "/editor") .maximizable(true) - .inner_size(1275.0, 800.0) - .min_inner_size(1275.0, 800.0) .focused(true) .build() { @@ -1935,46 +1505,7 @@ impl ShowCapWindow { .await; return Err(error); } - }; - lock_window_text_scale(&window); - - let (pos_x, pos_y) = cursor_monitor.center_position(1275.0, 800.0); - let _ = window.set_position(cursor_monitor.position(pos_x, pos_y)); - - #[cfg(windows)] - { - use tauri::LogicalSize; - if let Err(e) = window.set_size(LogicalSize::new(1275.0, 800.0)) { - warn!("Failed to set Editor window size on Windows: {}", e); - } - if let Err(e) = window.set_position(cursor_monitor.position(pos_x, pos_y)) { - warn!("Failed to position Editor window on Windows: {}", e); - } - } - - // Show immediately: the native background color is already - // themed, so the window can appear before the webview loads and - // the editor skeleton takes over. When window transparency is - // enabled we keep the old behaviour (the frontend reveals the - // window after applying the HudWindow effects) to avoid an - // opaque-to-transparent pop. - let transparency_enabled = GeneralSettingsStore::get(app) - .ok() - .flatten() - .map(|s| s.window_transparency) - .unwrap_or(false); - if !transparency_enabled { - window.show().ok(); - window.set_focus().ok(); } - - info!( - window_built_and_shown_ms = open_started.elapsed().as_millis() as u64, - shown_from_rust = !transparency_enabled, - "Editor open: window ready" - ); - - window } Self::ScreenshotEditor { path } => { hide_recording_windows(app, false); @@ -1989,11 +1520,10 @@ impl ShowCapWindow { ) .await; - let window = match self + match self .window_builder(app, "/screenshot-editor") .maximizable(true) .inner_size(1240.0, 800.0) - .min_inner_size(800.0, 600.0) .focused(true) .build() { @@ -2002,33 +1532,7 @@ impl ShowCapWindow { pending.cancel_prewarm(&window_label).await; return Err(error); } - }; - lock_window_text_scale(&window); - - let (pos_x, pos_y) = cursor_monitor.center_position(1240.0, 800.0); - let _ = window.set_position(cursor_monitor.position(pos_x, pos_y)); - - #[cfg(windows)] - { - use tauri::LogicalSize; - if let Err(e) = window.set_size(LogicalSize::new(1240.0, 800.0)) { - warn!( - "Failed to set ScreenshotEditor window size on Windows: {}", - e - ); - } - if let Err(e) = window.set_position(cursor_monitor.position(pos_x, pos_y)) { - warn!( - "Failed to position ScreenshotEditor window on Windows: {}", - e - ); - } } - - window.show().ok(); - window.set_focus().ok(); - - window } Self::Upgrade => { if let Some(main) = CapWindowId::Main.get(app) { @@ -2037,15 +1541,9 @@ impl ShowCapWindow { let window = self .window_builder(app, "/upgrade") - .inner_size(950.0, 850.0) - .min_inner_size(950.0, 850.0) - .resizable(false) .focused(true) .always_on_top(true) - .maximized(false) - .shadow(true) .build()?; - lock_window_text_scale(&window); let (pos_x, pos_y) = cursor_monitor.center_position(950.0, 850.0); let _ = window.set_position(cursor_monitor.position(pos_x, pos_y)); @@ -2061,9 +1559,6 @@ impl ShowCapWindow { } } - window.show().ok(); - window.set_focus().ok(); - window } Self::ModeSelect => { @@ -2073,15 +1568,9 @@ impl ShowCapWindow { let window = self .window_builder(app, "/mode-select") - .inner_size(580.0, 340.0) - .min_inner_size(580.0, 340.0) - .resizable(false) - .maximized(false) .maximizable(false) .focused(true) - .shadow(true) .build()?; - lock_window_text_scale(&window); let (pos_x, pos_y) = cursor_monitor.center_position(580.0, 340.0); let _ = window.set_position(cursor_monitor.position(pos_x, pos_y)); @@ -2097,9 +1586,6 @@ impl ShowCapWindow { } } - window.show().ok(); - window.set_focus().ok(); - window } Self::Onboarding => { @@ -2113,15 +1599,10 @@ impl ShowCapWindow { let window = self .window_builder(app, "/onboarding") .inner_size(width, height) - .min_inner_size(860.0, 690.0) - .resizable(false) - .maximized(false) .maximizable(false) .transparent(true) .focused(true) - .shadow(true) .build()?; - lock_window_text_scale(&window); let (pos_x, pos_y) = cursor_monitor.center_position(width, height); let _ = window.set_position(cursor_monitor.position(pos_x, pos_y)); @@ -2138,9 +1619,6 @@ impl ShowCapWindow { } } - window.show().ok(); - window.set_focus().ok(); - window } Self::Camera { centered } => { @@ -2217,9 +1695,6 @@ impl ShowCapWindow { .unwrap_or_else(|| CapWindowId::Camera.label()); let mut window_builder = self .window_builder_with_label(app, "/camera", label) - .maximized(false) - .resizable(false) - .shadow(false) .fullscreen(false) .always_on_top(true) .visible_on_all_workspaces(true) @@ -2234,8 +1709,7 @@ impl ShowCapWindow { state.camera_ws_port, centered, enable_native_camera_preview )) .content_protected(should_protect) - .transparent(true) - .visible(false); + .transparent(true); let window = match window_builder.build() { Ok(w) => w, @@ -2256,7 +1730,6 @@ impl ShowCapWindow { return Err(e); } }; - lock_window_text_scale(&window); #[cfg(target_os = "windows")] log_window_content_protection( @@ -2270,7 +1743,7 @@ impl ShowCapWindow { .map(|w| CursorMonitorInfo::from_window(&w)) .unwrap_or(cursor_monitor); - let preferred_monitor_name = display_name_for_position( + let preferred_monitor_name = display_utils::display_name_for_position( camera_monitor.x + camera_monitor.width / 2.0, camera_monitor.y + camera_monitor.height / 2.0, ); @@ -2286,11 +1759,15 @@ impl ShowCapWindow { .get(monitor_name) .cloned() .filter(|pos| { - is_position_on_monitor_name(monitor_name, pos.x, pos.y) + display_utils::is_position_on_monitor_name( + monitor_name, + pos.x, + pos.y, + ) }) .or_else(|| { settings.camera_window_position.filter(|pos| { - is_position_on_monitor_name( + display_utils::is_position_on_monitor_name( monitor_name, pos.x, pos.y, @@ -2300,9 +1777,11 @@ impl ShowCapWindow { } else { settings.camera_window_position.filter(|pos| { if let Some(display_id) = &pos.display_id { - is_position_on_display(display_id, pos.x, pos.y) + display_utils::is_position_on_display( + display_id, pos.x, pos.y, + ) } else { - is_position_on_any_screen(pos.x, pos.y) + display_utils::is_position_on_any_screen(pos.x, pos.y) } }) } @@ -2311,7 +1790,7 @@ impl ShowCapWindow { let camera_position = if let Some(pos) = saved_position { match display_for_saved_position(pos.x, pos.y, pos.display_id.as_ref()) { Some(display) => { - CursorMonitorInfo::from_display(&display).position(pos.x, pos.y) + CursorMonitorInfo::from(&display).position(pos.x, pos.y) } None => { tauri::Position::Logical(tauri::LogicalPosition::new(pos.x, pos.y)) @@ -2355,9 +1834,9 @@ impl ShowCapWindow { let panel_activation_guard = panel_activation_guard; move || { let _panel_activation_guard = panel_activation_guard; - use tauri_nspanel::cocoa::appkit::NSWindowCollectionBehavior; - use tauri_nspanel::panel_delegate; use crate::panel_manager::try_to_panel; + use objc2_app_kit::NSWindowCollectionBehavior; + use tauri_nspanel::WebviewWindowExt as NSPanelWebviewWindowExt; #[link(name = "CoreGraphics", kind = "framework")] unsafe extern "C" { @@ -2367,13 +1846,6 @@ impl ShowCapWindow { #[allow(non_upper_case_globals)] const kCGMaximumWindowLevelKey: i32 = 10; - let delegate = panel_delegate!(CameraPanelDelegate { - window_did_become_key, - window_did_resign_key - }); - - delegate.set_listener(Box::new(|_delegate_name: String| {})); - let panel = match try_to_panel(&window) { Ok(p) => p, Err(e) => { @@ -2384,16 +1856,14 @@ impl ShowCapWindow { } }; - panel.set_collection_behaviour( - NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenPrimary, + panel.set_collection_behavior( + NSWindowCollectionBehavior::CanJoinAllSpaces + | NSWindowCollectionBehavior::FullScreenPrimary, ); - panel.set_delegate(delegate); - let max_level = unsafe { CGWindowLevelForKey(kCGMaximumWindowLevelKey) }; - panel.set_level(max_level); + panel.set_level(i64::from(max_level)); if let Some(guard) = app.try_state::() { guard.ignore_for(1000); @@ -2455,10 +1925,7 @@ impl ShowCapWindow { let mut window_builder = self .window_builder(app, "/window-capture-occluder") - .maximized(false) - .resizable(false) .fullscreen(false) - .shadow(false) .always_on_top(true) .visible_on_all_workspaces(true) .content_protected(should_protect) @@ -2501,7 +1968,6 @@ impl ShowCapWindow { } let window = window_builder.build()?; - lock_window_text_scale(&window); #[cfg(target_os = "linux")] { @@ -2563,133 +2029,16 @@ impl ShowCapWindow { } #[cfg(target_os = "macos")] - { - crate::platform::set_window_level(window.as_ref().window(), 900); - } - - window - } - Self::CaptureArea { screen_id } => { - let title = CapWindowId::CaptureArea.title(); - let should_protect = should_protect_window(app, &title); - - let mut window_builder = self - .window_builder(app, "/capture-area") - .maximized(false) - .fullscreen(false) - .shadow(false) - .resizable(false) - .always_on_top(true) - .content_protected(should_protect) - .skip_taskbar(true) - .closable(true) - .decorations(false) - .transparent(true); - - let Some(display) = Display::from_id(screen_id) else { - return Err(tauri::Error::WindowNotFound); - }; - - #[cfg(target_os = "macos")] - if let Some(bounds) = display.raw_handle().logical_bounds() { - window_builder = window_builder - .inner_size(bounds.size().width(), bounds.size().height()) - .position(bounds.position().x(), bounds.position().y()); - } - - // On Windows a window's DPI scale isn't known until it's placed on a - // monitor, so sizing/positioning from logical bounds at build time is - // unreliable across monitors with different DPIs — the overlay ends up - // sized for the wrong monitor and no longer covers the target display, - // which truncates area selections on HiDPI secondary monitors. Build a - // placeholder and fix the geometry up after the window exists (below), - // mirroring the TargetSelectOverlay path. - #[cfg(windows)] - { - window_builder = window_builder.inner_size(100.0, 100.0).position(0.0, 0.0); - } - - #[cfg(target_os = "linux")] - if let Some(bounds) = display.raw_handle().physical_bounds() { - window_builder = window_builder - .inner_size(bounds.size().width(), bounds.size().height()) - .position(bounds.position().x(), bounds.position().y()); - } - - let window = window_builder.build()?; - lock_window_text_scale(&window); - - // Fix up the overlay geometry now that the window exists and its real - // per-monitor DPI is known: position with physical coordinates (which are - // unambiguous across monitors), then set the logical size so the window - // covers the full display. Verify the resulting physical size matches the - // display and re-apply once if the initial placement raced the DPI change. - #[cfg(windows)] - { - let Some(position) = display.raw_handle().physical_position() else { - warn!(display_id = %screen_id, "Missing display position for capture area overlay"); - return Err(tauri::Error::WindowNotFound); - }; - let Some(logical_size) = display.logical_size() else { - warn!(display_id = %screen_id, "Missing display logical size for capture area overlay"); - return Err(tauri::Error::WindowNotFound); - }; - let Some(physical_size) = display.physical_size() else { - warn!(display_id = %screen_id, "Missing display physical size for capture area overlay"); - return Err(tauri::Error::WindowNotFound); - }; - use tauri::{LogicalSize, PhysicalPosition}; - let _ = window.set_size(LogicalSize::new( - logical_size.width(), - logical_size.height(), - )); - let _ = window.set_position(PhysicalPosition::new(position.x(), position.y())); - tokio::time::sleep(std::time::Duration::from_millis(5)).await; - - match window.inner_size() { - Ok(actual_physical_size) - if physical_size.width() != actual_physical_size.width as f64 => - { - let _ = window.set_size(LogicalSize::new( - logical_size.width(), - logical_size.height(), - )); - } - Ok(_) => {} - Err(err) => { - warn!(%err, "Failed to read capture area overlay inner size"); - } - } - } - - #[cfg(target_os = "linux")] - if let Some(bounds) = display.raw_handle().physical_bounds() { - use tauri::{LogicalSize, PhysicalPosition}; - let _ = window.set_position(PhysicalPosition::new( - bounds.position().x(), - bounds.position().y(), - )); - let _ = window.set_size(LogicalSize::new( - bounds.size().width(), - bounds.size().height(), - )); - } - - #[cfg(target_os = "macos")] - crate::platform::set_window_level( - window.as_ref().window(), - objc2_app_kit::NSPopUpMenuWindowLevel, - ); - - // Hide the main window if the target monitor is the same - if let Some(main_window) = CapWindowId::Main.get(app) - && let (Ok(outer_pos), Ok(outer_size)) = - (main_window.outer_position(), main_window.outer_size()) - && let Ok(scale_factor) = main_window.scale_factor() - && display.intersects(outer_pos, outer_size, scale_factor) - { - let _ = main_window.minimize(); - }; + window.with_nswindow_on_main(|_, nswindow| { + use objc2_app_kit::NSWindowCollectionBehavior; + + nswindow.setLevel(900); + nswindow.setCollectionBehavior( + NSWindowCollectionBehavior::CanJoinAllSpaces + | NSWindowCollectionBehavior::FullScreenAuxiliary + | NSWindowCollectionBehavior::IgnoresCycle, + ); + })?; window } @@ -2706,66 +2055,26 @@ impl ShowCapWindow { #[cfg(target_os = "macos")] let panel_activation_guard = permissions::prepare_macos_panel_window(app); - #[cfg(target_os = "macos")] - let window = { - self.window_builder(app, "/in-progress-recording") - .maximized(false) - .resizable(false) - .fullscreen(false) - .shadow(false) - .always_on_top(true) - .transparent(true) - .visible_on_all_workspaces(true) - .content_protected(should_protect) - .inner_size(width, height) - .skip_taskbar(true) - .visible(false) - .initialization_script(format!( - "window.COUNTDOWN = {};", - countdown.unwrap_or_default() - )) - .build()? - }; - - #[cfg(windows)] - let window = self + let mut builder = self .window_builder(app, "/in-progress-recording") - .maximized(false) - .resizable(false) .fullscreen(false) - .shadow(false) .always_on_top(true) .transparent(true) .visible_on_all_workspaces(true) .content_protected(should_protect) .inner_size(width, height) - .skip_taskbar(false) + .skip_taskbar(cfg!(target_os = "macos")) + .resizable(!cfg!(target_os = "linux")) .initialization_script(format!( "window.COUNTDOWN = {};", countdown.unwrap_or_default() - )) - .build()?; + )); - #[cfg(target_os = "linux")] - let window = self - .window_builder(app, "/in-progress-recording") - .maximized(false) - .resizable(false) - .fullscreen(false) - .shadow(false) - .always_on_top(true) - .transparent(true) - .visible_on_all_workspaces(true) - .content_protected(should_protect) - .inner_size(width, height) - .skip_taskbar(false) - .initialization_script(format!( - "window.COUNTDOWN = {};", - countdown.unwrap_or_default() - )) - .build()?; + if cfg!(target_os = "linux") { + builder = builder.maximized(false); + } - lock_window_text_scale(&window); + let window = builder.build()?; #[cfg(target_os = "windows")] log_window_content_protection(&window, should_protect, &title); @@ -2798,12 +2107,10 @@ impl ShowCapWindow { app.run_on_main_thread({ let window = window.clone(); let app = app.clone(); - let panel_activation_guard = panel_activation_guard; + let _panel_activation_guard = panel_activation_guard; move || { - let _panel_activation_guard = panel_activation_guard; - use tauri_nspanel::cocoa::appkit::NSWindowCollectionBehavior; - use tauri_nspanel::panel_delegate; - use tauri_nspanel::WebviewWindowExt as NSPanelWebviewWindowExt; + use objc2_app_kit::{NSWindowCollectionBehavior, NSWindowStyleMask}; + use tauri_nspanel::{CollectionBehavior, Panel, StyleMask}; #[link(name = "CoreGraphics", kind = "framework")] unsafe extern "C" { @@ -2813,31 +2120,28 @@ impl ShowCapWindow { #[allow(non_upper_case_globals)] const kCGMaximumWindowLevelKey: i32 = 10; - let delegate = panel_delegate!(RecordingControlsPanelDelegate { - window_did_become_key, - window_did_resign_key - }); - - delegate.set_listener(Box::new(|_delegate_name: String| {})); - - let panel = match window.to_panel() { + let panel = match DefaultPanel::from_window(&window) { Ok(p) => p, Err(e) => { - tracing::error!("Failed to convert recording controls to panel: {:?}", e); + tracing::error!( + "Failed to convert recording controls to panel: {:?}", + e + ); crate::permissions::sync_macos_dock_visibility(&app); return; } }; - panel.set_collection_behaviour( - NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenPrimary, + panel.set_collection_behavior( + NSWindowCollectionBehavior::CanJoinAllSpaces + | NSWindowCollectionBehavior::FullScreenPrimary, ); - panel.set_delegate(delegate); + panel.set_style_mask(NSWindowStyleMask::NonactivatingPanel); - let max_level = unsafe { CGWindowLevelForKey(kCGMaximumWindowLevelKey) }; - panel.set_level(max_level); + let max_level = + unsafe { CGWindowLevelForKey(kCGMaximumWindowLevelKey) }; + panel.set_level(i64::from(max_level)); panel.order_front_regardless(); panel.show(); @@ -2870,10 +2174,7 @@ impl ShowCapWindow { let window = self .window_builder(app, "/recordings-overlay") - .maximized(false) - .resizable(false) .fullscreen(false) - .shadow(false) .always_on_top(true) .visible_on_all_workspaces(true) .accept_first_mouse(true) @@ -2882,10 +2183,9 @@ impl ShowCapWindow { .skip_taskbar(true) .transparent(true) .build()?; - lock_window_text_scale(&window); - let _ = - window.set_position(cursor_monitor.position(cursor_monitor.x, cursor_monitor.y)); + let _ = window + .set_position(cursor_monitor.position(cursor_monitor.x, cursor_monitor.y)); // The build-time inner_size above was interpreted with the DPI of // whatever monitor the window materialized on; now that it sits on the @@ -2899,8 +2199,8 @@ impl ShowCapWindow { )); tokio::time::sleep(std::time::Duration::from_millis(5)).await; - let expected_physical_width = (cursor_monitor.width * cursor_monitor.scale) - .round(); + let expected_physical_width = + (cursor_monitor.width * cursor_monitor.scale).round(); match window.inner_size() { Ok(actual_physical_size) if expected_physical_width != actual_physical_size.width as f64 => @@ -2922,29 +2222,30 @@ impl ShowCapWindow { app.run_on_main_thread({ let window = window.clone(); move || { - use tauri_nspanel::cocoa::appkit::NSWindowCollectionBehavior; use crate::panel_manager::try_to_panel; + use objc2_app_kit::{NSWindowCollectionBehavior, NSWindowStyleMask}; let panel = match try_to_panel(&window) { Ok(p) => p, Err(e) => { - tracing::error!("Failed to convert recordings overlay to panel: {}", e); + tracing::error!( + "Failed to convert recordings overlay to panel: {}", + e + ); return; } }; - panel.set_level(cocoa::appkit::NSMainMenuWindowLevel); + panel.set_level(i64::from(objc2_app_kit::NSMainMenuWindowLevel as i32)); + panel.set_style_mask(NSWindowStyleMask::NonactivatingPanel); - panel.set_collection_behaviour( - NSWindowCollectionBehavior::NSWindowCollectionBehaviorTransient - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorMoveToActiveSpace - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary - | NSWindowCollectionBehavior::NSWindowCollectionBehaviorIgnoresCycle, + panel.set_collection_behavior( + NSWindowCollectionBehavior::Transient + | NSWindowCollectionBehavior::MoveToActiveSpace + | NSWindowCollectionBehavior::FullScreenAuxiliary + | NSWindowCollectionBehavior::IgnoresCycle + | NSWindowCollectionBehavior::FullScreenDisallowsTiling, ); - - #[allow(non_upper_case_globals)] - const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7; - panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel); } }) .ok(); @@ -2956,17 +2257,45 @@ impl ShowCapWindow { } }; - // removing this for now as it causes windows to just stay hidden sometimes -_- - // window.hide().ok(); + let id = self.id(app); #[cfg(target_os = "macos")] - if let Some(position) = _id.traffic_lights_position() { - add_traffic_lights(&window, position); + { + if id.activates_dock() { + crate::permissions::sync_macos_dock_visibility(app); + } + + if id.frame_autosaves() { + let label = id.label(); + window.with_nswindow_on_main(move |_, nswindow| { + let autosave_name = objc2_foundation::NSString::from_str(&label); + nswindow.setFrameAutosaveName(&autosave_name); + nswindow.setFrameUsingName_force(&autosave_name, true); + })?; + } } - #[cfg(target_os = "macos")] - if _id.activates_dock() { - crate::permissions::sync_macos_dock_visibility(app); + if let Some(min) = id.min_size() { + let inner_size = window + .inner_size()? + .to_logical::(window.scale_factor()?); + if inner_size.width < min.0 || inner_size.height < min.1 { + window.set_size(LogicalSize::new(min.0, min.1))?; + + // macOS center considers the height of menubar and dock + #[cfg(target_os = "macos")] + window.with_nswindow_on_main(|_, nswindow| nswindow.center())?; + + #[cfg(not(target_os = "macos"))] + { + window.center()?; + } + } + } + + #[cfg(windows)] + { + lock_window_text_scale(&window); } Ok(window) @@ -2989,54 +2318,18 @@ impl ShowCapWindow { ) -> WebviewWindowBuilder<'a, Wry, AppHandle> { let id = self.id(app); - let settings = GeneralSettingsStore::get(app).ok().flatten(); - let window_transparency_enabled = settings - .as_ref() - .map(|s| s.window_transparency) - .unwrap_or(false); - let theme = settings - .map(|s| match s.theme { - AppTheme::System => None, - AppTheme::Light => Some(tauri::Theme::Light), - AppTheme::Dark => Some(tauri::Theme::Dark), - }) - .unwrap_or(None); + let theme: Option = GeneralSettingsStore::get(app) + .ok() + .flatten() + .and_then(|s| s.appearance.into()); let mut builder = WebviewWindow::builder(app, label, WebviewUrl::App(url.into())) .title(id.title()) .visible(false) + .resizable(id.resizable()) .accept_first_mouse(true) - .shadow(true) - .theme(theme) - .devtools(cfg!(debug_assertions)); - - if !id.is_transparent() { - let is_dark = match theme { - Some(tauri::Theme::Dark) => true, - Some(tauri::Theme::Light) => false, - None | Some(_) => is_system_dark_mode(), - }; - - let bg_color = if is_dark { "#141414" } else { "#ffffff" }; - let init_script = format!( - r#"(function(){{var s=document.createElement('style');s.textContent='html,body{{background-color:{bg_color}}}';document.documentElement.appendChild(s);}})();"# - ); - builder = builder.initialization_script(&init_script); - - // Native backing color so the window is themed before the webview's - // first paint, allowing windows to be shown immediately without a - // white/black flash. Skipped when the user has window transparency - // enabled: an opaque native background would sit behind the - // translucent webview content and defeat the effect. - if !window_transparency_enabled { - let native_bg = if is_dark { - tauri::window::Color(0x14, 0x14, 0x14, 0xff) - } else { - tauri::window::Color(0xff, 0xff, 0xff, 0xff) - }; - builder = builder.background_color(native_bg); - } - } + .shadow(!id.appears_transparent()) + .theme(theme); if let Some(min) = id.min_size() { builder = builder @@ -3046,10 +2339,11 @@ impl ShowCapWindow { #[cfg(target_os = "macos")] { - if id.traffic_lights_position().is_some() { + if let Some(pos) = id.traffic_lights_position() { builder = builder .hidden_title(true) - .title_bar_style(tauri::TitleBarStyle::Overlay); + .title_bar_style(tauri::TitleBarStyle::Overlay) + .traffic_light_position(pos.unwrap_or(DEFAULT_TRAFFIC_LIGHTS_INSET)); } else { builder = builder.decorations(false) } @@ -3073,35 +2367,36 @@ impl ShowCapWindow { builder } - pub fn id(&self, app: &AppHandle) -> CapWindowId { + pub fn id(&self, manager: &impl Manager) -> CapWindowId { match self { - ShowCapWindow::Main { .. } => CapWindowId::Main, - ShowCapWindow::Settings { .. } => CapWindowId::Settings, - ShowCapWindow::Editor { project_path } => { - let state = app.state::(); + CapWindow::Main { .. } => CapWindowId::Main, + CapWindow::Settings { .. } => CapWindowId::Settings, + CapWindow::Editor { project_path } => { + let state = manager.state::(); let s = state.ids.lock().unwrap(); - let id = s.iter().find(|(path, _)| path == project_path).unwrap().1; + let id = s + .iter() + .find(|(path, _)| path == project_path) + .map(|(_, id)| *id) + .unwrap_or_else(|| { + panic!("editor window id should be registered before calling CapWindow::id ({project_path:?})") + }); CapWindowId::Editor { id } } - ShowCapWindow::RecordingsOverlay => CapWindowId::RecordingsOverlay, - ShowCapWindow::TargetSelectOverlay { display_id, .. } => { - CapWindowId::TargetSelectOverlay { - display_id: display_id.clone(), - } - } - ShowCapWindow::WindowCaptureOccluder { screen_id } => { - CapWindowId::WindowCaptureOccluder { - screen_id: screen_id.clone(), - } - } - ShowCapWindow::CaptureArea { .. } => CapWindowId::CaptureArea, - ShowCapWindow::Camera { .. } => CapWindowId::Camera, - ShowCapWindow::InProgressRecording { .. } => CapWindowId::RecordingControls, - ShowCapWindow::Upgrade => CapWindowId::Upgrade, - ShowCapWindow::ModeSelect => CapWindowId::ModeSelect, - ShowCapWindow::Onboarding => CapWindowId::Onboarding, - ShowCapWindow::ScreenshotEditor { path } => { - let state = app.state::(); + CapWindow::RecordingsOverlay => CapWindowId::RecordingsOverlay, + CapWindow::TargetSelectOverlay { display_id, .. } => CapWindowId::TargetSelectOverlay { + display_id: display_id.clone(), + }, + CapWindow::WindowCaptureOccluder { screen_id } => CapWindowId::WindowCaptureOccluder { + screen_id: screen_id.clone(), + }, + CapWindow::Camera { .. } => CapWindowId::Camera, + CapWindow::InProgressRecording { .. } => CapWindowId::RecordingControls, + CapWindow::Upgrade => CapWindowId::Upgrade, + CapWindow::ModeSelect => CapWindowId::ModeSelect, + CapWindow::Onboarding => CapWindowId::Onboarding, + CapWindow::ScreenshotEditor { path } => { + let state = manager.state::(); let s = state.ids.lock().unwrap(); let id = s.iter().find(|(p, _)| p == path).unwrap().1; CapWindowId::ScreenshotEditor { id } @@ -3110,42 +2405,40 @@ impl ShowCapWindow { } } +#[cfg(windows)] fn lock_window_text_scale(_window: &WebviewWindow) { - #[cfg(windows)] - { - let scale_factor = match _window.scale_factor() { - Ok(scale_factor) => scale_factor, - Err(e) => { - warn!("Failed to read window scale factor: {}", e); - return; - } - }; + let scale_factor = match _window.scale_factor() { + Ok(scale_factor) => scale_factor, + Err(e) => { + warn!("Failed to read window scale factor: {}", e); + return; + } + }; - if let Err(e) = _window.with_webview(move |webview| unsafe { - use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller3; - use windows_core::Interface; + if let Err(e) = _window.with_webview(move |webview| unsafe { + use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller3; + use windows_core::Interface; - let controller = webview.controller(); + let controller = webview.controller(); - if let Err(e) = controller.SetZoomFactor(1.0) { - warn!("Failed to lock WebView zoom factor: {}", e); - } + if let Err(e) = controller.SetZoomFactor(1.0) { + warn!("Failed to lock WebView zoom factor: {}", e); + } - let Ok(controller3) = controller.cast::() else { - warn!("Failed to access WebView2 controller scale APIs"); - return; - }; + let Ok(controller3) = controller.cast::() else { + warn!("Failed to access WebView2 controller scale APIs"); + return; + }; - if let Err(e) = controller3.SetShouldDetectMonitorScaleChanges(false) { - warn!("Failed to disable WebView scale detection: {}", e); - } + if let Err(e) = controller3.SetShouldDetectMonitorScaleChanges(false) { + warn!("Failed to disable WebView scale detection: {}", e); + } - if let Err(e) = controller3.SetRasterizationScale(scale_factor) { - warn!("Failed to lock WebView rasterization scale: {}", e); - } - }) { - warn!("Failed to access platform WebView: {}", e); + if let Err(e) = controller3.SetRasterizationScale(scale_factor) { + warn!("Failed to lock WebView rasterization scale: {}", e); } + }) { + warn!("Failed to access platform WebView: {}", e); } } @@ -3177,83 +2470,6 @@ pub fn update_window_rasterization_scale(_window: &WebviewWindow, _scale_fa } } -#[cfg(target_os = "macos")] -fn add_traffic_lights(window: &WebviewWindow, controls_inset: Option>) { - use crate::platform::delegates; - - let target_window = window.clone(); - window - .run_on_main_thread(move || { - delegates::setup( - target_window.as_ref().window(), - controls_inset.unwrap_or(DEFAULT_TRAFFIC_LIGHTS_INSET), - ); - - let c_win = target_window.clone(); - target_window.on_window_event(move |event| match event { - tauri::WindowEvent::ThemeChanged(..) | tauri::WindowEvent::Focused(..) => { - position_traffic_lights_impl(&c_win.as_ref().window(), controls_inset); - } - _ => {} - }); - }) - .ok(); -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(window))] -pub fn set_theme(window: tauri::Window, theme: AppTheme) { - let _ = window.set_theme(match theme { - AppTheme::System => None, - AppTheme::Light => Some(tauri::Theme::Light), - AppTheme::Dark => Some(tauri::Theme::Dark), - }); - - #[cfg(target_os = "macos")] - match CapWindowId::from_str(window.label()) { - Ok(win) if win.traffic_lights_position().is_some() => position_traffic_lights(window, None), - Ok(_) | Err(_) => {} - } -} - -#[tauri::command] -#[specta::specta] -#[instrument(skip(_window))] -pub fn position_traffic_lights(_window: tauri::Window, _controls_inset: Option<(f64, f64)>) { - #[cfg(target_os = "macos")] - position_traffic_lights_impl( - &_window, - _controls_inset.map(LogicalPosition::from).or_else(|| { - // Attempt to get the default inset from the window's traffic lights position - CapWindowId::from_str(_window.label()) - .ok() - .and_then(|id| id.traffic_lights_position().flatten()) - }), - ); -} - -#[cfg(target_os = "macos")] -fn position_traffic_lights_impl( - window: &tauri::Window, - controls_inset: Option>, -) { - use crate::platform::delegates::{UnsafeWindowHandle, position_window_controls}; - let c_win = window.clone(); - window - .run_on_main_thread(move || { - let ns_window = match c_win.ns_window() { - Ok(handle) => handle, - Err(_) => return, - }; - position_window_controls( - UnsafeWindowHandle(ns_window), - &controls_inset.unwrap_or(DEFAULT_TRAFFIC_LIGHTS_INSET), - ); - }) - .ok(); -} - // Capture exclusion (WDA_EXCLUDEFROMCAPTURE / NSWindowSharingType::None) also hides // the window from "capture-based" displays such as virtual/indirect/dummy-HDMI or // mirrored monitors, making it invisible and unreachable. We therefore only protect @@ -3452,97 +2668,6 @@ pub fn refresh_window_content_protection(app: AppHandle) -> Result<(), Stri Ok(()) } -// Credits: tauri-plugin-window-state -trait MonitorExt { - fn intersects( - &self, - position: PhysicalPosition, - size: PhysicalSize, - scale: f64, - ) -> bool; -} - -impl MonitorExt for Display { - fn intersects( - &self, - position: PhysicalPosition, - size: PhysicalSize, - _scale: f64, - ) -> bool { - #[cfg(target_os = "macos")] - { - let Some(bounds) = self.raw_handle().logical_bounds() else { - return false; - }; - - let left = (bounds.position().x() * _scale) as i32; - let right = left + (bounds.size().width() * _scale) as i32; - let top = (bounds.position().y() * _scale) as i32; - let bottom = top + (bounds.size().height() * _scale) as i32; - - [ - (position.x, position.y), - (position.x + size.width as i32, position.y), - (position.x, position.y + size.height as i32), - ( - position.x + size.width as i32, - position.y + size.height as i32, - ), - ] - .into_iter() - .any(|(x, y)| x >= left && x < right && y >= top && y < bottom) - } - - #[cfg(windows)] - { - let Some(bounds) = self.raw_handle().physical_bounds() else { - return false; - }; - - let left = bounds.position().x() as i32; - let right = left + bounds.size().width() as i32; - let top = bounds.position().y() as i32; - let bottom = top + bounds.size().height() as i32; - - [ - (position.x, position.y), - (position.x + size.width as i32, position.y), - (position.x, position.y + size.height as i32), - ( - position.x + size.width as i32, - position.y + size.height as i32, - ), - ] - .into_iter() - .any(|(x, y)| x >= left && x < right && y >= top && y < bottom) - } - - #[cfg(target_os = "linux")] - { - let Some(bounds) = self.raw_handle().physical_bounds() else { - return false; - }; - - let left = bounds.position().x() as i32; - let right = left + bounds.size().width() as i32; - let top = bounds.position().y() as i32; - let bottom = top + bounds.size().height() as i32; - - [ - (position.x, position.y), - (position.x + size.width as i32, position.y), - (position.x, position.y + size.height as i32), - ( - position.x + size.width as i32, - position.y + size.height as i32, - ), - ] - .into_iter() - .any(|(x, y)| x >= left && x < right && y >= top && y < bottom) - } - } -} - #[specta::specta] #[tauri::command(async)] #[instrument(skip(_window))] @@ -3575,23 +2700,6 @@ pub async fn apply_macos_liquid_glass_background( } } -#[specta::specta] -#[tauri::command(async)] -#[instrument(skip(_window))] -pub fn set_window_transparent(_window: tauri::Window, _value: bool) { - #[cfg(target_os = "macos")] - { - let ns_win = _window - .ns_window() - .expect("Failed to get native window handle") - as *const objc2_app_kit::NSWindow; - - unsafe { - (*ns_win).setOpaque(!_value); - } - } -} - #[derive(Default, Clone)] pub struct EditorWindowIds { pub ids: Arc>>, diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 9af8659e426..f357187828c 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -117,8 +117,16 @@ "ext": ["cap"], "name": "Cap Project", "mimeType": "application/x-cap-project", - "role": "Editor" + "role": "Editor", + "rank": "Owner", + "description": "Cap Project", + "exportedType": { + "identifier": "so.cap.desktop.project", + "conformsTo": ["com.apple.package"] + } } - ] + ], + "copyright": "Copyright (c) 2023-present Cap Software, Inc.", + "homepage": "https://cap.so/home" } } diff --git a/apps/desktop/src/app.tsx b/apps/desktop/src/app.tsx index d55ade5dd38..fd79525d15d 100644 --- a/apps/desktop/src/app.tsx +++ b/apps/desktop/src/app.tsx @@ -1,30 +1,19 @@ -import { Route, Router, useCurrentMatches } from "@solidjs/router"; +import { Route, Router } from "@solidjs/router"; import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"; -import { - getCurrentWebviewWindow, - type WebviewWindow, -} from "@tauri-apps/api/webviewWindow"; import { message } from "@tauri-apps/plugin-dialog"; -import { - createEffect, - createSignal, - lazy, - onCleanup, - onMount, - Suspense, -} from "solid-js"; +import { lazy, onMount, Suspense } from "solid-js"; import { Toaster } from "solid-toast"; import "@cap/ui-solid/main.css"; import "unfonts.css"; import "./styles/theme.css"; +import { createEventListener } from "@solid-primitives/event-listener"; import { CapErrorBoundary } from "./components/CapErrorBoundary"; import WindowChromeLayout from "./routes/(window-chrome)"; import SettingsLayout from "./routes/(window-chrome)/settings"; -import { generalSettingsStore } from "./store"; import { initAnonymousUser } from "./utils/analytics"; -import { type AppTheme, commands } from "./utils/tauri"; +import { AutoRevealWindowOnReady } from "./utils/RevealWindow"; import titlebar from "./utils/titlebar-state"; const NewMainPage = lazy(() => import("./routes/(window-chrome)/new-main")); @@ -79,7 +68,6 @@ const OnboardingPage = lazy( const UpgradePage = lazy(() => import("./routes/(window-chrome)/upgrade")); const UpdatePage = lazy(() => import("./routes/(window-chrome)/update")); const CameraPage = lazy(() => import("./routes/camera")); -const CaptureAreaPage = lazy(() => import("./routes/capture-area")); const DebugPage = lazy(() => import("./routes/debug")); const EditorPage = lazy(() => import("./routes/editor")); const InProgressRecordingPage = lazy( @@ -121,8 +109,11 @@ export default function App() { } function Inner() { - const currentWindow = getCurrentWebviewWindow(); - createThemeListener(currentWindow); + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)"); + const apply = () => + document.documentElement.classList.toggle("dark", prefersDark.matches); + apply(); + createEventListener(prefersDark, "change", apply); onMount(() => { initAnonymousUser(); @@ -151,40 +142,20 @@ function Inner() { /> { - const matches = useCurrentMatches(); - - onMount(() => { - let autoShow = true; - for (const match of matches()) { - if (match.route.info?.AUTO_SHOW_WINDOW === false) - autoShow = false; - } - - if ( - location.pathname === "/" || - location.pathname === "/camera" - ) { - return; - } - - if (autoShow) { - void currentWindow.show(); - void currentWindow.setFocus(); - } else { - // The route reveals itself after its first themed paint to - // avoid a load flash. Safety net so the window is never left - // hidden if that self-reveal never runs (e.g. chunk load error). - setTimeout(() => void currentWindow.show(), 2000); - } - }); - - return {props.children}; - }} + root={(props) => ( + + {props.children} + + + )} > - + @@ -220,14 +191,13 @@ function Inner() { - - - + + (); - let disposed = false; - let stopSettingsListening: (() => void) | undefined; - let stopThemeListening: (() => void) | undefined; - - createEffect(() => { - update(appTheme()); - }); - - onMount(() => { - void generalSettingsStore - .get() - .then((settings) => { - if (!disposed) setAppTheme(settings?.theme ?? null); - }) - .catch((error) => - console.error("Failed to load general settings:", error), - ); - - void generalSettingsStore - .listen((settings) => { - setAppTheme(settings?.theme ?? null); - }) - .then((unlisten) => { - if (disposed) { - unlisten(); - return; - } - stopSettingsListening = unlisten; - }) - .catch((error) => - console.error("Failed to listen to general settings:", error), - ); - - void currentWindow - .onThemeChanged(() => update(appTheme())) - .then((unlisten) => { - if (disposed) { - unlisten(); - return; - } - stopThemeListening = unlisten; - }) - .catch((error) => - console.error("Failed to listen to window theme changes:", error), - ); - }); - - onCleanup(() => { - disposed = true; - stopSettingsListening?.(); - stopThemeListening?.(); - }); - - function update(appTheme: AppTheme | null | undefined) { - if (location.pathname === "/camera") return; - - if (appTheme === undefined || appTheme === null) return; - - const isDark = - appTheme === "dark" || - (appTheme === "system" && - window.matchMedia("(prefers-color-scheme: dark)").matches); - - try { - if (appTheme === "system") { - localStorage.removeItem("cap-theme"); - } else { - localStorage.setItem("cap-theme", appTheme); - } - } catch {} - - commands.setTheme(appTheme).then(() => { - document.documentElement.classList.toggle("dark", isDark); - }); - } -} diff --git a/apps/desktop/src/components/CapErrorBoundary.tsx b/apps/desktop/src/components/CapErrorBoundary.tsx index 2dca68d1d47..6b3cf0a6fe3 100644 --- a/apps/desktop/src/components/CapErrorBoundary.tsx +++ b/apps/desktop/src/components/CapErrorBoundary.tsx @@ -2,11 +2,13 @@ import { Button } from "@cap/ui-solid"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { ErrorBoundary, type ParentProps } from "solid-js"; +import { maybeShowWindow } from "~/utils/RevealWindow"; export function CapErrorBoundary(props: ParentProps) { return ( { + maybeShowWindow(); console.error(e); return (
@@ -19,9 +21,7 @@ export function CapErrorBoundary(props: ParentProps) {

diff --git a/apps/desktop/src/components/Cropper.tsx b/apps/desktop/src/components/Cropper.tsx index 0dd42288fb9..476ba3c814e 100644 --- a/apps/desktop/src/components/Cropper.tsx +++ b/apps/desktop/src/components/Cropper.tsx @@ -125,8 +125,8 @@ function moveBounds( ): CropBounds { return { ...bounds, - x: x !== null ? Math.round(x) : bounds.x, - y: y !== null ? Math.round(y) : bounds.y, + x: x !== null ? x : bounds.x, + y: y !== null ? y : bounds.y, }; } @@ -139,10 +139,10 @@ function resizeBounds( const fromX = bounds.x + bounds.width * origin.x; const fromY = bounds.y + bounds.height * origin.y; return { - x: Math.round(fromX - newWidth * origin.x), - y: Math.round(fromY - newHeight * origin.y), - width: Math.round(newWidth), - height: Math.round(newHeight), + x: fromX - newWidth * origin.x, + y: fromY - newHeight * origin.y, + width: newWidth, + height: newHeight, }; } @@ -237,7 +237,6 @@ export function Cropper( onInteraction?: (interacting: boolean) => void; onContextMenu?: (event: PointerEvent) => void; ref?: CropperRef | ((ref: CropperRef) => void); - class?: string; minSize?: Vec2; maxSize?: Vec2; targetSize?: Vec2; @@ -247,6 +246,8 @@ export function Cropper( snapToRatioEnabled?: boolean; useBackdropFilter?: boolean; allowLightMode?: boolean; + enableAnimation?: boolean; + onAnimationFrame?: (bounds: CropBounds) => void; }>, ) { let containerRef: HTMLDivElement | undefined; @@ -255,6 +256,7 @@ export function Cropper( let occBottomRef: HTMLDivElement | undefined; let occLeftRef: HTMLDivElement | undefined; let occRightRef: HTMLDivElement | undefined; + let plusRef: HTMLDivElement | undefined; const resolvedChildren = children(() => props.children); @@ -430,6 +432,10 @@ export function Cropper( } function animateToRawBounds(target: CropBounds, durationMs = 240) { + if (props.enableAnimation === false) { + setRawBounds(target); + } + const start = displayRawBounds(); if ( target.x === start.x && @@ -490,8 +496,8 @@ export function Cropper( const startBoundsReal = initialCrop ?? { x: 0, y: 0, - width: Math.round(target.x / 2), - height: Math.round(target.y / 2), + width: target.x / 2, + height: target.y / 2, }; let bounds = boundsToRaw(startBoundsReal); @@ -584,11 +590,18 @@ export function Cropper( setContainerSize({ x: width, y: height }); - setRawBoundsConstraining(boundsToRaw(preservedReal)); + // Only reconcile against previously-preserved bounds once we've + // actually initialized — otherwise this clobbers rawBounds with a + // constrained CROP_ZERO before init() has a chance to compute the + // real starting crop, which is what caused the top-left flash. + if (initialized) { + setRawBoundsConstraining(boundsToRaw(preservedReal)); + } if (!initialized && width > 1 && height > 1) { initialized = true; init(); + setIsReady(true); } }; @@ -597,13 +610,10 @@ export function Cropper( ); updateContainerSize(containerRef.clientWidth, containerRef.clientHeight); - setDisplayRawBounds(rawBounds()); - function init() { const bounds = computeInitialBounds(); setRawBoundsConstraining(bounds); setDisplayRawBounds(bounds); - setIsReady(true); } if (props.ref) { @@ -737,6 +747,38 @@ export function Cropper( if (animationFrameId !== null) cancelAnimationFrame(animationFrameId); }); + function liveHandleFromPointer( + anchor: Vec2, + pointX: number, + pointY: number, + original: HandleSide, + ): HandleSide { + const left = pointX < anchor.x; + const top = pointY < anchor.y; + + if (original.isCorner) { + return ( + HANDLES.find( + (h) => h.isCorner && (h.x === "l") === left && (h.y === "t") === top, + ) ?? original + ); + } + // Side handles only flip along their own axis, and must stay a side + // (y === "c" for horizontal, x === "c" for vertical) — otherwise this + // matches a corner that happens to share the same x/y label. + if (original.x !== "c") { + return ( + HANDLES.find((h) => h.y === "c" && (h.x === "l") === left) ?? original + ); + } + if (original.y !== "c") { + return ( + HANDLES.find((h) => h.x === "c" && (h.y === "t") === top) ?? original + ); + } + return original; + } + function onHandlePointerDown(handle: HandleSide, e: PointerEvent) { if (!containerRef || e.button !== 0) return; const target = e.currentTarget as HTMLElement; @@ -817,6 +859,36 @@ export function Cropper( } } + const anchor = context.isAltMode + ? { + x: context.startBounds.x + context.startBounds.width / 2, + y: context.startBounds.y + context.startBounds.height / 2, + } + : { + x: + context.startBounds.x + + (context.originalHandle.movable.left + ? context.startBounds.width + : 0), + y: + context.startBounds.y + + (context.originalHandle.movable.top + ? context.startBounds.height + : 0), + }; + + const live = liveHandleFromPointer( + anchor, + pointX, + pointY, + context.originalHandle, + ); + if (mouseState.hoveringHandle !== live) { + // Always clone to avoid possibly mutating the state directly. + // Without cloning, the handles can become mutated and break the functionality. + setMouseState("hoveringHandle", { ...live }); + } + const { min, max } = rawSizeConstraint(); const shiftKey = e.shiftKey; const ratioValue = aspectState.value; @@ -1056,27 +1128,41 @@ export function Cropper( on(displayRawBounds, (b, _prevIn, prevFrameId) => { if (prevFrameId) cancelAnimationFrame(prevFrameId); return requestAnimationFrame(() => { + if (!isReady()) return; + const x = Math.round(b.x); + const y = Math.round(b.y); + const right = Math.round(b.x + b.width); + const bottom = Math.round(b.y + b.height); + const width = right - x; + const height = bottom - y; + if (regionRef) { - regionRef.style.width = `${Math.round(b.width)}px`; - regionRef.style.height = `${Math.round(b.height)}px`; - regionRef.style.transform = `translate(${Math.round(b.x)}px,${Math.round(b.y)}px)`; + regionRef.style.width = `${width}px`; + regionRef.style.height = `${height}px`; + regionRef.style.transform = `translate(${x}px,${y}px)`; } if (occLeftRef) { - occLeftRef.style.width = `${Math.max(0, Math.round(b.x))}px`; + occLeftRef.style.width = `${Math.max(0, x)}px`; } if (occRightRef) { - occRightRef.style.left = `${Math.round(b.x + b.width)}px`; + occRightRef.style.left = `${right}px`; } if (occTopRef) { - occTopRef.style.left = `${Math.round(b.x)}px`; - occTopRef.style.width = `${Math.round(b.width)}px`; - occTopRef.style.height = `${Math.max(0, Math.round(b.y))}px`; + occTopRef.style.left = `${x}px`; + occTopRef.style.width = `${width}px`; + occTopRef.style.height = `${Math.max(0, y)}px`; } if (occBottomRef) { - occBottomRef.style.top = `${Math.round(b.y + b.height)}px`; - occBottomRef.style.left = `${Math.round(b.x)}px`; - occBottomRef.style.width = `${Math.round(b.width)}px`; + occBottomRef.style.top = `${bottom}px`; + occBottomRef.style.left = `${x}px`; + occBottomRef.style.width = `${width}px`; + } + if (plusRef) { + const w = Math.round(b.x + b.width) - Math.round(b.x); + const h = Math.round(b.y + b.height) - Math.round(b.y); + plusRef.style.transform = `translate(${Math.round(w / 2) - 2}px,${Math.round(h / 2) - 2}px) translate(-50%,-50%)`; } + props.onAnimationFrame?.({ x, y, width, height }); }); }), ); @@ -1108,7 +1194,7 @@ export function Cropper( {(transform) => (
e.stopPropagation()} > @@ -1150,8 +1237,17 @@ export function Cropper( onPointerDown={onRegionPointerDown} /> - -
+ +
{ + plusRef = el; + const b = displayRawBounds(); + const w = Math.round(b.x + b.width) - Math.round(b.x); + const h = Math.round(b.y + b.height) - Math.round(b.y); + el.style.transform = `translate(${Math.round(w / 2) - 1}px,${Math.round(h / 2) - 1}px) translate(-50%,-50%)`; + }} + class="absolute top-0 left-0 opacity-70 pointer-events-none" + >
@@ -1420,10 +1516,10 @@ function computeAspectRatioResize( finalBounds = slideBoundsIntoContainer(finalBounds, container.x, container.y); return { - x: Math.round(finalBounds.x), - y: Math.round(finalBounds.y), - width: Math.round(Math.max(1, finalBounds.width)), - height: Math.round(Math.max(1, finalBounds.height)), + x: finalBounds.x, + y: finalBounds.y, + width: Math.max(1, finalBounds.width), + height: Math.max(1, finalBounds.height), }; } @@ -1454,8 +1550,11 @@ function computeFreeResize( const expTop = Math.min(distH, center.y); const expBottom = Math.min(distH, container.y - center.y); - let newW = expLeft + expRight; - let newH = expTop + expBottom; + const changesX = handle.movable.left || handle.movable.right; + const changesY = handle.movable.top || handle.movable.bottom; + + let newW = changesX ? expLeft + expRight : startBounds.width; + let newH = changesY ? expTop + expBottom : startBounds.height; if (min) { newW = Math.max(newW, min.x); @@ -1477,10 +1576,10 @@ function computeFreeResize( } bounds = { - x: Math.round(center.x - newW / 2), - y: Math.round(center.y - newH / 2), - width: Math.round(newW), - height: Math.round(newH), + x: center.x - newW / 2, + y: center.y - newH / 2, + width: newW, + height: newH, }; } else { const anchor = { @@ -1549,10 +1648,10 @@ function computeFreeResize( } bounds = { - x: Math.round(newX), - y: Math.round(newY), - width: Math.round(newW), - height: Math.round(newH), + x: newX, + y: newY, + width: newW, + height: newH, }; } return { bounds, snappedRatio }; diff --git a/apps/desktop/src/components/titlebar/Titlebar.tsx b/apps/desktop/src/components/titlebar/Titlebar.tsx index a6522d3c015..6aa62ac9ad1 100644 --- a/apps/desktop/src/components/titlebar/Titlebar.tsx +++ b/apps/desktop/src/components/titlebar/Titlebar.tsx @@ -25,7 +25,6 @@ export default function Titlebar() { style={{ height: titlebarState.height, }} - data-tauri-drag-region > {left() ? ( <> @@ -55,7 +54,7 @@ export function WindowControls(props: ComponentProps<"div">) { /> -
+
); diff --git a/apps/desktop/src/components/titlebar/controls/CaptionControlsMacOS.tsx b/apps/desktop/src/components/titlebar/controls/CaptionControlsMacOS.tsx deleted file mode 100644 index dd745fe0a89..00000000000 --- a/apps/desktop/src/components/titlebar/controls/CaptionControlsMacOS.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { getCurrentWindow } from "@tauri-apps/api/window"; -import { cx } from "cva"; -import { - type ComponentProps, - createSignal, - onCleanup, - onMount, - Show, - splitProps, -} from "solid-js"; - -export default function CaptionControlsMacOS( - props: ComponentProps<"div"> & { showMinimize?: boolean; showZoom?: boolean }, -) { - const [local, otherProps] = splitProps(props, [ - "class", - "showMinimize", - "showZoom", - ]); - const currentWindow = getCurrentWindow(); - const [focused, setFocus] = createSignal(true); - const [hovered, setHovered] = createSignal(false); - - let unlisten: (() => void) | undefined; - onMount(async () => { - unlisten = await currentWindow.onFocusChanged(({ payload: focused }) => - setFocus(focused), - ); - }); - onCleanup(() => unlisten?.()); - - const showMinimize = () => local.showMinimize ?? true; - const showZoom = () => local.showZoom ?? true; - - const handleClose = async () => { - currentWindow.close(); - }; - - return ( -
setHovered(true)} - onMouseLeave={() => setHovered(false)} - {...otherProps} - > - - - currentWindow.minimize()} - /> - - - currentWindow.toggleMaximize()} - /> - -
- ); -} - -interface TrafficLightButtonProps { - type: "close" | "minimize" | "zoom"; - focused: boolean; - hovered: boolean; - onClick: () => void; -} - -function TrafficLightButton(props: TrafficLightButtonProps) { - const colors = { - close: { - bg: "#FF5F57", - bgHover: "#FF5F57", - iconColor: "#4D0000", - }, - minimize: { - bg: "#FEBC2E", - bgHover: "#FEBC2E", - iconColor: "#995700", - }, - zoom: { - bg: "#28C840", - bgHover: "#28C840", - iconColor: "#006500", - }, - }; - - const color = () => colors[props.type]; - - return ( - - ); -} - -interface TrafficLightIconProps { - type: "close" | "minimize" | "zoom"; - color: string; -} - -function TrafficLightIcon(props: TrafficLightIconProps) { - return ( - - {props.type === "close" && ( - - )} - {props.type === "minimize" && ( - - )} - {props.type === "zoom" && ( - - )} - - ); -} diff --git a/apps/desktop/src/entry-server.tsx b/apps/desktop/src/entry-server.tsx index 7533dd90e73..66da03fc520 100644 --- a/apps/desktop/src/entry-server.tsx +++ b/apps/desktop/src/entry-server.tsx @@ -9,17 +9,6 @@ export default createHandler(() => ( -