git.lucas.co / cce-gallery
widget gallery and compositor test bench

commitf0cd1ba3e485071fd2e8952db6cd9b241944e6c1
parent8a5511e55d
authorLucas Galante <[email protected]>
date2026-06-03 15:28
Update and align interface features

 Cargo.lock  | 721 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 Cargo.toml  |   4 +-
 README.md   |   2 +-
 src/main.rs | 310 ++++++++++++++++++++++----
 4 files changed, 988 insertions(+), 49 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 81d6851..84a29b5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
 [[package]]
 name = "arrayref"
 version = "0.3.9"
@@ -38,6 +44,143 @@ dependencies = [
  "libloading",
 ]
 
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix 1.1.4",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix 1.1.4",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix 1.1.4",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
 [[package]]
 name = "autocfg"
 version = "1.5.1"
@@ -86,6 +229,19 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
 
+[[package]]
+name = "blocking"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
 [[package]]
 name = "bumpalo"
 version = "3.20.3"
@@ -167,7 +323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
 
 [[package]]
-name = "clear-test-suite"
+name = "clear-test-interface"
 version = "0.1.0"
 dependencies = [
  "bytemuck",
@@ -183,6 +339,7 @@ dependencies = [
  "wayland-client",
  "wgpu",
  "xkeysym",
+ "zbus",
 ]
 
 [[package]]
@@ -198,6 +355,7 @@ dependencies = [
  "raw-window-handle",
  "resvg",
  "serde",
+ "serde_json",
  "smithay-client-toolkit",
  "tokio",
  "wayland-backend",
@@ -358,6 +516,33 @@ version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
 
+[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "equivalent"
 version = "1.0.2"
@@ -393,6 +578,33 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
 [[package]]
 name = "fdeflate"
 version = "0.3.7"
@@ -495,6 +707,25 @@ version = "0.3.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
 
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "futures-task"
 version = "0.3.32"
@@ -513,6 +744,19 @@ dependencies = [
  "slab",
 ]
 
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasip3",
+]
+
 [[package]]
 name = "gif"
 version = "0.13.3"
@@ -646,12 +890,24 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
 
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
 [[package]]
 name = "hexf-parse"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
 
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
 [[package]]
 name = "imagesize"
 version = "0.12.0"
@@ -666,8 +922,16 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
 dependencies = [
  "equivalent",
  "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
 ]
 
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
 [[package]]
 name = "jni-sys"
 version = "0.3.1"
@@ -742,6 +1006,12 @@ dependencies = [
  "smallvec",
 ]
 
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
 [[package]]
 name = "libc"
 version = "0.2.186"
@@ -836,6 +1106,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "metal"
 version = "0.31.0"
@@ -936,6 +1215,22 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
 [[package]]
 name = "parking_lot"
 version = "0.12.5"
@@ -977,6 +1272,17 @@ version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
 
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
 [[package]]
 name = "pkg-config"
 version = "0.3.33"
@@ -1022,6 +1328,25 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
 
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.106"
@@ -1055,6 +1380,12 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
 [[package]]
 name = "range-alloc"
 version = "0.1.5"
@@ -1251,6 +1582,12 @@ version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
 
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
 [[package]]
 name = "serde"
 version = "1.0.228"
@@ -1281,6 +1618,30 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "shlex"
 version = "2.0.1"
@@ -1480,6 +1841,19 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom",
+ "once_cell",
+ "rustix 1.1.4",
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "termcolor"
 version = "1.4.1"
@@ -1598,6 +1972,67 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
 [[package]]
 name = "ttf-parser"
 version = "0.20.0"
@@ -1610,6 +2045,17 @@ version = "0.21.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
 
+[[package]]
+name = "uds_windows"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "unicode-bidi"
 version = "0.3.18"
@@ -1703,6 +2149,17 @@ dependencies = [
  "xmlwriter",
 ]
 
+[[package]]
+name = "uuid"
+version = "1.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7"
+dependencies = [
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "version_check"
 version = "0.9.5"
@@ -1715,6 +2172,24 @@ version = "0.11.1+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
 
+[[package]]
+name = "wasip2"
+version = "1.0.3+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+dependencies = [
+ "wit-bindgen 0.57.1",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen 0.51.0",
+]
+
 [[package]]
 name = "wasm-bindgen"
 version = "0.2.122"
@@ -1770,6 +2245,40 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags 2.12.0",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
 [[package]]
 name = "wayland-backend"
 version = "0.3.15"
@@ -2151,6 +2660,109 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
+[[package]]
+name = "winnow"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags 2.12.0",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
 [[package]]
 name = "xcursor"
 version = "0.3.10"
@@ -2195,8 +2807,115 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1"
 
+[[package]]
+name = "zbus"
+version = "5.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285"
+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",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
+dependencies = [
+ "serde",
+ "winnow",
+ "zvariant",
+]
+
 [[package]]
 name = "zeno"
 version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zvariant"
+version = "5.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn",
+ "winnow",
+]
diff --git a/Cargo.toml b/Cargo.toml
index ccb35e4..1c32bb3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "clear-test-suite"
+name = "clear-test-interface"
 version = "0.1.0"
 edition = "2021"
 
@@ -17,3 +17,5 @@ calloop-wayland-source = "0.3.0"
 wayland-client = { version = "0.31", features = ["system"] }
 xkeysym = "0.2"
 resvg = "0.41.0"
+zbus = "5"
+
diff --git a/README.md b/README.md
index 40a3d03..640e520 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Clear Test Suite
+# Clear Test Interface
 
 This repository contains integration and verification tests for the `clear` desktop environment programs, specifically:
 - `clear-computing-environment-server` (Wayland compositor with background blur support)
diff --git a/src/main.rs b/src/main.rs
index c6166ba..c564687 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -41,12 +41,14 @@ use calloop_wayland_source::WaylandSource;
 struct Vertex {
     position: [f32; 2],
     color: [f32; 4],
+    clip_circle: [f32; 3],
 }
 
 impl Vertex {
-    const ATTRIBS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![
+    const ATTRIBS: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![
         0 => Float32x2,
         1 => Float32x4,
+        2 => Float32x3,
     ];
 
     fn desc() -> wgpu::VertexBufferLayout<'static> {
@@ -69,12 +71,12 @@ fn quad_vertices(
     let y1 = 1.0 - ((y + h) / surface_h) * 2.0;
 
     [
-        Vertex { position: [x0, y0], color },
-        Vertex { position: [x1, y0], color },
-        Vertex { position: [x0, y1], color },
-        Vertex { position: [x1, y0], color },
-        Vertex { position: [x1, y1], color },
-        Vertex { position: [x0, y1], color },
+        Vertex { position: [x0, y0], color, clip_circle: [0.0, 0.0, 0.0] },
+        Vertex { position: [x1, y0], color, clip_circle: [0.0, 0.0, 0.0] },
+        Vertex { position: [x0, y1], color, clip_circle: [0.0, 0.0, 0.0] },
+        Vertex { position: [x1, y0], color, clip_circle: [0.0, 0.0, 0.0] },
+        Vertex { position: [x1, y1], color, clip_circle: [0.0, 0.0, 0.0] },
+        Vertex { position: [x0, y1], color, clip_circle: [0.0, 0.0, 0.0] },
     ]
 }
 
@@ -95,6 +97,7 @@ fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buff
 enum Page {
     Widgets,
     Windows,
+    Xdg,
 }
 
 struct State {
@@ -275,7 +278,7 @@ impl State {
             };
             make_text_buffer(&mut font_system, &title, 16.0)
         } else {
-            make_text_buffer(&mut font_system, "Clear Test Suite - Widgets", 16.0)
+            make_text_buffer(&mut font_system, "Clear Test Interface - Widgets", 16.0)
         };
         let status_buffer = make_text_buffer(&mut font_system, "Select a test case to begin verification.", 12.0);
 
@@ -298,7 +301,7 @@ impl State {
         } else {
             vec![
                 Box::new(Header::new()), // 0
-                Box::new(Paginator::new(150.0, vec!["Widgets".to_string(), "Windows".to_string()])), // 1
+                Box::new(Paginator::new(56.0, vec!["Widgets".to_string(), "Windows".to_string(), "XDG".to_string()]).with_tabs_rotated(true)), // 1
                 Box::new(StatusBar::new()), // 2
                 
                 // "Widgets" page (indices 3..13)
@@ -354,6 +357,10 @@ impl State {
                 ).with_font_size(11.0).with_color([0x83, 0x83, 0x8a])), // 29 (Info panel description)
                 Box::new(RangeSlider::new().with_label("RangeSlider")), // 30 (RangeSlider widget)
                 Box::new(Trackpad::new().with_label("Trackpad")), // 31 (Trackpad widget)
+                Box::new(Panel::new(0.0, 0.0, 450.0, 200.0).with_label("XDG Desktop Portal FileChooser")), // 32
+                Box::new(Label::new("This page verifies the integration of the XDG Desktop Portal\nFile Chooser in the Clear environment.").with_font_size(12.0).with_color([0xcc, 0xcc, 0xd4])), // 33
+                Box::new(Button::new(0.0, 0.0, 180.0, 40.0).with_label("Open File Dialog")), // 34
+                Box::new(Button::new(0.0, 0.0, 180.0, 40.0).with_label("Save File Dialog")), // 35
             ]
         };
 
@@ -416,14 +423,16 @@ impl State {
             0..=2 => true,
             3..=13 | 30 | 31 => self.current_page == Page::Widgets,
             14..=29 => self.current_page == Page::Windows,
+            32..=35 => self.current_page == Page::Xdg,
             _ => false,
         }
     }
 
     fn update_page_title(&mut self) {
         let title = match self.current_page {
-            Page::Widgets => "Clear Test Suite - Widgets",
-            Page::Windows => "Clear Test Suite - Windows",
+            Page::Widgets => "Clear Test Interface - Widgets",
+            Page::Windows => "Clear Test Interface - Windows",
+            Page::Xdg => "Clear Test Interface - XDG Portal",
         };
         self.label_buffer = make_text_buffer(&mut self.font_system, title, 16.0);
     }
@@ -439,6 +448,7 @@ impl State {
                 0..=2 => true,
                 3..=13 | 30 | 31 => current_page == Page::Widgets,
                 14..=29 => current_page == Page::Windows,
+                32..=35 => current_page == Page::Xdg,
                 _ => false,
             }
         };
