diff --git a/src/application.rs b/src/application.rs index 91dd5b2..98155a4 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, collections::HashMap}; +use std::cell::RefCell; use gtk::{ gio, @@ -6,21 +6,14 @@ use gtk::{ prelude::*, subclass::prelude::*, }; -use log::{error, info, warn}; +use log::{info, warn}; use pipewire::{channel::Sender, spa::Direction}; use crate::{ view::{self}, - GtkMessage, PipewireLink, PipewireMessage, + GtkMessage, MediaType, PipewireLink, PipewireMessage, }; -#[derive(Debug, Copy, Clone)] -pub enum MediaType { - Audio, - Video, - Midi, -} - static STYLE: &str = include_str!("style.css"); mod imp { @@ -31,7 +24,6 @@ mod imp { #[derive(Default)] pub struct Application { pub(super) graphview: view::GraphView, - pub(super) state: RefCell, pub(super) pw_sender: OnceCell>>, } @@ -116,19 +108,12 @@ impl Application { @weak app => @default-return Continue(true), move |msg| { match msg { - PipewireMessage::NodeAdded { - id, - name, - media_type, - } => app.add_node(id, name, media_type), - PipewireMessage::PortAdded { - id, - node_id, - name, - direction, - } => app.add_port(id, name, node_id, direction), - PipewireMessage::LinkAdded { id, port_from, port_to } => app.add_link(id, port_from, port_to), - PipewireMessage::ObjectRemoved { id } => app.remove_global(id), + PipewireMessage::NodeAdded{ id, name } => app.add_node(id,name), + PipewireMessage::PortAdded{ id, node_id, name, direction, media_type} => app.add_port(id,name,node_id,direction,media_type), + PipewireMessage::LinkAdded{ id, node_from, port_from, node_to, port_to} => app.add_link(id,node_from,port_from,node_to,port_to), + PipewireMessage::NodeRemoved { id } => app.remove_node(id), + PipewireMessage::PortRemoved { id, node_id } => app.remove_port(id, node_id), + PipewireMessage::LinkRemoved { id } => app.remove_link(id) }; Continue(true) } @@ -139,40 +124,27 @@ impl Application { } /// Add a new node to the view. - pub fn add_node(&self, id: u32, name: String, media_type: Option) { + pub fn add_node(&self, id: u32, name: String) { info!("Adding node to graph: id {}", id); - let imp = imp::Application::from_instance(self); - - imp.state.borrow_mut().insert( - id, - Item::Node { - // widget: node_widget, - media_type, - }, - ); - - imp.graphview.add_node(id, view::Node::new(name.as_str())); + imp::Application::from_instance(self) + .graphview + .add_node(id, view::Node::new(name.as_str())); } /// Add a new port to the view. - pub fn add_port(&self, id: u32, name: String, node_id: u32, direction: Direction) { + pub fn add_port( + &self, + id: u32, + name: String, + node_id: u32, + direction: Direction, + media_type: Option, + ) { info!("Adding port to graph: id {}", id); let imp = imp::Application::from_instance(self); - // Find out the nodes media type so that the port can be colored. - let media_type = - if let Some(Item::Node { media_type, .. }) = imp.state.borrow().get(node_id) { - media_type.to_owned() - } else { - warn!("Node not found for Port {}", id); - None - }; - - // Save node_id so we can delete this port easily. - imp.state.borrow_mut().insert(id, Item::Port { node_id }); - let port = view::Port::new(id, name.as_str(), direction, media_type); // Create or delete a link if the widget emits the "port-toggled" signal. @@ -196,45 +168,13 @@ impl Application { } /// Add a new link to the view. - pub fn add_link(&self, id: u32, port_from: u32, port_to: u32) { + pub fn add_link(&self, id: u32, node_from: u32, port_from: u32, node_to: u32, port_to: u32) { info!("Adding link to graph: id {}", id); - let imp = imp::Application::from_instance(self); - let mut state = imp.state.borrow_mut(); - // FIXME: Links should be colored depending on the data they carry (video, audio, midi) like ports are. - let node_from = *match state.get(port_from) { - Some(Item::Port { node_id }) => node_id, - _ => { - error!( - "Tried to add link (id:{}), but its output port (id:{}) is not known", - id, port_from - ); - return; - } - }; - let node_to = *match state.get(port_to) { - Some(Item::Port { node_id }) => node_id, - _ => { - error!( - "Tried to add link (id:{}), but its input port (id:{}) is not known", - id, port_to - ); - return; - } - }; - - state.insert( - id, - Item::Link { - output_port: port_from, - input_port: port_to, - }, - ); - // Update graph to contain the new link. - imp.graphview.add_link( + imp::Application::from_instance(self).graphview.add_link( id, PipewireLink { node_from, @@ -249,54 +189,9 @@ impl Application { fn toggle_link(&self, port_from: u32, port_to: u32) { let imp = imp::Application::from_instance(self); let sender = imp.pw_sender.get().expect("pw_sender not set").borrow_mut(); - let state = imp.state.borrow_mut(); - - if let Some(id) = state.get_link_id(port_from, port_to) { - info!("Requesting removal of link with id {}", id); - - sender - .send(GtkMessage::DestroyGlobal(id)) - .expect("Failed to send message"); - } else { - info!( - "Requesting creation of link from port id:{} to port id:{}", - port_from, port_to - ); - - let node_from = state - .get_node_of_port(port_from) - .expect("Requested port not in state"); - let node_to = state - .get_node_of_port(port_to) - .expect("Requested port not in state"); - - sender - .send(GtkMessage::CreateLink(PipewireLink { - node_from, - port_from, - node_to, - port_to, - })) - .expect("Failed to send message"); - } - } - - /// Handle a global object being removed. - pub fn remove_global(&self, id: u32) { - let imp = imp::Application::from_instance(self); - - if let Some(item) = imp.state.borrow_mut().remove(id) { - match item { - Item::Node { .. } => self.remove_node(id), - Item::Port { node_id } => self.remove_port(id, node_id), - Item::Link { .. } => self.remove_link(id), - } - } else { - warn!( - "Attempted to remove item with id {} that is not saved in state", - id - ); - } + sender + .send(GtkMessage::ToggleLink { port_from, port_to }) + .expect("Failed to send message"); } /// Remove the node with the specified id from the view. @@ -324,83 +219,3 @@ impl Application { imp.graphview.remove_link(id); } } - -/// Any pipewire item we need to keep track of. -/// These will be saved in the [`Application`]s `state` struct associated with their id. -enum Item { - Node { - // Keep track of the nodes media type to color ports on it. - media_type: Option, - }, - Port { - // Save the id of the node this is on so we can remove the port from it - // when it is deleted. - node_id: u32, - }, - // We don't need to memorize anything about links right now, but we need to - // be able to find out an id is a link. - Link { - output_port: u32, - input_port: u32, - }, -} - -/// This struct keeps track of any relevant items and stores them under their IDs. -/// -/// Given two port ids, it can also efficiently find the id of the link that connects them. -#[derive(Default)] -struct State { - /// Map pipewire ids to items. - items: HashMap, - /// Map `(output port id, input port id)` tuples to the id of the link that connects them. - links: HashMap<(u32, u32), u32>, -} - -impl State { - /// Add a new item under the specified id. - fn insert(&mut self, id: u32, item: Item) { - if let Item::Link { - output_port, - input_port, - } = item - { - self.links.insert((output_port, input_port), id); - } - - self.items.insert(id, item); - } - - /// Get the item that has the specified id. - fn get(&self, id: u32) -> Option<&Item> { - self.items.get(&id) - } - - /// Get the id of the link that links the two specified ports. - fn get_link_id(&self, output_port: u32, input_port: u32) -> Option { - self.links.get(&(output_port, input_port)).copied() - } - - /// Remove the item with the specified id, returning it if it exists. - fn remove(&mut self, id: u32) -> Option { - let removed = self.items.remove(&id); - - if let Some(Item::Link { - output_port, - input_port, - }) = removed - { - self.links.remove(&(output_port, input_port)); - } - - removed - } - - /// Convenience function: Get the id of the node a port is on - fn get_node_of_port(&self, port: u32) -> Option { - if let Some(Item::Port { node_id }) = self.get(port) { - Some(*node_id) - } else { - None - } - } -} diff --git a/src/main.rs b/src/main.rs index 5934f3b..eaa5992 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,48 +2,59 @@ mod application; mod pipewire_connection; mod view; -use application::MediaType; use gtk::{ glib::{self, PRIORITY_DEFAULT}, prelude::*, }; use pipewire::spa::Direction; -/// Messages used GTK thread to command the pipewire thread. +/// Messages sent by the GTK thread to notify the pipewire thread. #[derive(Debug, Clone)] enum GtkMessage { - /// Create a new link. - CreateLink(PipewireLink), - /// Destroy the global with the specified id. - DestroyGlobal(u32), + /// Toggle a link between the two specified ports. + ToggleLink { port_from: u32, port_to: u32 }, /// Quit the event loop and let the thread finish. Terminate, } -/// Messages used pipewire thread to notify the GTK thread. +/// Messages sent by the pipewire thread to notify the GTK thread. #[derive(Debug, Clone)] enum PipewireMessage { - /// A new node has appeared. NodeAdded { id: u32, name: String, - media_type: Option, }, - /// A new port has appeared. PortAdded { id: u32, node_id: u32, name: String, direction: Direction, + media_type: Option, }, - /// A new link has appeared. LinkAdded { id: u32, + node_from: u32, port_from: u32, + node_to: u32, port_to: u32, }, - /// An object was removed - ObjectRemoved { id: u32 }, + NodeRemoved { + id: u32, + }, + PortRemoved { + id: u32, + node_id: u32, + }, + LinkRemoved { + id: u32, + }, +} + +#[derive(Debug, Copy, Clone)] +pub enum MediaType { + Audio, + Video, + Midi, } #[derive(Debug, Clone)] diff --git a/src/pipewire_connection.rs b/src/pipewire_connection.rs index 5fa1db1..ceee5e4 100644 --- a/src/pipewire_connection.rs +++ b/src/pipewire_connection.rs @@ -1,18 +1,18 @@ -use std::rc::Rc; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; use gtk::glib::{self, clone}; -use log::warn; +use log::{error, info, warn}; use pipewire::{ link::Link, prelude::*, properties, - registry::GlobalObject, + registry::{GlobalObject, Registry}, spa::{Direction, ForeignDict}, types::ObjectType, - Context, MainLoop, + Context, Core, MainLoop, }; -use crate::{application::MediaType, GtkMessage, PipewireMessage}; +use crate::{GtkMessage, MediaType, PipewireMessage}; /// The "main" function of the pipewire thread. pub(super) fn thread_main( @@ -21,59 +21,55 @@ pub(super) fn thread_main( ) { let mainloop = MainLoop::new().expect("Failed to create mainloop"); let context = Context::new(&mainloop).expect("Failed to create context"); - let core = context.connect(None).expect("Failed to connect to remote"); + let core = Rc::new(context.connect(None).expect("Failed to connect to remote")); let registry = Rc::new(core.get_registry().expect("Failed to get registry")); + let state = Rc::new(RefCell::new(State::new())); + let _receiver = pw_receiver.attach(&mainloop, { - let mainloop = mainloop.clone(); - clone!(@weak registry => move |msg| match msg { - GtkMessage::CreateLink(link) => { - if let Err(e) = core.create_object::( - "link-factory", - &properties! { - "link.output.node" => link.node_from.to_string(), - "link.output.port" => link.port_from.to_string(), - "link.input.node" => link.node_to.to_string(), - "link.input.port" => link.port_to.to_string(), - "object.linger" => "1" - }, - ) { - warn!("Failed to create link: {}", e); - } - } - GtkMessage::DestroyGlobal(id) => { - // FIXME: Handle error - registry.destroy_global(id); - } + clone!(@strong mainloop, @weak core, @weak registry, @strong state => move |msg| match msg { + GtkMessage::ToggleLink { port_from, port_to } => toggle_link(port_from, port_to, &core, ®istry, &state), GtkMessage::Terminate => mainloop.quit(), }) }); let _listener = registry .add_listener_local() - .global({ - let sender = gtk_sender.clone(); + .global(clone!(@strong gtk_sender, @strong state => move |global| match global.type_ { - ObjectType::Node => handle_node(global, &sender), - ObjectType::Port => handle_port(global, &sender), - ObjectType::Link => handle_link(global, &sender), + ObjectType::Node => handle_node(global, >k_sender, &state), + ObjectType::Port => handle_port(global, >k_sender, &state), + ObjectType::Link => handle_link(global, >k_sender, &state), _ => { // Other objects are not interesting to us } } - }) - .global_remove(move |id| { - gtk_sender - .send(PipewireMessage::ObjectRemoved { id }) - .expect("Failed to send message") - }) + )) + .global_remove(clone!(@strong state => move |id| { + if let Some(item) = state.borrow_mut().remove(id) { + gtk_sender.send(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 + ); + } + })) .register(); mainloop.run(); } /// Handle a new node being added -fn handle_node(node: &GlobalObject, sender: &glib::Sender) { +fn handle_node( + node: &GlobalObject, + sender: &glib::Sender, + state: &Rc>, +) { let props = node .props .as_ref() @@ -104,17 +100,25 @@ fn handle_node(node: &GlobalObject, sender: &glib::Sender, sender: &glib::Sender) { +fn handle_port( + port: &GlobalObject, + sender: &glib::Sender, + state: &Rc>, +) { let props = port .props .as_ref() @@ -131,18 +135,34 @@ fn handle_port(port: &GlobalObject, sender: &glib::Sender, sender: &glib::Sender) { +fn handle_link( + link: &GlobalObject, + sender: &glib::Sender, + state: &Rc>, +) { let props = link .props .as_ref() @@ -158,11 +178,170 @@ fn handle_link(link: &GlobalObject, sender: &glib::Sender node_id, + _ => { + error!( + "Tried to add link (id:{}), but its output port (id:{}) is not known", + link.id, port_from + ); + return; + } + }; + let node_to = *match state.get(port_to) { + Some(Item::Port { node_id }) => node_id, + _ => { + error!( + "Tried to add link (id:{}), but its input port (id:{}) is not known", + link.id, port_to + ); + return; + } + }; + + state.insert( + link.id, + Item::Link { + output_port: port_from, + input_port: port_to, + }, + ); + sender .send(PipewireMessage::LinkAdded { id: link.id, + node_from, port_from, + node_to, port_to, }) .expect("Failed to send message"); } + +/// Toggle a link between the two specified ports. +fn toggle_link( + port_from: u32, + port_to: u32, + core: &Rc, + registry: &Rc, + state: &Rc>, +) { + let state = state.borrow_mut(); + if let Some(id) = state.get_link_id(port_from, port_to) { + info!("Requesting removal of link with id {}", id); + + // FIXME: Handle error + registry.destroy_global(id); + } else { + info!( + "Requesting creation of link from port id:{} to port id:{}", + port_from, port_to + ); + + let node_from = state + .get_node_of_port(port_from) + .expect("Requested port not in state"); + let node_to = state + .get_node_of_port(port_to) + .expect("Requested port not in state"); + + if let Err(e) = core.create_object::( + "link-factory", + &properties! { + "link.output.node" => node_from.to_string(), + "link.output.port" => port_from.to_string(), + "link.input.node" => node_to.to_string(), + "link.input.port" => port_to.to_string(), + "object.linger" => "1" + }, + ) { + warn!("Failed to create link: {}", e); + } + } +} + +/// Any pipewire item we need to keep track of. +/// These will be saved in the [`Application`]s `state` struct associated with their id. +enum Item { + Node { + // Keep track of the nodes media type to color ports on it. + media_type: Option, + }, + Port { + // Save the id of the node this is on so we can remove the port from it + // when it is deleted. + node_id: u32, + }, + // We don't need to memorize anything about links right now, but we need to + // be able to find out an id is a link. + Link { + output_port: u32, + input_port: u32, + }, +} + +/// This struct keeps track of any relevant items and stores them under their IDs. +/// +/// Given two port ids, it can also efficiently find the id of the link that connects them. +#[derive(Default)] +struct State { + /// Map pipewire ids to items. + items: HashMap, + /// Map `(output port id, input port id)` tuples to the id of the link that connects them. + links: HashMap<(u32, u32), u32>, +} + +impl State { + /// Create a new, empty state. + fn new() -> Self { + Default::default() + } + + /// Add a new item under the specified id. + fn insert(&mut self, id: u32, item: Item) { + if let Item::Link { + output_port, + input_port, + } = item + { + self.links.insert((output_port, input_port), id); + } + + self.items.insert(id, item); + } + + /// Get the item that has the specified id. + fn get(&self, id: u32) -> Option<&Item> { + self.items.get(&id) + } + + /// Get the id of the link that links the two specified ports. + fn get_link_id(&self, output_port: u32, input_port: u32) -> Option { + self.links.get(&(output_port, input_port)).copied() + } + + /// Remove the item with the specified id, returning it if it exists. + fn remove(&mut self, id: u32) -> Option { + let removed = self.items.remove(&id); + + if let Some(Item::Link { + output_port, + input_port, + }) = removed + { + self.links.remove(&(output_port, input_port)); + } + + removed + } + + /// Convenience function: Get the id of the node a port is on + fn get_node_of_port(&self, port: u32) -> Option { + if let Some(Item::Port { node_id }) = self.get(port) { + Some(*node_id) + } else { + None + } + } +} diff --git a/src/view/port.rs b/src/view/port.rs index d91a367..8584a6f 100644 --- a/src/view/port.rs +++ b/src/view/port.rs @@ -7,7 +7,7 @@ use gtk::{ use log::warn; use pipewire::spa::Direction; -use crate::application::MediaType; +use crate::MediaType; mod imp { use once_cell::{sync::Lazy, unsync::OnceCell};