diff --git a/.gitignore b/.gitignore index 2f7896d..b83d222 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -target/ +/target/ diff --git a/Cargo.lock b/Cargo.lock index 0b0b2cf..5abc856 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,7 +35,7 @@ dependencies = [ "atspi-common", "serde", "thiserror 1.0.69", - "zvariant 4.2.0", + "zvariant", ] [[package]] @@ -77,7 +77,7 @@ dependencies = [ "futures-lite", "futures-util", "serde", - "zbus 4.4.0", + "zbus", ] [[package]] @@ -431,11 +431,11 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zbus 4.4.0", + "zbus", "zbus-lockstep", "zbus-lockstep-macros", - "zbus_names 3.0.0", - "zvariant 4.2.0", + "zbus_names", + "zvariant", ] [[package]] @@ -447,7 +447,7 @@ dependencies = [ "atspi-common", "atspi-proxies", "futures-lite", - "zbus 4.4.0", + "zbus", ] [[package]] @@ -458,8 +458,8 @@ checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" dependencies = [ "atspi-common", "serde", - "zbus 4.4.0", - "zvariant 4.2.0", + "zbus", + "zvariant", ] [[package]] @@ -501,6 +501,7 @@ version = "0.1.0" dependencies = [ "ab_glyph", "anyhow", + "bitflags 2.11.0", "clap", "dirs", "eframe", @@ -510,7 +511,6 @@ dependencies = [ "image", "libc", "memmap2", - "notify-rust", "png 0.17.16", "rand", "serde", @@ -549,15 +549,6 @@ dependencies = [ "objc2 0.5.2", ] -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2 0.6.4", -] - [[package]] name = "blocking" version = "1.6.2" @@ -910,15 +901,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] - [[package]] name = "digest" version = "0.10.7" @@ -1481,7 +1463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ "rustix 1.1.4", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -1959,7 +1941,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -2019,18 +2001,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "mac-notification-sys" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" -dependencies = [ - "cc", - "objc2 0.6.4", - "objc2-foundation 0.3.2", - "time", -] - [[package]] name = "malloc_buf" version = "0.0.6" @@ -2205,26 +2175,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "notify-rust" -version = "4.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" -dependencies = [ - "futures-lite", - "log", - "mac-notification-sys", - "serde", - "tauri-winrt-notification", - "zbus 5.14.0", -] - -[[package]] -name = "num-conv" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" - [[package]] name = "num-traits" version = "0.2.19" @@ -2297,7 +2247,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.11.0", - "block2 0.5.1", + "block2", "libc", "objc2 0.5.2", "objc2-core-data", @@ -2326,7 +2276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.11.0", - "block2 0.5.1", + "block2", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", @@ -2338,7 +2288,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "block2 0.5.1", + "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2350,7 +2300,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.11.0", - "block2 0.5.1", + "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2385,7 +2335,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2 0.5.1", + "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -2397,7 +2347,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "block2 0.5.1", + "block2", "objc2 0.5.2", "objc2-contacts", "objc2-foundation 0.2.2", @@ -2416,7 +2366,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.11.0", - "block2 0.5.1", + "block2", "dispatch", "libc", "objc2 0.5.2", @@ -2429,8 +2379,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.11.0", - "block2 0.6.2", - "libc", "objc2 0.6.4", "objc2-core-foundation", ] @@ -2452,7 +2400,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "block2 0.5.1", + "block2", "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", @@ -2465,7 +2413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.11.0", - "block2 0.5.1", + "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2477,7 +2425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.11.0", - "block2 0.5.1", + "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -2500,7 +2448,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.11.0", - "block2 0.5.1", + "block2", "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", @@ -2520,7 +2468,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "block2 0.5.1", + "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2532,7 +2480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.11.0", - "block2 0.5.1", + "block2", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", @@ -2621,7 +2569,7 @@ dependencies = [ "libc", "redox_syscall 0.5.18", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -2789,12 +2737,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2860,15 +2802,6 @@ dependencies = [ "serde", ] -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", -] - [[package]] name = "quick-xml" version = "0.39.2" @@ -3315,18 +3248,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "tauri-winrt-notification" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" -dependencies = [ - "quick-xml 0.37.5", - "thiserror 2.0.18", - "windows 0.61.3", - "windows-version", -] - [[package]] name = "tempfile" version = "3.26.0" @@ -3389,25 +3310,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - [[package]] name = "tiny-skia" version = "0.11.4" @@ -3602,17 +3504,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "1.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" -dependencies = [ - "js-sys", - "serde_core", - "wasm-bindgen", -] - [[package]] name = "version_check" version = "0.9.5" @@ -4115,28 +4006,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - [[package]] name = "windows-core" version = "0.52.0" @@ -4152,37 +4021,13 @@ version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", -] - [[package]] name = "windows-implement" version = "0.58.0" @@ -4194,17 +4039,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "windows-interface" version = "0.58.0" @@ -4216,39 +4050,12 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - [[package]] name = "windows-result" version = "0.2.0" @@ -4258,34 +4065,16 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-strings" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result 0.2.0", + "windows-result", "windows-targets 0.52.6", ] -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-sys" version = "0.45.0" @@ -4337,7 +4126,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -4392,7 +4181,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.1", + "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -4403,24 +4192,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-version" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -4611,7 +4382,7 @@ dependencies = [ "android-activity", "atomic-waker", "bitflags 2.11.0", - "block2 0.5.1", + "block2", "bytemuck", "calloop 0.13.0", "cfg_aliases 0.2.1", @@ -4903,44 +4674,9 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros 4.4.0", - "zbus_names 3.0.0", - "zvariant 4.2.0", -] - -[[package]] -name = "zbus" -version = "5.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" -dependencies = [ - "async-broadcast", - "async-executor", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "libc", - "ordered-stream", - "rustix 1.1.4", - "serde", - "serde_repr", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow", - "zbus_macros 5.14.0", - "zbus_names 4.3.1", - "zvariant 5.10.0", + "zbus_macros", + "zbus_names", + "zvariant", ] [[package]] @@ -4950,7 +4686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" dependencies = [ "zbus_xml", - "zvariant 4.2.0", + "zvariant", ] [[package]] @@ -4964,7 +4700,7 @@ dependencies = [ "syn 2.0.117", "zbus-lockstep", "zbus_xml", - "zvariant 4.2.0", + "zvariant", ] [[package]] @@ -4977,22 +4713,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "zvariant_utils 2.1.0", -] - -[[package]] -name = "zbus_macros" -version = "5.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", - "zbus_names 4.3.1", - "zvariant 5.10.0", - "zvariant_utils 3.3.0", + "zvariant_utils", ] [[package]] @@ -5003,18 +4724,7 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant 4.2.0", -] - -[[package]] -name = "zbus_names" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" -dependencies = [ - "serde", - "winnow", - "zvariant 5.10.0", + "zvariant", ] [[package]] @@ -5026,8 +4736,8 @@ dependencies = [ "quick-xml 0.30.0", "serde", "static_assertions", - "zbus_names 3.0.0", - "zvariant 4.2.0", + "zbus_names", + "zvariant", ] [[package]] @@ -5120,21 +4830,7 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zvariant_derive 4.2.0", -] - -[[package]] -name = "zvariant" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" -dependencies = [ - "endi", - "enumflags2", - "serde", - "winnow", - "zvariant_derive 5.10.0", - "zvariant_utils 3.3.0", + "zvariant_derive", ] [[package]] @@ -5147,20 +4843,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "zvariant_utils 2.1.0", -] - -[[package]] -name = "zvariant_derive" -version = "5.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", - "zvariant_utils 3.3.0", + "zvariant_utils", ] [[package]] @@ -5173,16 +4856,3 @@ dependencies = [ "quote", "syn 2.0.117", ] - -[[package]] -name = "zvariant_utils" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.117", - "winnow", -] diff --git a/Cargo.toml b/Cargo.toml index 0356d59..967daab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ clap = { version = "4", features = ["derive"] } serde = { version = "1", features = ["derive"] } serde_json = "1" wl-clipboard-rs = "0.9.3" -notify-rust = "4" wayland-client = "0.31" wayland-protocols-wlr = { version = "0.2", features = ["client"] } wayland-protocols = { version = "0.31", features = ["client", "staging", "unstable"] } @@ -38,9 +37,16 @@ thiserror = "1" anyhow = "1" libc = "0.2" tempfile = "3" +bitflags = "2" gbm = "0.18" memmap2 = "0.9" ab_glyph = { version = "0.2", optional = true } eframe = { version = "0.29", features = ["wayland"], optional = true } egui = { version = "0.29", optional = true } egui_extras = { version = "0.29", features = ["image"], optional = true } + +[profile.release] +lto = "fat" +codegen-units = 1 +strip = true +panic = "abort" diff --git a/protocols/hyprland-toplevel-export-v1.xml b/protocols/hyprland-toplevel-export-v1.xml new file mode 100644 index 0000000..b1185aa --- /dev/null +++ b/protocols/hyprland-toplevel-export-v1.xml @@ -0,0 +1,228 @@ + + + + Copyright © 2022 Vaxry + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + + This protocol allows clients to ask for exporting another toplevel's + surface(s) to a buffer. + + Particularly useful for sharing a single window. + + + + + This object is a manager which offers requests to start capturing from a + source. + + + + + Capture the next frame of a toplevel. (window) + + The captured frame will not contain any server-side decorations and will + ignore the compositor-set geometry, like e.g. rounded corners. + + It will contain all the subsurfaces and popups, however the latter will be clipped + to the geometry of the base surface. + + The handle parameter refers to the address of the window as seen in `hyprctl clients`. + For example, for d161e7b0 it would be 3512854448. + + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + Same as capture_toplevel, but with a zwlr_foreign_toplevel_handle_v1 handle. + + + + + + + + + + + This object represents a single frame. + + When created, a series of buffer events will be sent, each representing a + supported buffer type. The "buffer_done" event is sent afterwards to + indicate that all supported buffer types have been enumerated. The client + will then be able to send a "copy" request. If the capture is successful, + the compositor will send a "flags" followed by a "ready" event. + + wl_shm buffers are always supported, ie. the "buffer" event is guaranteed to be sent. + + If the capture failed, the "failed" event is sent. This can happen anytime + before the "ready" event. + + Once either a "ready" or a "failed" event is received, the client should + destroy the frame. + + + + + Provides information about wl_shm buffer parameters that need to be + used for this frame. This event is sent once after the frame is created + if wl_shm buffers are supported. + + + + + + + + + + Copy the frame to the supplied buffer. The buffer must have the + correct size, see hyprland_toplevel_export_frame_v1.buffer and + hyprland_toplevel_export_frame_v1.linux_dmabuf. The buffer needs to have a + supported format. + + If the frame is successfully copied, a "flags" and a "ready" event is + sent. Otherwise, a "failed" event is sent. + + This event will wait for appropriate damage to be copied, unless the ignore_damage + arg is set to a non-zero value. + + + + + + + + This event is sent right before the ready event when ignore_damage was + not set. It may be generated multiple times for each copy + request. + + The arguments describe a box around an area that has changed since the + last copy request that was derived from the current screencopy manager + instance. + + The union of all regions received between the call to copy + and a ready event is the total damage since the prior ready event. + + + + + + + + + + + + + + + + + + + Provides flags about the frame. This event is sent once before the + "ready" event. + + + + + + + Called as soon as the frame is copied, indicating it is available + for reading. This event includes the time at which presentation happened + at. + + The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples, + each component being an unsigned 32-bit value. Whole seconds are in + tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo, + and the additional fractional part in tv_nsec as nanoseconds. Hence, + for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part + may have an arbitrary offset at start. + + After receiving this event, the client should destroy the object. + + + + + + + + + This event indicates that the attempted frame copy has failed. + + After receiving this event, the client should destroy the object. + + + + + + Destroys the frame. This request can be sent at any time by the client. + + + + + + Provides information about linux-dmabuf buffer parameters that need to + be used for this frame. This event is sent once after the frame is + created if linux-dmabuf buffers are supported. + + + + + + + + + This event is sent once after all buffer events have been sent. + + The client should proceed to create a buffer of one of the supported + types, and send a "copy" request. + + + + diff --git a/src/capture.rs b/src/capture.rs index 5be796e..2d05ec4 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -314,60 +314,51 @@ impl AppState { } fn run_capture_all(self, opts: &CaptureOptions) -> Result { + // Hyprland IPC path if let Ok(mons) = crate::hyprland::monitors() { if !mons.is_empty() { let logical_total_w = mons .iter() .map(|m| m.x + (m.width as f64 / m.scale).round() as i64) .max() - .unwrap_or(0) as u32; + .unwrap_or(0) + .max(1) as u32; let logical_total_h = mons .iter() .map(|m| m.y + (m.height as f64 / m.scale).round() as i64) .max() - .unwrap_or(0) as u32; + .unwrap_or(0) + .max(1) as u32; + + // Capture all monitors in parallel — each thread opens its own + // Wayland connection. This turns N sequential captures (each + // gated on a compositor roundtrip) into N concurrent ones. + let captures: Vec>> = + std::thread::scope(|s| { + let handles: Vec<_> = mons + .iter() + .map(|mon| { + s.spawn(move || { + let lw = (mon.width as f64 / mon.scale).round() as u32; + let lh = (mon.height as f64 / mon.scale).round() as u32; + capture_and_resize(&mon.name, opts, lw, lh, false) + }) + }) + .collect(); + handles.into_iter().map(|h| h.join().unwrap()).collect() + }); + let mut canvas = image::RgbaImage::new(logical_total_w, logical_total_h); - for mon in &mons { - let state = AppState::connect()?; - let out = state - .outputs - .iter() - .find(|o| o.name == mon.name) - .map(|o| o.handle.clone()); - let out = match out { - Some(o) => o, - None => continue, - }; - let overlay = if opts.include_cursor { 1 } else { 0 }; - let mut state = state; - let frame = state.screencopy_mgr.as_ref().unwrap().capture_output( - overlay, - &out, - &state.qh, - (), - ); - state.frame = Some(frame); - state.frame_state = FrameState::Negotiating; - state.dispatch_to_ready()?; - let mon_img = state.read_pixels()?; - let logical_w = (mon.width as f64 / mon.scale).round() as u32; - let logical_h = (mon.height as f64 / mon.scale).round() as u32; - let scaled = mon_img.resize_exact( - logical_w, - logical_h, - image::imageops::FilterType::Lanczos3, - ); - image::imageops::overlay( - &mut canvas, - &scaled.to_rgba8(), - mon.x as i64, - mon.y as i64, - ); + for (mon, c) in mons.iter().zip(captures.into_iter()) { + if let Some(img) = c? { + image::imageops::overlay(&mut canvas, &img, mon.x as i64, mon.y as i64); + } } return Ok(image::DynamicImage::ImageRgba8(canvas)); } } + // Fallback: Wayland xdg_output info from the current connection. if self.outputs.is_empty() { return Err(BlastError::Capture("no monitors found".into())); } @@ -385,7 +376,6 @@ impl AppState { .max() .unwrap_or(0) .max(1) as u32; - let mut canvas = image::RgbaImage::new(logical_total_w, logical_total_h); let out_info: Vec<(String, i32, i32, i32, i32)> = self .outputs @@ -400,35 +390,26 @@ impl AppState { ) }) .collect(); + // Close this connection before spawning per-thread connections. + drop(self); - for (name, lx, ly, lw, lh) in &out_info { - let state = AppState::connect()?; - let out = state - .outputs + let captures: Vec>> = std::thread::scope(|s| { + let handles: Vec<_> = out_info .iter() - .find(|o| &o.name == name) - .or_else(|| state.outputs.first()) - .map(|o| o.handle.clone()); - let out = match out { - Some(o) => o, - None => continue, - }; - let overlay = if opts.include_cursor { 1 } else { 0 }; - let mut state = state; - let frame = - state - .screencopy_mgr - .as_ref() - .unwrap() - .capture_output(overlay, &out, &state.qh, ()); - state.frame = Some(frame); - state.frame_state = FrameState::Negotiating; - state.dispatch_to_ready()?; - let mon_img = state.read_pixels()?; - let lw = (*lw as u32).max(1); - let lh = (*lh as u32).max(1); - let scaled = mon_img.resize_exact(lw, lh, image::imageops::FilterType::Lanczos3); - image::imageops::overlay(&mut canvas, &scaled.to_rgba8(), *lx as i64, *ly as i64); + .map(|(name, _lx, _ly, lw, lh)| { + let lw = (*lw as u32).max(1); + let lh = (*lh as u32).max(1); + s.spawn(move || capture_and_resize(name, opts, lw, lh, true)) + }) + .collect(); + handles.into_iter().map(|h| h.join().unwrap()).collect() + }); + + let mut canvas = image::RgbaImage::new(logical_total_w, logical_total_h); + for ((_, lx, ly, _, _), c) in out_info.iter().zip(captures.into_iter()) { + if let Some(img) = c? { + image::imageops::overlay(&mut canvas, &img, *lx as i64, *ly as i64); + } } Ok(image::DynamicImage::ImageRgba8(canvas)) @@ -636,92 +617,22 @@ impl AppState { .map_err(|e| BlastError::Capture(format!("shm read: {e}")))?; let (width, height, stride) = (offer.width, offer.height, offer.stride); - let mut rgba: Vec = Vec::with_capacity((width * height * 4) as usize); - // All formats below are packed 32-bit little-endian values. - // The DRM name describes channel order from MSB to LSB, so in memory - // (little-endian) the channel order is reversed. - // e.g. ARGB8888 = [31:0] A:R:G:B -> memory bytes [B, G, R, A] - // ABGR8888 = [31:0] A:B:G:R -> memory bytes [R, G, B, A] - match offer.format { - wl_shm::Format::Argb8888 => { - // memory: [B, G, R, A] - for row in 0..height { - for col in 0..width { - let base = (row * stride + col * 4) as usize; - rgba.push(raw[base + 2]); // R - rgba.push(raw[base + 1]); // G - rgba.push(raw[base]); // B - rgba.push(raw[base + 3]); // A - } - } - } - wl_shm::Format::Xrgb8888 => { - // memory: [B, G, R, X] - for row in 0..height { - for col in 0..width { - let base = (row * stride + col * 4) as usize; - rgba.push(raw[base + 2]); // R - rgba.push(raw[base + 1]); // G - rgba.push(raw[base]); // B - rgba.push(255); // A - } - } - } - wl_shm::Format::Abgr8888 => { - // memory: [R, G, B, A] - for row in 0..height { - for col in 0..width { - let base = (row * stride + col * 4) as usize; - rgba.push(raw[base]); // R - rgba.push(raw[base + 1]); // G - rgba.push(raw[base + 2]); // B - rgba.push(raw[base + 3]); // A - } - } - } - wl_shm::Format::Xbgr8888 => { - // memory: [R, G, B, X] - for row in 0..height { - for col in 0..width { - let base = (row * stride + col * 4) as usize; - rgba.push(raw[base]); // R - rgba.push(raw[base + 1]); // G - rgba.push(raw[base + 2]); // B - rgba.push(255); // A - } - } - } - wl_shm::Format::Rgba8888 => { - // memory: [A, B, G, R] - for row in 0..height { - for col in 0..width { - let base = (row * stride + col * 4) as usize; - rgba.push(raw[base + 3]); // R - rgba.push(raw[base + 2]); // G - rgba.push(raw[base + 1]); // B - rgba.push(raw[base]); // A - } - } - } - wl_shm::Format::Bgra8888 => { - // memory: [A, R, G, B] - for row in 0..height { - for col in 0..width { - let base = (row * stride + col * 4) as usize; - rgba.push(raw[base + 1]); // R - rgba.push(raw[base + 2]); // G - rgba.push(raw[base + 3]); // B - rgba.push(raw[base]); // A - } - } - } + // DRM name describes channels MSB->LSB, so little-endian memory order is reversed. + // ARGB8888 mem = [B, G, R, A], XBGR8888 mem = [R, G, B, X], etc. + let rgba = match offer.format { + wl_shm::Format::Argb8888 => swizzle8::<2, 1, 0, 3>(&raw, width, height, stride), + wl_shm::Format::Xrgb8888 => swizzle8::<2, 1, 0, 4>(&raw, width, height, stride), + wl_shm::Format::Abgr8888 => swizzle8::<0, 1, 2, 3>(&raw, width, height, stride), + wl_shm::Format::Xbgr8888 => swizzle8::<0, 1, 2, 4>(&raw, width, height, stride), + wl_shm::Format::Rgba8888 => swizzle8::<3, 2, 1, 0>(&raw, width, height, stride), + wl_shm::Format::Bgra8888 => swizzle8::<1, 2, 3, 0>(&raw, width, height, stride), other => { return Err(BlastError::Capture(format!( "unhandled pixel format {other:?} ->>> please report this" ))); } - } + }; let img: ImageBuffer, Vec> = ImageBuffer::from_raw(width, height, rgba) .ok_or_else(|| BlastError::Capture("image buffer construction failed".into()))?; @@ -729,6 +640,61 @@ impl AppState { } } +// Multi-monitor capture helpers + +/// Capture a single output by name and resize the result to (target_w, target_h), +/// skipping the resize entirely when dimensions already match. Triangle filter +/// is used when a resize is needed — it's ~10x faster than Lanczos3 and the +/// quality difference is invisible on screenshots. +/// +/// `fallback_to_first` mirrors a quirk of the non-Hyprland code path: if the +/// named output isn't found, use the first available output instead. The +/// Hyprland path returns `Ok(None)` instead (silent skip). +fn capture_and_resize( + name: &str, + opts: &CaptureOptions, + target_w: u32, + target_h: u32, + fallback_to_first: bool, +) -> Result> { + let state = AppState::connect()?; + let out_handle = { + let found = state.outputs.iter().find(|o| o.name == name); + let chosen = if fallback_to_first { + found.or_else(|| state.outputs.first()) + } else { + found + }; + match chosen.map(|o| o.handle.clone()) { + Some(h) => h, + None => return Ok(None), + } + }; + + let mut state = state; + let overlay = if opts.include_cursor { 1 } else { 0 }; + let frame = state + .screencopy_mgr + .as_ref() + .unwrap() + .capture_output(overlay, &out_handle, &state.qh, ()); + state.frame = Some(frame); + state.frame_state = FrameState::Negotiating; + state.dispatch_to_ready()?; + let mon_img = state.read_pixels()?; + + let tw = target_w.max(1); + let th = target_h.max(1); + let scaled = if mon_img.width() == tw && mon_img.height() == th { + mon_img.to_rgba8() + } else { + mon_img + .resize_exact(tw, th, image::imageops::FilterType::Triangle) + .to_rgba8() + }; + Ok(Some(scaled)) +} + // DMA-buf capture helpers /// Try to allocate a LINEAR GBM buffer for the screencopy DMA-buf offer, /// create a wl_buffer, and kick off the copy. Returns true on success. @@ -806,7 +772,7 @@ fn try_alloc_capture_dmabuf(state: &mut AppState) -> bool { } /// Walk /dev/dri/renderD128..135 and return the first one we can open. -fn find_drm_render_node() -> Option { +pub(crate) fn find_drm_render_node() -> Option { for n in 128..=135u32 { if let Ok(f) = std::fs::OpenOptions::new() .read(true) @@ -832,116 +798,112 @@ const DRM_FORMAT_ARGB2101010: u32 = 0x30335241; // fourcc('A','R','3','0') const DRM_FORMAT_XBGR2101010: u32 = 0x30334258; // fourcc('X','B','3','0') const DRM_FORMAT_ABGR2101010: u32 = 0x30334241; // fourcc('A','B','3','0') +/// Swizzle a packed 32-bit-per-pixel buffer to tightly-packed RGBA8. +/// +/// Const params R/G/B/A select the source byte index (0..=3) for each output +/// channel. Pass `A = 4` to force opaque alpha (for X-formats). +/// +/// Pre-allocates the output to exact size and walks rows with `chunks_exact_mut`, +/// which lets LLVM autovectorize the inner permutation. Replaces the per-byte +/// `Vec::push` loop that ran ~10x slower. +#[inline] +fn swizzle8( + raw: &[u8], + width: u32, + height: u32, + stride: u32, +) -> Vec { + let w = width as usize; + let h = height as usize; + let s = stride as usize; + let row_bytes = w * 4; + let mut out = vec![0u8; row_bytes * h]; + for y in 0..h { + let src = &raw[y * s..y * s + row_bytes]; + let dst = &mut out[y * row_bytes..(y + 1) * row_bytes]; + for (sp, dp) in src.chunks_exact(4).zip(dst.chunks_exact_mut(4)) { + dp[0] = sp[R]; + dp[1] = sp[G]; + dp[2] = sp[B]; + // A == 4 is the sentinel for "force opaque" (X-formats). + // Monomorphization eliminates the branch. + dp[3] = if A == 4 { 0xFF } else { sp[A] }; + } + } + out +} + +/// 10-bit packed -> RGBA8 with R in the high channel position (ARGB2101010-family). +/// Bits: [31:30]=A/X [29:20]=R [19:10]=G [9:0]=B. +#[inline] +fn pack10_argb_to_rgba8(raw: &[u8], width: u32, height: u32, stride: u32) -> Vec { + let w = width as usize; + let h = height as usize; + let s = stride as usize; + let row_bytes = w * 4; + let mut out = vec![0u8; row_bytes * h]; + for y in 0..h { + let src = &raw[y * s..y * s + row_bytes]; + let dst = &mut out[y * row_bytes..(y + 1) * row_bytes]; + for (sp, dp) in src.chunks_exact(4).zip(dst.chunks_exact_mut(4)) { + let p = u32::from_le_bytes([sp[0], sp[1], sp[2], sp[3]]); + dp[0] = (((p >> 20) & 0x3FF) >> 2) as u8; + dp[1] = (((p >> 10) & 0x3FF) >> 2) as u8; + dp[2] = ((p & 0x3FF) >> 2) as u8; + dp[3] = 0xFF; + } + } + out +} + +/// 10-bit packed -> RGBA8 with B in the high channel position (ABGR2101010-family). +#[inline] +fn pack10_abgr_to_rgba8(raw: &[u8], width: u32, height: u32, stride: u32) -> Vec { + let w = width as usize; + let h = height as usize; + let s = stride as usize; + let row_bytes = w * 4; + let mut out = vec![0u8; row_bytes * h]; + for y in 0..h { + let src = &raw[y * s..y * s + row_bytes]; + let dst = &mut out[y * row_bytes..(y + 1) * row_bytes]; + for (sp, dp) in src.chunks_exact(4).zip(dst.chunks_exact_mut(4)) { + let p = u32::from_le_bytes([sp[0], sp[1], sp[2], sp[3]]); + dp[0] = ((p & 0x3FF) >> 2) as u8; + dp[1] = (((p >> 10) & 0x3FF) >> 2) as u8; + dp[2] = (((p >> 20) & 0x3FF) >> 2) as u8; + dp[3] = 0xFF; + } + } + out +} + /// Decode a LINEAR DMA-buf pixel buffer into RGBA8 using the DRM fourcc format. /// Supports the common 8-bit and 10-bit packed formats the compositor may offer. -fn decode_drm_pixels( +pub(crate) fn decode_drm_pixels( raw: &[u8], width: u32, height: u32, stride: u32, fourcc: u32, ) -> Result> { - let mut rgba: Vec = Vec::with_capacity((width * height * 4) as usize); - match fourcc { - DRM_FORMAT_ARGB8888 => { - for row in 0..height { - for col in 0..width { - let b = (row * stride + col * 4) as usize; - rgba.push(raw[b + 2]); - rgba.push(raw[b + 1]); - rgba.push(raw[b]); - rgba.push(raw[b + 3]); - } - } - } - DRM_FORMAT_XRGB8888 => { - for row in 0..height { - for col in 0..width { - let b = (row * stride + col * 4) as usize; - rgba.push(raw[b + 2]); - rgba.push(raw[b + 1]); - rgba.push(raw[b]); - rgba.push(255); - } - } - } - DRM_FORMAT_ABGR8888 => { - for row in 0..height { - for col in 0..width { - let b = (row * stride + col * 4) as usize; - rgba.push(raw[b]); - rgba.push(raw[b + 1]); - rgba.push(raw[b + 2]); - rgba.push(raw[b + 3]); - } - } - } - DRM_FORMAT_XBGR8888 => { - for row in 0..height { - for col in 0..width { - let b = (row * stride + col * 4) as usize; - rgba.push(raw[b]); - rgba.push(raw[b + 1]); - rgba.push(raw[b + 2]); - rgba.push(255); - } - } - } - DRM_FORMAT_RGBA8888 => { - for row in 0..height { - for col in 0..width { - let b = (row * stride + col * 4) as usize; - rgba.push(raw[b + 3]); - rgba.push(raw[b + 2]); - rgba.push(raw[b + 1]); - rgba.push(raw[b]); - } - } - } - DRM_FORMAT_BGRA8888 => { - for row in 0..height { - for col in 0..width { - let b = (row * stride + col * 4) as usize; - rgba.push(raw[b + 1]); - rgba.push(raw[b + 2]); - rgba.push(raw[b + 3]); - rgba.push(raw[b]); - } - } - } + DRM_FORMAT_ARGB8888 => Ok(swizzle8::<2, 1, 0, 3>(raw, width, height, stride)), + DRM_FORMAT_XRGB8888 => Ok(swizzle8::<2, 1, 0, 4>(raw, width, height, stride)), + DRM_FORMAT_ABGR8888 => Ok(swizzle8::<0, 1, 2, 3>(raw, width, height, stride)), + DRM_FORMAT_XBGR8888 => Ok(swizzle8::<0, 1, 2, 4>(raw, width, height, stride)), + DRM_FORMAT_RGBA8888 => Ok(swizzle8::<3, 2, 1, 0>(raw, width, height, stride)), + DRM_FORMAT_BGRA8888 => Ok(swizzle8::<1, 2, 3, 0>(raw, width, height, stride)), DRM_FORMAT_XRGB2101010 | DRM_FORMAT_ARGB2101010 => { - for row in 0..height { - for col in 0..width { - let b = (row * stride + col * 4) as usize; - let p = u32::from_le_bytes([raw[b], raw[b + 1], raw[b + 2], raw[b + 3]]); - rgba.push((((p >> 20) & 0x3FF) >> 2) as u8); - rgba.push((((p >> 10) & 0x3FF) >> 2) as u8); - rgba.push(((p & 0x3FF) >> 2) as u8); - rgba.push(255); - } - } + Ok(pack10_argb_to_rgba8(raw, width, height, stride)) } DRM_FORMAT_XBGR2101010 | DRM_FORMAT_ABGR2101010 => { - for row in 0..height { - for col in 0..width { - let b = (row * stride + col * 4) as usize; - let p = u32::from_le_bytes([raw[b], raw[b + 1], raw[b + 2], raw[b + 3]]); - rgba.push(((p & 0x3FF) >> 2) as u8); - rgba.push((((p >> 10) & 0x3FF) >> 2) as u8); - rgba.push((((p >> 20) & 0x3FF) >> 2) as u8); - rgba.push(255); - } - } - } - other => { - return Err(BlastError::Capture(format!( - "unhandled DRM format 0x{other:08x} in dmabuf capture -> please report this" - ))); + Ok(pack10_abgr_to_rgba8(raw, width, height, stride)) } + other => Err(BlastError::Capture(format!( + "unhandled DRM format 0x{other:08x} in dmabuf capture -> please report this" + ))), } - - Ok(rgba) } // Wayland dispatch @@ -1112,7 +1074,7 @@ delegate_noop!(AppState: ignore ZwpLinuxDmabufV1); delegate_noop!(AppState: ignore ZwpLinuxBufferParamsV1); delegate_noop!(AppState: ignore ZxdgOutputManagerV1); -fn create_shm_file(size: usize) -> std::io::Result { +pub(crate) fn create_shm_file(size: usize) -> std::io::Result { #[cfg(target_os = "linux")] { use std::ffi::CStr; diff --git a/src/gui/annotations.rs b/src/gui/annotations.rs index 2396faa..01fd7da 100644 --- a/src/gui/annotations.rs +++ b/src/gui/annotations.rs @@ -43,75 +43,350 @@ impl Tool { } } -// Annotation data +// Annotation trait + concrete types -#[derive(Debug, Clone)] -pub enum Annotation { - Rect { - r: [f32; 4], // x0,y0,x1,y1 in image-space - color: Color32, - thickness: f32, - filled: bool, - }, - Arrow { - from: [f32; 2], - to: [f32; 2], - color: Color32, - thickness: f32, - }, - Text { - pos: [f32; 2], - text: String, - color: Color32, - size: f32, - }, - Highlight { - r: [f32; 4], - color: Color32, - }, - Pixelate { - r: [f32; 4], - block: u32, - }, - Pencil { - points: Vec<[f32; 2]>, - color: Color32, - thickness: f32, - }, +type ToScreen<'a> = &'a dyn Fn(Pos2) -> Pos2; + +pub trait Annotation: std::fmt::Debug { + fn hit_test(&self, p: Pos2, tolerance: f32) -> bool; + fn translate(&mut self, delta: egui::Vec2); + fn paint( + &self, + painter: &egui::Painter, + to_screen: ToScreen<'_>, + image: &DynamicImage, + display_scale: f32, + ); + fn paint_selection(&self, painter: &egui::Painter, to_screen: ToScreen<'_>); + fn flatten(&self, img: &mut image::RgbaImage, iw: u32, ih: u32); + fn as_text(&self) -> Option<&TextAnn> { + None + } } -impl Annotation { - /// Returns true if the image-space point "p" hits this annotation. - pub fn hit_test(&self, p: Pos2, tolerance: f32) -> bool { - match self { - Annotation::Rect { r, .. } - | Annotation::Highlight { r, .. } - | Annotation::Pixelate { r, .. } => { - let outer = Rect::from_min_max( - Pos2::new(r[0] - tolerance, r[1] - tolerance), - Pos2::new(r[2] + tolerance, r[3] + tolerance), - ); - outer.contains(p) - } - Annotation::Arrow { from, to, .. } => { - point_seg_dist(p, Pos2::new(from[0], from[1]), Pos2::new(to[0], to[1])) < tolerance - } - Annotation::Text { - pos, text, size, .. - } => { - // Estimate bounding box: ~0.55 * size per character wide, size tall - let est_w = text.len() as f32 * size * 0.55; - let est_h = *size; - let r = Rect::from_min_max( - Pos2::new(pos[0] - tolerance, pos[1] - tolerance), - Pos2::new(pos[0] + est_w + tolerance, pos[1] + est_h + tolerance), - ); - r.contains(p) - } - Annotation::Pencil { points, .. } => points.windows(2).any(|w| { - point_seg_dist(p, Pos2::new(w[0][0], w[0][1]), Pos2::new(w[1][0], w[1][1])) - < tolerance - }), +#[derive(Debug, Clone)] +pub struct RectAnn { + pub r: [f32; 4], // x0,y0,x1,y1 in image-space + pub color: Color32, + pub thickness: f32, + pub filled: bool, +} + +#[derive(Debug, Clone)] +pub struct ArrowAnn { + pub from: [f32; 2], + pub to: [f32; 2], + pub color: Color32, + pub thickness: f32, +} + +#[derive(Debug, Clone)] +pub struct TextAnn { + pub pos: [f32; 2], + pub text: String, + pub color: Color32, + pub size: f32, +} + +#[derive(Debug, Clone)] +pub struct HighlightAnn { + pub r: [f32; 4], + pub color: Color32, +} + +#[derive(Debug, Clone)] +pub struct PixelateAnn { + pub r: [f32; 4], + pub block: u32, +} + +#[derive(Debug, Clone)] +pub struct PencilAnn { + pub points: Vec<[f32; 2]>, + pub color: Color32, + pub thickness: f32, +} + +// Hit-test / translate / paint impls + +fn rect_hit(r: &[f32; 4], p: Pos2, tolerance: f32) -> bool { + Rect::from_min_max( + Pos2::new(r[0] - tolerance, r[1] - tolerance), + Pos2::new(r[2] + tolerance, r[3] + tolerance), + ) + .contains(p) +} + +fn rect_translate(r: &mut [f32; 4], delta: egui::Vec2) { + r[0] += delta.x; + r[1] += delta.y; + r[2] += delta.x; + r[3] += delta.y; +} + +fn rect_selection(r: &[f32; 4], painter: &egui::Painter, to_screen: ToScreen<'_>) { + let dash = Color32::from_rgba_unmultiplied(255, 220, 0, 220); + let tl = to_screen(Pos2::new(r[0] - 3.0, r[1] - 3.0)); + let br = to_screen(Pos2::new(r[2] + 3.0, r[3] + 3.0)); + painter.rect_stroke(Rect::from_min_max(tl, br), 0.0, Stroke::new(1.5, dash)); +} + +impl Annotation for RectAnn { + fn hit_test(&self, p: Pos2, tolerance: f32) -> bool { + rect_hit(&self.r, p, tolerance) + } + fn translate(&mut self, delta: egui::Vec2) { + rect_translate(&mut self.r, delta); + } + fn paint( + &self, + painter: &egui::Painter, + to_screen: ToScreen<'_>, + _image: &DynamicImage, + _display_scale: f32, + ) { + let tl = to_screen(Pos2::new(self.r[0], self.r[1])); + let br = to_screen(Pos2::new(self.r[2], self.r[3])); + let rect = Rect::from_min_max(tl, br); + if self.filled { + painter.rect_filled(rect, 0.0, self.color); + } else { + painter.rect_stroke(rect, 0.0, Stroke::new(self.thickness, self.color)); + } + } + fn paint_selection(&self, painter: &egui::Painter, to_screen: ToScreen<'_>) { + rect_selection(&self.r, painter, to_screen); + } + fn flatten(&self, img: &mut image::RgbaImage, iw: u32, ih: u32) { + let x0 = self.r[0].round() as i32; + let y0 = self.r[1].round() as i32; + let x1 = self.r[2].round() as i32; + let y1 = self.r[3].round() as i32; + let t = self.thickness.round().max(1.0) as i32; + let c = to_rgba(self.color); + if self.filled { + fill_rect_img(img, x0, y0, x1, y1, c, iw, ih); + } else { + draw_rect_border(img, x0, y0, x1, y1, t, c, iw, ih); + } + } +} + +impl Annotation for ArrowAnn { + fn hit_test(&self, p: Pos2, tolerance: f32) -> bool { + point_seg_dist( + p, + Pos2::new(self.from[0], self.from[1]), + Pos2::new(self.to[0], self.to[1]), + ) < tolerance + } + fn translate(&mut self, delta: egui::Vec2) { + self.from[0] += delta.x; + self.from[1] += delta.y; + self.to[0] += delta.x; + self.to[1] += delta.y; + } + fn paint( + &self, + painter: &egui::Painter, + to_screen: ToScreen<'_>, + _image: &DynamicImage, + _display_scale: f32, + ) { + let sp = to_screen(Pos2::new(self.from[0], self.from[1])); + let ep = to_screen(Pos2::new(self.to[0], self.to[1])); + painter.arrow(sp, ep - sp, Stroke::new(self.thickness, self.color)); + } + fn paint_selection(&self, painter: &egui::Painter, to_screen: ToScreen<'_>) { + let dash = Color32::from_rgba_unmultiplied(255, 220, 0, 220); + painter.circle_stroke( + to_screen(Pos2::new(self.from[0], self.from[1])), + 5.0, + Stroke::new(1.5, dash), + ); + painter.circle_stroke( + to_screen(Pos2::new(self.to[0], self.to[1])), + 5.0, + Stroke::new(1.5, dash), + ); + } + fn flatten(&self, img: &mut image::RgbaImage, iw: u32, ih: u32) { + let c = to_rgba(self.color); + draw_line_img( + img, self.from[0], self.from[1], self.to[0], self.to[1], self.thickness, c, iw, ih, + ); + draw_arrowhead( + img, + self.from[0], + self.from[1], + self.to[0], + self.to[1], + self.thickness * 4.0, + c, + iw, + ih, + ); + } +} + +impl Annotation for TextAnn { + fn hit_test(&self, p: Pos2, tolerance: f32) -> bool { + // Estimate bounding box: ~0.55 * size per character wide, size tall + let est_w = self.text.len() as f32 * self.size * 0.55; + let est_h = self.size; + let r = Rect::from_min_max( + Pos2::new(self.pos[0] - tolerance, self.pos[1] - tolerance), + Pos2::new( + self.pos[0] + est_w + tolerance, + self.pos[1] + est_h + tolerance, + ), + ); + r.contains(p) + } + fn translate(&mut self, delta: egui::Vec2) { + self.pos[0] += delta.x; + self.pos[1] += delta.y; + } + fn paint( + &self, + painter: &egui::Painter, + to_screen: ToScreen<'_>, + _image: &DynamicImage, + display_scale: f32, + ) { + // size is in image-space pixels; multiply by display_scale -> screen logical pts + let screen_size = self.size * display_scale; + let sp = to_screen(Pos2::new(self.pos[0], self.pos[1])); + painter.text( + sp + egui::vec2(1.0, 1.0), + egui::Align2::LEFT_TOP, + &self.text, + egui::FontId::proportional(screen_size), + Color32::from_black_alpha(160), + ); + painter.text( + sp, + egui::Align2::LEFT_TOP, + &self.text, + egui::FontId::proportional(screen_size), + self.color, + ); + } + fn paint_selection(&self, painter: &egui::Painter, to_screen: ToScreen<'_>) { + let dash = Color32::from_rgba_unmultiplied(255, 220, 0, 220); + painter.circle_filled( + to_screen(Pos2::new(self.pos[0] - 2.0, self.pos[1] - 2.0)), + 4.0, + dash, + ); + } + fn flatten(&self, img: &mut image::RgbaImage, iw: u32, ih: u32) { + draw_text_img( + img, self.pos[0], self.pos[1], &self.text, self.size, self.color, iw, ih, + ); + } + fn as_text(&self) -> Option<&TextAnn> { + Some(self) + } +} + +impl Annotation for HighlightAnn { + fn hit_test(&self, p: Pos2, tolerance: f32) -> bool { + rect_hit(&self.r, p, tolerance) + } + fn translate(&mut self, delta: egui::Vec2) { + rect_translate(&mut self.r, delta); + } + fn paint( + &self, + painter: &egui::Painter, + to_screen: ToScreen<'_>, + _image: &DynamicImage, + _display_scale: f32, + ) { + let tl = to_screen(Pos2::new(self.r[0], self.r[1])); + let br = to_screen(Pos2::new(self.r[2], self.r[3])); + painter.rect_filled(Rect::from_min_max(tl, br), 0.0, self.color); + } + fn paint_selection(&self, painter: &egui::Painter, to_screen: ToScreen<'_>) { + rect_selection(&self.r, painter, to_screen); + } + fn flatten(&self, img: &mut image::RgbaImage, iw: u32, ih: u32) { + let x0 = self.r[0].round() as i32; + let y0 = self.r[1].round() as i32; + let x1 = self.r[2].round() as i32; + let y1 = self.r[3].round() as i32; + fill_rect_img(img, x0, y0, x1, y1, to_rgba(self.color), iw, ih); + } +} + +impl Annotation for PixelateAnn { + fn hit_test(&self, p: Pos2, tolerance: f32) -> bool { + rect_hit(&self.r, p, tolerance) + } + fn translate(&mut self, delta: egui::Vec2) { + rect_translate(&mut self.r, delta); + } + fn paint( + &self, + painter: &egui::Painter, + to_screen: ToScreen<'_>, + image: &DynamicImage, + _display_scale: f32, + ) { + paint_pixelate_preview(&self.r, self.block, painter, to_screen, image); + } + fn paint_selection(&self, painter: &egui::Painter, to_screen: ToScreen<'_>) { + rect_selection(&self.r, painter, to_screen); + } + fn flatten(&self, img: &mut image::RgbaImage, iw: u32, ih: u32) { + pixelate_region(img, self.r[0], self.r[1], self.r[2], self.r[3], self.block, iw, ih); + } +} + +impl Annotation for PencilAnn { + fn hit_test(&self, p: Pos2, tolerance: f32) -> bool { + self.points.windows(2).any(|w| { + point_seg_dist(p, Pos2::new(w[0][0], w[0][1]), Pos2::new(w[1][0], w[1][1])) < tolerance + }) + } + fn translate(&mut self, delta: egui::Vec2) { + for p in self.points.iter_mut() { + p[0] += delta.x; + p[1] += delta.y; + } + } + fn paint( + &self, + painter: &egui::Painter, + to_screen: ToScreen<'_>, + _image: &DynamicImage, + _display_scale: f32, + ) { + for w in self.points.windows(2) { + painter.line_segment( + [ + to_screen(Pos2::new(w[0][0], w[0][1])), + to_screen(Pos2::new(w[1][0], w[1][1])), + ], + Stroke::new(self.thickness, self.color), + ); + } + } + fn paint_selection(&self, painter: &egui::Painter, to_screen: ToScreen<'_>) { + let dash = Color32::from_rgba_unmultiplied(255, 220, 0, 220); + if let Some(p) = self.points.first() { + painter.circle_stroke( + to_screen(Pos2::new(p[0], p[1])), + 5.0, + Stroke::new(1.5, dash), + ); + } + } + fn flatten(&self, img: &mut image::RgbaImage, iw: u32, ih: u32) { + let c = to_rgba(self.color); + for w in self.points.windows(2) { + draw_line_img(img, w[0][0], w[0][1], w[1][0], w[1][1], self.thickness, c, iw, ih); } } } @@ -159,7 +434,7 @@ pub struct AnnotationLayer { pub text_size: f32, pub pixel_block: u32, pub rect_filled: bool, - pub annotations: Vec, + pub annotations: Vec>, pub drag: DragState, pub text_pending: Option<[f32; 2]>, pub selected: Option, @@ -256,13 +531,11 @@ impl AnnotationLayer { if double_click { if let Some(idx) = self.selected { if idx < self.annotations.len() { - if let Annotation::Text { - pos, text, size, .. - } = self.annotations[idx].clone() - { + if let Some(t) = self.annotations[idx].as_text() { + let (pos, text, size) = (t.pos, t.text.clone(), t.size); self.annotations.remove(idx); self.selected = None; - self.text_size = size; // already image-space, matches slider + self.text_size = size; self.text_pending = Some(pos); self.drag = DragState::TextInput { pos, buf: text }; return; @@ -276,7 +549,7 @@ impl AnnotationLayer { if let DragState::Moving { idx, ref mut last } = self.drag { let delta = ip - *last; if delta.length() > 0.5 && idx < self.annotations.len() { - translate_annotation(&mut self.annotations[idx], delta); + self.annotations[idx].translate(delta); *last = ip; } } @@ -325,11 +598,11 @@ impl AnnotationLayer { if released { if let DragState::PencilStroke { ref points } = self.drag { if points.len() >= 2 { - self.annotations.push(Annotation::Pencil { + self.annotations.push(Box::new(PencilAnn { points: points.iter().map(|p| [p.x, p.y]).collect(), color: self.color, thickness: self.thickness, - }); + })); } } self.drag = DragState::None; @@ -369,20 +642,28 @@ impl AnnotationLayer { if (r[2] - r[0]).abs() < 3.0 && (r[3] - r[1]).abs() < 3.0 { return; } + if let Some(ann) = self.build_from_drag(start, end, r) { + self.annotations.push(ann); + } + } + + /// Build an annotation from the current tool + drag rect. Returns None for tools + /// that don't produce rect/arrow annotations (Select, Text, Pencil). + fn build_from_drag(&self, start: Pos2, end: Pos2, r: [f32; 4]) -> Option> { match self.tool { - Tool::Rect => self.annotations.push(Annotation::Rect { + Tool::Rect => Some(Box::new(RectAnn { r, color: self.color, thickness: self.thickness, filled: self.rect_filled, - }), - Tool::Arrow => self.annotations.push(Annotation::Arrow { + })), + Tool::Arrow => Some(Box::new(ArrowAnn { from: [start.x, start.y], to: [end.x, end.y], color: self.color, thickness: self.thickness, - }), - Tool::Highlight => self.annotations.push(Annotation::Highlight { + })), + Tool::Highlight => Some(Box::new(HighlightAnn { r, color: Color32::from_rgba_unmultiplied( self.color.r(), @@ -390,12 +671,12 @@ impl AnnotationLayer { self.color.b(), 80, ), - }), - Tool::Pixelate => self.annotations.push(Annotation::Pixelate { + })), + Tool::Pixelate => Some(Box::new(PixelateAnn { r, block: self.pixel_block, - }), - _ => {} + })), + _ => None, } } @@ -406,15 +687,12 @@ impl AnnotationLayer { _ => String::new(), }; if !buf.trim().is_empty() { - // text_size is stored in image-space pixels, same coordinate system - // as pos, so it scales with zoom/resize just like every other annotation. - // !NOTE this is still bugged - self.annotations.push(Annotation::Text { + self.annotations.push(Box::new(TextAnn { pos, text: buf, color: self.color, size: self.text_size, - }); + })); } self.drag = DragState::None; } @@ -425,21 +703,23 @@ impl AnnotationLayer { pub fn paint( &self, painter: &egui::Painter, - to_screen: impl Fn(Pos2) -> Pos2 + Copy, + to_screen: impl Fn(Pos2) -> Pos2, image: &DynamicImage, display_scale: f32, ) { + let to_screen_dyn: ToScreen<'_> = &to_screen; for (i, ann) in self.annotations.iter().enumerate() { - paint_annotation(ann, painter, to_screen, image, display_scale); + ann.paint(painter, to_screen_dyn, image, display_scale); if self.selected == Some(i) { - paint_selection(ann, painter, to_screen); + ann.paint_selection(painter, to_screen_dyn); } } match &self.drag { DragState::Dragging { start, current } => { - if let Some(g) = self.ghost(*start, *current) { - paint_annotation(&g, painter, to_screen, image, display_scale); + if let Some(g) = self.build_from_drag(*start, *current, norm_rect(*start, *current)) + { + g.paint(painter, to_screen_dyn, image, display_scale); } } DragState::PencilStroke { points } => { @@ -473,166 +753,19 @@ impl AnnotationLayer { DragState::None => {} } } - - fn ghost(&self, start: Pos2, end: Pos2) -> Option { - let r = norm_rect(start, end); - match self.tool { - Tool::Rect => Some(Annotation::Rect { - r, - color: self.color, - thickness: self.thickness, - filled: self.rect_filled, - }), - Tool::Arrow => Some(Annotation::Arrow { - from: [start.x, start.y], - to: [end.x, end.y], - color: self.color, - thickness: self.thickness, - }), - Tool::Highlight => Some(Annotation::Highlight { - r, - color: Color32::from_rgba_unmultiplied( - self.color.r(), - self.color.g(), - self.color.b(), - 80, - ), - }), - Tool::Pixelate => Some(Annotation::Pixelate { - r, - block: self.pixel_block, - }), - _ => None, - } - } } fn norm_rect(a: Pos2, b: Pos2) -> [f32; 4] { [a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y)] } -/// Translate an annotation by delta (image-space pixels). -fn translate_annotation(ann: &mut Annotation, delta: egui::Vec2) { - match ann { - Annotation::Rect { r, .. } - | Annotation::Highlight { r, .. } - | Annotation::Pixelate { r, .. } => { - r[0] += delta.x; - r[1] += delta.y; - r[2] += delta.x; - r[3] += delta.y; - } - Annotation::Arrow { from, to, .. } => { - from[0] += delta.x; - from[1] += delta.y; - to[0] += delta.x; - to[1] += delta.y; - } - Annotation::Text { pos, .. } => { - pos[0] += delta.x; - pos[1] += delta.y; - } - Annotation::Pencil { points, .. } => { - for p in points.iter_mut() { - p[0] += delta.x; - p[1] += delta.y; - } - } - } -} - -// Screen space painting - -fn paint_annotation( - ann: &Annotation, - painter: &egui::Painter, - to_screen: impl Fn(Pos2) -> Pos2, - image: &DynamicImage, - display_scale: f32, -) { - match ann { - Annotation::Rect { - r, - color, - thickness, - filled, - } => { - let tl = to_screen(Pos2::new(r[0], r[1])); - let br = to_screen(Pos2::new(r[2], r[3])); - let rect = Rect::from_min_max(tl, br); - if *filled { - painter.rect_filled(rect, 0.0, *color); - } else { - painter.rect_stroke(rect, 0.0, Stroke::new(*thickness, *color)); - } - } - Annotation::Arrow { - from, - to, - color, - thickness, - } => { - let sp = to_screen(Pos2::new(from[0], from[1])); - let ep = to_screen(Pos2::new(to[0], to[1])); - painter.arrow(sp, ep - sp, Stroke::new(*thickness, *color)); - } - Annotation::Text { - pos, - text, - color, - size, - } => { - // size is in image-space pixels; multiply by display_scale -> screen logical pts - let screen_size = size * display_scale; - let sp = to_screen(Pos2::new(pos[0], pos[1])); - painter.text( - sp + egui::vec2(1.0, 1.0), - egui::Align2::LEFT_TOP, - text, - egui::FontId::proportional(screen_size), - Color32::from_black_alpha(160), - ); - painter.text( - sp, - egui::Align2::LEFT_TOP, - text, - egui::FontId::proportional(screen_size), - *color, - ); - } - Annotation::Highlight { r, color } => { - let tl = to_screen(Pos2::new(r[0], r[1])); - let br = to_screen(Pos2::new(r[2], r[3])); - painter.rect_filled(Rect::from_min_max(tl, br), 0.0, *color); - } - Annotation::Pixelate { r, block } => { - paint_pixelate_preview(r, *block, painter, to_screen, image); - } - Annotation::Pencil { - points, - color, - thickness, - } => { - for w in points.windows(2) { - painter.line_segment( - [ - to_screen(Pos2::new(w[0][0], w[0][1])), - to_screen(Pos2::new(w[1][0], w[1][1])), - ], - Stroke::new(*thickness, *color), - ); - } - } - } -} - /// Sample average block colors from the source image and draw them as filled /// rectangles, creating a pixelated preview. fn paint_pixelate_preview( r: &[f32; 4], block: u32, painter: &egui::Painter, - to_screen: impl Fn(Pos2) -> Pos2, + to_screen: ToScreen<'_>, image: &DynamicImage, ) { let (iw, ih) = image.dimensions(); @@ -684,123 +817,14 @@ fn paint_pixelate_preview( } } -fn paint_selection(ann: &Annotation, painter: &egui::Painter, to_screen: impl Fn(Pos2) -> Pos2) { - let dash = Color32::from_rgba_unmultiplied(255, 220, 0, 220); - match ann { - Annotation::Rect { r, .. } - | Annotation::Highlight { r, .. } - | Annotation::Pixelate { r, .. } => { - let tl = to_screen(Pos2::new(r[0] - 3.0, r[1] - 3.0)); - let br = to_screen(Pos2::new(r[2] + 3.0, r[3] + 3.0)); - painter.rect_stroke(Rect::from_min_max(tl, br), 0.0, Stroke::new(1.5, dash)); - } - Annotation::Arrow { from, to, .. } => { - painter.circle_stroke( - to_screen(Pos2::new(from[0], from[1])), - 5.0, - Stroke::new(1.5, dash), - ); - painter.circle_stroke( - to_screen(Pos2::new(to[0], to[1])), - 5.0, - Stroke::new(1.5, dash), - ); - } - Annotation::Text { pos, .. } => { - painter.circle_filled(to_screen(Pos2::new(pos[0] - 2.0, pos[1] - 2.0)), 4.0, dash); - } - Annotation::Pencil { points, .. } => { - if let Some(p) = points.first() { - painter.circle_stroke( - to_screen(Pos2::new(p[0], p[1])), - 5.0, - Stroke::new(1.5, dash), - ); - } - } - } -} - // Flatten annotations on to DynamicImage -pub fn flatten(base: &DynamicImage, annotations: &[Annotation]) -> DynamicImage { +pub fn flatten(base: &DynamicImage, annotations: &[Box]) -> DynamicImage { let mut out = base.to_rgba8(); let (iw, ih) = out.dimensions(); - for ann in annotations { - match ann { - Annotation::Rect { - r, - color, - thickness, - filled, - } => { - let x0 = r[0].round() as i32; - let y0 = r[1].round() as i32; - let x1 = r[2].round() as i32; - let y1 = r[3].round() as i32; - let t = (*thickness).round().max(1.0) as i32; - if *filled { - fill_rect_img(&mut out, x0, y0, x1, y1, to_rgba(*color), iw, ih); - } else { - draw_rect_border(&mut out, x0, y0, x1, y1, t, to_rgba(*color), iw, ih); - } - } - Annotation::Arrow { - from, - to, - color, - thickness, - } => { - let c = to_rgba(*color); - draw_line_img( - &mut out, from[0], from[1], to[0], to[1], *thickness, c, iw, ih, - ); - draw_arrowhead( - &mut out, - from[0], - from[1], - to[0], - to[1], - *thickness * 4.0, - c, - iw, - ih, - ); - } - Annotation::Text { - pos, - text, - color, - size, - } => { - draw_text_img(&mut out, pos[0], pos[1], text, *size, *color, iw, ih); - } - Annotation::Highlight { r, color } => { - let x0 = r[0].round() as i32; - let y0 = r[1].round() as i32; - let x1 = r[2].round() as i32; - let y1 = r[3].round() as i32; - fill_rect_img(&mut out, x0, y0, x1, y1, to_rgba(*color), iw, ih); - } - Annotation::Pixelate { r, block } => { - pixelate_region(&mut out, r[0], r[1], r[2], r[3], *block, iw, ih); - } - Annotation::Pencil { - points, - color, - thickness, - } => { - let c = to_rgba(*color); - for w in points.windows(2) { - draw_line_img( - &mut out, w[0][0], w[0][1], w[1][0], w[1][1], *thickness, c, iw, ih, - ); - } - } - } + ann.flatten(&mut out, iw, ih); } - DynamicImage::ImageRgba8(out) } diff --git a/src/gui/app.rs b/src/gui/app.rs index b4ed1a6..ee68061 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -214,7 +214,6 @@ impl BlastApp { select::HintBox { x: g.x as i32, y: g.y as i32, w: g.w as i32, h: g.h as i32, - label: w.class.clone(), } }) .collect() diff --git a/src/hyprland.rs b/src/hyprland.rs index 121e607..ae1a8c1 100644 --- a/src/hyprland.rs +++ b/src/hyprland.rs @@ -62,7 +62,6 @@ pub struct WindowInfo { pub at: [i64; 2], pub size: [i64; 2], pub class: String, - #[allow(dead_code)] pub title: String, pub workspace: WorkspaceRef, } diff --git a/src/main.rs b/src/main.rs index 124e9b4..0d947be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod paths; mod region; mod select; mod shhh; +mod toplevel_capture; mod wayland_windows; use std::{ @@ -351,14 +352,16 @@ fn run_copysave(subject: Subject, file: Option, ctx: Context) -> Result< let (img, what) = capture_subject(&subject, &ctx)?; let png = ctx.finalize_png(&img)?; - clipboard::copy_png(png.clone()).map_err(|e| { + // Write the file first so we can hand the PNG bytes off to the clipboard + // by value (avoids cloning the full image-sized Vec). + let dest = resolve_dest(file.as_deref()); + std::fs::write(&dest, &png).map_err(BlastError::Io)?; + + clipboard::copy_png(png).map_err(|e| { ctx.notify_err("Clipboard error", Some(&e.to_string())); e })?; - let dest = resolve_dest(file.as_deref()); - std::fs::write(&dest, &png).map_err(BlastError::Io)?; - let name = dest.display().to_string(); ctx.notify_ok( &format!("{what} copied and saved"), @@ -405,7 +408,22 @@ fn capture_subject(subject: &Subject, ctx: &Context) -> Result<(DynamicImage, St } Subject::Active => { ctx.maybe_wait(); - let win = hyprland::active_window()?; + // Prefer hyprland-toplevel-export-v1: gets the toplevel's own buffer + // (unoccluded, no SSD, no rounded-corner clipping). Falls back to + // wlr-screencopy + IPC-derived region if the protocol isn't bound. + let win = hyprland::active_window().ok(); + let label = win + .as_ref() + .map(|w| format!("{} window", w.class)) + .unwrap_or_else(|| "Active window".into()); + + if let Some(img) = toplevel_capture::capture_active(&ctx.capture_opts())? { + return Ok((img, label)); + } + + let win = win.ok_or_else(|| { + BlastError::Hyprland("no active window via IPC or toplevel protocol".into()) + })?; let bs = hyprland::border_size().unwrap_or(0); let geom = win.to_geometry(bs, 0); let img = capture::capture_region(geom, &ctx.capture_opts())?; @@ -431,27 +449,33 @@ fn capture_area(ctx: &Context) -> Result<(DynamicImage, String)> { let on_hyprland = std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok(); + // Keep the Hyprland client list around so we can correlate the selected + // rect back to a (class, title) and route through toplevel capture when + // the user snapped to a window. + let mut hypr_windows: Vec = Vec::new(); + let mut hypr_border: i64 = 0; + let mut hypr_bar: i64 = 0; + let geom = if on_hyprland { hyprland::with_animations_disabled(|border_size| { - // Prefer Wayland-native geometry; fall back to Hyprland IPC. + let bar = hyprland::get_option_int("plugin:hyprbars:bar_height").unwrap_or(0); + hypr_border = border_size; + hypr_bar = bar; + hypr_windows = hyprland::visible_windows().unwrap_or_default(); + let boxes = wayland_windows::visible_window_hints().unwrap_or_else(|| { - let bar = hyprland::get_option_int("plugin:hyprbars:bar_height").unwrap_or(0); - hyprland::visible_windows() - .map(|wins| { - wins.iter() - .map(|w| { - let g = w.to_geometry(border_size, bar); - select::HintBox { - x: g.x as i32, - y: g.y as i32, - w: g.w as i32, - h: g.h as i32, - label: w.class.clone(), - } - }) - .collect() + hypr_windows + .iter() + .map(|w| { + let g = w.to_geometry(border_size, bar); + select::HintBox { + x: g.x as i32, + y: g.y as i32, + w: g.w as i32, + h: g.h as i32, + } }) - .unwrap_or_default() + .collect() }); region::select_area_boxes(boxes, ctx.hint_color) })? @@ -460,6 +484,22 @@ fn capture_area(ctx: &Context) -> Result<(DynamicImage, String)> { region::select_area_boxes(boxes, ctx.hint_color)? }; + // If the selection snapped to a known Hyprland window, ask the compositor + // to export that toplevel directly — bypasses occlusion / SSD / rounded corners. + if on_hyprland { + if let Some(win) = hypr_windows.iter().find(|w| { + let g = w.to_geometry(hypr_border, hypr_bar); + g.x == geom.x && g.y == geom.y && g.w == geom.w && g.h == geom.h + }) { + if let Some(img) = + toplevel_capture::capture_by_identity(&win.class, &win.title, &ctx.capture_opts())? + { + freeze_guard.kill(); + return Ok((img, format!("{} window", win.class))); + } + } + } + let img = capture::capture_region(geom, &ctx.capture_opts())?; freeze_guard.kill(); Ok((img, "Area".into())) diff --git a/src/notify.rs b/src/notify.rs index 03ccfe4..3a75802 100644 --- a/src/notify.rs +++ b/src/notify.rs @@ -1,37 +1,317 @@ -use notify_rust::{Notification, Urgency}; +//! Minimal DBus client for org.freedesktop.Notifications.Notify. +//! +//! Implements just enough of the DBus binary wire protocol to fire a single +//! Notify call, without pulling in zbus / notify-rust. All multi-byte values +//! are little-endian; this client declares 'l' in the message header and only +//! handles little-endian replies (sufficient on every relevant platform). + +use std::env; +use std::io::{Read, Write}; +use std::os::unix::net::UnixStream; +use std::time::Duration; use crate::error::{BlastError, Result}; +const URGENCY_NORMAL: u8 = 1; +const URGENCY_CRITICAL: u8 = 2; + #[allow(dead_code)] pub fn notify_ok(summary: &str, body: Option<&str>, icon: Option<&str>) -> Result<()> { - let mut n = Notification::new(); - n.appname("blast").summary(summary).timeout(3000); - - if let Some(b) = body { - n.body(b); - } - if let Some(i) = icon { - n.icon(i); - } - - n.show().map_err(|e| BlastError::Notify(e.to_string()))?; - - Ok(()) + send_notify( + summary, + body.unwrap_or(""), + icon.unwrap_or(""), + URGENCY_NORMAL, + 3000, + ) } #[allow(dead_code)] pub fn notify_error(summary: &str, body: Option<&str>) -> Result<()> { - let mut n = Notification::new(); - n.appname("blast") - .summary(summary) - .urgency(Urgency::Critical) - .timeout(5000); + send_notify( + summary, + body.unwrap_or(""), + "", + URGENCY_CRITICAL, + 5000, + ) +} - if let Some(b) = body { - n.body(b); - } +fn err>(s: S) -> BlastError { + BlastError::Notify(s.into()) +} - n.show().map_err(|e| BlastError::Notify(e.to_string()))?; +fn io_err(e: std::io::Error) -> BlastError { + BlastError::Notify(e.to_string()) +} +fn send_notify( + summary: &str, + body: &str, + icon: &str, + urgency: u8, + timeout_ms: i32, +) -> Result<()> { + let path = session_bus_path().ok_or_else(|| err("no DBus session bus address"))?; + let mut sock = UnixStream::connect(&path).map_err(io_err)?; + sock.set_read_timeout(Some(Duration::from_secs(2))).ok(); + sock.set_write_timeout(Some(Duration::from_secs(2))).ok(); + + sasl_auth(&mut sock)?; + hello(&mut sock)?; + do_notify(&mut sock, summary, body, icon, urgency, timeout_ms)?; + Ok(()) +} + +fn session_bus_path() -> Option { + if let Ok(addr) = env::var("DBUS_SESSION_BUS_ADDRESS") { + if let Some(p) = parse_unix_path(&addr) { + return Some(p); + } + } + env::var("XDG_RUNTIME_DIR").ok().map(|d| format!("{d}/bus")) +} + +fn parse_unix_path(addr: &str) -> Option { + for chunk in addr.split(';') { + if let Some(rest) = chunk.strip_prefix("unix:") { + for kv in rest.split(',') { + if let Some(p) = kv.strip_prefix("path=") { + return Some(p.to_string()); + } + } + } + } + None +} + +// === SASL handshake === + +fn sasl_auth(sock: &mut UnixStream) -> Result<()> { + // DBus over unix: first byte must be a NUL; auth is sent as ASCII lines. + sock.write_all(&[0]).map_err(io_err)?; + let uid = unsafe { libc::getuid() }.to_string(); + let hex: String = uid.bytes().map(|b| format!("{:02x}", b)).collect(); + sock.write_all(format!("AUTH EXTERNAL {hex}\r\n").as_bytes()) + .map_err(io_err)?; + let line = read_line(sock)?; + if !line.starts_with("OK ") { + return Err(err(format!("dbus auth failed: {line}"))); + } + sock.write_all(b"BEGIN\r\n").map_err(io_err)?; + Ok(()) +} + +fn read_line(sock: &mut UnixStream) -> Result { + let mut buf = Vec::new(); + loop { + let mut b = [0u8; 1]; + sock.read_exact(&mut b).map_err(io_err)?; + buf.push(b[0]); + if buf.ends_with(b"\r\n") { + buf.truncate(buf.len() - 2); + return String::from_utf8(buf).map_err(|e| err(e.to_string())); + } + if buf.len() > 4096 { + return Err(err("dbus auth response too long")); + } + } +} + +// === Method calls === + +fn hello(sock: &mut UnixStream) -> Result<()> { + let msg = build_method_call( + 1, + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "Hello", + "org.freedesktop.DBus", + None, + &[], + 0, + ); + sock.write_all(&msg).map_err(io_err)?; + drain_message(sock)?; + Ok(()) +} + +fn do_notify( + sock: &mut UnixStream, + summary: &str, + body: &str, + icon: &str, + urgency: u8, + timeout_ms: i32, +) -> Result<()> { + // signature: susssasa{sv}i + let mut payload = Vec::new(); + write_string(&mut payload, "blast"); + write_u32(&mut payload, 0); // replaces_id + write_string(&mut payload, icon); + write_string(&mut payload, summary); + write_string(&mut payload, body); + write_empty_string_array(&mut payload); + write_urgency_hint(&mut payload, urgency); + align_to(&mut payload, 4); + payload.extend_from_slice(&timeout_ms.to_le_bytes()); + + let msg = build_method_call( + 2, + "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", + "Notify", + "org.freedesktop.Notifications", + Some("susssasa{sv}i"), + &payload, + 1, // NO_REPLY_EXPECTED + ); + sock.write_all(&msg).map_err(io_err)?; + Ok(()) +} + +// === Wire encoding helpers === + +fn align_to(buf: &mut Vec, n: usize) { + while buf.len() % n != 0 { + buf.push(0); + } +} + +fn write_u32(buf: &mut Vec, v: u32) { + align_to(buf, 4); + buf.extend_from_slice(&v.to_le_bytes()); +} + +fn write_string(buf: &mut Vec, s: &str) { + align_to(buf, 4); + buf.extend_from_slice(&(s.len() as u32).to_le_bytes()); + buf.extend_from_slice(s.as_bytes()); + buf.push(0); +} + +fn write_signature(buf: &mut Vec, sig: &str) { + buf.push(sig.len() as u8); + buf.extend_from_slice(sig.as_bytes()); + buf.push(0); +} + +fn write_empty_string_array(buf: &mut Vec) { + align_to(buf, 4); + buf.extend_from_slice(&0u32.to_le_bytes()); +} + +/// Marshal `a{sv}` containing a single "urgency" -> byte entry. +fn write_urgency_hint(buf: &mut Vec, urgency: u8) { + align_to(buf, 4); + let len_pos = buf.len(); + buf.extend_from_slice(&[0u8; 4]); // placeholder + align_to(buf, 8); // element alignment for dict_entry; not counted in length + let start = buf.len(); + // dict_entry { string key, variant value } + write_string(buf, "urgency"); + write_signature(buf, "y"); + buf.push(urgency); + let length = (buf.len() - start) as u32; + buf[len_pos..len_pos + 4].copy_from_slice(&length.to_le_bytes()); +} + +/// Build a complete METHOD_CALL message (header + optional body). +fn build_method_call( + serial: u32, + path: &str, + interface: &str, + member: &str, + destination: &str, + signature: Option<&str>, + body: &[u8], + flags: u8, +) -> Vec { + let mut out = Vec::with_capacity(128 + body.len()); + + // Fixed header (12 bytes) + out.push(b'l'); // little-endian + out.push(1); // METHOD_CALL + out.push(flags); + out.push(1); // protocol version + out.extend_from_slice(&(body.len() as u32).to_le_bytes()); + out.extend_from_slice(&serial.to_le_bytes()); + + // Header fields array a(yv) + let fields_len_pos = out.len(); + out.extend_from_slice(&[0u8; 4]); // placeholder for array byte-length + // Pad to element alignment (struct = 8); this padding is NOT counted in the array length. + align_to(&mut out, 8); + let fields_start = out.len(); + + write_field_object_path(&mut out, 1, path); + write_field_string(&mut out, 2, interface); + write_field_string(&mut out, 3, member); + write_field_string(&mut out, 6, destination); + if let Some(sig) = signature { + write_field_signature(&mut out, 8, sig); + } + + let fields_len = (out.len() - fields_start) as u32; + out[fields_len_pos..fields_len_pos + 4].copy_from_slice(&fields_len.to_le_bytes()); + + // The body always starts at an 8-byte aligned offset, even when empty. + // The reference dbus client (busctl) pads after the header fields + // regardless of body length; not doing so makes the bus drop our message. + align_to(&mut out, 8); + out.extend_from_slice(body); + + out +} + +fn write_field_object_path(buf: &mut Vec, code: u8, value: &str) { + align_to(buf, 8); + buf.push(code); + write_signature(buf, "o"); + write_string(buf, value); +} + +fn write_field_string(buf: &mut Vec, code: u8, value: &str) { + align_to(buf, 8); + buf.push(code); + write_signature(buf, "s"); + write_string(buf, value); +} + +fn write_field_signature(buf: &mut Vec, code: u8, value: &str) { + align_to(buf, 8); + buf.push(code); + write_signature(buf, "g"); + write_signature(buf, value); +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Live smoke test against the running session bus. Will pop a real notification. + /// Run with: cargo test --bin blast notify::tests::live_notification -- --ignored --nocapture + #[test] + #[ignore] + fn live_notification() { + notify_ok("blast notify test", Some("hand-rolled DBus works"), None) + .expect("notify failed"); + } +} + +/// Read and discard one DBus message (used to drain the Hello reply). +fn drain_message(sock: &mut UnixStream) -> Result<()> { + let mut fixed = [0u8; 16]; + sock.read_exact(&mut fixed).map_err(io_err)?; + if fixed[0] != b'l' { + return Err(err("dbus: only little-endian replies supported")); + } + let body_len = u32::from_le_bytes(fixed[4..8].try_into().unwrap()) as usize; + let fields_len = u32::from_le_bytes(fixed[12..16].try_into().unwrap()) as usize; + // Header is always padded to 8 before body (whether body is empty or not). + let header_pad = (8 - ((16 + fields_len) % 8)) % 8; + let total_after = fields_len + header_pad + body_len; + let mut discard = vec![0u8; total_after]; + sock.read_exact(&mut discard).map_err(io_err)?; Ok(()) } diff --git a/src/region.rs b/src/region.rs index de79b3c..28c555c 100644 --- a/src/region.rs +++ b/src/region.rs @@ -35,7 +35,6 @@ pub fn select_area( y: g.y as i32, w: g.w as i32, h: g.h as i32, - label: w.class.clone(), } }) .collect(); diff --git a/src/select.rs b/src/select.rs index 2cfae99..b438d84 100644 --- a/src/select.rs +++ b/src/select.rs @@ -55,7 +55,6 @@ pub struct HintBox { pub y: i32, pub w: i32, pub h: i32, - pub label: String, } struct PendingOut { @@ -92,14 +91,16 @@ struct Surf { } struct St { - conn: Connection, + // _conn and _seat are not read directly but must outlive the event queue: + // dropping Connection closes the socket, dropping WlSeat ends pointer/keyboard. + _conn: Connection, qh: QueueHandle, queue: Option>, compositor: Option, shm: Option, shell: Option, - seat: Option, + _seat: Option, pointer: Option, keyboard: Option, cursor_shape: Option, @@ -172,27 +173,6 @@ fn draw_border( } } -fn rect_union( - a: Option<(i32, i32, i32, i32)>, - b: Option<(i32, i32, i32, i32)>, -) -> Option<(i32, i32, i32, i32)> { - match (a, b) { - (None, None) => None, - (Some(r), None) | (None, Some(r)) => Some(r), - (Some(a), Some(b)) => { - let x0 = a.0.min(b.0); - let y0 = a.1.min(b.1); - let x1 = (a.0 + a.2).max(b.0 + b.2); - let y1 = (a.1 + a.3).max(b.1 + b.3); - Some((x0, y0, x1 - x0, y1 - y0)) - } - } -} - -fn expand(r: Option<(i32, i32, i32, i32)>, pad: i32) -> Option<(i32, i32, i32, i32)> { - r.map(|(x, y, w, h)| (x - pad, y - pad, w + 2 * pad, h + 2 * pad)) -} - fn alloc_shm(size: usize) -> std::io::Result { #[cfg(target_os = "linux")] unsafe { @@ -830,13 +810,13 @@ pub fn select_region( }; let mut st = St { - conn, + _conn: conn, qh, queue: Some(queue), compositor: Some(compositor), shm: Some(shm), shell: Some(shell), - seat: Some(seat), + _seat: Some(seat), pointer: None, keyboard: None, cursor_shape, diff --git a/src/shhh.rs b/src/shhh.rs index 37b214b..601094a 100644 --- a/src/shhh.rs +++ b/src/shhh.rs @@ -45,6 +45,11 @@ pub fn encode_png(img: &DynamicImage) -> Result> { let mut encoder = Encoder::new(&mut out, width, height); encoder.set_color(ColorType::Rgba); encoder.set_depth(BitDepth::Eight); + // Screenshots compress nearly as well at zlib level 1 as level 6 but + // encode much faster. `Up` is a cheap row filter that handles uniform + // backgrounds and horizontal borders well — typical for screenshots. + encoder.set_compression(png::Compression::Fast); + encoder.set_filter(png::FilterType::Up); let mut writer = encoder .write_header() .map_err(|e| BlastError::Image(e.to_string()))?; @@ -77,38 +82,45 @@ pub fn apply_and_encode(img: &DynamicImage, opts: &ShadowOptions) -> Result DynamicImage { let (width, height) = img.dimensions(); - let mut rounded: ImageBuffer, Vec> = ImageBuffer::new(width, height); + let mut rounded = img.to_rgba8(); + + // Nothing to do if no rounding requested or the image is smaller than the + // arc. Returning a straight copy preserves the previous behavior for the + // edge case. + if radius == 0 || width < 2 * radius || height < 2 * radius { + return DynamicImage::ImageRgba8(rounded); + } + let r = radius as f32; + // Each corner is a `radius x radius` square. `left`/`top` flip the + // distance-from-corner math so the same body covers all four quadrants. + // The original implementation iterated every pixel in the image to do + // identity copies for the 99% of pixels not in a corner. + let corners = [ + (0u32, 0u32, true, true), + (width - radius, 0, false, true), + (0, height - radius, true, false), + (width - radius, height - radius, false, false), + ]; - for (x, y, pixel) in img.to_rgba8().enumerate_pixels() { - let corner: Option<(f32, f32)> = if x < radius && y < radius { - Some((r - x as f32, r - y as f32)) - } else if x >= width - radius && y < radius { - Some((x as f32 - (width as f32 - r - 1.0), r - y as f32)) - } else if x < radius && y >= height - radius { - Some((r - x as f32, y as f32 - (height as f32 - r - 1.0))) - } else if x >= width - radius && y >= height - radius { - Some(( - x as f32 - (width as f32 - r - 1.0), - y as f32 - (height as f32 - r - 1.0), - )) - } else { - None - }; - - match corner { - None => rounded.put_pixel(x, y, *pixel), - Some((dx, dy)) => { - let dist = (dx * dx + dy * dy).sqrt(); - if dist <= r { - rounded.put_pixel(x, y, *pixel); + for (cx, cy, left, top) in corners { + for dy_in in 0..radius { + for dx_in in 0..radius { + let dx = if left { + r - dx_in as f32 } else { + dx_in as f32 + 1.0 + }; + let dy = if top { + r - dy_in as f32 + } else { + dy_in as f32 + 1.0 + }; + let dist = (dx * dx + dy * dy).sqrt(); + if dist > r { + let pixel = rounded.get_pixel_mut(cx + dx_in, cy + dy_in); let alpha = ((r + 1.0 - dist).max(0.0) * 255.0) as u8; - rounded.put_pixel( - x, - y, - Rgba([pixel[0], pixel[1], pixel[2], alpha.min(pixel[3])]), - ); + pixel[3] = alpha.min(pixel[3]); } } } @@ -172,34 +184,123 @@ fn create_shadow( let padding = spread + blur_radius * 2; let new_w = width + padding * 2; let new_h = height + padding * 2; + let new_w_us = new_w as usize; + let new_h_us = new_h as usize; - let mut shadow: ImageBuffer, Vec> = ImageBuffer::new(new_w, new_h); - imageops::overlay(&mut shadow, &img.to_rgba8(), padding as i64, padding as i64); + // The shadow has uniform RGB=(0,0,0); only its alpha varies. So we work on + // a single-channel mask (1/4 the memory bandwidth of the previous RGBA + // version, and the blur is much cheaper). + let img_rgba = img.to_rgba8(); + let img_data = img_rgba.as_raw(); + let mut mask = vec![0u8; new_w_us * new_h_us]; - for pixel in shadow.pixels_mut() { - let alpha = pixel[3] as f32 / 255.0 * shadow_alpha as f32; - *pixel = Rgba([0, 0, 0, alpha as u8]); - } - - let adjusted_blur = blur_radius + (spread as f32 / 2.0) as u32; - let blurred = imageops::blur(&shadow, adjusted_blur as f32); - - let mut cleaned: ImageBuffer, Vec> = ImageBuffer::new(new_w, new_h); - for (x, y, pixel) in blurred.enumerate_pixels() { - if pixel[3] > 0 { - let factor = (pixel[3] as f32 / 255.0).powf(0.5); - cleaned.put_pixel( - x, - y, - Rgba([ - (pixel[0] as f32 * factor) as u8, - (pixel[1] as f32 * factor) as u8, - (pixel[2] as f32 * factor) as u8, - (pixel[3] as f32 * factor) as u8, - ]), - ); + // 1) Stamp the source alpha (scaled by shadow_alpha) into the padded mask. + let scale = shadow_alpha as u32; + let pad = padding as usize; + let w_us = width as usize; + for y in 0..height as usize { + let dst_row = (y + pad) * new_w_us + pad; + let src_row = y * w_us * 4; + let dst = &mut mask[dst_row..dst_row + w_us]; + let src = &img_data[src_row..src_row + w_us * 4]; + for (i, dp) in dst.iter_mut().enumerate() { + let a = src[i * 4 + 3] as u32; + *dp = ((a * scale) / 255) as u8; } } - DynamicImage::ImageRgba8(cleaned) + // 2) Three passes of a separable box blur approximate a Gaussian (CLT). + // The original used image::imageops::blur on full RGBA — this version + // runs on 1/4 the data with cheaper per-pixel math. + let adjusted_blur = blur_radius + spread / 2; + if adjusted_blur > 0 { + let r = adjusted_blur as usize; + let mut temp = vec![0u8; mask.len()]; + for _ in 0..3 { + box_blur_h(&mask, &mut temp, new_w_us, new_h_us, r); + box_blur_v(&temp, &mut mask, new_w_us, new_h_us, r); + } + } + + // 3) sqrt falloff cleanup, baked into a 256-entry LUT. + // Original: factor = sqrt(a/255), each channel = orig * factor. + // RGB are all 0 so they stay 0; only alpha gets the falloff: a*sqrt(a/255). + let lut: [u8; 256] = { + let mut t = [0u8; 256]; + for a in 0..=255u32 { + let f = (a as f32 / 255.0).sqrt(); + t[a as usize] = (a as f32 * f) as u8; + } + t + }; + let mut out_rgba = vec![0u8; new_w_us * new_h_us * 4]; + for (i, &a) in mask.iter().enumerate() { + out_rgba[i * 4 + 3] = lut[a as usize]; + } + let buf: ImageBuffer, Vec> = ImageBuffer::from_raw(new_w, new_h, out_rgba) + .expect("shadow buffer construction"); + DynamicImage::ImageRgba8(buf) +} + +/// Single-channel horizontal box blur. For each output pixel x, averages a +/// (2r+1)-wide window of `src` centered on x. Out-of-bounds samples are +/// treated as 0 (correct here since the mask is padded with transparent). +fn box_blur_h(src: &[u8], dst: &mut [u8], w: usize, h: usize, r: usize) { + if w == 0 || h == 0 { + return; + } + let kernel = (2 * r + 1) as f32; + let inv = 1.0 / kernel; + for y in 0..h { + let row = y * w; + let row_src = &src[row..row + w]; + let row_dst = &mut dst[row..row + w]; + + // Initial sum: window centered at x=0 covers src[-r..=r], so only + // src[0..=min(r, w-1)] contributes. + let mut sum: u32 = 0; + let init_end = r.min(w - 1); + for i in 0..=init_end { + sum += row_src[i] as u32; + } + + for x in 0..w { + row_dst[x] = (sum as f32 * inv) as u8; + // Slide the window: add the new right edge, drop the old left edge. + let add_i = x + r + 1; + if add_i < w { + sum += row_src[add_i] as u32; + } + if x >= r { + sum -= row_src[x - r] as u32; + } + } + } +} + +/// Single-channel vertical box blur. Strided access pattern; cache behavior is +/// adequate for the mask sizes we work with (~few MB). +fn box_blur_v(src: &[u8], dst: &mut [u8], w: usize, h: usize, r: usize) { + if w == 0 || h == 0 { + return; + } + let kernel = (2 * r + 1) as f32; + let inv = 1.0 / kernel; + for x in 0..w { + let mut sum: u32 = 0; + let init_end = r.min(h - 1); + for i in 0..=init_end { + sum += src[i * w + x] as u32; + } + for y in 0..h { + dst[y * w + x] = (sum as f32 * inv) as u8; + let add_i = y + r + 1; + if add_i < h { + sum += src[add_i * w + x] as u32; + } + if y >= r { + sum -= src[(y - r) * w + x] as u32; + } + } + } } diff --git a/src/toplevel_capture.rs b/src/toplevel_capture.rs new file mode 100644 index 0000000..9b0c557 --- /dev/null +++ b/src/toplevel_capture.rs @@ -0,0 +1,716 @@ +//! Per-toplevel capture via `hyprland-toplevel-export-v1`. +//! +//! Bind `hyprland_toplevel_export_manager_v1` plus +//! `zwlr_foreign_toplevel_manager_v1`, find the activated toplevel, and +//! ask Hyprland to export it. The frame protocol mirrors wlr-screencopy: +//! buffer/linux_dmabuf events → buffer_done → copy → flags + ready. +//! +//! Returns None if either protocol global is missing, so the caller can +//! fall back to the wlr-screencopy path. + +use std::{ + fs::File, + os::unix::io::{AsFd, AsRawFd, IntoRawFd, OwnedFd}, +}; + +use gbm::{BufferObjectFlags, Device as GbmDevice}; +use image::{DynamicImage, ImageBuffer, Rgba}; +use wayland_client::{ + delegate_noop, event_created_child, + globals::{registry_queue_init, GlobalListContents}, + protocol::{wl_buffer, wl_registry, wl_shm, wl_shm_pool}, + Connection, Dispatch, EventQueue, Proxy, QueueHandle, +}; +use wayland_protocols::wp::linux_dmabuf::zv1::client::{ + zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1}, + zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, +}; +use wayland_protocols_wlr::foreign_toplevel::v1::client::{ + zwlr_foreign_toplevel_handle_v1::{self, State as WlrToplevelState, ZwlrForeignToplevelHandleV1}, + zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1}, +}; + +use crate::{ + capture::{create_shm_file, decode_drm_pixels, find_drm_render_node, CaptureOptions}, + error::{BlastError, Result}, +}; + +mod protocol { + #![allow( + dead_code, + non_camel_case_types, + unused_unsafe, + unused_variables, + non_upper_case_globals, + non_snake_case, + unused_imports, + clippy::all + )] + pub mod client { + use wayland_client; + use wayland_client::protocol::*; + use wayland_protocols_wlr::foreign_toplevel::v1::client::*; + + pub mod __interfaces { + use wayland_client::protocol::__interfaces::*; + use wayland_protocols_wlr::foreign_toplevel::v1::client::__interfaces::*; + wayland_scanner::generate_interfaces!("protocols/hyprland-toplevel-export-v1.xml"); + } + use self::__interfaces::*; + + wayland_scanner::generate_client_code!("protocols/hyprland-toplevel-export-v1.xml"); + } +} + +use protocol::client::{ + hyprland_toplevel_export_frame_v1::{self, HyprlandToplevelExportFrameV1}, + hyprland_toplevel_export_manager_v1::HyprlandToplevelExportManagerV1, +}; + +/// Capture the currently activated toplevel. Returns Ok(None) if the +/// required protocols aren't advertised (caller should fall back). +pub fn capture_active(opts: &CaptureOptions) -> Result> { + capture_with(opts, |toplevels| { + toplevels + .iter() + .find(|t| t.activated) + .map(|t| t.handle.clone()) + }) +} + +/// Capture the toplevel matching (app_id, title). Returns Ok(None) if the +/// required protocols aren't advertised, or no toplevel matches. +/// +/// Used by `area` to capture an exact window match when the user's selection +/// snaps to a known toplevel rect. Hyprland's `class` corresponds to the +/// Wayland app_id; titles must match byte-for-byte. +pub fn capture_by_identity( + app_id: &str, + title: &str, + opts: &CaptureOptions, +) -> Result> { + capture_with(opts, |toplevels| { + let mut matches = toplevels + .iter() + .filter(|t| t.app_id == app_id && t.title == title); + let first = matches.next()?; + // Ambiguous match: refuse rather than capture the wrong window. + if matches.next().is_some() { + return None; + } + Some(first.handle.clone()) + }) +} + +fn capture_with(opts: &CaptureOptions, pick: F) -> Result> +where + F: FnOnce(&[WlrToplevel]) -> Option, +{ + let conn = match Connection::connect_to_env() { + Ok(c) => c, + Err(_) => return Ok(None), + }; + let (globals, queue) = match registry_queue_init::(&conn) { + Ok(p) => p, + Err(_) => return Ok(None), + }; + let qh = queue.handle(); + + let shm: wl_shm::WlShm = match globals.bind(&qh, 1..=1, ()) { + Ok(s) => s, + Err(_) => return Ok(None), + }; + let export_mgr: HyprlandToplevelExportManagerV1 = match globals.bind(&qh, 2..=2, ()) { + Ok(m) => m, + Err(_) => return Ok(None), + }; + let tl_mgr: ZwlrForeignToplevelManagerV1 = match globals.bind(&qh, 1..=3, ()) { + Ok(m) => m, + Err(_) => return Ok(None), + }; + let linux_dmabuf: Option = globals.bind(&qh, 2..=4, ()).ok(); + + let mut st = St { + qh: qh.clone(), + queue: Some(queue), + shm, + shm_formats: Vec::new(), + linux_dmabuf, + toplevels: Vec::new(), + frame: None, + frame_state: FrameState::Negotiating, + buffer_offers: Vec::new(), + dmabuf_offers: Vec::new(), + chosen_offer: None, + chosen_dmabuf: None, + shm_file: None, + shm_pool: None, + wl_buffer: None, + gbm_bo: None, + y_invert: false, + }; + + // Enumerate toplevels and let their state/title/app_id events arrive. + st.roundtrip()?; + st.roundtrip()?; + + let handle = match pick(&st.toplevels) { + Some(h) => h, + None => { + tl_mgr.stop(); + let _ = st.roundtrip(); + return Ok(None); + } + }; + + let overlay = if opts.include_cursor { 1 } else { 0 }; + let frame = export_mgr.capture_toplevel_with_wlr_toplevel_handle(overlay, &handle, &qh, ()); + st.frame = Some(frame); + st.frame_state = FrameState::Negotiating; + + st.dispatch_to_ready()?; + + let img = st.read_pixels()?; + + tl_mgr.stop(); + Ok(Some(img)) +} + +// State machine + +#[derive(Debug, PartialEq)] +enum FrameState { + Negotiating, + Copying, + Ready, + Failed, +} + +#[derive(Clone)] +struct BufferOffer { + format: wl_shm::Format, + width: u32, + height: u32, + stride: u32, +} + +#[derive(Clone)] +struct DmabufOffer { + format: u32, + width: u32, + height: u32, +} + +struct DmabufChosen { + format: u32, + width: u32, + height: u32, + stride: u32, +} + +pub(crate) struct WlrToplevel { + handle: ZwlrForeignToplevelHandleV1, + app_id: String, + title: String, + activated: bool, +} + +struct St { + qh: QueueHandle, + queue: Option>, + + shm: wl_shm::WlShm, + shm_formats: Vec, + linux_dmabuf: Option, + + toplevels: Vec, + + // per-capture + frame: Option, + frame_state: FrameState, + buffer_offers: Vec, + dmabuf_offers: Vec, + chosen_offer: Option, + chosen_dmabuf: Option, + shm_file: Option, + shm_pool: Option, + wl_buffer: Option, + gbm_bo: Option>, + y_invert: bool, +} + +impl St { + fn roundtrip(&mut self) -> Result<()> { + let mut q = self + .queue + .take() + .ok_or_else(|| BlastError::Capture("queue missing".into()))?; + let r = q + .roundtrip(self) + .map_err(|e| BlastError::Capture(format!("roundtrip: {e}"))); + self.queue = Some(q); + r.map(|_| ()) + } + + fn dispatch_once(&mut self) -> Result<()> { + let mut q = self + .queue + .take() + .ok_or_else(|| BlastError::Capture("queue missing".into()))?; + let r = q + .blocking_dispatch(self) + .map_err(|e| BlastError::Capture(format!("dispatch: {e}"))); + self.queue = Some(q); + r.map(|_| ()) + } + + fn dispatch_to_ready(&mut self) -> Result<()> { + loop { + self.dispatch_once()?; + match self.frame_state { + FrameState::Ready => return Ok(()), + FrameState::Failed => { + return Err(BlastError::Capture("compositor rejected frame".into())) + } + _ => {} + } + } + } + + fn pick_offer(&self) -> Option { + const PREFERRED: &[wl_shm::Format] = &[ + wl_shm::Format::Argb8888, + wl_shm::Format::Xrgb8888, + wl_shm::Format::Abgr8888, + wl_shm::Format::Xbgr8888, + wl_shm::Format::Rgba8888, + wl_shm::Format::Bgra8888, + ]; + for &fmt in PREFERRED { + if let Some(o) = self + .buffer_offers + .iter() + .find(|o| o.format == fmt && self.shm_formats.contains(&o.format)) + { + return Some(o.clone()); + } + } + self.buffer_offers + .iter() + .find(|o| self.shm_formats.contains(&o.format)) + .cloned() + } + + fn alloc_and_copy(&mut self) -> bool { + if self.try_alloc_dmabuf() { + return true; + } + + let offer = match self.pick_offer() { + Some(o) => o, + None => return false, + }; + + let size = (offer.stride * offer.height) as usize; + let file = match create_shm_file(size) { + Ok(f) => f, + Err(_) => return false, + }; + let fd = match file.try_clone() { + Ok(f) => f.into_raw_fd(), + Err(_) => return false, + }; + + let pool = self.shm.create_pool( + unsafe { std::os::unix::io::BorrowedFd::borrow_raw(fd) }, + size as i32, + &self.qh, + (), + ); + let buf = pool.create_buffer( + 0, + offer.width as i32, + offer.height as i32, + offer.stride as i32, + offer.format, + &self.qh, + (), + ); + + self.chosen_offer = Some(offer); + self.shm_file = Some(file); + self.shm_pool = Some(pool); + self.wl_buffer = Some(buf.clone()); + + if let Some(frame) = &self.frame { + // ignore_damage = 1: we want the next frame, not waiting for damage. + frame.copy(&buf, 1); + true + } else { + false + } + } + + fn try_alloc_dmabuf(&mut self) -> bool { + let offer = match self.dmabuf_offers.first().cloned() { + Some(o) => o, + None => return false, + }; + let linux_dmabuf = match self.linux_dmabuf.clone() { + Some(d) => d, + None => return false, + }; + let drm_file = match find_drm_render_node() { + Some(f) => f, + None => return false, + }; + let gbm = match GbmDevice::new(drm_file) { + Ok(d) => d, + Err(_) => return false, + }; + let fmt = match gbm::Format::try_from(offer.format) { + Ok(f) => f, + Err(_) => return false, + }; + let bo = match gbm.create_buffer_object::<()>( + offer.width, + offer.height, + fmt, + BufferObjectFlags::RENDERING | BufferObjectFlags::LINEAR, + ) { + Ok(b) => b, + Err(_) => return false, + }; + let stride = bo.stride(); + let dma_fd: OwnedFd = match bo.fd() { + Ok(f) => f, + Err(_) => return false, + }; + + let params = linux_dmabuf.create_params(&self.qh, ()); + params.add(dma_fd.as_fd(), 0, 0, stride, 0, 0); + let wl_buf = params.create_immed( + offer.width as i32, + offer.height as i32, + offer.format, + zwp_linux_buffer_params_v1::Flags::empty(), + &self.qh, + (), + ); + + let frame = match self.frame.as_ref() { + Some(f) => f, + None => return false, + }; + frame.copy(&wl_buf, 1); + + self.chosen_dmabuf = Some(DmabufChosen { + format: offer.format, + width: offer.width, + height: offer.height, + stride, + }); + self.gbm_bo = Some(bo); + self.wl_buffer = Some(wl_buf); + true + } + + fn read_pixels(mut self) -> Result { + // DMA-buf path + if let Some(bo) = self.gbm_bo.take() { + let chosen = self + .chosen_dmabuf + .take() + .ok_or_else(|| BlastError::Capture("no dmabuf chosen offer".into()))?; + let dma_fd: OwnedFd = bo + .fd() + .map_err(|e| BlastError::Capture(format!("dmabuf fd export: {e}")))?; + let size = (chosen.stride * chosen.height) as usize; + let rgba = { + let ptr = unsafe { + libc::mmap( + std::ptr::null_mut(), + size, + libc::PROT_READ, + libc::MAP_SHARED, + dma_fd.as_raw_fd(), + 0, + ) + }; + if ptr == libc::MAP_FAILED { + return Err(BlastError::Capture(format!( + "dmabuf mmap failed: {}", + std::io::Error::last_os_error() + ))); + } + let raw = unsafe { std::slice::from_raw_parts(ptr as *const u8, size) }; + let result = decode_drm_pixels( + raw, + chosen.width, + chosen.height, + chosen.stride, + chosen.format, + ); + unsafe { libc::munmap(ptr, size) }; + result? + }; + + let mut img = ImageBuffer::from_raw(chosen.width, chosen.height, rgba) + .ok_or_else(|| BlastError::Capture("image buffer construction failed".into()))?; + if self.y_invert { + image::imageops::flip_vertical_in_place(&mut img); + } + return Ok(DynamicImage::ImageRgba8(img)); + } + + // SHM path + let offer = self + .chosen_offer + .take() + .ok_or_else(|| BlastError::Capture("no chosen offer after ready".into()))?; + let mut file = self + .shm_file + .take() + .ok_or_else(|| BlastError::Capture("no shm file after ready".into()))?; + + use std::io::{Read, Seek, SeekFrom}; + file.seek(SeekFrom::Start(0)) + .map_err(|e| BlastError::Capture(format!("shm seek: {e}")))?; + + let byte_count = (offer.stride * offer.height) as usize; + let mut raw = vec![0u8; byte_count]; + file.read_exact(&mut raw) + .map_err(|e| BlastError::Capture(format!("shm read: {e}")))?; + + let (width, height, stride) = (offer.width, offer.height, offer.stride); + let mut rgba: Vec = Vec::with_capacity((width * height * 4) as usize); + + match offer.format { + wl_shm::Format::Argb8888 => { + for row in 0..height { + for col in 0..width { + let base = (row * stride + col * 4) as usize; + rgba.push(raw[base + 2]); + rgba.push(raw[base + 1]); + rgba.push(raw[base]); + rgba.push(raw[base + 3]); + } + } + } + wl_shm::Format::Xrgb8888 => { + for row in 0..height { + for col in 0..width { + let base = (row * stride + col * 4) as usize; + rgba.push(raw[base + 2]); + rgba.push(raw[base + 1]); + rgba.push(raw[base]); + rgba.push(255); + } + } + } + wl_shm::Format::Abgr8888 => { + for row in 0..height { + for col in 0..width { + let base = (row * stride + col * 4) as usize; + rgba.push(raw[base]); + rgba.push(raw[base + 1]); + rgba.push(raw[base + 2]); + rgba.push(raw[base + 3]); + } + } + } + wl_shm::Format::Xbgr8888 => { + for row in 0..height { + for col in 0..width { + let base = (row * stride + col * 4) as usize; + rgba.push(raw[base]); + rgba.push(raw[base + 1]); + rgba.push(raw[base + 2]); + rgba.push(255); + } + } + } + wl_shm::Format::Rgba8888 => { + for row in 0..height { + for col in 0..width { + let base = (row * stride + col * 4) as usize; + rgba.push(raw[base + 3]); + rgba.push(raw[base + 2]); + rgba.push(raw[base + 1]); + rgba.push(raw[base]); + } + } + } + wl_shm::Format::Bgra8888 => { + for row in 0..height { + for col in 0..width { + let base = (row * stride + col * 4) as usize; + rgba.push(raw[base + 1]); + rgba.push(raw[base + 2]); + rgba.push(raw[base + 3]); + rgba.push(raw[base]); + } + } + } + other => { + return Err(BlastError::Capture(format!( + "unhandled pixel format {other:?}" + ))); + } + } + + let mut img: ImageBuffer, Vec> = ImageBuffer::from_raw(width, height, rgba) + .ok_or_else(|| BlastError::Capture("image buffer construction failed".into()))?; + if self.y_invert { + image::imageops::flip_vertical_in_place(&mut img); + } + Ok(DynamicImage::ImageRgba8(img)) + } +} + +// Dispatch impls + +impl Dispatch for St { + fn event( + _: &mut Self, + _: &wl_registry::WlRegistry, + _: wl_registry::Event, + _: &GlobalListContents, + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for St { + fn event( + state: &mut Self, + _: &wl_shm::WlShm, + event: wl_shm::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let wl_shm::Event::Format { format } = event { + if let wayland_client::WEnum::Value(f) = format { + state.shm_formats.push(f); + } + } + } +} + +impl Dispatch for St { + fn event( + state: &mut Self, + _: &ZwlrForeignToplevelManagerV1, + event: zwlr_foreign_toplevel_manager_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let zwlr_foreign_toplevel_manager_v1::Event::Toplevel { toplevel } = event { + state.toplevels.push(WlrToplevel { + handle: toplevel, + app_id: String::new(), + title: String::new(), + activated: false, + }); + } + } + event_created_child!(St, ZwlrForeignToplevelManagerV1, [ + 0 => (ZwlrForeignToplevelHandleV1, ()) + ]); +} + +impl Dispatch for St { + fn event( + state: &mut Self, + handle: &ZwlrForeignToplevelHandleV1, + event: zwlr_foreign_toplevel_handle_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let id = handle.id(); + let Some(tl) = state.toplevels.iter_mut().find(|t| t.handle.id() == id) else { + return; + }; + match event { + zwlr_foreign_toplevel_handle_v1::Event::AppId { app_id } => tl.app_id = app_id, + zwlr_foreign_toplevel_handle_v1::Event::Title { title } => tl.title = title, + zwlr_foreign_toplevel_handle_v1::Event::State { state: bytes } => { + tl.activated = bytes + .chunks_exact(4) + .map(|c| u32::from_ne_bytes([c[0], c[1], c[2], c[3]])) + .any(|s| s == WlrToplevelState::Activated as u32); + } + _ => {} + } + } +} + +impl Dispatch for St { + fn event( + state: &mut Self, + _: &HyprlandToplevelExportFrameV1, + event: hyprland_toplevel_export_frame_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + hyprland_toplevel_export_frame_v1::Event::Buffer { + format, + width, + height, + stride, + } => { + if let wayland_client::WEnum::Value(fmt) = format { + state.buffer_offers.push(BufferOffer { + format: fmt, + width, + height, + stride, + }); + } + } + hyprland_toplevel_export_frame_v1::Event::LinuxDmabuf { + format, + width, + height, + } => { + state.dmabuf_offers.push(DmabufOffer { + format, + width, + height, + }); + } + hyprland_toplevel_export_frame_v1::Event::BufferDone => { + if state.alloc_and_copy() { + state.frame_state = FrameState::Copying; + } else { + state.frame_state = FrameState::Failed; + } + } + hyprland_toplevel_export_frame_v1::Event::Flags { flags } => { + if let wayland_client::WEnum::Value(f) = flags { + state.y_invert = f + .contains(hyprland_toplevel_export_frame_v1::Flags::YInvert); + } + } + hyprland_toplevel_export_frame_v1::Event::Ready { .. } => { + state.frame_state = FrameState::Ready; + } + hyprland_toplevel_export_frame_v1::Event::Failed => { + state.frame_state = FrameState::Failed; + } + _ => {} + } + } +} + +delegate_noop!(St: ignore wl_shm_pool::WlShmPool); +delegate_noop!(St: ignore wl_buffer::WlBuffer); +delegate_noop!(St: ignore ZwpLinuxDmabufV1); +delegate_noop!(St: ignore ZwpLinuxBufferParamsV1); +delegate_noop!(St: ignore HyprlandToplevelExportManagerV1); diff --git a/src/wayland_windows.rs b/src/wayland_windows.rs index 59667e3..fbf3053 100644 --- a/src/wayland_windows.rs +++ b/src/wayland_windows.rs @@ -354,7 +354,6 @@ pub fn visible_window_hints() -> Option> { y: ly, w: lw, h: lh, - label: tl.app_id.clone(), }) }) .collect();