diff --git a/src/graph_manager.rs b/src/graph_manager.rs index 2fee185..da64b49 100644 --- a/src/graph_manager.rs +++ b/src/graph_manager.rs @@ -55,10 +55,14 @@ mod imp { @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, media_type } => imp.add_port(id, name.as_str(), node_id, direction, media_type), - PipewireMessage::LinkAdded{ id, port_from, port_to, active} => imp.add_link(id, port_from, port_to, active), + 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) @@ -96,14 +100,7 @@ mod imp { } /// Add a new port to the view. - fn add_port( - &self, - id: u32, - name: &str, - node_id: u32, - direction: pipewire::spa::Direction, - media_type: Option, - ) { + fn add_port(&self, id: u32, name: &str, node_id: u32, direction: pipewire::spa::Direction) { log::info!("Adding port to graph: id {}", id); let mut items = self.items.borrow_mut(); @@ -117,7 +114,7 @@ mod imp { return; }; - let port = graph::Port::new(id, name, direction, media_type); + let port = graph::Port::new(id, name, direction); // Create or delete a link if the widget emits the "port-toggled" signal. port.connect_local( @@ -139,6 +136,21 @@ mod imp { node.add_port(port); } + fn port_media_type_changed(&self, id: u32, media_type: MediaType) { + let items = self.items.borrow(); + + let Some(port) = items.get(&id) else { + log::warn!("Port (id: {id}) for changed media type not found in graph manager"); + return; + }; + let Some(port) = port.dynamic_cast_ref::() else { + log::warn!("Graph Manager item under port id {id} is not a port"); + return; + }; + + port.set_media_type(media_type.as_raw()) + } + /// Remove the port with the id `id` from the node with the id `node_id` /// from the view. fn remove_port(&self, id: u32, node_id: u32) { @@ -167,7 +179,14 @@ mod imp { } /// Add a new link to the view. - fn add_link(&self, id: u32, output_port_id: u32, input_port_id: u32, active: bool) { + fn add_link( + &self, + id: u32, + output_port_id: u32, + input_port_id: u32, + active: bool, + media_type: MediaType, + ) { log::info!("Adding link to graph: id {}", id); let mut items = self.items.borrow_mut(); @@ -193,6 +212,7 @@ mod imp { link.set_output_port(Some(&output_port)); link.set_input_port(Some(&input_port)); link.set_active(active); + link.set_media_type(media_type); items.insert(id, link.clone().upcast()); @@ -223,6 +243,20 @@ mod imp { link.set_active(active); } + fn link_format_changed(&self, id: u32, media_type: pipewire::spa::format::MediaType) { + let items = self.items.borrow(); + + let Some(link) = items.get(&id) else { + log::warn!("Link (id: {id}) for changed media type not found in graph manager"); + return; + }; + let Some(link) = link.dynamic_cast_ref::() else { + log::warn!("Graph Manager item under link id {id} is not a link"); + return; + }; + link.set_media_type(media_type); + } + // Toggle a link between the two specified ports on the remote pipewire server. fn toggle_link(&self, port_from: u32, port_to: u32) { let sender = self.pw_sender.get().expect("pw_sender shoud be set"); diff --git a/src/main.rs b/src/main.rs index d917440..fefd353 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,7 @@ mod pipewire_connection; mod ui; use gtk::prelude::*; -use pipewire::spa::Direction; +use pipewire::spa::{format::MediaType, Direction}; /// Messages sent by the GTK thread to notify the pipewire thread. #[derive(Debug, Clone)] @@ -44,18 +44,26 @@ pub enum PipewireMessage { node_id: u32, name: String, direction: Direction, - media_type: Option, + }, + PortFormatChanged { + id: u32, + media_type: MediaType, }, LinkAdded { id: u32, port_from: u32, port_to: u32, active: bool, + media_type: MediaType, }, LinkStateChanged { id: u32, active: bool, }, + LinkFormatChanged { + id: u32, + media_type: MediaType, + }, NodeRemoved { id: u32, }, @@ -74,13 +82,6 @@ pub enum NodeType { Output, } -#[derive(Debug, Copy, Clone)] -pub enum MediaType { - Audio, - Video, - Midi, -} - #[derive(Debug, Clone)] pub struct PipewireLink { pub node_from: u32, diff --git a/src/pipewire_connection/mod.rs b/src/pipewire_connection/mod.rs index df5ca80..2138582 100644 --- a/src/pipewire_connection/mod.rs +++ b/src/pipewire_connection/mod.rs @@ -21,11 +21,15 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc}; use gtk::glib::{self, clone}; use log::{debug, info, warn}; use pipewire::{ - link::{Link, LinkChangeMask, LinkListener, LinkState}, + link::{Link, LinkChangeMask, LinkInfo, LinkListener, LinkState}, + port::{Port, PortChangeMask, PortInfo, PortListener}, prelude::*, properties, registry::{GlobalObject, Registry}, - spa::{Direction, ForeignDict}, + spa::{ + param::{ParamInfoFlags, ParamType}, + ForeignDict, + }, types::ObjectType, Context, Core, MainLoop, }; @@ -34,6 +38,10 @@ use crate::{GtkMessage, MediaType, NodeType, PipewireMessage}; use state::{Item, State}; enum ProxyItem { + Port { + proxy: Port, + _listener: PortListener, + }, Link { _proxy: Link, _listener: LinkListener, @@ -67,7 +75,7 @@ pub(super) fn thread_main( .global(clone!(@strong gtk_sender, @weak registry, @strong proxies, @strong state => move |global| match global.type_ { ObjectType::Node => handle_node(global, >k_sender, &state), - ObjectType::Port => handle_port(global, >k_sender, &state), + ObjectType::Port => handle_port(global, >k_sender, ®istry, &proxies, &state), ObjectType::Link => handle_link(global, >k_sender, ®istry, &proxies, &state), _ => { // Other objects are not interesting to us @@ -115,19 +123,6 @@ fn handle_node( .unwrap_or_default(), ); - // FIXME: Instead of checking these props, the "EnumFormat" parameter should be checked instead. - let media_type = props.get("media.class").and_then(|class| { - if class.contains("Audio") { - Some(MediaType::Audio) - } else if class.contains("Video") { - Some(MediaType::Video) - } else if class.contains("Midi") { - Some(MediaType::Midi) - } else { - None - } - }); - let media_class = |class: &str| { if class.contains("Sink") || class.contains("Input") { Some(NodeType::Input) @@ -149,13 +144,7 @@ fn handle_node( }) .or_else(|| props.get("media.class").and_then(media_class)); - state.borrow_mut().insert( - node.id, - Item::Node { - // widget: node_widget, - media_type, - }, - ); + state.borrow_mut().insert(node.id, Item::Node); sender .send(PipewireMessage::NodeAdded { @@ -170,44 +159,106 @@ fn handle_node( fn handle_port( port: &GlobalObject, sender: &glib::Sender, + registry: &Rc, + proxies: &Rc>>, state: &Rc>, ) { - let props = port - .props - .as_ref() - .expect("Port object is missing properties"); - let name = props.get("port.name").unwrap_or_default().to_string(); - let node_id: u32 = props - .get("node.id") - .expect("Port has no node.id property!") - .parse() - .expect("Could not parse node.id property"); - let direction = if matches!(props.get("port.direction"), Some("in")) { - Direction::Input - } else { - Direction::Output + let port_id = port.id; + let proxy: Port = registry.bind(port).expect("Failed to bind to port proxy"); + let listener = proxy + .add_listener_local() + .info( + 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) + } + })) + .register(); + + proxies.borrow_mut().insert( + port.id, + ProxyItem::Port { + proxy, + _listener: listener, + }, + ); +} + +fn handle_port_info( + info: &PortInfo, + proxies: &Rc>>, + state: &Rc>, + sender: &glib::Sender, +) { + debug!("Received port info: {:?}", info); + + let id = info.id(); + let proxies = proxies.borrow(); + let Some(ProxyItem::Port { proxy, .. }) = proxies.get(&id) else { + log::error!("Received info on unknown port with id {id}"); + return; }; - // Find out the nodes media type so that the port can be colored. - let media_type = if let Some(Item::Node { media_type, .. }) = state.borrow().get(node_id) { - media_type.to_owned() - } else { - warn!("Node not found for Port {}", port.id); - None - }; + let mut state = state.borrow_mut(); - // Save node_id so we can delete this port easily. - state.borrow_mut().insert(port.id, Item::Port { node_id }); + if let Some(Item::Port { .. }) = state.get(id) { + // Info was an update, figure out if we should notify the GTK thread + if info.change_mask().contains(PortChangeMask::PARAMS) { + // TODO: React to param changes + } + } else { + // First time we get info. We can now notify the gtk thread of a new link. + let props = info.props().expect("Port object is missing properties"); + let name = props.get("port.name").unwrap_or_default().to_string(); + let node_id: u32 = props + .get("node.id") + .expect("Port has no node.id property!") + .parse() + .expect("Could not parse node.id property"); + + state.insert(id, Item::Port { node_id }); + + let params = info.params(); + let enum_format_info = params + .iter() + .find(|param| param.id() == ParamType::EnumFormat); + if let Some(enum_format_info) = enum_format_info { + if enum_format_info.flags().contains(ParamInfoFlags::READ) { + proxy.enum_params(0, Some(ParamType::EnumFormat), 0, u32::MAX); + } + } + + sender + .send(PipewireMessage::PortAdded { + id, + node_id, + name, + direction: info.direction(), + }) + .expect("Failed to send message"); + } +} + +fn handle_port_enum_format( + port_id: u32, + param: Option<&pipewire::spa::pod::Pod>, + sender: &glib::Sender, +) { + let media_type = param + .and_then(|param| pipewire::spa::param::format_utils::parse_format(param).ok()) + .map(|(media_type, _media_subtype)| media_type) + .unwrap_or(MediaType::Unknown); sender - .send(PipewireMessage::PortAdded { - id: port.id, - node_id, - name, - direction, + .send(PipewireMessage::PortFormatChanged { + id: port_id, media_type, }) - .expect("Failed to send message"); + .expect("Failed to send message") } /// Handle a new link being added @@ -227,38 +278,7 @@ fn handle_link( let listener = proxy .add_listener_local() .info(clone!(@strong state, @strong sender => move |info| { - debug!("Received link info: {:?}", info); - - let id = info.id(); - - let mut state = state.borrow_mut(); - if let Some(Item::Link { .. }) = state.get(id) { - // Info was an update - figure out if we should notify the gtk thread - if info.change_mask().contains(LinkChangeMask::STATE) { - sender.send(PipewireMessage::LinkStateChanged { - id, - active: matches!(info.state(), LinkState::Active) - }).expect("Failed to send message"); - } - // TODO -- check other values that might have changed - } else { - // First time we get info. We can now notify the gtk thread of a new link. - let port_from = info.output_port_id(); - let port_to = info.input_port_id(); - - state.insert(id, Item::Link { - port_from, port_to - }); - - sender.send(PipewireMessage::LinkAdded { - id, - port_from, - port_to, - active: matches!(info.state(), LinkState::Active) - }).expect( - "Failed to send message" - ); - } + handle_link_info(info, &state, &sender); })) .register(); @@ -271,6 +291,53 @@ fn handle_link( ); } +fn handle_link_info( + info: &LinkInfo, + state: &Rc>, + sender: &glib::Sender, +) { + debug!("Received link info: {:?}", info); + + let id = info.id(); + + let mut state = state.borrow_mut(); + if let Some(Item::Link { .. }) = state.get(id) { + // Info was an update - figure out if we should notify the gtk thread + if info.change_mask().contains(LinkChangeMask::STATE) { + sender + .send(PipewireMessage::LinkStateChanged { + id, + active: matches!(info.state(), LinkState::Active), + }) + .expect("Failed to send message"); + } + if info.change_mask().contains(LinkChangeMask::FORMAT) { + sender + .send(PipewireMessage::LinkFormatChanged { + id, + media_type: get_link_media_type(info), + }) + .expect("Failed to send message"); + } + } else { + // First time we get info. We can now notify the gtk thread of a new link. + let port_from = info.output_port_id(); + let port_to = info.input_port_id(); + + state.insert(id, Item::Link { port_from, port_to }); + + sender + .send(PipewireMessage::LinkAdded { + id, + port_from, + port_to, + active: matches!(info.state(), LinkState::Active), + media_type: get_link_media_type(info), + }) + .expect("Failed to send message"); + } +} + /// Toggle a link between the two specified ports. fn toggle_link( port_from: u32, @@ -312,3 +379,13 @@ fn toggle_link( } } } + +fn get_link_media_type(link_info: &LinkInfo) -> MediaType { + let media_type = link_info + .format() + .and_then(|format| pipewire::spa::param::format_utils::parse_format(format).ok()) + .map(|(media_type, _media_subtype)| media_type) + .unwrap_or(MediaType::Unknown); + + media_type +} diff --git a/src/pipewire_connection/state.rs b/src/pipewire_connection/state.rs index 098bda5..fe06a63 100644 --- a/src/pipewire_connection/state.rs +++ b/src/pipewire_connection/state.rs @@ -16,15 +16,10 @@ use std::collections::HashMap; -use crate::MediaType; - /// Any pipewire item we need to keep track of. /// These will be saved in the `State` struct associated with their id. pub(super) enum Item { - Node { - // Keep track of the nodes media type to color ports on it. - media_type: Option, - }, + Node, Port { // Save the id of the node this is on so we can remove the port from it // when it is deleted. diff --git a/src/style.css b/src/style.css index 330160c..d7a7c7f 100644 --- a/src/style.css +++ b/src/style.css @@ -15,23 +15,23 @@ SPDX-License-Identifier: GPL-3.0-only */ -@define-color audio rgb(50,100,240); -@define-color video rgb(200,200,0); -@define-color midi rgb(200,0,50); -@define-color graphview-link #808080; +@define-color media-type-audio rgb( 50, 100, 240); +@define-color media-type-video rgb(200, 200, 0); +@define-color media-type-midi rgb(200, 0, 50); +@define-color media-type-unknown rgb(128, 128, 128); .audio { - background: @audio; + background: @media-type-audio; color: black; } .video { - background: @video; + background: @media-type-video; color: black; } .midi { - background: @midi; + background: @media-type-midi; color: black; } diff --git a/src/ui/graph/graph_view.rs b/src/ui/graph/graph_view.rs index b69b41f..8197be5 100644 --- a/src/ui/graph/graph_view.rs +++ b/src/ui/graph/graph_view.rs @@ -43,8 +43,27 @@ mod imp { }; use log::warn; use once_cell::sync::Lazy; + use pipewire::spa::format::MediaType; use pipewire::spa::Direction; + pub struct Colors { + audio: gdk::RGBA, + video: gdk::RGBA, + midi: gdk::RGBA, + unknown: gdk::RGBA, + } + + impl Colors { + pub fn color_for_media_type(&self, media_type: MediaType) -> &gdk::RGBA { + match media_type { + MediaType::Audio => &self.audio, + MediaType::Video => &self.video, + MediaType::Stream | MediaType::Application => &self.midi, + _ => &self.unknown, + } + } + } + pub struct DragState { node: glib::WeakRef, /// This stores the offset of the pointer to the origin of the node, @@ -510,6 +529,7 @@ mod imp { 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(); @@ -523,6 +543,13 @@ mod imp { 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(), + ); + // 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. @@ -551,7 +578,7 @@ mod imp { }; } - fn draw_dragged_link(&self, port: &Port, link_cr: &cairo::Context) { + fn draw_dragged_link(&self, port: &Port, link_cr: &cairo::Context, colors: &Colors) { let Some(port_anchor) = port.compute_point(&*self.obj(), &port.link_anchor()) else { return; }; @@ -579,7 +606,9 @@ mod imp { _ => unreachable!(), }; - self.draw_link(link_cr, output_anchor, input_anchor, false); + let color = &colors.color_for_media_type(MediaType::from_raw(port.media_type())); + + self.draw_link(link_cr, output_anchor, input_anchor, false, color); } fn snapshot_links(&self, widget: &super::GraphView, snapshot: >k::Snapshot) { @@ -594,30 +623,45 @@ mod imp { link_cr.set_line_width(2.0 * self.zoom_factor.get()); - let rgba = widget - .style_context() - .lookup_color("graphview-link") - .unwrap_or(gtk::gdk::RGBA::BLACK); - - link_cr.set_source_rgba( - rgba.red().into(), - rgba.green().into(), - rgba.blue().into(), - rgba.alpha().into(), - ); + let colors = Colors { + audio: widget + .style_context() + .lookup_color("media-type-audio") + .expect("color not found"), + video: widget + .style_context() + .lookup_color("media-type-video") + .expect("color not found"), + midi: widget + .style_context() + .lookup_color("media-type-midi") + .expect("color not found"), + unknown: widget + .style_context() + .lookup_color("media-type-unknown") + .expect("color not found"), + }; for link in self.links.borrow().iter() { + let color = &colors.color_for_media_type(link.media_type()); + // TODO: Do not draw links when they are outside the view let Some((output_anchor, input_anchor)) = self.get_link_coordinates(link) else { warn!("Could not get allocation of ports of link: {:?}", link); continue; }; - self.draw_link(&link_cr, &output_anchor, &input_anchor, link.active()); + self.draw_link( + &link_cr, + &output_anchor, + &input_anchor, + link.active(), + color, + ); } if let Some(port) = self.dragged_port.upgrade() { - self.draw_dragged_link(&port, &link_cr); + self.draw_dragged_link(&port, &link_cr, &colors); } } @@ -793,6 +837,12 @@ impl GraphView { graph.queue_draw(); }), ); + link.connect_notify_local( + Some("media-type"), + glib::clone!(@weak self as graph => move |_, _| { + graph.queue_draw(); + }), + ); self.imp().links.borrow_mut().insert(link); self.queue_draw(); } diff --git a/src/ui/graph/link.rs b/src/ui/graph/link.rs index 4663956..2f9f7df 100644 --- a/src/ui/graph/link.rs +++ b/src/ui/graph/link.rs @@ -15,6 +15,7 @@ // SPDX-License-Identifier: GPL-3.0-only use gtk::{glib, prelude::*, subclass::prelude::*}; +use pipewire::spa::format::MediaType; use super::Port; @@ -25,11 +26,22 @@ mod imp { use once_cell::sync::Lazy; - #[derive(Default)] pub struct Link { pub output_port: glib::WeakRef, pub input_port: glib::WeakRef, pub active: Cell, + pub media_type: Cell, + } + + impl Default for Link { + fn default() -> Self { + Self { + output_port: glib::WeakRef::default(), + input_port: glib::WeakRef::default(), + active: Cell::default(), + media_type: Cell::new(MediaType::Unknown), + } + } } #[glib::object_subclass] @@ -53,6 +65,10 @@ mod imp { .default_value(false) .flags(glib::ParamFlags::READWRITE) .build(), + glib::ParamSpecUInt::builder("media-type") + .default_value(MediaType::Unknown.as_raw()) + .flags(glib::ParamFlags::READWRITE) + .build(), ] }); @@ -64,6 +80,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(), _ => unimplemented!(), } } @@ -73,6 +90,9 @@ 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())), _ => unimplemented!(), } } @@ -111,6 +131,14 @@ impl Link { pub fn set_active(&self, active: bool) { self.set_property("active", active); } + + pub fn media_type(&self) -> MediaType { + MediaType::from_raw(self.property("media-type")) + } + + pub fn set_media_type(&self, media_type: MediaType) { + self.set_property("media-type", media_type.as_raw()) + } } impl Default for Link { diff --git a/src/ui/graph/port.rs b/src/ui/graph/port.rs index 752936a..47c15bc 100644 --- a/src/ui/graph/port.rs +++ b/src/ui/graph/port.rs @@ -23,20 +23,26 @@ use gtk::{ }; use pipewire::spa::Direction; -use crate::MediaType; - mod imp { use super::*; + use std::cell::Cell; + use once_cell::{sync::Lazy, unsync::OnceCell}; - use pipewire::spa::Direction; + use pipewire::spa::{format::MediaType, Direction}; /// Graphical representation of a pipewire port. - #[derive(Default, glib::Properties)] + #[derive(glib::Properties)] #[properties(wrapper_type = super::Port)] pub struct Port { #[property(get, set, construct_only)] pub(super) pipewire_id: OnceCell, + #[property( + type = u32, + get = |_| self.media_type.get().as_raw(), + set = Self::set_media_type + )] + pub(super) media_type: Cell, #[property( name = "name", type = String, get = |this: &Self| this.label.text().to_string(), @@ -49,6 +55,17 @@ mod imp { pub(super) direction: OnceCell, } + impl Default for Port { + fn default() -> Self { + Self { + pipewire_id: OnceCell::default(), + media_type: Cell::new(MediaType::Unknown), + label: gtk::Label::default(), + direction: OnceCell::default(), + } + } + } + #[glib::object_subclass] impl ObjectSubclass for Port { const NAME: &'static str = "HelvumPort"; @@ -184,6 +201,26 @@ mod imp { obj.add_controller(drop_target); } } + + impl Port { + fn set_media_type(&self, media_type: u32) { + let media_type = MediaType::from_raw(media_type); + + self.media_type.set(media_type); + + for css_class in ["video", "audio", "midi"] { + self.obj().remove_css_class(css_class) + } + + // Color the port according to its media type. + match media_type { + MediaType::Video => self.obj().add_css_class("video"), + MediaType::Audio => self.obj().add_css_class("audio"), + MediaType::Application | MediaType::Stream => self.obj().add_css_class("midi"), + _ => {} + } + } + } } glib::wrapper! { @@ -192,7 +229,7 @@ glib::wrapper! { } impl Port { - pub fn new(id: u32, name: &str, direction: Direction, media_type: Option) -> Self { + pub fn new(id: u32, name: &str, direction: Direction) -> Self { // Create the widget and initialize needed fields let res: Self = glib::Object::builder() .property("pipewire-id", id) @@ -208,14 +245,6 @@ impl Port { // Display a grab cursor when the mouse is over the port so the user knows it can be dragged to another port. res.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref()); - // Color the port according to its media type. - match media_type { - Some(MediaType::Video) => res.add_css_class("video"), - Some(MediaType::Audio) => res.add_css_class("audio"), - Some(MediaType::Midi) => res.add_css_class("midi"), - None => {} - } - res }