@@ -533,6 +543,7 @@ impl State {
                 0..=2 => true,
                 3..=13 | 30 | 31 => current_page == Page::Widgets,
                 14..=29 => current_page == Page::Windows,
+                32..=35 => current_page == Page::Xdg,
                 _ => false,
             }
         };
@@ -557,7 +568,7 @@ impl State {
         text_viewport.update(queue, viewport);
 
         let scale_f32 = *scale as f32;
-        let left_margin = if is_child { 20.0 } else { 170.0 };
+        let left_margin = if is_child { 20.0 } else { 76.0 };
 
         let mut areas: Vec<TextArea> = vec![
             TextArea {
@@ -772,6 +783,7 @@ impl State {
                 0..=2 => true,
                 3..=13 | 30 | 31 => current_page == Page::Widgets,
                 14..=29 => current_page == Page::Windows,
+                32..=35 => current_page == Page::Xdg,
                 _ => false,
             }
         };
@@ -797,37 +809,42 @@ fn demo_positions(sw: f32, sh: f32) -> Vec<(f32, f32, f32, f32)> {
         (0.0, sh - 28.0, sw, 28.0),         // 2 status_bar
 
         // "Widgets" page only
-        (170.0, 50.0, 140.0, 40.0),          // 3 verify_opacity
-        (320.0, 50.0, 140.0, 40.0),          // 4 verify_blur
-        (470.0, 50.0, 140.0, 40.0),          // 5 verify_layout
-        (170.0, 120.0, 400.0, 200.0),        // 6 panel
-        (170.0, 340.0, 140.0, 40.0),         // 7 run_diagnostics
-        (320.0, 340.0, 140.0, 40.0),         // 8 reset
-        (170.0, 410.0, 24.0, 24.0),          // 9 checkbox
-        (280.0, 410.0, 48.0, 24.0),          // 10 toggle
-        (400.0, 410.0, 160.0, 24.0),         // 11 progress_bar
-        (170.0, 470.0, 300.0, 24.0),         // 12 slider
-        (500.0, 470.0, 120.0, 24.0),         // 13 spinbox
+        (76.0, 50.0, 140.0, 40.0),          // 3 verify_opacity
+        (226.0, 50.0, 140.0, 40.0),          // 4 verify_blur
+        (376.0, 50.0, 140.0, 40.0),          // 5 verify_layout
+        (76.0, 120.0, 400.0, 200.0),        // 6 panel
+        (76.0, 340.0, 140.0, 40.0),         // 7 run_diagnostics
+        (226.0, 340.0, 140.0, 40.0),         // 8 reset
+        (76.0, 410.0, 24.0, 24.0),          // 9 checkbox
+        (186.0, 410.0, 48.0, 24.0),          // 10 toggle
+        (306.0, 410.0, 160.0, 24.0),         // 11 progress_bar
+        (76.0, 470.0, 300.0, 24.0),         // 12 slider
+        (406.0, 470.0, 120.0, 24.0),         // 13 spinbox
 
         // "Windows" page only
-        (170.0, 100.0, 400.0, 250.0),        // 14 Panel (Window area)
-        (170.0, 360.0, 140.0, 40.0),         // 15 Button: Create Window
-        (320.0, 360.0, 140.0, 40.0),         // 16 Button: Tile Windows
-        (170.0, 410.0, 24.0, 24.0),          // 17 Checkbox: Enable Opacity
-        (205.0, 415.0, 150.0, 16.0),         // 18 Label: "Enable Opacity"
-        (170.0, 444.0, 300.0, 32.0),         // 19 Slider: Transparency level
-        (480.0, 452.0, 150.0, 16.0),         // 20 Label: "Transparency level"
-        (470.0, 360.0, 175.0, 40.0),         // 21 Dropdown: Window type
-        (470.0, 410.0, 175.0, 40.0),         // 22 Dropdown: Window shape
-        (170.0, 490.0, 24.0, 24.0),          // 23 Checkbox: Enable Border
-        (205.0, 495.0, 150.0, 16.0),         // 24 Label: "Enable Border"
-        (470.0, 490.0, 175.0, 40.0),         // 25 Spinbox: Width
-        (470.0, 540.0, 175.0, 40.0),         // 26 Spinbox: Height
-        (590.0, 100.0, 190.0, 250.0),        // 27 Panel: Info background
-        (600.0, 110.0, 170.0, 20.0),         // 28 Label: Info header
-        (600.0, 140.0, 170.0, 200.0),        // 29 Label: Info description
-        (170.0, 530.0, 300.0, 24.0),         // 30 RangeSlider
-        (590.0, 120.0, 190.0, 200.0),        // 31 Trackpad
+        (76.0, 100.0, 400.0, 250.0),        // 14 Panel (Window area)
+        (76.0, 360.0, 140.0, 40.0),         // 15 Button: Create Window
+        (226.0, 360.0, 140.0, 40.0),         // 16 Button: Tile Windows
+        (76.0, 410.0, 24.0, 24.0),          // 17 Checkbox: Enable Opacity
+        (111.0, 415.0, 150.0, 16.0),         // 18 Label: "Enable Opacity"
+        (76.0, 444.0, 300.0, 32.0),         // 19 Slider: Transparency level
+        (386.0, 452.0, 150.0, 16.0),         // 20 Label: "Transparency level"
+        (376.0, 360.0, 175.0, 40.0),         // 21 Dropdown: Window type
+        (376.0, 410.0, 175.0, 40.0),         // 22 Dropdown: Window shape
+        (76.0, 490.0, 24.0, 24.0),          // 23 Checkbox: Enable Border
+        (111.0, 495.0, 150.0, 16.0),         // 24 Label: "Enable Border"
+        (376.0, 490.0, 175.0, 40.0),         // 25 Spinbox: Width
+        (376.0, 540.0, 175.0, 40.0),         // 26 Spinbox: Height
+        (496.0, 100.0, 190.0, 250.0),        // 27 Panel: Info background
+        (506.0, 110.0, 170.0, 20.0),         // 28 Label: Info header
+        (506.0, 140.0, 170.0, 200.0),        // 29 Label: Info description
+        (76.0, 530.0, 300.0, 24.0),         // 30 RangeSlider
+        (496.0, 120.0, 190.0, 200.0),        // 31 Trackpad
+        // "XDG" page only
+        (76.0, 100.0, 450.0, 200.0),        // 32 Panel: XDG Portal background
+        (96.0, 140.0, 410.0, 40.0),         // 33 Label: XDG explanation
+        (96.0, 220.0, 180.0, 40.0),         // 34 Button: Open File Dialog
+        (296.0, 220.0, 180.0, 40.0),         // 35 Button: Save File Dialog
     ]
 }
 
@@ -868,6 +885,7 @@ struct AppState {
     initial_width: f32,
     initial_height: f32,
     child_shape: Option<String>,
+    sender: calloop::channel::Sender<String>,
 }
 
 impl CompositorHandler for AppState {
@@ -1039,6 +1057,7 @@ impl PointerHandler for AppState {
                                     0..=2 => true,
                                     3..=13 | 30 | 31 => current_page == Page::Widgets,
                                     14..=29 => current_page == Page::Windows,
+                                    32..=35 => current_page == Page::Xdg,
                                     _ => false,
                                 }
                             };
@@ -1076,6 +1095,7 @@ impl PointerHandler for AppState {
                                 0..=2 => true,
                                 3..=13 | 30 | 31 => current_page == Page::Widgets,
                                 14..=29 => current_page == Page::Windows,
+                                32..=35 => current_page == Page::Xdg,
                                 _ => false,
                             }
                         };
