Merge branch 'volume-control' into 'main'

feat: Volume Control

See merge request pipewire/helvum!71
This commit is contained in:
Florian Olk 2025-09-15 11:58:19 +00:00
commit eee1aedae6
17 changed files with 1022 additions and 252 deletions

View file

@ -3,7 +3,7 @@ stages:
- lint
.flatpak:
image: 'quay.io/gnome_infrastructure/gnome-runtime-images:gnome-45'
image: 'quay.io/gnome_infrastructure/gnome-runtime-images:gnome-46'
variables:
FLATPAK_BUILD_DIR: _build
MANIFEST_PATH: build-aux/org.pipewire.Helvum.json

379
Cargo.lock generated
View file

@ -11,12 +11,35 @@ dependencies = [
"memchr",
]
[[package]]
name = "annotate-snippets"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e"
dependencies = [
"unicode-width",
"yansi-term",
]
[[package]]
name = "anyhow"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "async-channel"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3"
dependencies = [
"concurrent-queue",
"event-listener",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "autocfg"
version = "1.1.0"
@ -25,16 +48,17 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bindgen"
version = "0.66.1"
version = "0.69.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7"
checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
dependencies = [
"bitflags 2.4.0",
"annotate-snippets",
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"lazy_static",
"lazycell",
"peeking_take_while",
"proc-macro2",
"quote",
"regex",
@ -43,12 +67,6 @@ dependencies = [
"syn 2.0.37",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.0"
@ -57,23 +75,22 @@ checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
[[package]]
name = "cairo-rs"
version = "0.18.2"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c0466dfa8c0ee78deef390c274ad756801e0a6dbb86c5ef0924a298c5761c4d"
checksum = "2650f66005301bd33cc486dec076e1293c4cecf768bc7ba9bf5d2b1be339b99c"
dependencies = [
"bitflags 2.4.0",
"bitflags",
"cairo-sys-rs",
"glib",
"libc",
"once_cell",
"thiserror",
]
[[package]]
name = "cairo-sys-rs"
version = "0.18.2"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
checksum = "fd3bb3119664efbd78b5e6c93957447944f16bdbced84c17a9f41c7829b81e64"
dependencies = [
"glib-sys",
"libc",
@ -125,6 +142,15 @@ dependencies = [
"libloading",
]
[[package]]
name = "concurrent-queue"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "convert_case"
version = "0.6.0"
@ -140,19 +166,52 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b"
[[package]]
name = "crossbeam-utils"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "either"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "event-listener"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]]
name = "field-offset"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
dependencies = [
"memoffset 0.9.0",
"memoffset",
"rustc_version",
]
@ -221,22 +280,21 @@ dependencies = [
[[package]]
name = "gdk-pixbuf"
version = "0.18.0"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbc9c2ed73a81d556b65d08879ba4ee58808a6b1927ce915262185d6d547c6f3"
checksum = "f6a23f8a0b5090494fd04924662d463f8386cc678dd3915015a838c1a3679b92"
dependencies = [
"gdk-pixbuf-sys",
"gio",
"glib",
"libc",
"once_cell",
]
[[package]]
name = "gdk-pixbuf-sys"
version = "0.18.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
checksum = "3dcbd04c1b2c4834cc008b4828bc917d062483b88d26effde6342e5622028f96"
dependencies = [
"gio-sys",
"glib-sys",
@ -247,9 +305,9 @@ dependencies = [
[[package]]
name = "gdk4"
version = "0.7.3"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edb019ad581f8ecf8ea8e4baa6df7c483a95b5a59be3140be6a9c3b0c632af6"
checksum = "9100b25604183f2fd97f55ef087fae96ab4934d7215118a35303e422688e6e4b"
dependencies = [
"cairo-rs",
"gdk-pixbuf",
@ -262,9 +320,9 @@ dependencies = [
[[package]]
name = "gdk4-sys"
version = "0.7.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbab43f332a3cf1df9974da690b5bb0e26720ed09a228178ce52175372dcfef0"
checksum = "d0b76874c40bb8d1c7d03a7231e23ac75fa577a456cd53af32ec17ec8f121626"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
@ -279,9 +337,9 @@ dependencies = [
[[package]]
name = "gio"
version = "0.18.2"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57052f84e8e5999b258e8adf8f5f2af0ac69033864936b8b6838321db2f759b1"
checksum = "c64947d08d7fbb03bf8ad1f25a8ac6cf4329bc772c9b7e5abe7bf9493c81194f"
dependencies = [
"futures-channel",
"futures-core",
@ -290,7 +348,6 @@ dependencies = [
"gio-sys",
"glib",
"libc",
"once_cell",
"pin-project-lite",
"smallvec",
"thiserror",
@ -298,24 +355,24 @@ dependencies = [
[[package]]
name = "gio-sys"
version = "0.18.1"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
checksum = "bcf8e1d9219bb294636753d307b030c1e8a032062cba74f493c431a5c8b81ce4"
dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
"winapi",
"windows-sys",
]
[[package]]
name = "glib"
version = "0.18.2"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c316afb01ce8067c5eaab1fc4f2cd47dc21ce7b6296358605e2ffab23ccbd19"
checksum = "01e191cc1af1f35b9699213107068cd3fe05d9816275ac118dc785a0dd8faebf"
dependencies = [
"bitflags 2.4.0",
"bitflags",
"futures-channel",
"futures-core",
"futures-executor",
@ -328,20 +385,18 @@ dependencies = [
"libc",
"log",
"memchr",
"once_cell",
"smallvec",
"thiserror",
]
[[package]]
name = "glib-macros"
version = "0.18.2"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8da903822b136d42360518653fcf154455defc437d3e7a81475bf9a95ff1e47"
checksum = "9972bb91643d589c889654693a4f1d07697fdcb5d104b5c44fb68649ba1bf68d"
dependencies = [
"heck",
"heck 0.5.0",
"proc-macro-crate",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.37",
@ -349,9 +404,9 @@ dependencies = [
[[package]]
name = "glib-sys"
version = "0.18.1"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
checksum = "630f097773d7c7a0bb3258df4e8157b47dc98bbfa0e60ad9ab56174813feced4"
dependencies = [
"libc",
"system-deps",
@ -365,9 +420,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "gobject-sys"
version = "0.18.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
checksum = "c85e2b1080b9418dd0c58b498da3a5c826030343e0ef07bde6a955d28de54979"
dependencies = [
"glib-sys",
"libc",
@ -376,9 +431,9 @@ dependencies = [
[[package]]
name = "graphene-rs"
version = "0.18.1"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2228cda1505613a7a956cca69076892cfbda84fc2b7a62b94a41a272c0c401"
checksum = "99e4d388e96c5f29e2b2f67045d229ddf826d0a8d6d282f94ed3b34452222c91"
dependencies = [
"glib",
"graphene-sys",
@ -387,9 +442,9 @@ dependencies = [
[[package]]
name = "graphene-sys"
version = "0.18.1"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4144cee8fc8788f2a9b73dc5f1d4e1189d1f95305c4cb7bd9c1af1cfa31f59"
checksum = "236ed66cc9b18d8adf233716f75de803d0bf6fc806f60d14d948974a12e240d0"
dependencies = [
"glib-sys",
"libc",
@ -399,9 +454,9 @@ dependencies = [
[[package]]
name = "gsk4"
version = "0.7.3"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d958e351d2f210309b32d081c832d7de0aca0b077aa10d88336c6379bd01f7e"
checksum = "c65036fc8f99579e8cb37b12487969b707ab23ec8ab953682ff347cbd15d396e"
dependencies = [
"cairo-rs",
"gdk4",
@ -414,9 +469,9 @@ dependencies = [
[[package]]
name = "gsk4-sys"
version = "0.7.3"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12bd9e3effea989f020e8f1ff3fa3b8c63ba93d43b899c11a118868853a56d55"
checksum = "bd24c814379f9c3199dc53e52253ee8d0f657eae389ab282c330505289d24738"
dependencies = [
"cairo-sys-rs",
"gdk4-sys",
@ -430,9 +485,9 @@ dependencies = [
[[package]]
name = "gtk4"
version = "0.7.3"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb51aa3e9728575a053e1f43543cd9992ac2477e1b186ad824fd4adfb70842"
checksum = "aa82753b8c26277e4af1446c70e35b19aad4fb794a7b143859e7eeb9a4025d83"
dependencies = [
"cairo-rs",
"field-offset",
@ -451,9 +506,9 @@ dependencies = [
[[package]]
name = "gtk4-macros"
version = "0.7.2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d57ec49cf9b657f69a05bca8027cff0a8dfd0c49e812be026fc7311f2163832f"
checksum = "40300bf071d2fcd4c94eacc09e84ec6fe73129d2ceb635cf7e55b026b5443567"
dependencies = [
"anyhow",
"proc-macro-crate",
@ -465,9 +520,9 @@ dependencies = [
[[package]]
name = "gtk4-sys"
version = "0.7.3"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54d8c4aa23638ce9faa2caf7e2a27d4a1295af2155c8e8d28c4d4eeca7a65eb8"
checksum = "0db1b104138f087ccdc81d2c332de5dd049b89de3d384437cc1093b17cd2da18"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
@ -494,14 +549,22 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "helvum"
version = "0.5.1"
dependencies = [
"async-channel",
"glib",
"libadwaita",
"libc",
"log",
"natord",
"once_cell",
"pipewire",
]
@ -516,6 +579,15 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -530,9 +602,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libadwaita"
version = "0.5.3"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fe7e70c06507ed10a16cda707f358fbe60fe0dc237498f78c686ade92fd979c"
checksum = "91b4990248b9e1ec5e72094a2ccaea70ec3809f88f6fd52192f2af306b87c5d9"
dependencies = [
"gdk-pixbuf",
"gdk4",
@ -546,9 +618,9 @@ dependencies = [
[[package]]
name = "libadwaita-sys"
version = "0.5.3"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e10aaa38de1d53374f90deeb4535209adc40cc5dba37f9704724169bceec69a"
checksum = "23a748e4e92be1265cd9e93d569c0b5dfc7814107985aa6743d670ab281ea1a8"
dependencies = [
"gdk4-sys",
"gio-sys",
@ -578,11 +650,11 @@ dependencies = [
[[package]]
name = "libspa"
version = "0.7.2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0434617020ddca18b86067912970c55410ca654cdafd775480322f50b857a8c4"
checksum = "65f3a4b81b2a2d8c7f300643676202debd1b7c929dbf5c9bb89402ea11d19810"
dependencies = [
"bitflags 2.4.0",
"bitflags",
"cc",
"convert_case",
"cookie-factory",
@ -595,9 +667,9 @@ dependencies = [
[[package]]
name = "libspa-sys"
version = "0.7.2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e70ca3f3e70f858ef363046d06178c427b4e0b63d210c95fd87d752679d345"
checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f"
dependencies = [
"bindgen",
"cc",
@ -612,18 +684,9 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "memchr"
version = "2.6.3"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
[[package]]
name = "memoffset"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
dependencies = [
"autocfg",
]
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "memoffset"
@ -641,16 +704,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nix"
version = "0.26.4"
name = "natord"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c"
[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 1.3.2",
"bitflags",
"cfg-if",
"libc",
"memoffset 0.7.1",
"pin-utils",
]
[[package]]
@ -665,28 +732,27 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.18.0"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "pango"
version = "0.18.0"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06a9e54b831d033206160096b825f2070cf5fda7e35167b1c01e9e774f9202d1"
checksum = "b1264d13deb823cc652f26cfe59afb1ec4b9db2a5bd27c41b738c879cc1bfaa1"
dependencies = [
"gio",
"glib",
"libc",
"once_cell",
"pango-sys",
]
[[package]]
name = "pango-sys"
version = "0.18.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
checksum = "f52ef6a881c19fbfe3b1484df5cad411acaaba29dbec843941c3110d19f340ea"
dependencies = [
"glib-sys",
"gobject-sys",
@ -695,10 +761,10 @@ dependencies = [
]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
name = "parking"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
[[package]]
name = "pin-project-lite"
@ -714,12 +780,12 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pipewire"
version = "0.7.2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2d009c8dd65e890b515a71950f7e4c801523b8894ff33863a40830bf762e9e9"
checksum = "08e645ba5c45109106d56610b3ee60eb13a6f2beb8b74f8dc8186cf261788dda"
dependencies = [
"anyhow",
"bitflags 2.4.0",
"bitflags",
"libc",
"libspa",
"libspa-sys",
@ -731,9 +797,9 @@ dependencies = [
[[package]]
name = "pipewire-sys"
version = "0.7.2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "890c084e7b737246cb4799c86b71a0e4da536031ff7473dd639eba9f95039f64"
checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112"
dependencies = [
"bindgen",
"libspa-sys",
@ -742,18 +808,17 @@ dependencies = [
[[package]]
name = "pkg-config"
version = "0.3.27"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
dependencies = [
"once_cell",
"toml_edit",
"toml_edit 0.21.1",
]
[[package]]
@ -894,9 +959,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.11.1"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "syn"
@ -927,7 +992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3"
dependencies = [
"cfg-expr",
"heck",
"heck 0.4.1",
"pkg-config",
"toml",
"version-compare",
@ -968,14 +1033,14 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
"toml_edit 0.19.15",
]
[[package]]
name = "toml_datetime"
version = "0.6.3"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
dependencies = [
"serde",
]
@ -993,6 +1058,17 @@ dependencies = [
"winnow",
]
[[package]]
name = "toml_edit"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
@ -1005,6 +1081,12 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "version-compare"
version = "0.1.1"
@ -1039,6 +1121,72 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
[[package]]
name = "windows_i686_gnu"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
[[package]]
name = "windows_i686_msvc"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
[[package]]
name = "winnow"
version = "0.5.15"
@ -1047,3 +1195,12 @@ checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc"
dependencies = [
"memchr",
]
[[package]]
name = "yansi-term"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1"
dependencies = [
"winapi",
]

View file

@ -14,12 +14,14 @@ categories = ["gui", "multimedia"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
pipewire = "0.7.1"
adw = { version = "0.5", package = "libadwaita", features = ["v1_4"] }
glib = { version = "0.18", features = ["log"] }
pipewire = "0.8.0"
adw = { version = "0.6", package = "libadwaita", features = ["v1_4"] }
glib = { version = "0.19", features = ["log"] }
async-channel = "2.2"
log = "0.4.11"
once_cell = "1.7.2"
once_cell = "1.19"
libc = "0.2"
natord = "1.0.9"

View file

@ -27,7 +27,7 @@ $ flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.fl
Then install the required flatpak platform and SDK, if you dont have them already:
```shell
$ flatpak install org.gnome.{Platform,Sdk}//45 org.freedesktop.Sdk.Extension.rust-stable//23.08 org.freedesktop.Sdk.Extension.llvm16//23.08
$ flatpak install org.gnome.{Platform,Sdk}//46 org.freedesktop.Sdk.Extension.rust-stable//23.08 org.freedesktop.Sdk.Extension.llvm16//23.08
```
To compile and install as a flatpak, clone the project, change to the project directory, and run:

View file

@ -1,7 +1,7 @@
{
"id": "org.pipewire.Helvum",
"runtime": "org.gnome.Platform",
"runtime-version": "45",
"runtime-version": "46",
"sdk": "org.gnome.Sdk",
"sdk-extensions": [
"org.freedesktop.Sdk.Extension.rust-stable",

View file

@ -16,11 +16,12 @@
use adw::{
gio,
glib::{self, clone, Receiver},
glib::{self, clone},
gtk,
prelude::*,
subclass::prelude::*,
};
use log::error;
use pipewire::channel::Sender;
use crate::{graph_manager::GraphManager, ui, GtkMessage, PipewireMessage};
@ -30,11 +31,14 @@ static APP_ID: &str = "org.pipewire.Helvum";
static VERSION: &str = env!("CARGO_PKG_VERSION");
static AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
const DEFAULT_REMOTE_NAME: &str = "Default Remote";
mod imp {
use super::*;
use std::cell::OnceCell;
use adw::subclass::prelude::AdwApplicationImpl;
use once_cell::unsync::OnceCell;
#[derive(Default)]
pub struct Application {
@ -130,6 +134,31 @@ mod imp {
about_window.present();
}
pub(super) fn setup_options(&self, pw_sender: Sender<GtkMessage>) {
let obj = &*self.obj();
obj.add_main_option(
"socket",
glib::char::Char::from(b's'),
glib::OptionFlags::NONE,
glib::OptionArg::String,
"PipeWire socket to connect",
Some("PATH"),
);
let current_remote_label = obj.imp().window.current_remote_label();
obj.connect_handle_local_options(clone!(@strong pw_sender => move |_, opts| {
match opts.lookup::<String>("socket") {
Ok(p) => {
current_remote_label.set_label(p.as_deref().unwrap_or(DEFAULT_REMOTE_NAME));
pw_sender.send(GtkMessage::Connect(p)).unwrap();
},
Err(e) => error!("Invalid socket path: {e}"),
}
-1
}));
}
}
}
@ -143,7 +172,7 @@ impl Application {
/// Create the view.
/// This will set up the entire user interface and prepare it for being run.
pub(super) fn new(
gtk_receiver: Receiver<PipewireMessage>,
gtk_receiver: async_channel::Receiver<PipewireMessage>,
pw_sender: Sender<GtkMessage>,
) -> Self {
let app: Application = glib::Object::builder()
@ -152,6 +181,8 @@ impl Application {
let imp = app.imp();
imp.setup_options(pw_sender.clone());
imp.graph_manager
.set(GraphManager::new(
&imp.window.graph(),

View file

@ -21,11 +21,11 @@ use pipewire::channel::Sender as PwSender;
use crate::{ui::graph::GraphView, GtkMessage, PipewireMessage};
mod imp {
use glib::closure_local;
use super::*;
use std::{cell::RefCell, collections::HashMap};
use once_cell::unsync::OnceCell;
use std::{cell::OnceCell, cell::RefCell, collections::HashMap};
use crate::{ui::graph, MediaType, NodeType};
@ -53,36 +53,61 @@ mod imp {
impl ObjectImpl for GraphManager {}
impl GraphManager {
pub fn attach_receiver(&self, receiver: glib::Receiver<crate::PipewireMessage>) {
receiver.attach(None, glib::clone!(
@weak self as imp => @default-return glib::ControlFlow::Continue,
move |msg| {
match msg {
PipewireMessage::NodeAdded { id, name, node_type } => imp.add_node(id, name.as_str(), node_type),
PipewireMessage::NodeNameChanged { id, name, media_name } => imp.node_name_changed(id, &name, &media_name),
PipewireMessage::PortAdded { id, node_id, name, direction } => imp.add_port(id, name.as_str(), node_id, direction),
PipewireMessage::PortFormatChanged { id, media_type } => imp.port_media_type_changed(id, media_type),
PipewireMessage::LinkAdded {
id, port_from, port_to, active, media_type
} => imp.add_link(id, port_from, port_to, active, media_type),
PipewireMessage::LinkStateChanged { id, active } => imp.link_state_changed(id, active),
PipewireMessage::LinkFormatChanged { id, media_type } => imp.link_format_changed(id, media_type),
PipewireMessage::NodeRemoved { id } => imp.remove_node(id),
PipewireMessage::PortRemoved { id, node_id } => imp.remove_port(id, node_id),
PipewireMessage::LinkRemoved { id } => imp.remove_link(id),
PipewireMessage::Connecting => {
imp.obj().connection_banner().set_revealed(true);
}
PipewireMessage::Connected => {
imp.obj().connection_banner().set_revealed(false);
},
PipewireMessage::Disconnected => {
imp.clear();
},
};
glib::ControlFlow::Continue
}
));
pub async fn receive(&self, receiver: async_channel::Receiver<crate::PipewireMessage>) {
loop {
let Ok(msg) = receiver.recv().await else {
continue;
};
match msg {
PipewireMessage::NodeAdded {
id,
name,
node_type,
} => self.add_node(id, name.as_str(), node_type),
PipewireMessage::NodeNameChanged {
id,
name,
media_name,
} => self.node_name_changed(id, &name, &media_name),
PipewireMessage::NodePropsChanged { id, volume, mute } => {
self.node_props_changed(id, volume, mute);
}
PipewireMessage::PortAdded {
id,
node_id,
name,
direction,
} => self.add_port(id, name.as_str(), node_id, direction),
PipewireMessage::PortFormatChanged { id, media_type } => {
self.port_media_type_changed(id, media_type)
}
PipewireMessage::LinkAdded {
id,
port_from,
port_to,
active,
media_type,
} => self.add_link(id, port_from, port_to, active, media_type),
PipewireMessage::LinkStateChanged { id, active } => {
self.link_state_changed(id, active)
}
PipewireMessage::LinkFormatChanged { id, media_type } => {
self.link_format_changed(id, media_type)
}
PipewireMessage::NodeRemoved { id } => self.remove_node(id),
PipewireMessage::PortRemoved { id, node_id } => self.remove_port(id, node_id),
PipewireMessage::LinkRemoved { id } => self.remove_link(id),
PipewireMessage::Connecting => {
self.obj().connection_banner().set_revealed(true);
}
PipewireMessage::Connected => {
self.obj().connection_banner().set_revealed(false);
}
PipewireMessage::Disconnected => {
self.clear();
}
};
}
}
/// Add a new node to the view.
@ -90,6 +115,31 @@ mod imp {
log::info!("Adding node to graph: id {}", id);
let node = graph::Node::new(name, id);
let sender = self.pw_sender.get().expect("pw_sender shoud be set");
let s = sender.clone();
node.connect_closure(
"volume-changed",
false,
closure_local!(move |n: graph::Node, volume: f32| {
s.send(crate::GtkMessage::NodeVolumeChanged {
id: n.pipewire_id(),
volume,
})
.expect("Failed to send message");
}),
);
let s = sender.clone();
node.connect_closure(
"mute-changed",
false,
closure_local!(move |n: graph::Node, mute: bool| {
s.send(crate::GtkMessage::NodeMuteChanged {
id: n.pipewire_id(),
mute,
})
.expect("Failed to send message");
}),
);
self.items.borrow_mut().insert(id, node.clone().upcast());
@ -130,7 +180,13 @@ mod imp {
}
/// Add a new port to the view.
fn add_port(&self, id: u32, name: &str, node_id: u32, direction: pipewire::spa::Direction) {
fn add_port(
&self,
id: u32,
name: &str,
node_id: u32,
direction: pipewire::spa::utils::Direction,
) {
log::info!("Adding port to graph: id {}", id);
let mut items = self.items.borrow_mut();
@ -273,7 +329,11 @@ mod imp {
link.set_active(active);
}
fn link_format_changed(&self, id: u32, media_type: pipewire::spa::format::MediaType) {
fn link_format_changed(
&self,
id: u32,
media_type: pipewire::spa::param::format::MediaType,
) {
let items = self.items.borrow();
let Some(link) = items.get(&id) else {
@ -315,6 +375,26 @@ mod imp {
self.items.borrow_mut().clear();
self.obj().graph().clear();
}
fn node_props_changed(&self, id: u32, volume: Option<f32>, mute: Option<bool>) {
let items = self.items.borrow();
let Some(node) = items.get(&id) else {
log::warn!("Node (id: {id}) for changed name not found in graph manager");
return;
};
let Some(node) = node.dynamic_cast_ref::<graph::Node>() else {
log::warn!("Graph Manager item under node (id: {id}) is not a node");
return;
};
if let Some(vol) = volume {
node.set_node_volume(vol as f64)
}
if let Some(mute) = mute {
node.set_mute(mute);
}
}
}
}
@ -322,19 +402,23 @@ glib::wrapper! {
pub struct GraphManager(ObjectSubclass<imp::GraphManager>);
}
async fn receive(graph_manager: GraphManager, receiver: async_channel::Receiver<PipewireMessage>) {
graph_manager.imp().receive(receiver).await
}
impl GraphManager {
pub fn new(
graph: &GraphView,
connection_banner: &adw::Banner,
sender: PwSender<GtkMessage>,
receiver: glib::Receiver<PipewireMessage>,
receiver: async_channel::Receiver<PipewireMessage>,
) -> Self {
let res: Self = glib::Object::builder()
.property("graph", graph)
.property("connection-banner", connection_banner)
.build();
res.imp().attach_receiver(receiver);
glib::MainContext::default().spawn_local(receive(res.clone(), receiver));
assert!(
res.imp().pw_sender.set(sender).is_ok(),
"Should be able to set pw_sender)"

View file

@ -20,15 +20,28 @@ mod pipewire_connection;
mod ui;
use adw::{gtk, prelude::*};
use pipewire::spa::{format::MediaType, Direction};
use pipewire::spa::{param::format::MediaType, utils::Direction};
/// Messages sent by the GTK thread to notify the pipewire thread.
#[derive(Debug, Clone)]
pub enum GtkMessage {
/// Toggle a link between the two specified ports.
ToggleLink { port_from: u32, port_to: u32 },
ToggleLink {
port_from: u32,
port_to: u32,
},
/// Connect to PipeWire service.
Connect(Option<String>),
/// Quit the event loop and let the thread finish.
Terminate,
NodeVolumeChanged {
id: u32,
volume: f32,
},
NodeMuteChanged {
id: u32,
mute: bool,
},
}
/// Messages sent by the pipewire thread to notify the GTK thread.
@ -44,6 +57,11 @@ pub enum PipewireMessage {
name: String,
media_name: String,
},
NodePropsChanged {
id: u32,
volume: Option<f32>,
mute: Option<bool>,
},
PortAdded {
id: u32,
node_id: u32,
@ -120,7 +138,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Start the pipewire thread with channels in both directions.
let (gtk_sender, gtk_receiver) = glib::MainContext::channel(glib::Priority::DEFAULT);
let (gtk_sender, gtk_receiver) = async_channel::unbounded();
let (pw_sender, pw_receiver) = pipewire::channel::channel();
let pw_thread =
std::thread::spawn(move || pipewire_connection::thread_main(gtk_sender, pw_receiver));

View file

@ -0,0 +1,191 @@
// FIXME: use libspa methods as soon as they are available
use std::{
borrow::BorrowMut,
mem::MaybeUninit,
os::raw::c_void,
pin::Pin,
ptr::{null, null_mut},
};
use pipewire::spa::{
pod::{builder::Builder, Pod},
sys::{
spa_pod, spa_pod_array, spa_pod_array_body, spa_pod_frame, spa_pod_object,
spa_pod_object_body, spa_pod_prop, spa_pod_prop_first, spa_pod_prop_is_inside,
spa_pod_prop_next,
},
};
#[derive(Clone)]
pub struct PodPropsIter<'a> {
size: u32,
body: &'a spa_pod_object_body,
cur_prop: *const spa_pod_prop,
}
impl<'a> PodPropsIter<'a> {
fn new(pod: &'a Pod) -> Self {
unsafe {
let raw_pod = pod.as_raw_ptr();
let size = (*raw_pod).size;
let obj = raw_pod as *const spa_pod_object;
let body = &(*obj).body;
let cur_prop = null();
Self {
size,
body,
cur_prop,
}
}
}
}
pub struct PodPropKV<'a> {
k: u32,
v: &'a Pod,
}
impl<'a> PodPropKV<'a> {
pub fn key(&self) -> u32 {
self.k
}
pub fn value(&self) -> &'a Pod {
self.v
}
}
impl<'a> Iterator for PodPropsIter<'a> {
type Item = PodPropKV<'a>;
fn next(&mut self) -> Option<Self::Item> {
unsafe {
if self.cur_prop.is_null() {
self.cur_prop = spa_pod_prop_first(self.body);
} else {
self.cur_prop = spa_pod_prop_next(self.cur_prop);
}
if spa_pod_prop_is_inside(self.body, self.size, self.cur_prop) {
let inner_pod = Pod::from_raw(&(*self.cur_prop).value);
Some(PodPropKV {
k: (*self.cur_prop).key,
v: inner_pod,
})
} else {
None
}
}
}
}
#[derive(Clone)]
pub struct PodArrayIter<'a> {
pod: &'a Pod,
size: u32,
body: *const spa_pod_array_body,
iter: *mut spa_pod,
}
impl<'a> PodArrayIter<'a> {
fn new(pod: &'a Pod) -> Self {
assert!(pod.is_array());
unsafe {
let raw_pod = pod.as_raw_ptr();
let arr = raw_pod as *const spa_pod_array;
let body = &(*arr).body as *const spa_pod_array_body;
let size = (*raw_pod).size;
let iter = null_mut();
Self {
pod,
body,
size,
iter,
}
}
}
}
impl<'a> Iterator for PodArrayIter<'a> {
type Item = &'a Pod;
fn next(&mut self) -> Option<Self::Item> {
unsafe {
if self.iter.is_null() {
self.iter = self
.pod
.as_raw_ptr()
.byte_offset(size_of::<spa_pod_array_body>() as isize);
} else {
self.iter = self.iter.byte_offset((*self.body).child.size as isize);
}
// FIXME: replace with spa_ptrinside in gnome47 platform
let p1 = self.body as *const c_void;
let p2 = self.iter as *const c_void;
let s1 = self.size as usize;
let s2 = (*self.body).child.size as usize;
if (*self.body).child.size > 0
&& p1 <= p2 && s2 <= s1 && p2.offset_from(p1) as usize <= s1 - s2
{
Some(pipewire::spa::pod::Pod::from_raw(self.iter))
} else {
None
}
}
}
}
pub trait PodExt {
fn iter_props(&self) -> PodPropsIter;
fn iter_array(&self) -> PodArrayIter;
}
impl PodExt for Pod {
fn iter_props(&self) -> PodPropsIter {
PodPropsIter::new(self)
}
fn iter_array(&self) -> PodArrayIter {
PodArrayIter::new(self)
}
}
// FIXME: Replace as soon as libspa fixes its builder API
pub struct FrameWrapper {
_inner: Pin<Box<MaybeUninit<spa_pod_frame>>>,
}
pub trait BuilderExt {
fn push_object_frame(&mut self, type_: u32, id: u32) -> Result<FrameWrapper, ()>;
fn push_array_frame(&mut self) -> Result<FrameWrapper, ()>;
fn pop_frame(&mut self, frame: &mut FrameWrapper) -> &Pod;
}
impl<'d> BuilderExt for Builder<'d> {
fn push_object_frame(&mut self, type_: u32, id: u32) -> Result<FrameWrapper, ()> {
unsafe {
let mut obj_frame = FrameWrapper {
_inner: Box::pin(MaybeUninit::zeroed()),
};
if self
.push_object(obj_frame._inner.borrow_mut(), type_, id)
.is_err()
{
Err(())
} else {
Ok(obj_frame)
}
}
}
fn push_array_frame(&mut self) -> Result<FrameWrapper, ()> {
unsafe {
let mut array_frame = FrameWrapper {
_inner: Box::pin(MaybeUninit::zeroed()),
};
if self.push_array(array_frame._inner.borrow_mut()).is_err() {
Err(())
} else {
Ok(array_frame)
}
}
}
fn pop_frame(&mut self, frame: &mut FrameWrapper) -> &Pod {
unsafe {
let pod_ptr = pipewire::spa::sys::spa_pod_builder_pop(
self.as_raw_ptr(),
frame._inner.borrow_mut().assume_init_mut(),
) as *const pipewire::spa::sys::spa_pod;
Pod::from_raw(pod_ptr)
}
}
}

View file

@ -14,31 +14,30 @@
//
// SPDX-License-Identifier: GPL-3.0-only
mod libspa_ext;
mod state;
use std::{
cell::{Cell, RefCell},
collections::HashMap,
rc::Rc,
time::Duration,
};
use std::{cell::RefCell, collections::HashMap, rc::Rc, time::Duration};
use adw::glib::{self, clone};
use libspa_ext::{BuilderExt, PodExt as _};
use log::{debug, error, info, warn};
use pipewire::{
context::Context,
core::{Core, PW_ID_CORE},
keys,
link::{Link, LinkChangeMask, LinkInfo, LinkListener, LinkState},
node::{Node, NodeInfo, NodeListener},
port::{Port, PortChangeMask, PortInfo, PortListener},
prelude::*,
properties,
link::{Link, LinkChangeMask, LinkInfoRef, LinkListener, LinkState},
main_loop::MainLoop,
node::{Node, NodeInfoRef, NodeListener},
port::{Port, PortChangeMask, PortInfoRef, PortListener},
properties::{properties, Properties},
registry::{GlobalObject, Registry},
spa::{
param::{ParamInfoFlags, ParamType},
ForeignDict, SpaResult,
sys::{SPA_PARAM_Props, SPA_PROP_channelVolumes, SPA_PROP_mute, SPA_TYPE_OBJECT_Props},
utils::{dict::DictRef, result::SpaResult, SpaTypes},
},
types::ObjectType,
Context, Core, MainLoop,
};
use crate::{GtkMessage, MediaType, NodeType, PipewireMessage};
@ -48,6 +47,7 @@ enum ProxyItem {
Node {
_proxy: Node,
_listener: NodeListener,
_channels: Option<usize>,
},
Port {
proxy: Port,
@ -59,44 +59,78 @@ enum ProxyItem {
},
}
struct LoopState {
is_stopped: bool,
props: Properties,
}
impl LoopState {
fn handle_message(&mut self, msg: GtkMessage) -> bool {
match msg {
GtkMessage::Terminate => self.is_stopped = true,
GtkMessage::Connect(remote) => match remote {
Some(s) => self.props.insert(*keys::REMOTE_NAME, s),
None => self.props.remove(*keys::REMOTE_NAME),
},
_ => return false,
}
true
}
}
/// The "main" function of the pipewire thread.
pub(super) fn thread_main(
gtk_sender: glib::Sender<PipewireMessage>,
gtk_sender: async_channel::Sender<PipewireMessage>,
mut pw_receiver: pipewire::channel::Receiver<GtkMessage>,
) {
let mainloop = MainLoop::new().expect("Failed to create mainloop");
let mainloop = MainLoop::new(None).expect("Failed to create mainloop");
let context = Rc::new(Context::new(&mainloop).expect("Failed to create context"));
let is_stopped = Rc::new(Cell::new(false));
let loop_state = Rc::new(RefCell::new(LoopState {
is_stopped: false,
props: properties! {
"media.category" => "Manager",
},
}));
let mut is_connecting = false;
while !is_stopped.get() {
// Wait PipeWire service to connect from command line arguments.
let receiver = pw_receiver.attach(mainloop.loop_(), {
clone!(@strong mainloop, @strong loop_state => move |msg|
if loop_state.borrow_mut().handle_message(msg) {
mainloop.quit();
}
)
});
mainloop.run();
pw_receiver = receiver.deattach();
while !loop_state.borrow().is_stopped {
// Try to connect
let core = match context.connect(Some(properties! {
"media.category" => "Manager"
})) {
let props = loop_state.borrow().props.clone();
let core = match context.connect(Some(props)) {
Ok(core) => Rc::new(core),
Err(_) => {
if !is_connecting {
is_connecting = true;
gtk_sender
.send(PipewireMessage::Connecting)
.send_blocking(PipewireMessage::Connecting)
.expect("Failed to send message");
}
// If connection is failed, try to connect again in 200ms
let interval = Some(Duration::from_millis(200));
let timer = mainloop.add_timer(clone!(@strong mainloop => move |_| {
mainloop.quit();
}));
let timer = mainloop
.loop_()
.add_timer(clone!(@strong mainloop => move |_| {
mainloop.quit();
}));
timer.update_timer(interval, None).into_result().unwrap();
let receiver = pw_receiver.attach(&mainloop, {
clone!(@strong mainloop, @strong is_stopped => move |msg|
if let GtkMessage::Terminate = msg {
// main thread requested stop
is_stopped.set(true);
let receiver = pw_receiver.attach(mainloop.loop_(), {
clone!(@strong mainloop, @strong loop_state => move |msg|
if loop_state.borrow_mut().handle_message(msg) {
mainloop.quit();
}
)
@ -112,7 +146,7 @@ pub(super) fn thread_main(
if is_connecting {
is_connecting = false;
gtk_sender
.send(PipewireMessage::Connected)
.send_blocking(PipewireMessage::Connected)
.expect("Failed to send message");
}
@ -122,33 +156,54 @@ pub(super) fn thread_main(
let proxies = Rc::new(RefCell::new(HashMap::new()));
let state = Rc::new(RefCell::new(State::new()));
let receiver = pw_receiver.attach(&mainloop, {
clone!(@strong mainloop, @weak core, @weak registry, @strong state, @strong is_stopped => move |msg| match msg {
let receiver = pw_receiver.attach(mainloop.loop_(), {
clone!(@strong mainloop, @weak core, @weak registry, @strong state, @strong loop_state, @strong proxies => move |msg| match msg {
GtkMessage::ToggleLink { port_from, port_to } => toggle_link(port_from, port_to, &core, &registry, &state),
GtkMessage::Terminate => {
// main thread requested stop
is_stopped.set(true);
GtkMessage::Terminate | GtkMessage::Connect(_) => {
loop_state.borrow_mut().handle_message(msg);
mainloop.quit();
}
GtkMessage::NodeVolumeChanged { id, volume } => {
let proxies = proxies.borrow();
let Some(ProxyItem::Node { _proxy: node, _channels: channels, .. }) = proxies.get(&id) else {
error!("Received info on unknown node with id {id}");
return;
};
if let Some(channels) = channels {
set_node_param_props(node, vec![volume; *channels], None)
} else {
warn!("Volume change triggered on a node with no known channels");
}
}
GtkMessage::NodeMuteChanged { id, mute } => {
let proxies = proxies.borrow();
let Some(ProxyItem::Node { _proxy: node, .. }) = proxies.get(&id) else {
error!("Received info on unknown node with id {id}");
return;
};
set_node_param_props(node, vec![], Some(mute))
}
})
});
let gtk_sender = gtk_sender.clone();
let _listener = core.add_listener_local()
.error(clone!(@strong mainloop, @strong gtk_sender, @strong is_stopped => move |id, _seq, res, message| {
if id != pipewire::PW_ID_CORE {
return;
}
let _listener = core
.add_listener_local()
.error(
clone!(@strong mainloop, @strong gtk_sender => move |id, _seq, res, message| {
if id != PW_ID_CORE {
return;
}
if res == -libc::EPIPE {
gtk_sender.send(PipewireMessage::Disconnected)
.expect("Failed to send message");
mainloop.quit();
} else {
let serr = SpaResult::from_c(res).into_result().unwrap_err();
error!("Pipewire Core received error {serr}: {message}");
}
}))
if res == -libc::EPIPE {
gtk_sender.send_blocking(PipewireMessage::Disconnected)
.expect("Failed to send message");
mainloop.quit();
} else {
let serr = SpaResult::from_c(res).into_result().unwrap_err();
error!("Pipewire Core received error {serr}: {message}");
}
}),
)
.register();
let _listener = registry
@ -163,9 +218,9 @@ pub(super) fn thread_main(
}
}
))
.global_remove(clone!(@strong proxies, @strong state => move |id| {
.global_remove(clone!(@strong gtk_sender, @strong proxies, @strong state => move |id| {
if let Some(item) = state.borrow_mut().remove(id) {
gtk_sender.send(match item {
gtk_sender.send_blocking(match item {
Item::Node { .. } => PipewireMessage::NodeRemoved {id},
Item::Port { node_id } => PipewireMessage::PortRemoved {id, node_id},
Item::Link { .. } => PipewireMessage::LinkRemoved {id},
@ -183,11 +238,15 @@ pub(super) fn thread_main(
mainloop.run();
pw_receiver = receiver.deattach();
gtk_sender
.send_blocking(PipewireMessage::Disconnected)
.expect("Failed to send message");
}
}
/// Get the nicest possible name for the node, using a fallback chain of possible name attributes
fn get_node_name(props: &ForeignDict) -> &str {
fn get_node_name(props: &DictRef) -> &str {
props
.get(&keys::NODE_DESCRIPTION)
.or_else(|| props.get(&keys::NODE_NICK))
@ -197,8 +256,8 @@ fn get_node_name(props: &ForeignDict) -> &str {
/// Handle a new node being added
fn handle_node(
node: &GlobalObject<ForeignDict>,
sender: &glib::Sender<PipewireMessage>,
node: &GlobalObject<&DictRef>,
sender: &async_channel::Sender<PipewireMessage>,
registry: &Rc<Registry>,
proxies: &Rc<RefCell<HashMap<u32, ProxyItem>>>,
state: &Rc<RefCell<State>>,
@ -233,7 +292,7 @@ fn handle_node(
state.borrow_mut().insert(node.id, Item::Node);
sender
.send(PipewireMessage::NodeAdded {
.send_blocking(PipewireMessage::NodeAdded {
id: node.id,
name,
node_type,
@ -241,11 +300,18 @@ fn handle_node(
.expect("Failed to send message");
let proxy: Node = registry.bind(node).expect("Failed to bind to node proxy");
proxy.subscribe_params(&[ParamType::Props]);
let id = node.id;
let listener = proxy
.add_listener_local()
.info(clone!(@strong sender, @strong proxies => move |info| {
handle_node_info(info, &sender, &proxies);
}))
.param(clone!(@strong sender, @strong proxies => move |_: i32, param_type: pipewire::spa::param::ParamType, _: u32, _:u32, pod: Option<&pipewire::spa::pod::Pod>| {
if let Some(pod) = pod {
handle_node_params(id, param_type, pod, &sender, &proxies);
}
}))
.register();
proxies.borrow_mut().insert(
@ -253,13 +319,14 @@ fn handle_node(
ProxyItem::Node {
_proxy: proxy,
_listener: listener,
_channels: None,
},
);
}
fn handle_node_info(
info: &NodeInfo,
sender: &glib::Sender<PipewireMessage>,
info: &NodeInfoRef,
sender: &async_channel::Sender<PipewireMessage>,
proxies: &Rc<RefCell<HashMap<u32, ProxyItem>>>,
) {
debug!("Received node info: {:?}", info);
@ -276,7 +343,7 @@ fn handle_node_info(
let name = get_node_name(props).to_string();
sender
.send(PipewireMessage::NodeNameChanged {
.send_blocking(PipewireMessage::NodeNameChanged {
id,
name,
media_name: media_name.to_string(),
@ -285,10 +352,93 @@ fn handle_node_info(
}
}
fn handle_node_params(
id: u32,
param_type: pipewire::spa::param::ParamType,
pod: &pipewire::spa::pod::Pod,
sender: &async_channel::Sender<PipewireMessage>,
proxies: &Rc<RefCell<HashMap<u32, ProxyItem>>>,
) {
debug!(
"Received node params of type {:?}: {:?}",
param_type,
pod.type_()
);
let mut proxies = proxies.borrow_mut();
let Some(ProxyItem::Node {
_channels: channels,
..
}) = proxies.get_mut(&id)
else {
error!("Received info on unknown node with id {id}");
return;
};
if pod.type_() == SpaTypes::Object {
let mut volume = None;
let mut mute = None;
for prop in pod.iter_props() {
if prop.key() == SPA_PROP_channelVolumes && prop.value().is_array() {
let mut iter = prop.value().iter_array().filter(|p| p.is_float());
*channels = Some(iter.clone().count());
// We're only interested in the "master" volume of the node, so take first
// channel volume as volume
volume = iter.next().map(|p| {
p.get_float()
.expect("values in POD array should have been float")
// Calculate cubic volume value from linear
.cbrt()
});
} else if prop.key() == SPA_PROP_mute && prop.value().is_bool() {
mute = prop.value().get_bool().ok();
}
}
sender
.send_blocking(PipewireMessage::NodePropsChanged { id, volume, mute })
.expect("Failed to send message");
}
}
fn set_node_param_props(node: &Node, channel_volumes: Vec<f32>, mute: Option<bool>) {
let mut buffer = vec![0u8; 1024];
let mut builder = pipewire::spa::pod::builder::Builder::new(&mut buffer);
let mut obj_frame = builder
.push_object_frame(SPA_TYPE_OBJECT_Props, SPA_PARAM_Props)
.expect("Failed to push param props object to builder");
if let Some(m) = mute {
builder
.add_prop(SPA_PROP_mute, 0)
.expect("Failed to add mute prop");
builder
.add_bool(m)
.expect("Failed to add value for mute prop");
}
if !channel_volumes.is_empty() {
builder
.add_prop(SPA_PROP_channelVolumes, 0)
.expect("Failed to add channelVolumes prop");
let mut array_frame = builder
.push_array_frame()
.expect("Failed to push array value for channelVolumes prop");
for v in channel_volumes {
// Volume is cubic, but we display in linear
builder
.add_float(v * v * v)
.expect("Failed to add array entry for channelVolumes prop");
}
builder.pop_frame(&mut array_frame);
}
let pod = builder.pop_frame(&mut obj_frame);
node.set_param(ParamType::Props, 0, pod);
}
/// Handle a new port being added
fn handle_port(
port: &GlobalObject<ForeignDict>,
sender: &glib::Sender<PipewireMessage>,
port: &GlobalObject<&DictRef>,
sender: &async_channel::Sender<PipewireMessage>,
registry: &Rc<Registry>,
proxies: &Rc<RefCell<HashMap<u32, ProxyItem>>>,
state: &Rc<RefCell<State>>,
@ -319,10 +469,10 @@ fn handle_port(
}
fn handle_port_info(
info: &PortInfo,
info: &PortInfoRef,
proxies: &Rc<RefCell<HashMap<u32, ProxyItem>>>,
state: &Rc<RefCell<State>>,
sender: &glib::Sender<PipewireMessage>,
sender: &async_channel::Sender<PipewireMessage>,
) {
debug!("Received port info: {:?}", info);
@ -363,7 +513,7 @@ fn handle_port_info(
}
sender
.send(PipewireMessage::PortAdded {
.send_blocking(PipewireMessage::PortAdded {
id,
node_id,
name,
@ -376,7 +526,7 @@ fn handle_port_info(
fn handle_port_enum_format(
port_id: u32,
param: Option<&pipewire::spa::pod::Pod>,
sender: &glib::Sender<PipewireMessage>,
sender: &async_channel::Sender<PipewireMessage>,
) {
let media_type = param
.and_then(|param| pipewire::spa::param::format_utils::parse_format(param).ok())
@ -384,7 +534,7 @@ fn handle_port_enum_format(
.unwrap_or(MediaType::Unknown);
sender
.send(PipewireMessage::PortFormatChanged {
.send_blocking(PipewireMessage::PortFormatChanged {
id: port_id,
media_type,
})
@ -393,8 +543,8 @@ fn handle_port_enum_format(
/// Handle a new link being added
fn handle_link(
link: &GlobalObject<ForeignDict>,
sender: &glib::Sender<PipewireMessage>,
link: &GlobalObject<&DictRef>,
sender: &async_channel::Sender<PipewireMessage>,
registry: &Rc<Registry>,
proxies: &Rc<RefCell<HashMap<u32, ProxyItem>>>,
state: &Rc<RefCell<State>>,
@ -422,9 +572,9 @@ fn handle_link(
}
fn handle_link_info(
info: &LinkInfo,
info: &LinkInfoRef,
state: &Rc<RefCell<State>>,
sender: &glib::Sender<PipewireMessage>,
sender: &async_channel::Sender<PipewireMessage>,
) {
debug!("Received link info: {:?}", info);
@ -435,7 +585,7 @@ fn handle_link_info(
// Info was an update - figure out if we should notify the gtk thread
if info.change_mask().contains(LinkChangeMask::STATE) {
sender
.send(PipewireMessage::LinkStateChanged {
.send_blocking(PipewireMessage::LinkStateChanged {
id,
active: matches!(info.state(), LinkState::Active),
})
@ -443,7 +593,7 @@ fn handle_link_info(
}
if info.change_mask().contains(LinkChangeMask::FORMAT) {
sender
.send(PipewireMessage::LinkFormatChanged {
.send_blocking(PipewireMessage::LinkFormatChanged {
id,
media_type: get_link_media_type(info),
})
@ -457,7 +607,7 @@ fn handle_link_info(
state.insert(id, Item::Link { port_from, port_to });
sender
.send(PipewireMessage::LinkAdded {
.send_blocking(PipewireMessage::LinkAdded {
id,
port_from,
port_to,
@ -495,7 +645,7 @@ fn toggle_link(
.get_node_of_port(port_to)
.expect("Requested port not in state");
if let Err(e) = core.create_object::<Link, _>(
if let Err(e) = core.create_object::<Link>(
"link-factory",
&properties! {
"link.output.node" => node_from.to_string(),
@ -510,7 +660,7 @@ fn toggle_link(
}
}
fn get_link_media_type(link_info: &LinkInfo) -> MediaType {
fn get_link_media_type(link_info: &LinkInfoRef) -> MediaType {
let media_type = link_info
.format()
.and_then(|format| pipewire::spa::param::format_utils::parse_format(format).ok())

View file

@ -42,8 +42,8 @@ mod imp {
use adw::gtk::gdk::{self};
use log::warn;
use once_cell::sync::Lazy;
use pipewire::spa::format::MediaType;
use pipewire::spa::Direction;
use pipewire::spa::param::format::MediaType;
use pipewire::spa::utils::Direction;
pub struct Colors {
audio: gdk::RGBA,
@ -787,7 +787,10 @@ impl GraphView {
420.0
};
let y = imp
// FIXME: We are currently using a constant 180 for the height of the nodes, this could cause problems for nodes with a lot of ports.
let node_height = 180.0;
let mut column = imp
.nodes
.borrow()
.iter()
@ -800,11 +803,24 @@ impl GraphView {
// Only look for other nodes that have a similar x coordinate
(x - x2).abs() < 50.0
})
.max_by(|y1, y2| {
// Get max in column
y1.partial_cmp(y2).unwrap_or(Ordering::Equal)
.collect::<Vec<_>>();
column
.sort_unstable_by(|(_, a_y), (_, b_y)| a_y.partial_cmp(b_y).unwrap_or(Ordering::Less));
let y = column
.windows(2)
.map(|w| {
// Calculate space between this node and the next
let (a_y, b_y) = (w[0].1, w[1].1);
let diff_next = b_y - a_y;
(a_y, diff_next)
})
.map_or(20_f32, |(_x, y)| y + 120.0);
.find(|(_y, diff_next)| *diff_next >= 2.0 * node_height)
.map_or(
// If we didn't find enough space between nodes, append to bottom
column.last().map_or(20_f32, |(_x, y)| y + node_height),
// Put new node after below the node we found
|(y, _)| y + node_height,
);
imp.nodes.borrow_mut().insert(node, Point::new(x, y));
}

View file

@ -15,7 +15,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use adw::{glib, prelude::*, subclass::prelude::*};
use pipewire::spa::format::MediaType;
use pipewire::spa::param::format::MediaType;
use super::Port;

View file

@ -15,16 +15,19 @@
// SPDX-License-Identifier: GPL-3.0-only
use adw::{glib, gtk, prelude::*, subclass::prelude::*};
use pipewire::spa::Direction;
use pipewire::spa::utils::Direction;
use super::Port;
mod imp {
use glib::subclass::Signal;
use super::*;
use std::{
cell::{Cell, RefCell},
collections::HashSet,
sync::OnceLock,
};
#[derive(glib::Properties, gtk::CompositeTemplate, Default)]
@ -55,6 +58,22 @@ mod imp {
#[template_child]
pub(super) media_name: TemplateChild<gtk::Label>,
#[template_child]
pub(super) node_mute_button: TemplateChild<gtk::Button>,
#[property(
name = "node-volume", type = f64,
get = |this: &Self| {
let val = this.node_volume.value();
val/100.0
},
set = |this: &Self, val : f64| {
this.node_volume.set_value(val*100.0);
self.node_volume.set_visible(true);
self.node_mute_button.set_visible(true);
}
)]
#[template_child]
pub(super) node_volume: TemplateChild<gtk::Scale>,
#[template_child]
pub(super) separator: TemplateChild<gtk::Separator>,
#[template_child]
pub(super) port_grid: TemplateChild<gtk::Grid>,
@ -91,6 +110,11 @@ mod imp {
// Display a grab cursor when the mouse is over the label so the user knows the node can be dragged.
self.node_name
.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref());
self.node_volume
.set_adjustment(&gtk::Adjustment::new(0.0, 0.0, 100.0, 0.5, 5.0, 0.0));
self.set_up_volume_ctl();
}
fn dispose(&self) {
@ -98,6 +122,20 @@ mod imp {
child.unparent();
}
}
fn signals() -> &'static [Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| {
vec![
Signal::builder("volume-changed")
.param_types([f32::static_type()])
.build(),
Signal::builder("mute-changed")
.param_types([bool::static_type()])
.build(),
]
})
}
}
impl WidgetImpl for Node {}
@ -129,8 +167,8 @@ mod imp {
_ => unreachable!(),
});
ports_out.sort_unstable_by_key(|port| port.name());
ports_in.sort_unstable_by_key(|port| port.name());
ports_out.sort_unstable_by(|&a, &b| natord::compare(&a.name(), &b.name()));
ports_in.sort_unstable_by(|&a, &b| natord::compare(&a.name(), &b.name()));
// In case no ports have been added to the port, hide the seperator as it is not needed
self.separator
@ -144,6 +182,28 @@ mod imp {
self.port_grid.attach(port, 1, i.try_into().unwrap(), 1, 1);
}
}
fn set_up_volume_ctl(&self) {
let pipewire_id = self.pipewire_id.clone();
let obj = self.obj().clone();
self.node_volume.connect_change_value(move |_, _, val| {
let percent = format!("{:.3}%", val.to_string());
log::info!("Changing volume of {} to {}", pipewire_id.get(), percent);
obj.emit_by_name::<()>("volume-changed", &[&(val as f32 / 100.0f32)]);
glib::Propagation::Proceed
});
let pipewire_id = self.pipewire_id.clone();
let obj = self.obj().clone();
self.node_mute_button.connect_clicked(move |button| {
let is_muted = button
.icon_name()
.expect("icon_name for mute button should be set")
== "audio-volume-muted";
log::info!("Changing mute of {} to {}", pipewire_id.get(), !is_muted);
obj.emit_by_name::<()>("mute-changed", &[&(!is_muted)]);
});
}
}
}
@ -174,4 +234,12 @@ impl Node {
log::warn!("Tried to remove non-existant port widget from node");
}
}
pub fn set_mute(&self, is_muted: bool) {
self.imp().node_mute_button.set_icon_name(if is_muted {
"audio-volume-muted"
} else {
"audio-volume-high"
});
}
}

View file

@ -41,6 +41,27 @@
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">horizontal</property>
<child>
<object class="GtkButton" id="node_mute_button">
<property name="icon_name">audio-volume-high</property>
<property name="visible">false</property>
</object>
</child>
<child>
<object class="GtkScale" id="node_volume">
<property name="orientation">horizontal</property>
<property name="draw-value">true</property>
<property name="value-pos">right</property>
<property name="digits">0</property>
<property name="visible">false</property>
<property name="hexpand">true</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkSeparator" id="separator">
<!-- The node will show the seperator only once ports are added to it -->

View file

@ -21,17 +21,17 @@ use adw::{
prelude::*,
subclass::prelude::*,
};
use pipewire::spa::Direction;
use pipewire::spa::utils::Direction;
use super::PortHandle;
mod imp {
use super::*;
use std::cell::Cell;
use std::cell::{Cell, OnceCell};
use once_cell::{sync::Lazy, unsync::OnceCell};
use pipewire::spa::{format::MediaType, Direction};
use once_cell::sync::Lazy;
use pipewire::spa::{param::format::MediaType, utils::Direction};
/// Graphical representation of a pipewire port.
#[derive(gtk::CompositeTemplate, glib::Properties)]

View file

@ -15,6 +15,9 @@ mod imp {
#[property(type = adw::Banner, get = |_| self.connection_banner.clone())]
pub connection_banner: TemplateChild<adw::Banner>,
#[template_child]
#[property(type = gtk::Label, get = |_| self.current_remote_label.clone())]
pub current_remote_label: TemplateChild<gtk::Label>,
#[template_child]
#[property(type = graph::GraphView, get = |_| self.graph.clone())]
pub graph: TemplateChild<graph::GraphView>,
}

View file

@ -13,11 +13,40 @@
<template class="HelvumWindow" parent="AdwApplicationWindow">
<property name="default-width">1280</property>
<property name="default-height">720</property>
<property name="title">Helvum - Pipewire Patchbay</property>
<child>
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar" id="header_bar">
<property name="title-widget">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="label">Helvum - Pipewire Patchbay</property>
<property name="single-line-mode">True</property>
<property name="ellipsize">end</property>
<property name="width-chars">5</property>
<property name="vexpand">yes</property>
<property name="valign">end</property>
<style>
<class name="title"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel" id="current_remote_label">
<property name="single-line-mode">True</property>
<property name="ellipsize">start</property>
<property name="vexpand">yes</property>
<property name="valign">start</property>
<style>
<class name="caption"/>
<class name="dim-label"/>
</style>
</object>
</child>
</object>
</property>
<child type="end">
<object class="GtkMenuButton">
<property name="icon-name">open-menu-symbolic</property>