From 9d290793e7a1ea102ede16331c9e2e655925db75 Mon Sep 17 00:00:00 2001 From: Naveen Prashanth Date: Wed, 6 May 2026 20:08:13 +0530 Subject: [PATCH] Use typesafe GEnums --- src/graph_manager.rs | 2 +- src/main.rs | 4 +- src/ui/graph/graph_view.rs | 19 ++-- src/ui/graph/link.rs | 21 ++-- src/ui/graph/node.rs | 9 +- src/ui/graph/port.rs | 206 ++++++++++++++++++++++++++++--------- 6 files changed, 181 insertions(+), 80 deletions(-) diff --git a/src/graph_manager.rs b/src/graph_manager.rs index 83cfdef..ad08573 100644 --- a/src/graph_manager.rs +++ b/src/graph_manager.rs @@ -239,7 +239,7 @@ mod imp { return; }; - port.set_media_type(media_type.as_raw()) + port.set_media_type(media_type) } /// Remove the port with the id `id` from the node with the id `node_id` diff --git a/src/main.rs b/src/main.rs index 1ed51bd..3d02d08 100644 --- a/src/main.rs +++ b/src/main.rs @@ -114,8 +114,10 @@ pub enum PipewireMessage { Disconnected, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, glib::Enum)] +#[enum_type(name = "HelvumNodeType")] pub enum NodeType { + #[default] Input, Output, } diff --git a/src/ui/graph/graph_view.rs b/src/ui/graph/graph_view.rs index 7488416..44d0e88 100644 --- a/src/ui/graph/graph_view.rs +++ b/src/ui/graph/graph_view.rs @@ -39,9 +39,9 @@ mod imp { use std::cell::{Cell, RefCell}; use std::collections::{HashMap, HashSet}; - use adw::gtk::gdk::{self}; + use adw::gtk::gdk; use libspa::param::format::MediaType; - use libspa::utils::Direction; + use crate::ui::graph::PortDirection; use log::warn; use std::sync::LazyLock; @@ -617,13 +617,12 @@ mod imp { }); let other_anchor = picked_port_anchor.unwrap_or(drag_cursor); - let (output_anchor, input_anchor) = match Direction::from_raw(port.direction()) { - Direction::Output => (&port_anchor, &other_anchor), - Direction::Input => (&other_anchor, &port_anchor), - _ => unreachable!(), + let (output_anchor, input_anchor) = match port.port_direction() { + PortDirection::Output => (&port_anchor, &other_anchor), + PortDirection::Input => (&other_anchor, &port_anchor), }; - let mut media_type = MediaType::from_raw(port.media_type()); + let mut media_type = MediaType::from(port.media_type()); if media_type == MediaType::Unknown { media_type = MediaType::Unknown; } @@ -642,14 +641,14 @@ mod imp { }; for link in self.links.borrow().iter() { - let mut media_type = link.media_type(); + let mut media_type = MediaType::from(link.media_type()); // If link media type is unknown, try to fall back to port media types. if media_type == MediaType::Unknown { if let Some(output_port) = link.output_port() { - media_type = MediaType::from_raw(output_port.media_type()); + media_type = MediaType::from(output_port.media_type()); } else if let Some(input_port) = link.input_port() { - media_type = MediaType::from_raw(input_port.media_type()); + media_type = MediaType::from(input_port.media_type()); } } diff --git a/src/ui/graph/link.rs b/src/ui/graph/link.rs index 491a5d3..9b542cd 100644 --- a/src/ui/graph/link.rs +++ b/src/ui/graph/link.rs @@ -17,7 +17,7 @@ use adw::{glib, prelude::*, subclass::prelude::*}; use libspa::param::format::MediaType; -use super::Port; +use super::{Port, PortMediaType}; mod imp { use super::*; @@ -30,7 +30,7 @@ mod imp { pub output_port: glib::WeakRef, pub input_port: glib::WeakRef, pub active: Cell, - pub media_type: Cell, + pub media_type: Cell, } impl Default for Link { @@ -39,7 +39,7 @@ mod imp { output_port: glib::WeakRef::default(), input_port: glib::WeakRef::default(), active: Cell::default(), - media_type: Cell::new(MediaType::Unknown), + media_type: Cell::new(PortMediaType::Unknown), } } } @@ -65,8 +65,7 @@ mod imp { .default_value(false) .flags(glib::ParamFlags::READWRITE) .build(), - glib::ParamSpecUInt::builder("media-type") - .default_value(MediaType::Unknown.as_raw()) + glib::ParamSpecEnum::builder::("media-type") .flags(glib::ParamFlags::READWRITE) .build(), ] @@ -80,7 +79,7 @@ mod imp { "output-port" => self.output_port.upgrade().to_value(), "input-port" => self.input_port.upgrade().to_value(), "active" => self.active.get().to_value(), - "media-type" => self.media_type.get().as_raw().to_value(), + "media-type" => self.media_type.get().to_value(), _ => unimplemented!(), } } @@ -90,9 +89,7 @@ mod imp { "output-port" => self.output_port.set(value.get().unwrap()), "input-port" => self.input_port.set(value.get().unwrap()), "active" => self.active.set(value.get().unwrap()), - "media-type" => self - .media_type - .set(MediaType::from_raw(value.get().unwrap())), + "media-type" => self.media_type.set(value.get().expect("Value should be a PortMediaType")), _ => unimplemented!(), } } @@ -132,12 +129,12 @@ impl Link { self.set_property("active", active); } - pub fn media_type(&self) -> MediaType { - MediaType::from_raw(self.property("media-type")) + pub fn media_type(&self) -> PortMediaType { + self.property("media-type") } pub fn set_media_type(&self, media_type: MediaType) { - self.set_property("media-type", media_type.as_raw()) + self.set_property("media-type", PortMediaType::from(media_type)) } } diff --git a/src/ui/graph/node.rs b/src/ui/graph/node.rs index da3290b..e27b342 100644 --- a/src/ui/graph/node.rs +++ b/src/ui/graph/node.rs @@ -15,7 +15,7 @@ // SPDX-License-Identifier: GPL-3.0-only use adw::{glib, gtk, prelude::*, subclass::prelude::*}; -use libspa::utils::Direction; +use crate::ui::graph::PortDirection; use crate::NodeId; use super::Port; @@ -120,14 +120,13 @@ mod imp { ports .iter() - .for_each(|port| match Direction::from_raw(port.direction()) { - Direction::Output => { + .for_each(|port| match port.port_direction() { + PortDirection::Output => { ports_out.push(port); } - Direction::Input => { + PortDirection::Input => { ports_in.push(port); } - _ => unreachable!(), }); ports_out.sort_unstable_by_key(|port| port.name()); diff --git a/src/ui/graph/port.rs b/src/ui/graph/port.rs index e79cf18..64afe0a 100644 --- a/src/ui/graph/port.rs +++ b/src/ui/graph/port.rs @@ -21,8 +21,9 @@ use adw::{ prelude::*, subclass::prelude::*, }; +pub use imp::{PortDirection, PortMediaType}; use crate::PortId; -use libspa::utils::Direction; +use libspa::{param::format::MediaType, utils::Direction}; use super::PortHandle; @@ -31,37 +32,78 @@ mod imp { use std::cell::Cell; - use libspa::{param::format::MediaType, utils::Direction}; + use libspa::param::format::MediaType; use std::sync::LazyLock; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, glib::Enum)] + #[enum_type(name = "HelvumPortDirection")] + pub enum PortDirection { + Input, + #[default] + Output, + } + + impl From for PortDirection { + fn from(direction: Direction) -> Self { + match direction { + Direction::Input => PortDirection::Input, + Direction::Output => PortDirection::Output, + _ => PortDirection::Input, + } + } + } + + impl From for Direction { + fn from(direction: PortDirection) -> Self { + match direction { + PortDirection::Input => Direction::Input, + PortDirection::Output => Direction::Output, + } + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, glib::Enum)] + #[enum_type(name = "HelvumPortMediaType")] + pub enum PortMediaType { + #[default] + Unknown, + Audio, + Video, + Application, + Stream, + } + + impl From for PortMediaType { + fn from(m: MediaType) -> Self { + match m { + MediaType::Audio => PortMediaType::Audio, + MediaType::Video => PortMediaType::Video, + MediaType::Application => PortMediaType::Application, + MediaType::Stream => PortMediaType::Stream, + _ => PortMediaType::Unknown, + } + } + } + + impl From for MediaType { + fn from(m: PortMediaType) -> Self { + match m { + PortMediaType::Audio => MediaType::Audio, + PortMediaType::Video => MediaType::Video, + PortMediaType::Application => MediaType::Application, + PortMediaType::Stream => MediaType::Stream, + _ => MediaType::Unknown, + } + } + } + /// Graphical representation of a pipewire port. - #[derive(gtk::CompositeTemplate, glib::Properties)] - #[properties(wrapper_type = super::Port)] + #[derive(gtk::CompositeTemplate)] #[template(file = "port.ui")] pub struct Port { - #[property(get, set, construct_only)] pub(super) pipewire_id: Cell, - #[property( - type = u32, - get = |_| self.media_type.get().as_raw(), - set = Self::set_media_type - )] - pub(super) media_type: Cell, - #[property( - type = u32, - get = |_| self.direction.get().as_raw(), - set = Self::set_direction, - construct_only - )] - pub(super) direction: Cell, - #[property( - name = "name", type = String, - get = |this: &Self| this.label.text().to_string(), - set = |this: &Self, val| { - this.label.set_text(val); - this.label.set_tooltip_text(Some(val)); - } - )] + pub(super) media_type: Cell, + pub(super) direction: Cell, #[template_child] pub(super) label: TemplateChild, #[template_child] @@ -72,8 +114,8 @@ mod imp { fn default() -> Self { Self { pipewire_id: Cell::new(0), - media_type: Cell::new(MediaType::Unknown), - direction: Cell::new(Direction::Output), + media_type: Cell::new(PortMediaType::Unknown), + direction: Cell::new(PortDirection::Output), label: TemplateChild::default(), handle: TemplateChild::default(), } @@ -97,13 +139,15 @@ mod imp { } } - #[glib::derived_properties] impl ObjectImpl for Port { 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); + gtk::prelude::WidgetExt::set_direction(&*self.obj(), gtk::TextDirection::Ltr); + + // Initial UI update + self.update_ui_for_direction(); // Display a grab cursor when the mouse is over the port so the user knows it can be dragged to another port. self.obj() @@ -122,6 +166,57 @@ mod imp { SIGNALS.as_ref() } + + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: LazyLock> = LazyLock::new(|| { + vec![ + glib::ParamSpecEnum::builder::("port-direction") + .construct_only() + .build(), + glib::ParamSpecUInt::builder("pipewire-id") + .construct_only() + .build(), + glib::ParamSpecEnum::builder::("media-type") + .build(), + glib::ParamSpecString::builder("name") + .build(), + ] + }); + PROPERTIES.as_ref() + } + + fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + match pspec.name() { + "port-direction" => { + let val = value.get().expect("Value should be a PortDirection"); + self.direction.set(val); + self.update_ui_for_direction(); + } + "pipewire-id" => { + self.pipewire_id.set(value.get().expect("Value should be a u32")); + } + "media-type" => { + let val = value.get().expect("Value should be a PortMediaType"); + self.set_media_type(val); + } + "name" => { + let val: String = value.get().expect("Value should be a String"); + self.label.set_text(&val); + self.label.set_tooltip_text(Some(&val)); + } + _ => unimplemented!(), + } + } + + fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "port-direction" => self.direction.get().to_value(), + "pipewire-id" => self.pipewire_id.get().to_value(), + "media-type" => self.media_type.get().to_value(), + "name" => self.label.text().to_string().to_value(), + _ => unimplemented!(), + } + } } impl WidgetImpl for Port { @@ -164,8 +259,8 @@ mod imp { let (_, nat_handle_width, _, _) = self.handle.measure(gtk::Orientation::Horizontal, width); - match Direction::from_raw(self.obj().direction()) { - Direction::Input => { + match self.obj().port_direction() { + PortDirection::Input => { let alloc = gtk::Allocation::new( -nat_handle_width / 2, (height - nat_handle_height) / 2, @@ -182,7 +277,7 @@ mod imp { ); self.label.size_allocate(&alloc, -1); } - Direction::Output => { + PortDirection::Output => { let alloc = gtk::Allocation::new( width - (nat_handle_width / 2), (height - nat_handle_height) / 2, @@ -194,7 +289,6 @@ mod imp { let alloc = gtk::Allocation::new(0, 0, width - (nat_handle_width / 2), height); self.label.size_allocate(&alloc, -1); } - _ => unreachable!(), } } } @@ -277,10 +371,9 @@ mod imp { return false; } - let (output_port, input_port) = match Direction::from_raw(port.direction()) { - Direction::Output => (&port, &other_port), - Direction::Input => (&other_port, &port), - _ => unreachable!(), + let (output_port, input_port) = match port.port_direction() { + PortDirection::Output => (&port, &other_port), + PortDirection::Input => (&other_port, &port), }; port.emit_by_name::<()>( @@ -293,9 +386,7 @@ mod imp { obj.add_controller(drop_target); } - fn set_media_type(&self, media_type: u32) { - let media_type = MediaType::from_raw(media_type); - + fn set_media_type(&self, media_type: PortMediaType) { self.media_type.set(media_type); for css_class in ["video", "audio", "midi"] { @@ -303,7 +394,7 @@ mod imp { } // Color the port according to its media type. - match media_type { + match MediaType::from(media_type) { MediaType::Video => self.handle.add_css_class("video"), MediaType::Audio => self.handle.add_css_class("audio"), MediaType::Application | MediaType::Stream => self.handle.add_css_class("midi"), @@ -311,21 +402,18 @@ mod imp { } } - fn set_direction(&self, direction: u32) { - let direction = Direction::from_raw(direction); - - self.direction.set(direction); + fn update_ui_for_direction(&self) { + let direction = self.direction.get(); match direction { - Direction::Input => { + PortDirection::Input => { self.obj().set_halign(gtk::Align::Start); self.label.set_halign(gtk::Align::Start); } - Direction::Output => { + PortDirection::Output => { self.obj().set_halign(gtk::Align::End); self.label.set_halign(gtk::Align::End); } - _ => unreachable!(), } } } @@ -340,11 +428,27 @@ impl Port { pub fn new(id: PortId, name: &str, direction: Direction) -> Self { glib::Object::builder() .property("pipewire-id", id.0) - .property("direction", direction.as_raw()) + .property("port-direction", PortDirection::from(direction)) .property("name", name) .build() } + pub fn port_direction(&self) -> PortDirection { + self.property("port-direction") + } + + pub fn name(&self) -> String { + self.property("name") + } + + pub fn set_media_type(&self, media_type: MediaType) { + self.set_property("media-type", PortMediaType::from(media_type)); + } + + pub fn media_type(&self) -> PortMediaType { + self.property("media-type") + } + pub fn pw_id(&self) -> PortId { PortId(self.property("pipewire-id")) } @@ -360,6 +464,6 @@ impl Port { } pub fn is_linkable_to(&self, other_port: &Self) -> bool { - self.direction() != other_port.direction() + self.port_direction() != other_port.port_direction() } }