reduce size of binary, remove notify-rust dep
This commit is contained in:
parent
87045180dc
commit
b58c2df1d9
15 changed files with 2104 additions and 1101 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1 +1 @@
|
|||
target/
|
||||
/target/
|
||||
|
|
|
|||
418
Cargo.lock
generated
418
Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
228
protocols/hyprland-toplevel-export-v1.xml
Normal file
228
protocols/hyprland-toplevel-export-v1.xml
Normal 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>
|
||||
444
src/capture.rs
444
src/capture.rs
|
|
@ -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;
|
||||
let mut canvas = image::RgbaImage::new(logical_total_w, logical_total_h);
|
||||
for mon in &mons {
|
||||
let state = AppState::connect()?;
|
||||
let out = state
|
||||
.outputs
|
||||
.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()
|
||||
.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,
|
||||
);
|
||||
.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, 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()?;
|
||||
.map(|(name, _lx, _ly, lw, lh)| {
|
||||
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);
|
||||
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,118 +798,114 @@ 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);
|
||||
Ok(pack10_abgr_to_rgba8(raw, width, height, stride))
|
||||
}
|
||||
}
|
||||
}
|
||||
other => {
|
||||
return Err(BlastError::Capture(format!(
|
||||
other => Err(BlastError::Capture(format!(
|
||||
"unhandled DRM format 0x{other:08x} in dmabuf capture -> please report this"
|
||||
)));
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(rgba)
|
||||
}
|
||||
|
||||
// Wayland dispatch
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for AppState {
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -43,75 +43,350 @@ impl Tool {
|
|||
}
|
||||
}
|
||||
|
||||
// Annotation data
|
||||
// Annotation trait + concrete types
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
},
|
||||
pub struct RectAnn {
|
||||
pub r: [f32; 4], // x0,y0,x1,y1 in image-space
|
||||
pub color: Color32,
|
||||
pub thickness: f32,
|
||||
pub filled: bool,
|
||||
}
|
||||
|
||||
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(
|
||||
#[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),
|
||||
);
|
||||
outer.contains(p)
|
||||
}
|
||||
Annotation::Arrow { from, to, .. } => {
|
||||
point_seg_dist(p, Pos2::new(from[0], from[1]), Pos2::new(to[0], to[1])) < tolerance
|
||||
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,
|
||||
);
|
||||
}
|
||||
Annotation::Text {
|
||||
pos, text, size, ..
|
||||
} => {
|
||||
}
|
||||
|
||||
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 = text.len() as f32 * size * 0.55;
|
||||
let est_h = *size;
|
||||
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(pos[0] - tolerance, pos[1] - tolerance),
|
||||
Pos2::new(pos[0] + est_w + tolerance, pos[1] + est_h + tolerance),
|
||||
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)
|
||||
}
|
||||
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
|
||||
}),
|
||||
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);
|
||||
ann.flatten(&mut out, 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DynamicImage::ImageRgba8(out)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
66
src/main.rs
66
src/main.rs
|
|
@ -8,6 +8,7 @@ mod paths;
|
|||
mod region;
|
||||
mod select;
|
||||
mod shhh;
|
||||
mod toplevel_capture;
|
||||
mod wayland_windows;
|
||||
|
||||
use std::{
|
||||
|
|
@ -351,14 +352,16 @@ fn run_copysave(subject: Subject, file: Option<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,14 +449,23 @@ 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 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()
|
||||
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(|| {
|
||||
hypr_windows
|
||||
.iter()
|
||||
.map(|w| {
|
||||
let g = w.to_geometry(border_size, bar);
|
||||
select::HintBox {
|
||||
|
|
@ -446,12 +473,9 @@ fn capture_area(ctx: &Context) -> Result<(DynamicImage, String)> {
|
|||
y: g.y as i32,
|
||||
w: g.w as i32,
|
||||
h: g.h as i32,
|
||||
label: w.class.clone(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
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()))
|
||||
|
|
|
|||
326
src/notify.rs
326
src/notify.rs
|
|
@ -1,37 +1,317 @@
|
|||
use notify_rust::{Notification, Urgency};
|
||||
//! Minimal DBus client for org.freedesktop.Notifications.Notify.
|
||||
//!
|
||||
//! Implements just enough of the DBus binary wire protocol to fire a single
|
||||
//! Notify call, without pulling in zbus / notify-rust. All multi-byte values
|
||||
//! are little-endian; this client declares 'l' in the message header and only
|
||||
//! handles little-endian replies (sufficient on every relevant platform).
|
||||
|
||||
use std::env;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::error::{BlastError, Result};
|
||||
|
||||
const URGENCY_NORMAL: u8 = 1;
|
||||
const URGENCY_CRITICAL: u8 = 2;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn notify_ok(summary: &str, body: Option<&str>, icon: Option<&str>) -> Result<()> {
|
||||
let mut n = Notification::new();
|
||||
n.appname("blast").summary(summary).timeout(3000);
|
||||
|
||||
if let Some(b) = body {
|
||||
n.body(b);
|
||||
}
|
||||
if let Some(i) = icon {
|
||||
n.icon(i);
|
||||
}
|
||||
|
||||
n.show().map_err(|e| BlastError::Notify(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
send_notify(
|
||||
summary,
|
||||
body.unwrap_or(""),
|
||||
icon.unwrap_or(""),
|
||||
URGENCY_NORMAL,
|
||||
3000,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn notify_error(summary: &str, body: Option<&str>) -> Result<()> {
|
||||
let mut n = Notification::new();
|
||||
n.appname("blast")
|
||||
.summary(summary)
|
||||
.urgency(Urgency::Critical)
|
||||
.timeout(5000);
|
||||
|
||||
if let Some(b) = body {
|
||||
n.body(b);
|
||||
send_notify(
|
||||
summary,
|
||||
body.unwrap_or(""),
|
||||
"",
|
||||
URGENCY_CRITICAL,
|
||||
5000,
|
||||
)
|
||||
}
|
||||
|
||||
n.show().map_err(|e| BlastError::Notify(e.to_string()))?;
|
||||
fn err<S: Into<String>>(s: S) -> BlastError {
|
||||
BlastError::Notify(s.into())
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
203
src/shhh.rs
203
src/shhh.rs
|
|
@ -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),
|
||||
))
|
||||
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 {
|
||||
None
|
||||
dx_in as f32 + 1.0
|
||||
};
|
||||
|
||||
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);
|
||||
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
716
src/toplevel_capture.rs
Normal 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);
|
||||
|
|
@ -354,7 +354,6 @@ pub fn visible_window_hints() -> Option<Vec<HintBox>> {
|
|||
y: ly,
|
||||
w: lw,
|
||||
h: lh,
|
||||
label: tl.app_id.clone(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue