Hi,

I took a stab at this one and could improve Matthias' patch so helvum now builds and runs fine with current unstable/testing crates. Please note I haven't tested it beyond starting helvum and checking the displayed graph made sense on my system.

Attached is the new patch version.

Cheers,
Arnaud
From: Matthias Geiger <[email protected]>
Date: Sun, 7 Sep 2025 19:14:51 +0200
Subject: [PATCH] feat: Port to gtk-rs 0.10

Co-authored-by: Arnaud Ferraris <[email protected]>
---
 Cargo.toml                     |   4 +-
 src/application.rs             |  44 ++++++----
 src/graph_manager.rs           |  19 +++--
 src/pipewire_connection/mod.rs | 188 ++++++++++++++++++++++++++++-------------
 src/ui/graph/graph_view.rs     |  47 +++++++----
 src/ui/graph/node.rs           |   3 +-
 src/ui/graph/port.rs           |   3 +-
 src/ui/graph/port_handle.rs    |   3 +-
 src/ui/graph/zoomentry.rs      |  79 ++++++++++-------
 src/ui/window.rs               |   2 +-
 10 files changed, 261 insertions(+), 131 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 291e82a..b469cc4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,8 +15,8 @@ categories = ["gui", "multimedia"]
 
 [dependencies]
 pipewire = "0.8.0"
-adw = { version = ">= 0.6, <= 0.8", package = "libadwaita", features = ["v1_4"] }
-glib = { version = ">= 0.19, <= 0.21", features = ["log"] }
+adw = { version = "0.8", package = "libadwaita", features = ["v1_4"] }
+glib = { version = "0.21", features = ["log"] }
 async-channel = "2.2"
 
 log = "0.4.11"
diff --git a/src/application.rs b/src/application.rs
index 9daf385..7e1997d 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -64,10 +64,14 @@ mod imp {
 
             let zoom_set_action =
                 gio::SimpleAction::new("set-zoom", Some(&f64::static_variant_type()));
-            zoom_set_action.connect_activate(clone!(@weak graphview => move|_, param| {
-                let zoom_factor = param.unwrap().get::<f64>().unwrap();
-                graphview.set_zoom_factor(zoom_factor, None)
-            }));
+            zoom_set_action.connect_activate(clone!(
+                #[weak]
+                graphview,
+                move|_, param| {
+                    let zoom_factor = param.unwrap().get::<f64>().unwrap();
+                    graphview.set_zoom_factor(zoom_factor, None)
+                }
+            ));
             self.window.add_action(&zoom_set_action);
 
             self.window.show();
@@ -101,9 +105,13 @@ mod imp {
 
             // Add <Control-Q> shortcut for quitting the application.
             let quit = gtk::gio::SimpleAction::new("quit", None);
-            quit.connect_activate(clone!(@weak obj => move |_, _| {
-                obj.quit();
-            }));
+            quit.connect_activate(clone!(
+                #[weak]
+                obj,
+                move |_, _| {
+                    obj.quit();
+                }
+            ));
             obj.set_accels_for_action("app.quit", &["<Control>Q"]);
             obj.add_action(&quit);
 
@@ -148,16 +156,20 @@ mod imp {
             );
 
             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}"),
+            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}"),
+                    }
+                    std::ops::ControlFlow::Continue(())
                 }
-                -1
-            }));
+            ));
         }
     }
 }
