reduce size of binary, remove notify-rust dep

This commit is contained in:
entailz 2026-05-13 15:28:17 -07:00
parent 87045180dc
commit b58c2df1d9
15 changed files with 2104 additions and 1101 deletions

2
.gitignore vendored
View file

@ -1 +1 @@
target/
/target/

418
Cargo.lock generated
View file

@ -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",
]

View file

@ -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"

View file

@ -0,0 +1,228 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="hyprland_toplevel_export_v1">
<copyright>
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.
</copyright>
<description summary="capturing the contents of toplevel windows">
This protocol allows clients to ask for exporting another toplevel's
surface(s) to a buffer.
Particularly useful for sharing a single window.
</description>
<interface name="hyprland_toplevel_export_manager_v1" version="2">
<description summary="manager to inform clients and begin capturing">
This object is a manager which offers requests to start capturing from a
source.
</description>
<request name="capture_toplevel">
<description summary="capture a toplevel">
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.
</description>
<arg name="frame" type="new_id" interface="hyprland_toplevel_export_frame_v1"/>
<arg name="overlay_cursor" type="int"
summary="composite cursor onto the frame"/>
<arg name="handle" type="uint" summary="the handle of the toplevel (window) to be captured"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the manager">
All objects created by the manager will still remain valid, until their
appropriate destroy request has been called.
</description>
</request>
<!-- Version 2 -->
<request name="capture_toplevel_with_wlr_toplevel_handle" since="2">
<description summary="capture a toplevel">
Same as capture_toplevel, but with a zwlr_foreign_toplevel_handle_v1 handle.
</description>
<arg name="frame" type="new_id" interface="hyprland_toplevel_export_frame_v1"/>
<arg name="overlay_cursor" type="int"
summary="composite cursor onto the frame"/>
<arg name="handle" type="object" interface="zwlr_foreign_toplevel_handle_v1" allow-null="false" summary="the zwlr_foreign_toplevel_handle_v1 handle of the toplevel to be captured"/>
</request>
<!-- End Version 2 -->
</interface>
<interface name="hyprland_toplevel_export_frame_v1" version="2">
<description summary="a frame ready for copy">
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.
</description>
<event name="buffer">
<description summary="wl_shm buffer information">
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.
</description>
<arg name="format" type="uint" enum="wl_shm.format" summary="buffer format"/>
<arg name="width" type="uint" summary="buffer width"/>
<arg name="height" type="uint" summary="buffer height"/>
<arg name="stride" type="uint" summary="buffer stride"/>
</event>
<request name="copy">
<description summary="copy the frame">
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.
</description>
<arg name="buffer" type="object" interface="wl_buffer"/>
<arg name="ignore_damage" type="int"/>
</request>
<event name="damage">
<description summary="carries the coordinates of the damaged region">
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.
</description>
<arg name="x" type="uint" summary="damaged x coordinates"/>
<arg name="y" type="uint" summary="damaged y coordinates"/>
<arg name="width" type="uint" summary="current width"/>
<arg name="height" type="uint" summary="current height"/>
</event>
<enum name="error">
<entry name="already_used" value="0"
summary="the object has already been used to copy a wl_buffer"/>
<entry name="invalid_buffer" value="1"
summary="buffer attributes are invalid"/>
</enum>
<enum name="flags" bitfield="true">
<entry name="y_invert" value="1" summary="contents are y-inverted"/>
</enum>
<event name="flags">
<description summary="frame flags">
Provides flags about the frame. This event is sent once before the
"ready" event.
</description>
<arg name="flags" type="uint" enum="flags" summary="frame flags"/>
</event>
<event name="ready">
<description summary="indicates frame is available for reading">
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.
</description>
<arg name="tv_sec_hi" type="uint"
summary="high 32 bits of the seconds part of the timestamp"/>
<arg name="tv_sec_lo" type="uint"
summary="low 32 bits of the seconds part of the timestamp"/>
<arg name="tv_nsec" type="uint"
summary="nanoseconds part of the timestamp"/>
</event>
<event name="failed">
<description summary="frame copy failed">
This event indicates that the attempted frame copy has failed.
After receiving this event, the client should destroy the object.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="delete this object, used or not">
Destroys the frame. This request can be sent at any time by the client.
</description>
</request>
<event name="linux_dmabuf">
<description summary="linux-dmabuf buffer information">
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.
</description>
<arg name="format" type="uint" summary="fourcc pixel format"/>
<arg name="width" type="uint" summary="buffer width"/>
<arg name="height" type="uint" summary="buffer height"/>
</event>
<event name="buffer_done">
<description summary="all buffer types reported">
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.
</description>
</event>
</interface>
</protocol>

View file

