12 Commits

Author SHA1 Message Date
Tom Wagner
d7dd6033a6 graphview: Draw links using new gdk path API instead of cairo 2024-03-25 13:55:49 +01:00
Tom Wagner
f32559511d Update to latest gtk-rs crates 2024-03-25 10:08:38 +01:00
Tom Wagner
57cba6381b flatpak: Update runtime to gnome 46 2024-03-25 09:49:16 +01:00
Dorinda Bassey
d1b9b0f11f update Helvum to track the latest pipewire0.8.0
update Helvum to track the latest pipewire0.8.0
2024-03-19 14:06:57 +01:00
Tom A. Wagner
4549ba6ff5 ui: Force LTR direction on the nodes port grid and on ports
Forces the direction on the nodes internal port grid and on the ports themselves
to be always left-to-right (LTR), to avoid the UI becoming messed up when defaulting
to right-to-left (RTL).

Previously, when using RTL, the side of input and output ports would be swapped,
causing the port handles to be inside the node instead of at the edge.
2023-12-09 16:01:11 +01:00
Denis Drakhnia
e78d6f5fb4 ui: Move view with middle mouse button. 2023-11-18 14:19:20 +02:00
Denis Drakhnia
96c079d29e ui: Display node media name in graph view 2023-10-13 11:40:21 +00:00
Tom A. Wagner
5d4931b418 pw: Set media.category property to manager
This will make the session manager give Helvum full permissions even when
used from flatpak or otherwise restricted, so that we can always change
the graph even if permissions become more restricted in the future.
2023-10-12 08:36:08 +00:00
Tom A. Wagner
b983ade736 ui: Move "disconnected" banner from headerbar into content
The change to AdwToolbarView put the disconnected banner into the toolbar, resulting in a weird-looking
separator when the bar is shown.

This moves the banner into the "content" widget of the AdwToolbarView to fix that issue.
2023-10-11 22:48:37 +02:00
Angelo Verlain
94d5e95695 use AdwToolbarView 2023-10-11 22:48:37 +02:00
Angelo Verlain Shema
e1f63ddd28 Use responsive design 2023-10-10 18:16:23 +00:00
Angelo Verlain
903df21ba3 attach about window 2023-10-06 17:46:59 +02:00
19 changed files with 690 additions and 312 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

371
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,11 +549,19 @@ 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",
"gtk4",
"libadwaita",
"libc",
"log",
@@ -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"
@@ -642,15 +705,13 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nix"
version = "0.26.4"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 1.3.2",
"bitflags",
"cfg-if",
"libc",
"memoffset 0.7.1",
"pin-utils",
]
[[package]]
@@ -665,28 +726,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 +755,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 +774,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 +791,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 +802,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 +953,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 +986,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 +1027,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 +1052,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 +1075,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 +1115,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 +1189,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_3"] }
glib = { version = "0.18", features = ["log"] }
pipewire = "0.8.0"
adw = { version = "0.6", package = "libadwaita", features = ["v1_4"] }
gtk = { version = "0.8", package = "gtk4", features = ["v4_14"] }
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"

View File

@@ -23,7 +23,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

@@ -11,8 +11,8 @@ gnome = import('gnome')
base_id = 'org.pipewire.Helvum'
dependency('glib-2.0', version: '>= 2.66')
dependency('gtk4', version: '>= 4.4.0')
dependency('libadwaita-1', version: '>= 1.3')
dependency('gtk4', version: '>= 4.14.0')
dependency('libadwaita-1', version: '>= 1.4')
dependency('libpipewire-0.3')
desktop_file_validate = find_program('desktop-file-validate', required: false)

View File