diff --git a/src/graph_manager.rs b/src/graph_manager.rs
index b80f0d3..480d801 100644
--- a/src/graph_manager.rs
+++ b/src/graph_manager.rs
@@ -176,15 +176,20 @@ mod imp {
             port.connect_local(
                 "port_toggled",
                 false,
-                glib::clone!(@weak self as app => @default-return None, move |args| {
-                    // Args always look like this: &[widget, id_port_from, id_port_to]
-                    let port_from = args[1].get::<u32>().unwrap();
-                    let port_to = args[2].get::<u32>().unwrap();
+                glib::clone!(
+                    #[weak(rename_to = app)]
+                    self,
+                    #[upgrade_or_default]
+                    move |args| {
+                        // Args always look like this: &[widget, id_port_from, id_port_to]
+                        let port_from = args[1].get::<u32>().unwrap();
+                        let port_to = args[2].get::<u32>().unwrap();
 
-                    app.toggle_link(port_from, port_to);
+                        app.toggle_link(port_from, port_to);
 
-                    None
-                }),
+                        None
+                    }
+                ),
             );
 
             items.insert(id, port.clone().upcast());
diff --git a/src/pipewire_connection/mod.rs b/src/pipewire_connection/mod.rs
index 6141087..7d898a4 100644
--- a/src/pipewire_connection/mod.rs
+++ b/src/pipewire_connection/mod.rs
@@ -92,8 +92,12 @@ pub(super) fn thread_main(
 
     // 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) {
+        clone!(
+            #[strong]
+            mainloop,
+            #[strong]
+            loop_state,
+            move |msg| if loop_state.borrow_mut().handle_message(msg) {
                 mainloop.quit();
             }
         )
@@ -119,19 +123,29 @@ pub(super) fn thread_main(
 
                 let timer = mainloop
                     .loop_()
-                    .add_timer(clone!(@strong mainloop => move |_| {
-                        mainloop.quit();
-                    }));
+                    .add_timer(clone!(
+                        #[strong]
+                        mainloop,
+                        move |_| {
+                            mainloop.quit();
+                        }
+                    ));
 
                 timer.update_timer(interval, None).into_result().unwrap();
 
                 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();
-                        }
-                    )
-                });
+                    clone!(
+                        #[strong]
+                        mainloop,
+                        #[strong]
+                        loop_state,
+                        move |msg|
+                            if loop_state.borrow_mut().handle_message(msg) {
+                                mainloop.quit();
+                            }
+                        )
+                    }
+                );
 
                 mainloop.run();
                 pw_receiver = receiver.deattach();