@ -314,60 +314,51 @@ impl AppState {
}
fn run_capture_all(self, opts: &CaptureOptions) -> Result<DynamicImage> {
// 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<Result<Option<image::RgbaImage>>> =
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<Result<Option<image::RgbaImage>>> = 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<u8> = 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<Rgba<u8>, Vec<u8>> = 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<Option<image::RgbaImage>> {
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<File> {
pub(crate) fn find_drm_render_node() -> Option<File> {
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<const R: usize, const G: usize, const B: usize, const A: usize>(
raw: &[u8],
width: u32,
height: u32,
stride: u32,
) -> Vec<u8> {
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<u8> {
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<u8> {
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<Vec<u8>> {
let mut rgba: Vec<u8> = 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<File> {
pub(crate) fn create_shm_file(size: usize) -> std::io::Result<File> {
#[cfg(target_os = "linux")]
{
use std::ffi::CStr;

View file

@ -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<Annotation>,
pub annotations: Vec<Box<dyn Annotation>>,
pub drag: DragState,
pub text_pending: Option<[f32; 2]>,
pub selected: Option<usize>,
@ -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<Box<dyn Annotation>> {
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<Annotation> {
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<dyn Annotation>]) -> 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)
}

View file

@ -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()

View file

@ -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,
}

View file

@ -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<String>, 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<hyprland::WindowInfo> = 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()))

View file

@ -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: Into<String>>(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<String> {
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<String> {
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<String> {
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<u8>, n: usize) {
while buf.len() % n != 0 {
buf.push(0);
}
}
fn write_u32(buf: &mut Vec<u8>, v: u32) {
align_to(buf, 4);
buf.extend_from_slice(&v.to_le_bytes());
}
fn write_string(buf: &mut Vec<u8>, 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<u8>, 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<u8>) {
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<u8>, 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<u8> {
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<u8>, 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<u8>, 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<u8>, 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(())
}

View file

@ -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();

View file

@ -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<St>,
queue: Option<EventQueue<St>>,
compositor: Option<wl_compositor::WlCompositor>,
shm: Option<wl_shm::WlShm>,
shell: Option<ZwlrLayerShellV1>,
seat: Option<wl_seat::WlSeat>,
_seat: Option<wl_seat::WlSeat>,
pointer: Option<wl_pointer::WlPointer>,
keyboard: Option<wl_keyboard::WlKeyboard>,
cursor_shape: Option<WpCursorShapeManagerV1>,
@ -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<File> {
#[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,

View file

@ -45,6 +45,11 @@ pub fn encode_png(img: &DynamicImage) -> Result<Vec<u8>> {
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<Vec<
pub fn round_corners(img: &DynamicImage, radius: u32) -> DynamicImage {
let (width, height) = img.dimensions();
let mut rounded: ImageBuffer<Rgba<u8>, Vec<u8>> = 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<Rgba<u8>, Vec<u8>> = 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<Rgba<u8>, Vec<u8>> = 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<Rgba<u8>, Vec<u8>> = 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;
}
}
}
}

716
src/toplevel_capture.rs Normal file
View file

@ -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<Option<DynamicImage>> {
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<Option<DynamicImage>> {
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<F>(opts: &CaptureOptions, pick: F) -> Result<Option<DynamicImage>>
where
F: FnOnce(&[WlrToplevel]) -> Option<ZwlrForeignToplevelHandleV1>,
{
let conn = match Connection::connect_to_env() {
Ok(c) => c,
Err(_) => return Ok(None),
};
let (globals, queue) = match registry_queue_init::<St>(&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<ZwpLinuxDmabufV1> = 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<St>,
queue: Option<EventQueue<St>>,
shm: wl_shm::WlShm,
shm_formats: Vec<wl_shm::Format>,
linux_dmabuf: Option<ZwpLinuxDmabufV1>,
toplevels: Vec<WlrToplevel>,
// per-capture
frame: Option<HyprlandToplevelExportFrameV1>,
frame_state: FrameState,
buffer_offers: Vec<BufferOffer>,
dmabuf_offers: Vec<DmabufOffer>,
chosen_offer: Option<BufferOffer>,
chosen_dmabuf: Option<DmabufChosen>,
shm_file: Option<File>,
shm_pool: Option<wl_shm_pool::WlShmPool>,
wl_buffer: Option<wl_buffer::WlBuffer>,
gbm_bo: Option<gbm::BufferObject<()>>,
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<BufferOffer> {
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<DynamicImage> {
// 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<u8> = 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<Rgba<u8>, Vec<u8>> = 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<wl_registry::WlRegistry, GlobalListContents> for St {
fn event(
_: &mut Self,
_: &wl_registry::WlRegistry,
_: wl_registry::Event,
_: &GlobalListContents,
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<wl_shm::WlShm, ()> for St {
fn event(
state: &mut Self,
_: &wl_shm::WlShm,
event: wl_shm::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let wl_shm::Event::Format { format } = event {
if let wayland_client::WEnum::Value(f) = format {
state.shm_formats.push(f);
}
}
}
}
impl Dispatch<ZwlrForeignToplevelManagerV1, ()> for St {
fn event(
state: &mut Self,
_: &ZwlrForeignToplevelManagerV1,
event: zwlr_foreign_toplevel_manager_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
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<ZwlrForeignToplevelHandleV1, ()> for St {
fn event(
state: &mut Self,
handle: &ZwlrForeignToplevelHandleV1,
event: zwlr_foreign_toplevel_handle_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
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<HyprlandToplevelExportFrameV1, ()> for St {
fn event(
state: &mut Self,
_: &HyprlandToplevelExportFrameV1,
event: hyprland_toplevel_export_frame_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
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);

View file

@ -354,7 +354,6 @@ pub fn visible_window_hints() -> Option<Vec<HintBox>> {
y: ly,
w: lw,
h: lh,
label: tl.app_id.clone(),
})
})
.collect();