@@ -1148,6 +1168,7 @@ impl PointerHandler for AppState {
                                 0..=2 => true,
                                 3..=13 | 30 | 31 => current_page == Page::Widgets,
                                 14..=29 => current_page == Page::Windows,
+                                32..=35 => current_page == Page::Xdg,
                                 _ => false,
                             }
                         };
@@ -1179,10 +1200,14 @@ impl PointerHandler for AppState {
                                         st.current_page = Page::Widgets;
                                         st.update_page_title();
                                         st.update_status_text("Viewing Widgets Page");
-                                    } else {
+                                    } else if selected == 1 {
                                         st.current_page = Page::Windows;
                                         st.update_page_title();
                                         st.update_status_text("Viewing Windows Page");
+                                    } else {
+                                        st.current_page = Page::Xdg;
+                                        st.update_page_title();
+                                        st.update_status_text("Viewing XDG Page");
                                     }
                                     st.apply_layout();
                                     st.upload_vertices();
@@ -1358,6 +1383,30 @@ impl PointerHandler for AppState {
                                         st.update_status_text("Window Action: Tile active client windows");
                                         changed = true;
                                     }
+                                } else if st.current_page == Page::Xdg {
+                                    let mut open_file = false;
+                                    let mut save_file = false;
+                                    if st.widgets[34].take_click() {
+                                        open_file = true;
+                                    } else if st.widgets[35].take_click() {
+                                        save_file = true;
+                                    }
+
+                                    if open_file {
+                                        st.update_status_text("Opening Open File Dialog...");
+                                        let sender_clone = self.sender.clone();
+                                        std::thread::spawn(move || {
+                                            open_file_dialog_portal(sender_clone);
+                                        });
+                                        changed = true;
+                                    } else if save_file {
+                                        st.update_status_text("Opening Save File Dialog...");
+                                        let sender_clone = self.sender.clone();
+                                        std::thread::spawn(move || {
+                                            save_file_dialog_portal(sender_clone);
+                                        });
+                                        changed = true;
+                                    }
                                 }
                             }
                         }