@@ -154,38 +168,64 @@ pub(super) fn thread_main(
         let state = Rc::new(RefCell::new(State::new()));
 
         let receiver = pw_receiver.attach(mainloop.loop_(), {
-            clone!(@strong mainloop, @weak core, @weak registry, @strong state, @strong loop_state => move |msg| match msg {
-                GtkMessage::ToggleLink { port_from, port_to } => toggle_link(port_from, port_to, &core, &registry, &state),
-                GtkMessage::Terminate | GtkMessage::Connect(_) => {
-                    loop_state.borrow_mut().handle_message(msg);
-                    mainloop.quit();
+            clone!(
+                #[strong]
+                mainloop,
+                #[weak]
+                core,
+                #[weak]
+                registry,
+                #[strong]
+                state,
+                #[strong]
+                loop_state,
+                move |msg| match msg {
+                    GtkMessage::ToggleLink { port_from, port_to } => toggle_link(port_from, port_to, &core, &registry, &state),
+                    GtkMessage::Terminate | GtkMessage::Connect(_) => {
+                        loop_state.borrow_mut().handle_message(msg);
+                        mainloop.quit();
+                    }
                 }
-            })
+            )
         });
 
         let _listener = core
             .add_listener_local()
             .error(
-                clone!(@strong mainloop, @strong gtk_sender => move |id, _seq, res, message| {
-                    if id != PW_ID_CORE {
-                        return;
-                    }
+                clone!(
+                    #[strong]
+                    mainloop,
+                    #[strong]
+                    gtk_sender,
+                    move |id, _seq, res, message| {
+                        if id != PW_ID_CORE {
+                            return;
+                        }
 
-                    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}");
+                        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
             .add_listener_local()
-            .global(clone!(@strong gtk_sender, @weak registry, @strong proxies, @strong state =>
+            .global(clone!(
+                #[strong]
+                gtk_sender,
+                #[weak]
+                registry,
+                #[strong]
+                proxies,
+                #[strong]
+                state,
                 move |global| match global.type_ {
                     ObjectType::Node => handle_node(global, &gtk_sender, &registry, &proxies, &state),
                     ObjectType::Port => handle_port(global, &gtk_sender, &registry, &proxies, &state),
@@ -195,22 +235,30 @@ pub(super) fn thread_main(
                     }
                 }
             ))
-            .global_remove(clone!(@strong gtk_sender, @strong proxies, @strong state => move |id| {
-                if let Some(item) = state.borrow_mut().remove(id) {
-                    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},
-                    }).expect("Failed to send message");
-                } else {
-                    warn!(
-                        "Attempted to remove item with id {} that is not saved in state",
-                        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_blocking(match item {
+                            Item::Node { .. } => PipewireMessage::NodeRemoved {id},
+                            Item::Port { node_id } => PipewireMessage::PortRemoved {id, node_id},
+                            Item::Link { .. } => PipewireMessage::LinkRemoved {id},
+                        }).expect("Failed to send message");
+                    } else {
+                        warn!(
+                            "Attempted to remove item with id {} that is not saved in state",
+                            id
+                        );
+                    }
 
-                proxies.borrow_mut().remove(&id);
-            }))
+                    proxies.borrow_mut().remove(&id);
+                }
+            ))
             .register();
 
         mainloop.run();
@@ -279,9 +327,15 @@ fn handle_node(
     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);
-        }))
+        .info(clone!(
+            #[strong]
+            sender,
+            #[strong]
+            proxies,
+            move |info| {
+                handle_node_info(info, &sender, &proxies);
+            }
+        ))
         .register();
 
     proxies.borrow_mut().insert(
@@ -334,15 +388,27 @@ fn handle_port(
     let listener = proxy
         .add_listener_local()
         .info(
-            clone!(@strong proxies, @strong state, @strong sender => move |info| {
-                handle_port_info(info, &proxies, &state, &sender);
-            }),
+            clone!(
+                #[strong]
+                proxies,
+                #[strong]
+                state,
+                #[strong]
+                sender,
+                move |info| {
+                    handle_port_info(info, &proxies, &state, &sender);
+                }
+            ),
         )
-        .param(clone!(@strong sender => move |_, param_id, _, _, param| {
-            if param_id == ParamType::EnumFormat {
-                handle_port_enum_format(port_id, param, &sender)
+        .param(clone!(
+            #[strong]
+            sender,
+            move |_, param_id, _, _, param| {
+                if param_id == ParamType::EnumFormat {
+                    handle_port_enum_format(port_id, param, &sender)
+                }
             }
-        }))
+        ))
         .register();
 
     proxies.borrow_mut().insert(
@@ -443,9 +509,15 @@ fn handle_link(
     let proxy: Link = registry.bind(link).expect("Failed to bind to link proxy");
     let listener = proxy
         .add_listener_local()
-        .info(clone!(@strong state, @strong sender => move |info| {
-            handle_link_info(info, &state, &sender);
-        }))
+        .info(clone!(
+            #[strong]
+            state,
+            #[strong]
+            sender,
+            move |info| {
+                handle_link_info(info, &state, &sender);
+            }
+        ))
         .register();
 
     proxies.borrow_mut().insert(
diff --git a/src/ui/graph/graph_view.rs b/src/ui/graph/graph_view.rs
index 55334cc..6d8de65 100644
--- a/src/ui/graph/graph_view.rs
+++ b/src/ui/graph/graph_view.rs
@@ -386,14 +386,18 @@ mod imp {
                 Port::static_type(),
                 glib::Priority::DEFAULT,
                 Option::<&gio::Cancellable>::None,
-                clone!(@weak self as imp => move|value| {
-                    let Ok(value) = value else {
-                        return;
-                    };
-                    let port: &Port = value.get().expect("Value should contain a port");
+                clone!(
+                    #[weak(rename_to = imp)]
+                    self,
+                    move|value| {
+                        let Ok(value) = value else {
+                            return;
+                        };
+                        let port: &Port = value.get().expect("Value should contain a port");
 
-                    imp.dragged_port.set(Some(port));
-                }),
+                        imp.dragged_port.set(Some(port));
+                    }
+                ),
             );
 
             self.obj().queue_draw();
@@ -689,7 +693,11 @@ mod imp {
 
             if let Some(adjustment) = adjustment {
                 adjustment
-                    .connect_value_changed(clone!(@weak obj => move |_|  obj.queue_allocate() ));
+                    .connect_value_changed(clone!(
+                        #[weak]
+                        obj,
+                        move |_| obj.queue_allocate()
+                    ));
             }
         }
 
@@ -720,7 +728,8 @@ mod imp {
 
 glib::wrapper! {
     pub struct GraphView(ObjectSubclass<imp::GraphView>)
-        @extends gtk::Widget;
+        @extends gtk::Widget,
+        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Scrollable;
 }
 
 impl GraphView {
@@ -838,15 +847,23 @@ impl GraphView {
     pub fn add_link(&self, link: Link) {
         link.connect_notify_local(
             Some("active"),
-            glib::clone!(@weak self as graph => move |_, _| {
-                graph.queue_draw();
-            }),
+            glib::clone!(
+                #[weak(rename_to = graph)]
+                self,
+                move |_, _| {
+                    graph.queue_draw();
+                }
+            ),
         );
         link.connect_notify_local(
             Some("media-type"),
-            glib::clone!(@weak self as graph => move |_, _| {
-                graph.queue_draw();
-            }),
+            glib::clone!(
+                #[weak(rename_to = graph)]
+                self,
+                move |_, _| {
+                    graph.queue_draw();
+                }
+            ),
         );
         self.imp().links.borrow_mut().insert(link);
         self.queue_draw();
diff --git a/src/ui/graph/node.rs b/src/ui/graph/node.rs
index 79eaa8d..30a3768 100644
--- a/src/ui/graph/node.rs
+++ b/src/ui/graph/node.rs
@@ -149,7 +149,8 @@ mod imp {
 
 glib::wrapper! {
     pub struct Node(ObjectSubclass<imp::Node>)
-        @extends gtk::Widget;
+        @extends gtk::Widget,
+        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
 }
 
 impl Node {
diff --git a/src/ui/graph/port.rs b/src/ui/graph/port.rs
index b84aae3..ce48a1e 100644
--- a/src/ui/graph/port.rs
+++ b/src/ui/graph/port.rs
@@ -328,7 +328,8 @@ mod imp {
 
 glib::wrapper! {
     pub struct Port(ObjectSubclass<imp::Port>)
-        @extends gtk::Widget;
+        @extends gtk::Widget,
+        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
 }
 
 impl Port {
diff --git a/src/ui/graph/port_handle.rs b/src/ui/graph/port_handle.rs
index a9ca8cc..2024d4c 100644
--- a/src/ui/graph/port_handle.rs
+++ b/src/ui/graph/port_handle.rs
@@ -61,7 +61,8 @@ mod imp {
 
 glib::wrapper! {
     pub struct PortHandle(ObjectSubclass<imp::PortHandle>)
-        @extends gtk::Widget;
+        @extends gtk::Widget,
+        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
 }
 
 impl PortHandle {
diff --git a/src/ui/graph/zoomentry.rs b/src/ui/graph/zoomentry.rs
index 667236b..4109676 100644
--- a/src/ui/graph/zoomentry.rs
+++ b/src/ui/graph/zoomentry.rs
@@ -66,36 +66,52 @@ mod imp {
             self.parent_constructed();
 
             self.zoom_out_button
-                .connect_clicked(clone!(@weak self as imp => move |_| {
-                    let graphview = imp.graphview.borrow();
-                    if let Some(ref graphview) = *graphview {
-                        graphview.set_zoom_factor(graphview.zoom_factor() - 0.1, None);
-                    }
-                }));
-
-            self.zoom_in_button
-                .connect_clicked(clone!(@weak self as imp => move |_| {
-                    let graphview = imp.graphview.borrow();
-                    if let Some(ref graphview) = *graphview {
-                        graphview.set_zoom_factor(graphview.zoom_factor() + 0.1, None);
-                    }
-                }));
-
-            self.entry
-                .connect_activate(clone!(@weak self as imp => move |entry| {
-                    if let Ok(zoom_factor) = entry.text().trim_matches('%').parse::<f64>() {
+                .connect_clicked(clone!(
+                    #[weak(rename_to = imp)]
+                    self,
+                    move |_| {
                         let graphview = imp.graphview.borrow();
                         if let Some(ref graphview) = *graphview {
-                            graphview.set_zoom_factor(zoom_factor / 100.0, None);
+                            graphview.set_zoom_factor(graphview.zoom_factor() - 0.1, None);
                         }
                     }
-                }));
-            self.entry
-                .connect_icon_press(clone!(@weak self as imp => move |_, pos| {
-                    if pos == gtk::EntryIconPosition::Secondary {
-                        imp.popover.show();
+                ));
+
+            self.zoom_in_button
+                .connect_clicked(clone!(
+                    #[weak(rename_to = imp)]
+                    self,
+                    move |_| {
+                        let graphview = imp.graphview.borrow();
+                        if let Some(ref graphview) = *graphview {
+                            graphview.set_zoom_factor(graphview.zoom_factor() + 0.1, None);
+                        }
                     }
-                }));
+                ));
+
+            self.entry
+                .connect_activate(clone!(
+                    #[weak(rename_to = imp)]
+                    self,
+                    move |entry| {
+                        if let Ok(zoom_factor) = entry.text().trim_matches('%').parse::<f64>() {
+                            let graphview = imp.graphview.borrow();
+                            if let Some(ref graphview) = *graphview {
+                                graphview.set_zoom_factor(zoom_factor / 100.0, None);
+                            }
+                        }
+                    }
+                ));
+            self.entry
+                .connect_icon_press(clone!(
+                    #[weak(rename_to = imp)]
+                    self,
+                    move |_, pos| {
+                        if pos == gtk::EntryIconPosition::Secondary {
+                            imp.popover.show();
+                        }
+                    }
+                ));
 
             self.popover.set_parent(&self.entry.get());
         }
@@ -132,9 +148,13 @@ mod imp {
                     if let Some(ref widget) = widget {
                         widget.connect_notify_local(
                             Some("zoom-factor"),
-                            clone!(@weak self as imp => move |graphview, _| {
-                                imp.update_zoom_factor_text(graphview.zoom_factor());
-                            }),
+                            clone!(
+                                #[weak(rename_to = imp)]
+                                self,
+                                move |graphview, _| {
+                                    imp.update_zoom_factor_text(graphview.zoom_factor());
+                                }
+                            ),
                         );
                         self.update_zoom_factor_text(widget.zoom_factor());
                     }
@@ -161,7 +181,8 @@ mod imp {
 
 glib::wrapper! {
     pub struct ZoomEntry(ObjectSubclass<imp::ZoomEntry>)
-        @extends gtk::Box, gtk::Widget;
+        @extends gtk::Box, gtk::Widget,
+        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
 }
 
 impl ZoomEntry {
diff --git a/src/ui/window.rs b/src/ui/window.rs
index 7c10354..a35f102 100644
--- a/src/ui/window.rs
+++ b/src/ui/window.rs
@@ -52,7 +52,7 @@ mod imp {
 glib::wrapper! {
     pub struct Window(ObjectSubclass<imp::Window>)
         @extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,
-        @implements gio::ActionGroup, gio::ActionMap;
+        @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
 }
 
 impl Window {

Attachment: OpenPGP_0xD3EBB5966BB99196.asc
Description: OpenPGP public key

Attachment: OpenPGP_signature.asc
Description: OpenPGP digital signature

Reply via email to