@@ -16,7 +16,7 @@
use adw::{
gio,
glib::{self, clone, Receiver},
glib::{self, clone},
gtk,
prelude::*,
subclass::prelude::*,
@@ -33,8 +33,9 @@ static AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
mod imp {
use super::*;
use std::cell::OnceCell;
use adw::subclass::prelude::AdwApplicationImpl;
use once_cell::unsync::OnceCell;
#[derive(Default)]
pub struct Application {
@@ -112,9 +113,12 @@ mod imp {
}
fn show_about_dialog(&self) {
let obj = &*self.obj();
let window = obj.active_window().unwrap();
let authors: Vec<&str> = AUTHORS.split(':').collect();
let about_window = adw::AboutWindow::builder()
.transient_for(&window)
.application_icon(APP_ID)
.application_name("Helvum")
.developer_name("Tom Wagner")
@@ -140,7 +144,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()

View File

@@ -23,9 +23,7 @@ use crate::{ui::graph::GraphView, GtkMessage, PipewireMessage};
mod imp {
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,35 +51,58 @@ 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::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::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.
@@ -95,6 +116,23 @@ mod imp {
self.obj().graph().add_node(node, node_type);
}
/// Update a node tooltip to the view.
fn node_name_changed(&self, id: u32, node_name: &str, media_name: &str) {
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;
};
node.set_node_name(node_name);
node.set_media_name(media_name);
}
/// Remove the node with the specified id from the view.
fn remove_node(&self, id: u32) {
log::info!("Removing node from graph: id {}", id);
@@ -112,7 +150,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();
@@ -255,7 +299,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 {
@@ -304,19 +352,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,7 +20,7 @@ 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)]
@@ -39,6 +39,11 @@ pub enum PipewireMessage {
name: String,
node_type: Option<NodeType>,
},
NodeNameChanged {
id: u32,
name: String,
media_name: String,
},
PortAdded {
id: u32,
node_id: u32,
@@ -115,7 +120,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

@@ -26,23 +26,31 @@ use std::{
use adw::glib::{self, clone};
use log::{debug, error, info, warn};
use pipewire::{
link::{Link, LinkChangeMask, LinkInfo, LinkListener, LinkState},
port::{Port, PortChangeMask, PortInfo, PortListener},
prelude::*,
properties,
context::Context,
core::{Core, PW_ID_CORE},
keys,
link::{Link, LinkChangeMask, LinkInfoRef, LinkListener, LinkState},
main_loop::MainLoop,
node::{Node, NodeInfoRef, NodeListener},
port::{Port, PortChangeMask, PortInfoRef, PortListener},
properties::properties,
registry::{GlobalObject, Registry},
spa::{
param::{ParamInfoFlags, ParamType},
ForeignDict, SpaResult,
utils::dict::DictRef,
utils::result::SpaResult,
},
types::ObjectType,
Context, Core, MainLoop,
};
use crate::{GtkMessage, MediaType, NodeType, PipewireMessage};
use state::{Item, State};
enum ProxyItem {
Node {
_proxy: Node,
_listener: NodeListener,
},
Port {
proxy: Port,
_listener: PortListener,
@@ -55,36 +63,40 @@ enum ProxyItem {
/// 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 mut is_connecting = false;
while !is_stopped.get() {
// Try to connect
let core = match context.connect(None) {
let core = match context.connect(Some(properties! {
"media.category" => "Manager"
})) {
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, {
let receiver = pw_receiver.attach(mainloop.loop_(), {
clone!(@strong mainloop, @strong is_stopped => move |msg|
if let GtkMessage::Terminate = msg {
// main thread requested stop
@@ -104,7 +116,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");
}
@@ -114,7 +126,7 @@ 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, {
let receiver = pw_receiver.attach(mainloop.loop_(), {
clone!(@strong mainloop, @weak core, @weak registry, @strong state, @strong is_stopped => move |msg| match msg {
GtkMessage::ToggleLink { port_from, port_to } => toggle_link(port_from, port_to, &core, &registry, &state),
GtkMessage::Terminate => {
@@ -128,12 +140,12 @@ pub(super) fn thread_main(
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 {
if id != PW_ID_CORE {
return;
}
if res == -libc::EPIPE {
gtk_sender.send(PipewireMessage::Disconnected)
gtk_sender.send_blocking(PipewireMessage::Disconnected)
.expect("Failed to send message");
mainloop.quit();
} else {
@@ -147,7 +159,7 @@ pub(super) fn thread_main(
.add_listener_local()
.global(clone!(@strong gtk_sender, @weak registry, @strong proxies, @strong state =>
move |global| match global.type_ {
ObjectType::Node => handle_node(global, &gtk_sender, &state),
ObjectType::Node => handle_node(global, &gtk_sender, &registry, &proxies, &state),
ObjectType::Port => handle_port(global, &gtk_sender, &registry, &proxies, &state),
ObjectType::Link => handle_link(global, &gtk_sender, &registry, &proxies, &state),
_ => {
@@ -157,7 +169,7 @@ pub(super) fn thread_main(
))
.global_remove(clone!(@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},
@@ -178,10 +190,21 @@ pub(super) fn thread_main(
}
}
/// Get the nicest possible name for the node, using a fallback chain of possible name attributes
fn get_node_name(props: &DictRef) -> &str {
props
.get(&keys::NODE_DESCRIPTION)
.or_else(|| props.get(&keys::NODE_NICK))
.or_else(|| props.get(&keys::NODE_NAME))
.unwrap_or_default()
}
/// 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>>,
) {
let props = node
@@ -189,15 +212,7 @@ fn handle_node(
.as_ref()
.expect("Node object is missing properties");
// Get the nicest possible name for the node, using a fallback chain of possible name attributes.
let name = String::from(
props
.get("node.description")
.or_else(|| props.get("node.nick"))
.or_else(|| props.get("node.name"))
.unwrap_or_default(),
);
let name = get_node_name(props).to_string();
let media_class = |class: &str| {
if class.contains("Sink") || class.contains("Input") {
Some(NodeType::Input)
@@ -222,18 +237,62 @@ 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,
})
.expect("Failed to send message");
let proxy: Node = registry.bind(node).expect("Failed to bind to node proxy");
let listener = proxy
.add_listener_local()
.info(clone!(@strong sender, @strong proxies => move |info| {
handle_node_info(info, &sender, &proxies);
}))
.register();
proxies.borrow_mut().insert(
node.id,
ProxyItem::Node {
_proxy: proxy,
_listener: listener,
},
);
}
fn handle_node_info(
info: &NodeInfoRef,
sender: &async_channel::Sender<PipewireMessage>,
proxies: &Rc<RefCell<HashMap<u32, ProxyItem>>>,
) {
debug!("Received node info: {:?}", info);
let id = info.id();
let proxies = proxies.borrow();
let Some(ProxyItem::Node { .. }) = proxies.get(&id) else {
error!("Received info on unknown node with id {id}");
return;
};
let props = info.props().expect("NodeInfo object is missing properties");
if let Some(media_name) = props.get(&keys::MEDIA_NAME) {
let name = get_node_name(props).to_string();
sender
.send_blocking(PipewireMessage::NodeNameChanged {
id,
name,
media_name: media_name.to_string(),
})
.expect("Failed to send message");
}
}
/// 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>>,
@@ -264,10 +323,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);
@@ -308,7 +367,7 @@ fn handle_port_info(
}
sender
.send(PipewireMessage::PortAdded {
.send_blocking(PipewireMessage::PortAdded {
id,
node_id,
name,
@@ -321,7 +380,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())
@@ -329,7 +388,7 @@ fn handle_port_enum_format(
.unwrap_or(MediaType::Unknown);
sender
.send(PipewireMessage::PortFormatChanged {
.send_blocking(PipewireMessage::PortFormatChanged {
id: port_id,
media_type,
})
@@ -338,8 +397,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>>,
@@ -367,9 +426,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);
@@ -380,7 +439,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),
})
@@ -388,7 +447,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),
})
@@ -402,7 +461,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,
@@ -440,7 +499,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(),
@@ -455,7 +514,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

@@ -41,7 +41,7 @@ node {
background-color: @headerbar_bg_color;
}
node label.heading {
node .node-title {
padding: 4px 7px;
}
@@ -53,3 +53,20 @@ port-handle {
border-radius: 50%;
background-color: @media-type-unknown;
}
button.rounded {
padding: 6px;
border-radius: 9999px;
}
entry.rounded {
border-radius: 9999px;
}
entry.rounded > :first-child {
padding-left: 12px;
}
entry.rounded > :nth-child(2) {
padding-right: 12px;
}

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,
@@ -93,6 +93,9 @@ mod imp {
// Memorized data for an in-progress zoom gesture
pub zoom_gesture_initial_zoom: Cell<Option<f64>>,
pub zoom_gesture_anchor: Cell<Option<(f64, f64)>>,
// This keeps track of an ongoing move view gesture.
pub move_view_state: Cell<(f64, f64)>,
}
impl Default for GraphView {
@@ -108,6 +111,7 @@ mod imp {
port_drag_cursor: Cell::new(Point::new(0.0, 0.0)),
zoom_gesture_initial_zoom: Default::default(),
zoom_gesture_anchor: Default::default(),
move_view_state: Default::default(),
}
}
}
@@ -136,6 +140,7 @@ mod imp {
self.setup_port_drag_and_drop();
self.setup_scroll_zooming();
self.setup_zoom_gesture();
self.setup_move_view();
}
fn dispose(&self) {
@@ -465,38 +470,66 @@ mod imp {
self.obj().add_controller(zoom_gesture);
}
fn draw_link(
fn setup_move_view(&self) {
let drag_controller = gtk::GestureDrag::new();
drag_controller.set_button(gtk::gdk::BUTTON_MIDDLE);
// TODO: set `all-scroll` cursor while dragging view
drag_controller.connect_drag_begin(|drag_controller, _, _| {
let widget = drag_controller
.widget()
.downcast::<super::GraphView>()
.unwrap();
widget.imp().move_view_state.set((0.0, 0.0));
});
drag_controller.connect_drag_update(|drag_controller, x, y| {
let widget = drag_controller
.widget()
.downcast::<super::GraphView>()
.unwrap();
let imp = widget.imp();
let state = imp.move_view_state.replace((x, y));
let delta_x = state.0 - x;
let delta_y = state.1 - y;
let hadjustment_ref = imp.hadjustment.borrow();
let vadjustment_ref = imp.vadjustment.borrow();
let hadjustment = hadjustment_ref.as_ref().unwrap();
let vadjustment = vadjustment_ref.as_ref().unwrap();
let new_hadjustment = hadjustment.value() + delta_x;
let new_vadjustment = vadjustment.value() + delta_y;
hadjustment.set_value(new_hadjustment);
vadjustment.set_value(new_vadjustment);
});
self.obj().add_controller(drag_controller);
}
fn snapshot_link(
&self,
link_cr: &cairo::Context,
snapshot: &gtk::Snapshot,
output_anchor: &Point,
input_anchor: &Point,
active: bool,
color: &gdk::RGBA,
) {
let output_x: f64 = output_anchor.x().into();
let output_y: f64 = output_anchor.y().into();
let input_x: f64 = input_anchor.x().into();
let input_y: f64 = input_anchor.y().into();
// Use dashed line for inactive links, full line otherwise.
if active {
link_cr.set_dash(&[], 0.0);
} else {
link_cr.set_dash(&[10.0, 5.0], 0.0);
}
link_cr.set_source_rgba(
color.red().into(),
color.green().into(),
color.blue().into(),
color.alpha().into(),
);
let output_x = output_anchor.x();
let output_y = output_anchor.y();
let input_x = input_anchor.x();
let input_y = input_anchor.y();
// If the output port is farther right than the input port and they have
// a similar y coordinate, apply a y offset to the control points
// so that the curve sticks out a bit.
let y_control_offset = if output_x > input_x {
f64::max(0.0, 25.0 - (output_y - input_y).abs())
f32::max(0.0, 25.0 - (output_y - input_y).abs())
} else {
0.0
};
@@ -504,9 +537,10 @@ mod imp {
// Place curve control offset by half the x distance between the two points.
// This makes the curve scale well for varying distances between the two ports,
// especially when the output port is farther right than the input port.
let half_x_dist = f64::abs(output_x - input_x) / 2.0;
link_cr.move_to(output_x, output_y);
link_cr.curve_to(
let half_x_dist = f32::abs(output_x - input_x) / 2.0;
let path_builder = gsk::PathBuilder::new();
path_builder.move_to(output_x, output_y);
path_builder.cubic_to(
output_x + half_x_dist,
output_y - y_control_offset,
input_x - half_x_dist,
@@ -514,13 +548,20 @@ mod imp {
input_x,
input_y,
);
let path = path_builder.to_path();
if let Err(e) = link_cr.stroke() {
warn!("Failed to draw graphview links: {}", e);
};
let stroke = gsk::Stroke::new(2.0 * (self.zoom_factor.get() as f32));
// Use dashed line for inactive links, full line otherwise.
if active {
stroke.set_dash(&[]);
} else {
stroke.set_dash(&[10.0, 5.0]);
}
snapshot.append_stroke(&path, &stroke, color);
}
fn draw_dragged_link(&self, port: &Port, link_cr: &cairo::Context, colors: &Colors) {
fn snapshot_dragged_link(&self, snapshot: &gtk::Snapshot, port: &Port, colors: &Colors) {
let Some(port_anchor) = port.compute_point(&*self.obj(), &port.link_anchor()) else {
return;
};
@@ -550,21 +591,10 @@ mod imp {
let color = &colors.color_for_media_type(MediaType::from_raw(port.media_type()));
self.draw_link(link_cr, output_anchor, input_anchor, false, color);
self.snapshot_link(snapshot, output_anchor, input_anchor, false, color);
}
fn snapshot_links(&self, widget: &super::GraphView, snapshot: &gtk::Snapshot) {
let alloc = widget.allocation();
let link_cr = snapshot.append_cairo(&graphene::Rect::new(
0.0,
0.0,
alloc.width() as f32,
alloc.height() as f32,
));
link_cr.set_line_width(2.0 * self.zoom_factor.get());
let colors = Colors {
audio: widget
.style_context()
@@ -593,8 +623,8 @@ mod imp {
continue;
};
self.draw_link(
&link_cr,
self.snapshot_link(
snapshot,
&output_anchor,
&input_anchor,
link.active(),
@@ -603,7 +633,7 @@ mod imp {
}
if let Some(port) = self.dragged_port.upgrade() {
self.draw_dragged_link(&port, &link_cr, &colors);
self.snapshot_dragged_link(&snapshot, &port, &colors);
}
}

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,7 +15,7 @@
// 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;
@@ -34,15 +34,26 @@ mod imp {
#[property(get, set, construct_only)]
pub(super) pipewire_id: Cell<u32>,
#[property(
name = "name", type = String,
get = |this: &Self| this.label.text().to_string(),
name = "node-name", type = String,
get = |this: &Self| this.node_name.text().to_string(),
set = |this: &Self, val| {
this.label.set_text(val);
this.label.set_tooltip_text(Some(val));
this.node_name.set_text(val);
this.node_name.set_tooltip_text(Some(val));
}
)]
#[template_child]
pub(super) label: TemplateChild<gtk::Label>,
pub(super) node_name: TemplateChild<gtk::Label>,
#[property(
name = "media-name", type = String,
get = |this: &Self| this.media_name.text().to_string(),
set = |this: &Self, val| {
this.media_name.set_text(val);
this.media_name.set_tooltip_text(Some(val));
this.media_name.set_visible(!val.is_empty());
}
)]
#[template_child]
pub(super) media_name: TemplateChild<gtk::Label>,
#[template_child]
pub(super) separator: TemplateChild<gtk::Separator>,
#[template_child]
@@ -74,8 +85,11 @@ mod imp {
fn constructed(&self) {
self.parent_constructed();
// Force left-to-right direction for the ports grid to avoid messed up UI when defaulting to right-to-left
self.port_grid.set_direction(gtk::TextDirection::Ltr);
// Display a grab cursor when the mouse is over the label so the user knows the node can be dragged.
self.label
self.node_name
.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref());
}
@@ -141,7 +155,7 @@ glib::wrapper! {
impl Node {
pub fn new(name: &str, pipewire_id: u32) -> Self {
glib::Object::builder()
.property("name", name)
.property("node-name", name)
.property("pipewire-id", pipewire_id)
.build()
}

View File

@@ -9,14 +9,36 @@
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="label">
<object class="GtkBox">
<style>
<class name="heading"></class>
<class name="node-title"></class>
</style>
<property name="wrap">true</property>
<property name="ellipsize">PANGO_ELLIPSIZE_END</property>
<property name="lines">2</property>
<property name="max-width-chars">20</property>
<property name="orientation">vertical</property>
<property name="spacing">1</property>
<child>
<object class="GtkLabel" id="node_name">
<style>
<class name="heading"></class>
</style>
<property name="wrap">true</property>
<property name="ellipsize">PANGO_ELLIPSIZE_END</property>
<property name="lines">2</property>
<property name="max-width-chars">20</property>
</object>
</child>
<child>
<object class="GtkLabel" id="media_name">
<style>
<class name="dim-label"></class>
<class name="caption"></class>
</style>
<property name="visible">false</property>
<property name="wrap">true</property>
<property name="ellipsize">PANGO_ELLIPSIZE_END</property>
<property name="lines">2</property>
<property name="max-width-chars">20</property>
</object>
</child>
</object>
</child>
<child>

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)]
@@ -101,6 +101,9 @@ mod imp {
fn constructed(&self) {
self.parent_constructed();
// Force left-to-right direction for the ports grid to avoid messed up UI when defaulting to right-to-left
self.obj().set_direction(gtk::TextDirection::Ltr);
// Display a grab cursor when the mouse is over the port so the user knows it can be dragged to another port.
self.obj()
.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref());

View File

@@ -34,6 +34,7 @@ mod imp {
menu.append(Some("200%"), Some("win.set-zoom(2.0)"));
menu.append(Some("300%"), Some("win.set-zoom(3.0)"));
let popover = gtk::PopoverMenu::from_model(Some(&menu));
popover.set_position(gtk::PositionType::Top);
ZoomEntry {
graphview: Default::default(),

View File

@@ -1,26 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="HelvumZoomEntry" parent="GtkBox">
<child>
<object class="GtkButton" id="zoom_out_button">
<property name="icon-name">zoom-out-symbolic</property>
<property name="tooltip-text">Zoom out</property>
</object>
</child>
<property name="spacing">12</property>
<child>
<object class="GtkEntry" id="entry">
<property name="secondary-icon-name">go-down-symbolic</property>
<property name="input-purpose">digits</property>
<property name="max-width-chars">5</property>
<style>
<class name="osd"/>
<class name="rounded"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="zoom_out_button">
<property name="icon-name">zoom-out-symbolic</property>
<property name="tooltip-text">Zoom out</property>
<style>
<class name="osd"/>
<class name="rounded"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="zoom_in_button">
<property name="icon-name">zoom-in-symbolic</property>
<property name="tooltip-text">Zoom in</property>
<style>
<class name="osd"/>
<class name="rounded"/>
</style>
</object>
</child>
<style>
<class name="linked"/>
</style>
</template>
</interface>
</interface>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="Adw" version="1.0"/>
<requires lib="Adw" version="1.4"/>
<menu id="primary_menu">
<section>
<item>
@@ -15,44 +15,51 @@
<property name="default-height">720</property>
<property name="title">Helvum - Pipewire Patchbay</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar" id="header_bar">
<child type="end">
<object class="GtkBox">
<property name="spacing">6</property>
<object class="GtkMenuButton">
<property name="icon-name">open-menu-symbolic</property>
<property name="menu-model">primary_menu</property>
</object>
</child>
</object>
</child>
<property name="content">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwBanner" id="connection_banner">
<property name="title" translatable="yes">Disconnected</property>
<property name="revealed">false</property>
</object>
</child>
<child>
<object class="GtkOverlay">
<child>
<object class="GtkScrolledWindow">
<child>
<object class="HelvumGraphView" id="graph">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="HelvumZoomEntry">
<property name="zoomed-widget">graph</property>
</object>
</child>
<child>
<object class="GtkMenuButton">
<property name="icon-name">open-menu-symbolic</property>
<property name="menu-model">primary_menu</property>
<property name="halign">end</property>
<property name="valign">end</property>
<property name="margin-end">24</property>
<property name="margin-bottom">24</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwBanner" id="connection_banner">
<property name="title" translatable="yes">Disconnected</property>
<property name="revealed">false</property>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<child>
<object class="HelvumGraphView" id="graph">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
</object>
</child>
</object>
</child>
</property>
</object>
</child>
</template>