@@ -1367,7 +1416,23 @@ impl PointerHandler for AppState {
                         }
                     }
                 }
-                PointerEventKind::Axis { .. } => {}
+                PointerEventKind::Axis { horizontal, vertical, .. } => {
+                    if let Some(st) = &mut self.state {
+                        let h_scroll = horizontal.absolute as f32;
+                        let v_scroll = vertical.absolute as f32;
+                        let delta = clear_ui::widget::MouseScrollDelta::LineDelta(-h_scroll / 10.0, -v_scroll / 10.0);
+                        let mut changed = false;
+                        for w in &mut st.widgets {
+                            if w.mouse_wheel(&delta, st.cursor_x, st.cursor_y) {
+                                changed = true;
+                            }
+                        }
+                        if changed {
+                            st.upload_vertices();
+                            self.redraw = true;
+                        }
+                    }
+                }
             }
         }
     }
@@ -1591,6 +1656,8 @@ fn main() {
     let (globals, mut event_queue) = registry_queue_init(&conn).unwrap();
     let qh = event_queue.handle();
 
+    let (sender, channel) = calloop::channel::channel::<String>();
+
     let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
     let xdg_shell_state = XdgShell::bind(&globals, &qh).unwrap();
     let shm_state = Shm::bind(&globals, &qh).unwrap();
@@ -1622,6 +1689,7 @@ fn main() {
         initial_width: c_w,
         initial_height: c_h,
         child_shape: child_shape.clone(),
+        sender,
     };
 
     event_queue.roundtrip(&mut app).unwrap();
@@ -1662,8 +1730,8 @@ fn main() {
         }
         window.set_app_id(&app_id);
     } else {
-        window.set_title("Clear Test Suite - Diagnostics Dashboard");
-        window.set_app_id("clear-test-suite");
+        window.set_title("Clear Test Interface - Diagnostics Dashboard");
+        window.set_app_id("clear-test-interface");
     }
     if is_child {
         window.set_min_size(Some((c_w as u32, c_h as u32)));
@@ -1694,7 +1762,17 @@ fn main() {
 
     let mut event_loop = EventLoop::try_new().unwrap();
     let loop_handle = event_loop.handle();
-    WaylandSource::new(conn, event_queue).insert(loop_handle).unwrap();
+    WaylandSource::new(conn, event_queue).insert(loop_handle.clone()).unwrap();
+
+    loop_handle.insert_source(channel, |event, _metadata, app_state: &mut AppState| {
+        if let calloop::channel::Event::Msg(msg) = event {
+            if let Some(state) = &mut app_state.state {
+                state.update_status_text(&msg);
+                state.upload_vertices();
+            }
+            app_state.redraw = true;
+        }
+    }).unwrap();
 
     let mut last_tick = std::time::Instant::now();
     loop {
@@ -1726,3 +1804,143 @@ fn main() {
         }
     }
 }
+
+fn percent_decode(s: &str) -> String {
+    let mut bytes = Vec::new();
+    let mut chars = s.as_bytes().iter();
+    while let Some(&b) = chars.next() {
+        if b == b'%' {
+            if let (Some(&h), Some(&l)) = (chars.next(), chars.next()) {
+                if let Ok(hex) = String::from_utf8(vec![h, l]) {
+                    if let Ok(decoded) = u8::from_str_radix(&hex, 16) {
+                        bytes.push(decoded);
+                        continue;
+                    }
+                }
+            }
+        }
+        bytes.push(b);
+    }
+    String::from_utf8_lossy(&bytes).into_owned()
+}
+
+fn open_file_dialog_portal(sender: calloop::channel::Sender<String>) {
+    let result = (|| -> Result<(), Box<dyn std::error::Error>> {
+        let conn = zbus::blocking::Connection::session()?;
+        let mut options = std::collections::HashMap::new();
+        options.insert("handle_token".to_string(), zbus::zvariant::Value::from("clear_test_open_token"));
+
+        let msg: zbus::Message = conn.call_method(
+            Some("org.freedesktop.portal.Desktop"),
+            "/org/freedesktop/portal/desktop",
+            Some("org.freedesktop.portal.FileChooser"),
+            "OpenFile",
+            &("", "Open File Dialog", &options),
+        )?;
+        let reply: zbus::zvariant::OwnedObjectPath = msg.body().deserialize()?;
+
+        let request_proxy = zbus::blocking::Proxy::new(
+            &conn,
+            "org.freedesktop.portal.Desktop",
+            reply,
+            "org.freedesktop.portal.Request",
+        )?;
+
+        let mut signal_stream = request_proxy.receive_signal("Response")?;
+        if let Some(msg) = signal_stream.next() {
+            let (response_code, results): (u32, std::collections::HashMap<String, zbus::zvariant::OwnedValue>) = msg.body().deserialize()?;
+            if response_code == 0 {
+                if let Some(val) = results.get("uris") {
+                    if let Ok(uris) = Vec::<String>::try_from(val.clone()) {
+                        let mut file_paths = Vec::new();
+                        for uri in uris {
+                            let path = uri.trim_start_matches("file://");
+                            file_paths.push(percent_decode(path));
+                        }
+                        if !file_paths.is_empty() {
+                            let _ = sender.send(format!("Selected: {}", file_paths.join(", ")));
+                        } else {
+                            let _ = sender.send("Selected no files".to_string());
+                        }
+                    } else {
+                        let _ = sender.send("Selected success, but failed to parse URIs".to_string());
+                    }
+                } else {
+                    let _ = sender.send("Selected success, but no uris".to_string());
+                }
+            } else if response_code == 1 {
+                let _ = sender.send("File dialog cancelled by user".to_string());
+            } else {
+                let _ = sender.send(format!("File dialog closed (code {})", response_code));
+            }
+        } else {
+            let _ = sender.send("Request closed without response".to_string());
+        }
+        Ok(())
+    })();
+
+    if let Err(e) = result {
+        let _ = sender.send(format!("Error: {}", e));
+    }
+}
+
+fn save_file_dialog_portal(sender: calloop::channel::Sender<String>) {
+    let result = (|| -> Result<(), Box<dyn std::error::Error>> {
+        let conn = zbus::blocking::Connection::session()?;
+        let mut options = std::collections::HashMap::new();
+        options.insert("handle_token".to_string(), zbus::zvariant::Value::from("clear_test_save_token"));
+
+        let msg: zbus::Message = conn.call_method(
+            Some("org.freedesktop.portal.Desktop"),
+            "/org/freedesktop/portal/desktop",
+            Some("org.freedesktop.portal.FileChooser"),
+            "SaveFile",
+            &("", "Save File Dialog", &options),
+        )?;
+        let reply: zbus::zvariant::OwnedObjectPath = msg.body().deserialize()?;
+
+        let request_proxy = zbus::blocking::Proxy::new(
+            &conn,
+            "org.freedesktop.portal.Desktop",
+            reply,
+            "org.freedesktop.portal.Request",
+        )?;
+
+        let mut signal_stream = request_proxy.receive_signal("Response")?;
+        if let Some(msg) = signal_stream.next() {
+            let (response_code, results): (u32, std::collections::HashMap<String, zbus::zvariant::OwnedValue>) = msg.body().deserialize()?;
+            if response_code == 0 {
+                if let Some(val) = results.get("uris") {
+                    if let Ok(uris) = Vec::<String>::try_from(val.clone()) {
+                        let mut file_paths = Vec::new();
+                        for uri in uris {
+                            let path = uri.trim_start_matches("file://");
+                            file_paths.push(percent_decode(path));
+                        }
+                        if !file_paths.is_empty() {
+                            let _ = sender.send(format!("Saved to: {}", file_paths.join(", ")));
+                        } else {
+                            let _ = sender.send("Saved to no file".to_string());
+                        }
+                    } else {
+                        let _ = sender.send("Save success, but failed to parse URIs".to_string());
+                    }
+                } else {
+                    let _ = sender.send("Save success, but no uris".to_string());
+                }
+            } else if response_code == 1 {
+                let _ = sender.send("Save dialog cancelled by user".to_string());
+            } else {
+                let _ = sender.send(format!("Save dialog closed (code {})", response_code));
+            }
+        } else {
+            let _ = sender.send("Request closed without response".to_string());
+        }
+        Ok(())
+    })();
+
+    if let Err(e) = result {
+        let _ = sender.send(format!("Error: {}", e));
+    }
+}
+