feat: picker windows, layout, state and icons
This commit is contained in:
1 parent
3f529f3a65
commit
c6736b0ac3
8 files changed
+780
No files matched your search
Generated
+207
@@ -189,6 +189,12 @@ version = "1.5.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.23.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@@ -436,6 +442,12 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "color_quant"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
@@ -528,6 +540,12 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-url"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch"
|
name = "dispatch"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -819,6 +837,12 @@ dependencies = [
|
|||||||
"zlib-rs",
|
"zlib-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "float-cmp"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -840,6 +864,28 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fontconfig-parser"
|
||||||
|
version = "0.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
|
||||||
|
dependencies = [
|
||||||
|
"roxmltree 0.20.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fontdb"
|
||||||
|
version = "0.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2660c5e9157bf76d2db1294e4a9feba604ef610819a3b591088d0d8392a3290f"
|
||||||
|
dependencies = [
|
||||||
|
"fontconfig-parser",
|
||||||
|
"log",
|
||||||
|
"memmap2",
|
||||||
|
"slotmap",
|
||||||
|
"tinyvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -924,6 +970,16 @@ dependencies = [
|
|||||||
"r-efi 6.0.0",
|
"r-efi 6.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gif"
|
||||||
|
version = "0.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
|
||||||
|
dependencies = [
|
||||||
|
"color_quant",
|
||||||
|
"weezl",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gl_generator"
|
name = "gl_generator"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -1130,6 +1186,22 @@ dependencies = [
|
|||||||
"tiff",
|
"tiff",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "image-webp"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder-lite",
|
||||||
|
"quick-error",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "imagesize"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65b27460c2c92b037f3f94c538ed9a3342f3fdf923606781629ccb35f82d042a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.2"
|
version = "2.14.2"
|
||||||
@@ -1912,6 +1984,12 @@ dependencies = [
|
|||||||
"indexmap",
|
"indexmap",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pico-args"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.13"
|
version = "1.1.13"
|
||||||
@@ -2134,6 +2212,48 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "resvg"
|
||||||
|
version = "0.48.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67e3803f97b999e80cbf7c6ecdd07a8102204d92e1633cf48783720c521196bd"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"gif",
|
||||||
|
"image-webp",
|
||||||
|
"log",
|
||||||
|
"pico-args",
|
||||||
|
"rgb",
|
||||||
|
"svgtypes",
|
||||||
|
"tiny-skia",
|
||||||
|
"usvg",
|
||||||
|
"zune-jpeg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rgb"
|
||||||
|
version = "0.8.53"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "roxmltree"
|
||||||
|
version = "0.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "roxmltree"
|
||||||
|
version = "0.21.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -2319,6 +2439,21 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simplecss"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "siphasher"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "skrifa"
|
name = "skrifa"
|
||||||
version = "0.44.0"
|
version = "0.44.0"
|
||||||
@@ -2461,6 +2596,9 @@ name = "strict-num"
|
|||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||||
|
dependencies = [
|
||||||
|
"float-cmp",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
@@ -2468,6 +2606,16 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "svgtypes"
|
||||||
|
version = "0.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d"
|
||||||
|
dependencies = [
|
||||||
|
"kurbo",
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.119"
|
version = "2.0.119"
|
||||||
@@ -2592,6 +2740,12 @@ dependencies = [
|
|||||||
"strict-num",
|
"strict-num",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec"
|
||||||
|
version = "1.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.53.1"
|
version = "1.53.1"
|
||||||
@@ -2740,6 +2894,12 @@ dependencies = [
|
|||||||
"rustc-hash 2.1.3",
|
"rustc-hash 2.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-bidi"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-general-category"
|
name = "unicode-general-category"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -2752,18 +2912,58 @@ version = "1.0.26"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-script"
|
||||||
|
version = "0.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-segmentation"
|
name = "unicode-segmentation"
|
||||||
version = "1.13.3"
|
version = "1.13.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-vo"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "usvg"
|
||||||
|
version = "0.48.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "977d0a4abdef933f424a99fe09f95576e089b90aebc6f016a3bc813762493e91"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"data-url",
|
||||||
|
"flate2",
|
||||||
|
"fontdb",
|
||||||
|
"harfrust",
|
||||||
|
"imagesize",
|
||||||
|
"kurbo",
|
||||||
|
"log",
|
||||||
|
"pico-args",
|
||||||
|
"roxmltree 0.21.1",
|
||||||
|
"simplecss",
|
||||||
|
"siphasher",
|
||||||
|
"skrifa",
|
||||||
|
"strict-num",
|
||||||
|
"svgtypes",
|
||||||
|
"tiny-skia-path",
|
||||||
|
"unicode-bidi",
|
||||||
|
"unicode-script",
|
||||||
|
"unicode-vo",
|
||||||
|
"xmlwriter",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -2797,6 +2997,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"libloading",
|
"libloading",
|
||||||
"niri-ipc",
|
"niri-ipc",
|
||||||
|
"resvg",
|
||||||
"rustix 1.1.5",
|
"rustix 1.1.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_ignored",
|
"serde_ignored",
|
||||||
@@ -3601,6 +3802,12 @@ version = "0.8.29"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xmlwriter"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.59"
|
version = "0.8.59"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ image = { version = "0.25.10", default-features = false, features = ["png"] }
|
|||||||
khronos-egl = { version = "6.0.0", features = ["dynamic"] }
|
khronos-egl = { version = "6.0.0", features = ["dynamic"] }
|
||||||
libc = "0.2.189"
|
libc = "0.2.189"
|
||||||
libloading = "0.8"
|
libloading = "0.8"
|
||||||
|
resvg = "0.48.1"
|
||||||
niri-ipc = "=26.4.0"
|
niri-ipc = "=26.4.0"
|
||||||
rustix = { version = "1.1.5", features = ["event", "fs", "pipe", "process", "stdio"] }
|
rustix = { version = "1.1.5", features = ["event", "fs", "pipe", "process", "stdio"] }
|
||||||
serde = { version = "1.0.229", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ mod lock;
|
|||||||
mod log;
|
mod log;
|
||||||
mod niri;
|
mod niri;
|
||||||
mod output;
|
mod output;
|
||||||
|
// The picker lands in pieces; Task 6 of the theme plan removes this.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
mod picker;
|
||||||
mod region;
|
mod region;
|
||||||
mod render;
|
mod render;
|
||||||
mod selection;
|
mod selection;
|
||||||
|
|||||||
+56
@@ -107,6 +107,26 @@ pub fn is_our_capture(event: &Event, path: &Path) -> bool {
|
|||||||
matches!(event, Event::ScreenshotCaptured { path: Some(p) } if Path::new(p) == path)
|
matches!(event, Event::ScreenshotCaptured { path: Some(p) } if Path::new(p) == path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every window niri has.
|
||||||
|
#[allow(dead_code)] // the picker (theme plan Task 6) removes this
|
||||||
|
pub fn windows() -> Result<Vec<Window>> {
|
||||||
|
match request(Request::Windows)? {
|
||||||
|
Response::Windows(windows) => Ok(windows),
|
||||||
|
other => Err(anyhow!("unexpected niri reply: {other:?}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `windows` most recently used first: the focused one, then by focus
|
||||||
|
/// time, then those never focused, by id.
|
||||||
|
#[allow(dead_code)] // the picker (theme plan Task 6) removes this
|
||||||
|
pub fn mru(mut windows: Vec<Window>) -> Vec<Window> {
|
||||||
|
windows.sort_by_key(|w| {
|
||||||
|
let at = w.focus_timestamp.map(|t| (t.secs, t.nanos));
|
||||||
|
(!w.is_focused, std::cmp::Reverse(at), w.id)
|
||||||
|
});
|
||||||
|
windows
|
||||||
|
}
|
||||||
|
|
||||||
/// The output showing the workspace a window is on, if niri knows it.
|
/// The output showing the workspace a window is on, if niri knows it.
|
||||||
pub fn output_of(workspace_id: Option<u64>, workspaces: &[Workspace]) -> Option<String> {
|
pub fn output_of(workspace_id: Option<u64>, workspaces: &[Workspace]) -> Option<String> {
|
||||||
let id = workspace_id?;
|
let id = workspace_id?;
|
||||||
@@ -132,6 +152,42 @@ pub fn parse_version(v: &str) -> Option<(u32, u32)> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn window(id: u64, focused: bool, at: Option<u64>) -> Window {
|
||||||
|
Window {
|
||||||
|
id,
|
||||||
|
title: None,
|
||||||
|
app_id: None,
|
||||||
|
pid: None,
|
||||||
|
workspace_id: None,
|
||||||
|
is_focused: focused,
|
||||||
|
is_floating: false,
|
||||||
|
is_urgent: false,
|
||||||
|
layout: niri_ipc::WindowLayout {
|
||||||
|
pos_in_scrolling_layout: None,
|
||||||
|
tile_size: (0.0, 0.0),
|
||||||
|
window_size: (0, 0),
|
||||||
|
tile_pos_in_workspace_view: None,
|
||||||
|
window_offset_in_tile: (0.0, 0.0),
|
||||||
|
},
|
||||||
|
focus_timestamp: at.map(|secs| niri_ipc::Timestamp { secs, nanos: 0 }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn most_recently_used_first() {
|
||||||
|
let order: Vec<u64> = mru(vec![
|
||||||
|
window(1, false, Some(10)),
|
||||||
|
window(2, false, None),
|
||||||
|
window(3, true, Some(5)),
|
||||||
|
window(4, false, Some(20)),
|
||||||
|
window(5, false, None),
|
||||||
|
])
|
||||||
|
.iter()
|
||||||
|
.map(|w| w.id)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(order, [3, 4, 1, 2, 5]);
|
||||||
|
}
|
||||||
|
|
||||||
fn ws(id: u64, output: Option<&str>) -> Workspace {
|
fn ws(id: u64, output: Option<&str>) -> Workspace {
|
||||||
Workspace {
|
Workspace {
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
//! Finding and loading a window's app icon: app id → desktop file →
|
||||||
|
//! `Icon=` → the icon theme (with its `Inherits`), then hicolor.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use image::RgbaImage;
|
||||||
|
|
||||||
|
/// Where to look.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Dirs {
|
||||||
|
/// `$XDG_DATA_HOME` then `$XDG_DATA_DIRS`.
|
||||||
|
pub data: Vec<PathBuf>,
|
||||||
|
/// The icon theme's directory name.
|
||||||
|
pub theme: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dirs {
|
||||||
|
pub fn from_env() -> Dirs {
|
||||||
|
let home = crate::config::home();
|
||||||
|
let mut data = vec![
|
||||||
|
std::env::var_os("XDG_DATA_HOME")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| home.join(".local/share")),
|
||||||
|
];
|
||||||
|
let dirs =
|
||||||
|
std::env::var("XDG_DATA_DIRS").unwrap_or_else(|_| "/usr/local/share:/usr/share".into());
|
||||||
|
data.extend(dirs.split(':').filter(|d| !d.is_empty()).map(PathBuf::from));
|
||||||
|
let theme = std::fs::read_to_string(home.join(".config/gtk-3.0/settings.ini"))
|
||||||
|
.ok()
|
||||||
|
.and_then(|t| {
|
||||||
|
t.lines().find_map(|l| {
|
||||||
|
let (k, v) = l.split_once('=')?;
|
||||||
|
(k.trim() == "gtk-icon-theme-name").then(|| v.trim().to_string())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "hicolor".into());
|
||||||
|
Dirs { data, theme }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIZES: [&str; 6] = ["scalable", "256x256", "128x128", "96x96", "64x64", "48x48"];
|
||||||
|
|
||||||
|
/// The icon file for `app_id`, if any.
|
||||||
|
pub fn find(app_id: &str, dirs: &Dirs) -> Option<PathBuf> {
|
||||||
|
let icon = desktop_icon(app_id, dirs).unwrap_or_else(|| app_id.to_string());
|
||||||
|
let path = Path::new(&icon);
|
||||||
|
if path.is_absolute() {
|
||||||
|
return path.exists().then(|| path.to_path_buf());
|
||||||
|
}
|
||||||
|
themed(&icon, dirs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Icon=` from the app's desktop file.
|
||||||
|
fn desktop_icon(app_id: &str, dirs: &Dirs) -> Option<String> {
|
||||||
|
let wanted = app_id.to_lowercase();
|
||||||
|
let mut by_class = None;
|
||||||
|
for dir in &dirs.data {
|
||||||
|
let apps = dir.join("applications");
|
||||||
|
let exact = apps.join(format!("{app_id}.desktop"));
|
||||||
|
if let Some(icon) = icon_line(&exact) {
|
||||||
|
return Some(icon);
|
||||||
|
}
|
||||||
|
let Ok(entries) = std::fs::read_dir(&apps) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
let stem = path
|
||||||
|
.file_stem()
|
||||||
|
.map(|s| s.to_string_lossy().to_lowercase())
|
||||||
|
.unwrap_or_default();
|
||||||
|
if path.extension().is_some_and(|e| e == "desktop") {
|
||||||
|
if stem == wanted {
|
||||||
|
if let Some(icon) = icon_line(&path) {
|
||||||
|
return Some(icon);
|
||||||
|
}
|
||||||
|
} else if by_class.is_none() {
|
||||||
|
let text = std::fs::read_to_string(&path).unwrap_or_default();
|
||||||
|
let class = text.lines().find_map(|l| l.strip_prefix("StartupWMClass="));
|
||||||
|
if class.is_some_and(|c| c.trim().to_lowercase() == wanted) {
|
||||||
|
by_class = icon_line(&path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
by_class
|
||||||
|
}
|
||||||
|
|
||||||
|
fn icon_line(desktop: &Path) -> Option<String> {
|
||||||
|
let text = std::fs::read_to_string(desktop).ok()?;
|
||||||
|
text.lines()
|
||||||
|
.find_map(|l| l.strip_prefix("Icon="))
|
||||||
|
.map(|i| i.trim().to_string())
|
||||||
|
.filter(|i| !i.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `name` in the theme, its parents, hicolor, then pixmaps.
|
||||||
|
fn themed(name: &str, dirs: &Dirs) -> Option<PathBuf> {
|
||||||
|
let mut themes = vec![dirs.theme.clone()];
|
||||||
|
let mut i = 0;
|
||||||
|
while i < themes.len() && i < 8 {
|
||||||
|
for parent in inherits(&themes[i], dirs) {
|
||||||
|
if !themes.contains(&parent) {
|
||||||
|
themes.push(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if !themes.iter().any(|t| t == "hicolor") {
|
||||||
|
themes.push("hicolor".into());
|
||||||
|
}
|
||||||
|
for theme in &themes {
|
||||||
|
for dir in &dirs.data {
|
||||||
|
let root = dir.join("icons").join(theme);
|
||||||
|
if !root.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for size in SIZES {
|
||||||
|
for ext in ["svg", "png"] {
|
||||||
|
let p = root.join(size).join("apps").join(format!("{name}.{ext}"));
|
||||||
|
if p.exists() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Other layouts: any <size>/apps or apps/<size>.
|
||||||
|
if let Ok(entries) = std::fs::read_dir(&root) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
for sub in [entry.path().join("apps"), entry.path()] {
|
||||||
|
for ext in ["svg", "png"] {
|
||||||
|
let p = sub.join(format!("{name}.{ext}"));
|
||||||
|
if p.exists() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dirs.data.iter().find_map(|dir| {
|
||||||
|
["svg", "png"]
|
||||||
|
.iter()
|
||||||
|
.map(|ext| dir.join("pixmaps").join(format!("{name}.{ext}")))
|
||||||
|
.find(|p| p.exists())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inherits(theme: &str, dirs: &Dirs) -> Vec<String> {
|
||||||
|
dirs.data
|
||||||
|
.iter()
|
||||||
|
.find_map(|d| std::fs::read_to_string(d.join("icons").join(theme).join("index.theme")).ok())
|
||||||
|
.and_then(|t| {
|
||||||
|
t.lines()
|
||||||
|
.find_map(|l| l.strip_prefix("Inherits=").map(str::to_string))
|
||||||
|
})
|
||||||
|
.map(|v| {
|
||||||
|
v.split(',')
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The icon at `size` × `size` px (straight alpha).
|
||||||
|
pub fn load(path: &Path, size: u32) -> Option<RgbaImage> {
|
||||||
|
if path.extension().is_some_and(|e| e == "svg") {
|
||||||
|
let data = std::fs::read(path).ok()?;
|
||||||
|
let tree = resvg::usvg::Tree::from_data(&data, &resvg::usvg::Options::default()).ok()?;
|
||||||
|
let mut pixmap = resvg::tiny_skia::Pixmap::new(size, size)?;
|
||||||
|
let s = tree.size();
|
||||||
|
let k = size as f32 / s.width().max(s.height());
|
||||||
|
resvg::render(
|
||||||
|
&tree,
|
||||||
|
resvg::tiny_skia::Transform::from_scale(k, k),
|
||||||
|
&mut pixmap.as_mut(),
|
||||||
|
);
|
||||||
|
let mut img = RgbaImage::new(size, size);
|
||||||
|
for (dst, src) in img.pixels_mut().zip(pixmap.pixels()) {
|
||||||
|
let c = src.demultiply();
|
||||||
|
dst.0 = [c.red(), c.green(), c.blue(), c.alpha()];
|
||||||
|
}
|
||||||
|
Some(img)
|
||||||
|
} else {
|
||||||
|
let img = image::ImageReader::open(path)
|
||||||
|
.ok()?
|
||||||
|
.with_guessed_format()
|
||||||
|
.ok()?
|
||||||
|
.decode()
|
||||||
|
.ok()?;
|
||||||
|
Some(image::imageops::resize(
|
||||||
|
&img.to_rgba8(),
|
||||||
|
size,
|
||||||
|
size,
|
||||||
|
image::imageops::FilterType::Triangle,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
fn png(path: &std::path::Path, rgba: [u8; 4]) {
|
||||||
|
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||||
|
image::RgbaImage::from_pixel(8, 8, image::Rgba(rgba))
|
||||||
|
.save(path)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(path: &std::path::Path, text: &str) {
|
||||||
|
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||||
|
fs::write(path, text).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
const SVG: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><rect width="16" height="16" fill="#ff0000"/></svg>"##;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn finds_icons_through_desktop_files_and_themes() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let data = tmp.path().join("share");
|
||||||
|
let dirs = Dirs {
|
||||||
|
data: vec![data.clone()],
|
||||||
|
theme: "mytheme".into(),
|
||||||
|
};
|
||||||
|
// By desktop file name, icon in hicolor.
|
||||||
|
write(
|
||||||
|
&data.join("applications/org.telegram.desktop.desktop"),
|
||||||
|
"[Desktop Entry]\nName=Telegram\nIcon=telegram\n",
|
||||||
|
);
|
||||||
|
png(
|
||||||
|
&data.join("icons/hicolor/256x256/apps/telegram.png"),
|
||||||
|
[0, 0, 255, 255],
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
find("org.telegram.desktop", &dirs),
|
||||||
|
Some(data.join("icons/hicolor/256x256/apps/telegram.png"))
|
||||||
|
);
|
||||||
|
// By StartupWMClass, icon in a theme that inherits hicolor.
|
||||||
|
write(
|
||||||
|
&data.join("applications/foo.desktop"),
|
||||||
|
"[Desktop Entry]\nIcon=foo-icon\nStartupWMClass=FooApp\n",
|
||||||
|
);
|
||||||
|
write(
|
||||||
|
&data.join("icons/mytheme/index.theme"),
|
||||||
|
"[Icon Theme]\nName=My\nInherits=hicolor\n",
|
||||||
|
);
|
||||||
|
png(
|
||||||
|
&data.join("icons/hicolor/48x48/apps/foo-icon.png"),
|
||||||
|
[0, 255, 0, 255],
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
find("FooApp", &dirs),
|
||||||
|
Some(data.join("icons/hicolor/48x48/apps/foo-icon.png"))
|
||||||
|
);
|
||||||
|
// The theme wins over hicolor, scalable over bitmaps.
|
||||||
|
write(&data.join("icons/mytheme/scalable/apps/telegram.svg"), SVG);
|
||||||
|
assert_eq!(
|
||||||
|
find("org.telegram.desktop", &dirs),
|
||||||
|
Some(data.join("icons/mytheme/scalable/apps/telegram.svg"))
|
||||||
|
);
|
||||||
|
// An absolute Icon= path.
|
||||||
|
let abs = tmp.path().join("abs.png");
|
||||||
|
png(&abs, [1, 2, 3, 255]);
|
||||||
|
write(
|
||||||
|
&data.join("applications/bar.desktop"),
|
||||||
|
&format!("[Desktop Entry]\nIcon={}\n", abs.display()),
|
||||||
|
);
|
||||||
|
assert_eq!(find("bar", &dirs), Some(abs));
|
||||||
|
// No desktop file: the app id itself as the icon name.
|
||||||
|
png(
|
||||||
|
&data.join("icons/hicolor/64x64/apps/kitty.png"),
|
||||||
|
[9, 9, 9, 255],
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
find("kitty", &dirs),
|
||||||
|
Some(data.join("icons/hicolor/64x64/apps/kitty.png"))
|
||||||
|
);
|
||||||
|
assert_eq!(find("unknown.app", &dirs), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loads_svg_and_png_at_the_asked_size() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let svg = tmp.path().join("x.svg");
|
||||||
|
fs::write(&svg, SVG).unwrap();
|
||||||
|
let img = load(&svg, 48).unwrap();
|
||||||
|
assert_eq!(img.dimensions(), (48, 48));
|
||||||
|
assert_eq!(img.get_pixel(24, 24).0, [255, 0, 0, 255]);
|
||||||
|
let p = tmp.path().join("x.png");
|
||||||
|
png(&p, [0, 0, 255, 255]);
|
||||||
|
let img = load(&p, 32).unwrap();
|
||||||
|
assert_eq!(img.dimensions(), (32, 32));
|
||||||
|
assert_eq!(img.get_pixel(5, 5).0, [0, 0, 255, 255]);
|
||||||
|
assert!(load(&tmp.path().join("none.png"), 32).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
//! Where the picker's cards go, in logical px of the output.
|
||||||
|
|
||||||
|
/// A card at rest: icon, title and app name.
|
||||||
|
pub const CARD: (f32, f32) = (200.0, 200.0);
|
||||||
|
pub const GAP: f32 = 24.0;
|
||||||
|
/// The selected card's size relative to the others.
|
||||||
|
pub const GROW: f32 = 1.1;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Rect {
|
||||||
|
pub x: f32,
|
||||||
|
pub y: f32,
|
||||||
|
pub w: f32,
|
||||||
|
pub h: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rect {
|
||||||
|
pub fn contains(&self, p: (f32, f32)) -> bool {
|
||||||
|
p.0 >= self.x && p.0 < self.x + self.w && p.1 >= self.y && p.1 < self.y + self.h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `n` cards centred on an output of `output` logical px: rows wrap inside
|
||||||
|
/// 90 % of the width, and the grid shrinks to fit 60 % of the height. The
|
||||||
|
/// `selected` card grows about its centre.
|
||||||
|
pub fn cards(n: usize, output: (f32, f32), selected: usize) -> Vec<Rect> {
|
||||||
|
if n == 0 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let (max_w, max_h) = (output.0 * 0.9, output.1 * 0.6);
|
||||||
|
// Shrink until the grid fits both ways.
|
||||||
|
let mut scale = 1.0f32;
|
||||||
|
let (per_row, rows) = loop {
|
||||||
|
let (cw, g) = (CARD.0 * scale, GAP * scale);
|
||||||
|
let per_row = (((max_w + g) / (cw + g)).floor() as usize).clamp(1, n);
|
||||||
|
let rows = n.div_ceil(per_row);
|
||||||
|
let grid_h = rows as f32 * CARD.1 * scale + (rows - 1) as f32 * g;
|
||||||
|
if grid_h <= max_h || scale < 0.2 {
|
||||||
|
break (per_row, rows);
|
||||||
|
}
|
||||||
|
scale *= 0.95;
|
||||||
|
};
|
||||||
|
let (cw, ch, g) = (CARD.0 * scale, CARD.1 * scale, GAP * scale);
|
||||||
|
let grid_h = rows as f32 * ch + (rows - 1) as f32 * g;
|
||||||
|
let top = (output.1 - grid_h) / 2.0;
|
||||||
|
(0..n)
|
||||||
|
.map(|i| {
|
||||||
|
let (row, col) = (i / per_row, i % per_row);
|
||||||
|
let in_row = per_row.min(n - row * per_row);
|
||||||
|
let row_w = in_row as f32 * cw + (in_row - 1) as f32 * g;
|
||||||
|
let r = Rect {
|
||||||
|
x: (output.0 - row_w) / 2.0 + col as f32 * (cw + g),
|
||||||
|
y: top + row as f32 * (ch + g),
|
||||||
|
w: cw,
|
||||||
|
h: ch,
|
||||||
|
};
|
||||||
|
if i == selected { grow(r) } else { r }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grow(r: Rect) -> Rect {
|
||||||
|
let (w, h) = (r.w * GROW, r.h * GROW);
|
||||||
|
Rect {
|
||||||
|
x: r.x - (w - r.w) / 2.0,
|
||||||
|
y: r.y - (h - r.h) / 2.0,
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The card under `p`, the selected (grown) one first.
|
||||||
|
pub fn hit(cards: &[Rect], p: (f32, f32)) -> Option<usize> {
|
||||||
|
let mut order: Vec<usize> = (0..cards.len()).collect();
|
||||||
|
order.sort_by(|a, b| (cards[*b].w).total_cmp(&cards[*a].w));
|
||||||
|
order.into_iter().find(|i| cards[*i].contains(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `selected` moved by `delta`, wrapping around `n` cards.
|
||||||
|
pub fn step(selected: usize, n: usize, delta: i32) -> usize {
|
||||||
|
if n == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
(selected as i64 + delta as i64).rem_euclid(n as i64) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn overlap(a: &Rect, b: &Rect) -> bool {
|
||||||
|
a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_card_is_centred() {
|
||||||
|
let c = cards(1, (1920.0, 1080.0), 0);
|
||||||
|
let r = c[0];
|
||||||
|
assert!(
|
||||||
|
((r.x + r.w / 2.0) - 960.0).abs() < 0.01 && ((r.y + r.h / 2.0) - 540.0).abs() < 0.01
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn five_cards_fit_one_row_on_1080p() {
|
||||||
|
let c = cards(5, (1920.0, 1080.0), 2);
|
||||||
|
let unselected: Vec<_> = c
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, _)| *i != 2)
|
||||||
|
.map(|(_, r)| r.y)
|
||||||
|
.collect();
|
||||||
|
assert!(unselected.windows(2).all(|w| w[0] == w[1]), "one row");
|
||||||
|
for i in 0..5 {
|
||||||
|
for j in i + 1..5 {
|
||||||
|
assert!(!overlap(&c[i], &c[j]), "{i} and {j} overlap");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn many_cards_wrap_and_shrink_to_fit() {
|
||||||
|
let (w, h) = (1366.0, 768.0);
|
||||||
|
let c = cards(20, (w, h), 0);
|
||||||
|
assert_eq!(c.len(), 20);
|
||||||
|
let left = c.iter().map(|r| r.x).fold(f32::MAX, f32::min);
|
||||||
|
let right = c.iter().map(|r| r.x + r.w).fold(0.0, f32::max);
|
||||||
|
let top = c.iter().skip(1).map(|r| r.y).fold(f32::MAX, f32::min);
|
||||||
|
let bottom = c.iter().skip(1).map(|r| r.y + r.h).fold(0.0, f32::max);
|
||||||
|
assert!(right - left <= w * 0.9 + 1.0, "{left}..{right}");
|
||||||
|
assert!(bottom - top <= h * 0.6 + 1.0, "{top}..{bottom}");
|
||||||
|
for i in 1..20 {
|
||||||
|
for j in i + 1..20 {
|
||||||
|
assert!(!overlap(&c[i], &c[j]), "{i} and {j} overlap");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_selected_card_grows_about_its_centre() {
|
||||||
|
let plain = cards(3, (1920.0, 1080.0), 0);
|
||||||
|
let chosen = cards(3, (1920.0, 1080.0), 1);
|
||||||
|
let (a, b) = (plain[1], chosen[1]);
|
||||||
|
assert!((b.w - a.w * 1.1).abs() < 0.01);
|
||||||
|
assert!(((a.x + a.w / 2.0) - (b.x + b.w / 2.0)).abs() < 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hits_and_gaps() {
|
||||||
|
let c = cards(3, (1920.0, 1080.0), 0);
|
||||||
|
let mid = |r: Rect| (r.x + r.w / 2.0, r.y + r.h / 2.0);
|
||||||
|
assert_eq!(hit(&c, mid(c[2])), Some(2));
|
||||||
|
assert_eq!(hit(&c, (5.0, 5.0)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn steps_wrap() {
|
||||||
|
assert_eq!(step(4, 5, 1), 0);
|
||||||
|
assert_eq!(step(0, 5, -1), 4);
|
||||||
|
assert_eq!(step(2, 5, 1), 3);
|
||||||
|
assert_eq!(step(0, 0, 1), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
//! `valw window`: an Alt+Tab-style window picker over the paint shader.
|
||||||
|
|
||||||
|
pub mod icons;
|
||||||
|
pub mod layout;
|
||||||
|
pub mod state;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//! The picker remembers which motion it used last, to alternate them.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
pub const SWIRL: f32 = 1.0;
|
||||||
|
pub const FLOW: f32 = 0.0;
|
||||||
|
|
||||||
|
/// `$XDG_STATE_HOME/valw/picker`.
|
||||||
|
pub fn default_path() -> PathBuf {
|
||||||
|
std::env::var_os("XDG_STATE_HOME")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| crate::config::home().join(".local/state"))
|
||||||
|
.join("valw/picker")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The motion for this run: the other one than last time (a swirl the
|
||||||
|
/// first time, or when the file is unreadable). Remembers it.
|
||||||
|
pub fn next_motion(path: &Path) -> f32 {
|
||||||
|
let last = std::fs::read_to_string(path).ok();
|
||||||
|
let next = match last.as_deref().map(str::trim) {
|
||||||
|
Some("swirl") => FLOW,
|
||||||
|
_ => SWIRL,
|
||||||
|
};
|
||||||
|
if let Some(dir) = path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(dir);
|
||||||
|
}
|
||||||
|
let _ = std::fs::write(path, if next == SWIRL { "swirl\n" } else { "flow\n" });
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn motion_alternates_and_starts_with_a_swirl() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("deep/picker");
|
||||||
|
assert_eq!(next_motion(&path), SWIRL);
|
||||||
|
assert_eq!(next_motion(&path), FLOW);
|
||||||
|
assert_eq!(next_motion(&path), SWIRL);
|
||||||
|
std::fs::write(&path, "garbage").unwrap();
|
||||||
|
assert_eq!(next_motion(&path), SWIRL);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user