diff --git a/.gitignore b/.gitignore index ea8c4bf..947afee 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.vscode/* diff --git a/Cargo.lock b/Cargo.lock index 5deae13..608f792 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -679,7 +679,7 @@ dependencies = [ [[package]] name = "egui_nodes" version = "0.1.3" -source = "git+https://github.com/Ax9D/egui_nodes?rev=74eccc4#74eccc4087b474ef4f98b772d8946c3fc17aad0d" +source = "git+https://github.com/Ax9D/egui_nodes?rev=3544d61#3544d610a3a4d361c91f270982e1292f33476bd9" dependencies = [ "derivative", "egui", diff --git a/Cargo.toml b/Cargo.toml index 929aa28..f3c9f83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ simple_logger = "1.13.0" # egui stuff eframe = { version = "0.15.0", features = ["persistence"] } egui = "0.15.0" -egui_nodes = {git = "https://github.com/Ax9D/egui_nodes", rev="74eccc4"} +egui_nodes = {git = "https://github.com/Ax9D/egui_nodes", rev="3544d61"} serde = { version = "1", features = ["derive"] } [profile.release] diff --git a/README.md b/README.md index 77be1a7..75a6d0a 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,22 @@ This is still a WIP, node layouting is kinda jank at the moment. # Installation - A compiled binary is available on the [releases page](https://github.com/Ax9D/pw-viz/releases). ## Building from source -Clone the repo: +To build pw-viz, you will need to have Rust installed. The recommended way to install Rust is from the [official download page](https://www.rust-lang.org/tools/install), using rustup. + +### Stable Release +Download and extract the source code to the latest release over on the [releases page](https://github.com/Ax9D/pw-viz/releases). + +### Main branch +Alternatively, you can clone the main branch, although its NOT guaranteed to be stable or bug free. ``` git clone https://github.com/Ax9D/pw-viz -cd pw-viz ``` -To build pw-viz, you will need to have Rust installed. The recommended way to install Rust is from the [official download page](https://www.rust-lang.org/tools/install), using rustup. - -Once Rust is installed, you can build pw-viz: +### Build +Next, `cd` into your source folder and then start the build using: ``` cargo build --release ``` @@ -47,8 +50,8 @@ Zooming is not supported currently * A modified fork of [egui-nodes](https://github.com/haighcam/egui_nodes): A egui port of [imnodes](https://github.com/Nelarius/imnodes) # Thanks / Alternatives -Pipewire connection code is inspired by helvum's implementation, -[helvum](https://gitlab.freedesktop.org/ryuukyu/helvum): A GTK patchbay for pipewire. +Pipewire connection code is inspired by helvum's implementation +* [helvum](https://gitlab.freedesktop.org/ryuukyu/helvum): A GTK patchbay for pipewire. # License pw-viz is licensed under the terms of the GNU General Public License v3.0. See LICENSE for more information. diff --git a/assets/demo.png b/assets/demo.png index 4d3ca98..e5da3cf 100644 Binary files a/assets/demo.png and b/assets/demo.png differ diff --git a/src/id.rs b/src/id.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/main.rs b/src/main.rs index eb583e2..cfbe276 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,11 +13,15 @@ fn main() -> Result<(), Box> { let (sender, receiver) = std::sync::mpsc::channel(); let (pwsender, pwreciever) = pipewire::channel::channel(); - // Set up pipewire thread - let pw_thread_handle = thread::spawn(move || { - let sender = Rc::new(sender); - pipewire_impl::thread_main(sender, pwreciever).expect("Failed to init pipewire client"); - }); + //Set's up pipewire thread + let pw_thread_handle = thread::Builder::new() + .name("Pipewire".to_string()) + .spawn(move || { + let sender = Rc::new(sender); + + pipewire_impl::thread_main(sender, pwreciever).expect("Failed to init pipewire client"); + }) + .expect("Failed to create pipewire thread"); ui::run_graph_ui(receiver, pwsender); diff --git a/src/pipewire_impl/mod.rs b/src/pipewire_impl/mod.rs index 67b4052..e646e09 100644 --- a/src/pipewire_impl/mod.rs +++ b/src/pipewire_impl/mod.rs @@ -16,9 +16,11 @@ pub enum PipewireMessage { NodeAdded { id: u32, name: String, + description: Option, media_type: Option, }, PortAdded { + node_name: String, node_id: u32, id: u32, name: String, @@ -26,8 +28,8 @@ pub enum PipewireMessage { }, LinkAdded { id: u32, - from_node: u32, - to_node: u32, + from_node_name: String, + to_node_name: String, from_port: u32, to_port: u32, @@ -37,9 +39,11 @@ pub enum PipewireMessage { active: bool, }, NodeRemoved { + name: String, id: u32, }, PortRemoved { + node_name: String, node_id: u32, id: u32, }, @@ -113,11 +117,17 @@ pub fn thread_main( .global_remove(move |id| match state_rm.borrow_mut().remove(id) { Some(object) => { let message = match object { - state::GlobalObject::Node => PipewireMessage::NodeRemoved { id }, + state::GlobalObject::Node { name } => PipewireMessage::NodeRemoved { name, id }, state::GlobalObject::Link => PipewireMessage::LinkRemoved { id }, - state::GlobalObject::Port { node_id, id } => { - PipewireMessage::PortRemoved { node_id, id } - } + state::GlobalObject::Port { + node_name, + node_id, + id, + } => PipewireMessage::PortRemoved { + node_name, + node_id, + id, + }, }; sender_rm .send(message) @@ -141,12 +151,9 @@ pub fn thread_main( UiMessage::RemoveLink(link_id) => { remove_link(link_id, &state, ®istry); } - UiMessage::AddLink { - from_node, - to_node, - from_port, - to_port, - } => add_link(from_port, to_port, from_node, to_node, &core), + UiMessage::AddLink { from_port, to_port } => { + add_link(&state, from_port, to_port, &core) + } UiMessage::Exit => mainloop.quit(), } }); @@ -166,9 +173,11 @@ fn handle_node( .as_ref() .expect("Node object doesn't have properties"); + let description = props.get("node.description"); + let name = props .get("node.nick") - .or_else(|| props.get("node.description")) + .or(description) .or_else(|| props.get("node.name")) .unwrap_or_default() .to_string(); @@ -185,12 +194,16 @@ fn handle_node( } }); - state.borrow_mut().add(node.id, state::GlobalObject::Node); + state + .borrow_mut() + .add(node.id, state::GlobalObject::Node { name: name.clone() }); + let description = description.map(|desc| desc.to_string()); sender .send(PipewireMessage::NodeAdded { id: node.id, name, + description, media_type, }) .expect("Failed to send pipewire message"); @@ -219,6 +232,16 @@ fn handle_link( let to_port = info.input_port_id(); let mut state = state.borrow_mut(); + + let from_node_name = match state.get(from_node).expect("Id wasn't registered") { + state::GlobalObject::Node { name } => name.clone(), + _ => unreachable!(), + }; + let to_node_name = match state.get(to_node).expect("Id wasn't registered") { + state::GlobalObject::Node { name } => name.clone(), + _ => unreachable!(), + }; + if let Some(&state::GlobalObject::Link) = state.get(id) { if info.change_mask().contains(LinkChangeMask::STATE) { sender @@ -230,8 +253,8 @@ fn handle_link( log::debug!("New pipewire link was added : {}", id); sender .send(PipewireMessage::LinkAdded { - from_node, - to_node, + from_node_name, + to_node_name, from_port, to_port, id, @@ -245,8 +268,32 @@ fn handle_link( .borrow_mut() .insert(link.id, ProxyLink { proxy, listener }); } +fn add_link(state: &Rc>, from_port: u32, to_port: u32, core: &Rc) { + let state = state.borrow(); + let from_port_ob = state + .get(from_port) + .expect(&format!("Port with id {} was never registered", from_port)); + let from_node = *match from_port_ob { + state::GlobalObject::Port { + node_name: _, + node_id, + id: _, + } => node_id, + _ => unreachable!(), + }; + + let to_port_ob = state + .get(to_port) + .expect(&format!("Port with id {} was never registered", to_port)); + let to_node = *match to_port_ob { + state::GlobalObject::Port { + node_name: _, + node_id, + id: _, + } => node_id, + _ => unreachable!(), + }; -fn add_link(from_port: u32, to_port: u32, from_node: u32, to_node: u32, core: &Rc) { core.create_object::( "link-factory", &pipewire::properties! { @@ -288,15 +335,29 @@ fn handle_port( .parse::() .expect("Couldn't parse node.id as u32"); + let mut state = state.borrow_mut(); + + let node_name = match state + .get(node_id) + .expect(&format!("Node with id {} was never registered", node_id)) + { + state::GlobalObject::Node { name } => name, + _ => { + unreachable!() + } + } + .clone(); + let port_type = match props.get("port.direction") { Some("in") => PortType::Input, Some("out") => PortType::Output, _ => PortType::Unknown, }; - state.borrow_mut().add( + state.add( port.id, state::GlobalObject::Port { + node_name: node_name.clone(), node_id, id: port.id, }, @@ -304,6 +365,7 @@ fn handle_port( sender .send(PipewireMessage::PortAdded { + node_name, node_id, id: port.id, name, diff --git a/src/pipewire_impl/state.rs b/src/pipewire_impl/state.rs index 407d1a8..0a5e5e7 100644 --- a/src/pipewire_impl/state.rs +++ b/src/pipewire_impl/state.rs @@ -1,9 +1,15 @@ use std::collections::HashMap; pub enum GlobalObject { - Node, + Node { + name: String, + }, Link, - Port { node_id: u32, id: u32 }, + Port { + node_name: String, + node_id: u32, + id: u32, + }, } /// For internal state tracking, this has to be done because pipewire only provides ids of the objects it removes, diff --git a/src/ui/graph.rs b/src/ui/graph.rs index fe9b5d1..3a8d3a6 100644 --- a/src/ui/graph.rs +++ b/src/ui/graph.rs @@ -1,11 +1,14 @@ -use egui::Widget; -use egui_nodes::{LinkArgs, NodeArgs, NodeConstructor, PinArgs}; use std::collections::{HashMap, HashSet}; -use super::{link::Link, node::Node, Theme}; +use egui_nodes::{LinkArgs, NodeArgs, NodeConstructor}; + use crate::pipewire_impl::MediaType; -/// Represents changes to any links that might happend in the ui +use super::id::Id; + +use super::{link::Link, node::Node, port::Port, Theme}; + +/// Represents changes to any links that might have happend in the ui /// These changes are used to send updates to the pipewire thread pub enum LinkUpdate { Created { @@ -19,53 +22,112 @@ pub enum LinkUpdate { pub struct Graph { nodes_ctx: egui_nodes::Context, - nodes: HashMap, // Node id to Node - links: HashMap, // Link id to Link + nodes: HashMap, //Node id to Node + links: HashMap, //Link id to Link } impl Graph { pub fn new() -> Self { + //context.attribute_flag_push(egui_nodes::AttributeFlags::EnableLinkCreationOnSnap); + //context.attribute_flag_push(egui_nodes::AttributeFlags::EnableLinkDetachWithDragClick); + let mut nodes_ctx = egui_nodes::Context::default(); + + nodes_ctx.style.link_bezier_offset_coefficient = egui::vec2(0.50, 0.0); + nodes_ctx.style.link_line_segments_per_length = 0.15; + Self { - nodes_ctx: egui_nodes::Context::default(), + nodes_ctx, nodes: HashMap::new(), links: HashMap::new(), } } + fn get_or_create_node(&mut self, name: String) -> &mut Node { + let id = Id::new(&name); + self.nodes.entry(id).or_insert_with(|| { + log::debug!("Created new ui node: {}", name); - pub fn add_node(&mut self, node: Node) { - log::debug!("New node: {}", node.name); - self.nodes.insert(node.id, node); + Node::new(id, name) + }) } + pub fn add_node( + &mut self, + name: String, + id: u32, + description: Option, + media_type: Option, + ) { + self.get_or_create_node(name) + .add_pw_node(id, description, media_type) + } + pub fn remove_node(&mut self, name: &str, id: u32) { + let mut remove_ui_node = false; - pub fn remove_node(&mut self, id: u32) -> Option { - let removed = self.nodes.remove(&id); - match removed { - Some(ref node) => log::debug!("Removed node: {}", node.name), - None => log::warn!("Node with id {} doesn't exist", id), + if let Some(node) = self.nodes.get_mut(&Id::new(name)) { + remove_ui_node = node.remove_pw_node(id); + } else { + log::error!("Node with name: {} was not registered", name); } - removed - } - #[allow(dead_code)] - pub fn get_node(&self, id: u32) -> Option<&Node> { - self.nodes.get(&id) - } - pub fn get_node_mut(&mut self, id: u32) -> Option<&mut Node> { - self.nodes.get_mut(&id) - } + //If there are no more pw nodes remove the ui node + if remove_ui_node { + let removed_node = self + .nodes + .remove(&Id::new(name)) + .expect("Node was never added"); - #[allow(dead_code)] - pub fn get_link(&self, id: u32) -> Option<&Link> { - self.links.get(&id) + log::debug!("Removing node {}", removed_node.name()); + } } - #[allow(dead_code)] - pub fn get_link_mut(&mut self, id: u32) -> Option<&mut Link> { - self.links.get_mut(&id) + pub fn add_port(&mut self, node_name: String, node_id: u32, port: Port) { + self.get_or_create_node(node_name).add_port(node_id, port) + } + pub fn remove_port(&mut self, node_name: &str, node_id: u32, port_id: u32) { + if let Some(node) = self.nodes.get_mut(&Id::new(node_name)) { + node.remove_port(node_id, port_id); + } else { + log::error!("Node with name: {} was not registered", node_name); + } } + pub fn add_link( + &mut self, + id: u32, + from_node_name: String, + to_node_name: String, + from_port: u32, + to_port: u32, + ) { + log::debug!( + "{}.{}->{}.{}", + from_node_name, + from_port, + to_node_name, + to_port + ); + + let from_node = self + .nodes + .get(&Id::new(from_node_name)) + .expect("Node with provided name doesn't exist") + .id(); - pub fn add_link(&mut self, link: Link) { - log::debug!("{}->{}", link.from_port, link.to_port); - self.links.insert(link.id, link); + let to_node = self + .nodes + .get(&Id::new(to_node_name)) + .expect("Node with provided name doesn't exist") + .id(); + log::debug!("{:?} {:?}", from_node, to_node); + + self.links.insert( + id, + Link { + id, + from_node, + to_node, + from_port, + to_port, + active: true, + }, + ); } pub fn remove_link(&mut self, id: u32) -> Option { let removed = self.links.remove(&id); @@ -75,105 +137,92 @@ impl Graph { } removed } + #[allow(dead_code)] + fn get_link(&self, id: u32) -> Option<&Link> { + self.links.get(&id) + } + #[allow(dead_code)] + fn get_link_mut(&mut self, id: u32) -> Option<&mut Link> { + self.links.get_mut(&id) + } + fn topo_sort_( + node_id: Id, + visited: &mut HashSet, + adj_list: &HashMap>, + stack: &mut Vec, + ) { + visited.insert(node_id); + + for node_id in &adj_list[&node_id] { + if !visited.contains(node_id) { + Self::topo_sort_(*node_id, visited, adj_list, stack); + } + } - /// Naive, inefficient and weird implementation of Kahn's algorithm - fn topo_sort(&self) -> Vec { - // FIX ME + stack.push(node_id); + } + //TODO: Handle stack overflows + fn top_sort(&self) -> Vec { + let mut stack = Vec::new(); - // Node id to in-degree(no. of nodes that output to this node) - let mut indegrees = self - .nodes - .keys() - .map(|&id| { - let count = self - .links - .values() - .filter(|link| link.to_node == id) - .map(|link| link.from_node) - .collect::>() - .len(); - (id, count) - }) - .collect::>(); + let mut visited = HashSet::new(); - // Adjacency hashmap, maps node id to neighbouring node ids let adj_list = self .nodes - .keys() - .map(|&id| { + .values() + .map(|node| { let adj = self .links .values() - .filter(|link| link.from_node == id) + .filter(|link| !link.is_self_link()) + .filter(|link| link.from_node == node.id()) .map(|link| link.to_node) - .collect::>(); - (id, adj) + .collect::>(); + (node.id(), adj) }) - .collect::>(); - - // println!("Indegrees {:?}", indegrees); - // println!("Adj list {:?}", self.adj_list); - - let mut queue: Vec = Vec::new(); + .collect::>(); - for node_id in self.nodes.keys() { - // Put nodes which are "detached"(i.e of in-degree=0) from the graph into the queue for processing - if indegrees[node_id] == 0 { - queue.push(*node_id); + for node in self.nodes.values() { + if !visited.contains(&node.id()) { + Self::topo_sort_(node.id(), &mut visited, &adj_list, &mut stack) } } - let mut top_order = Vec::new(); - let mut count = 0; - while !queue.is_empty() { - // println!("Queue: {:?}", queue); - let u = queue.remove(0); // Remove from the front of the queue - top_order.push(u); - if let Some(adj_nodes) = adj_list.get(&u) { - // Check nodes that lead out from this node - for node_id in adj_nodes { - // Remove link from parent node to this node - let indegree_of_node = indegrees.get_mut(node_id).unwrap(); - *indegree_of_node -= 1; - - // Check if that detached the node from the graph - if *indegree_of_node == 0 { - // If it did, we have a new detached node to process - queue.push(*node_id); - } - } - } - count += 1; - } + stack.reverse(); - if count != self.nodes.len() { - log::error!("Cycle detected"); - } - top_order + stack } - - pub fn draw( - &mut self, - ctx: &egui::CtxRef, - ui: &mut egui::Ui, - theme: &Theme, + pub fn draw<'graph, 'ui>( + &'graph mut self, + ctx: &'ui egui::CtxRef, + ui: &'ui mut egui::Ui, + theme: &'ui Theme, ) -> Option { - // Find the topologically sorted order of nodes in the graph - // Nodes are currently laid out based on this order - let order = self.topo_sort(); - - // println!("{:?}", order); - // Ctrl is used to trigger the debug view let debug_view = ctx.input().modifiers.ctrl; let mut ui_nodes = Vec::with_capacity(self.nodes.len()); - let mut prev_pos = egui::pos2(ui.available_width() / 4.0, ui.available_height() / 2.0); - let mut padding = egui::pos2(75.0, 150.0); - for node_id in order { - let node = self.nodes.get_mut(&node_id).unwrap(); + self.nodes_ctx.style.colors[egui_nodes::ColorStyle::NodeBackground as usize] = + theme.node_background; + self.nodes_ctx.style.colors[egui_nodes::ColorStyle::NodeBackgroundHovered as usize] = + theme.node_background_hovered; + self.nodes_ctx.style.colors[egui_nodes::ColorStyle::NodeBackgroundSelected as usize] = + theme.node_background_hovered; + + ui.vertical_centered(|ui| { + if ui.button("Arrange").clicked() { + log::debug!("Relayouting"); + for node in self.nodes.values_mut() { + node.position = None; + } + + //self.nodes_ctx.reset_panniing(egui::Vec2::ZERO); + } + }); + + for node in self.nodes.values() { let mut ui_node = NodeConstructor::new( - node.id as usize, + node.id().value() as usize, NodeArgs { titlebar: Some(theme.titlebar), titlebar_hovered: Some(theme.titlebar_hovered), @@ -182,46 +231,8 @@ impl Graph { }, ); - // if node.position.is_none() { - // // Horizontally shift each node to the right of the previous one - // // Also put it at a random point vertically - // ui_node.with_origin(egui::pos2( - // prev_pos.x + padding.x, - // rand::random::() * ui.available_height(), - // )); - // } else { - // ui_node.with_origin(egui::pos2( - // ui.available_width() / 4.0, - // rand::random::() * ui.available_height(), - // )); - // } - - let node_position = node.position.unwrap_or_else(|| { - padding.y *= -1.0; - egui::pos2(prev_pos.x + padding.x, prev_pos.y + padding.y) - }); - ui_node.with_origin(node_position); - - prev_pos = node_position; - - let kind = match node.media_type { - Some(MediaType::Audio) => "🔉", - Some(MediaType::Video) => "💻", - Some(MediaType::Midi) => "🎹", - None => "", - }; - - let title = { - if debug_view { - // Display node id if in debug view - format!("{}[{}]{}", node.name, node.id, kind) - } else { - format!("{} {}", node.name, kind) - } - }; + node.draw(&mut ui_node, theme, debug_view); - ui_node.with_title(|ui| egui::Label::new(title).text_color(theme.text_color).ui(ui)); - Self::draw_ports(&mut ui_node, node, theme, debug_view); ui_nodes.push(ui_node); } @@ -244,8 +255,30 @@ impl Graph { }) }); - for (&id, node) in self.nodes.iter_mut() { - node.position = self.nodes_ctx.get_node_pos_screen_space(id as usize); + let mut prev_pos = egui::pos2(ui.available_width() / 4.0, ui.available_height() / 2.0); + let mut padding = egui::pos2(75.0, 150.0); + + //Find the topologically sorted order of nodes in the graph + //Nodes are currently laid out based on this order + let order = self.top_sort(); + for node_id in order { + let node = self.nodes.get_mut(&node_id).unwrap(); + + if !node.position.is_some() { + padding.y *= -1.0; + let node_position = egui::pos2(prev_pos.x + padding.x, prev_pos.y + padding.y); + + node.position = Some(node_position); + self.nodes_ctx + .set_node_pos_grid_space(node_id.value() as usize, node_position); + + prev_pos = node_position; + } else { + prev_pos = self + .nodes_ctx + .get_node_pos_grid_space(node_id.value() as usize) + .unwrap(); + } } if let Some(link) = self.nodes_ctx.link_destroyed() { @@ -271,61 +304,4 @@ impl Graph { None } } - - fn draw_ports(ui_node: &mut NodeConstructor, node: &Node, theme: &Theme, debug: bool) { - let mut ports = node.ports.values().collect::>(); - - // Sorts ports based on alphabetical ordering - ports.sort_by(|a, b| a.name.cmp(&b.name)); - - for port in ports { - let (background, hovered) = match node.media_type { - Some(MediaType::Audio) => (theme.audio_port, theme.audio_port_hovered), - Some(MediaType::Video) => (theme.video_port, theme.video_port_hovered), - Some(MediaType::Midi) => (egui::Color32::RED, egui::Color32::LIGHT_RED), - None => (egui::Color32::GRAY, egui::Color32::LIGHT_GRAY), - }; - let port_name = { - if debug { - format!("{} [{}]", port.name, port.id) - } else { - format!("{} ", port.name) - } - }; - - match port.port_type { - crate::pipewire_impl::PortType::Input => { - ui_node.with_input_attribute( - port.id as usize, - PinArgs { - background: Some(background), - hovered: Some(hovered), - ..Default::default() - }, - |ui| { - egui::Label::new(port_name) - //.text_color(theme.text_color) - .ui(ui) - }, - ); - } - crate::pipewire_impl::PortType::Output => { - ui_node.with_output_attribute( - port.id as usize, - PinArgs { - background: Some(background), - hovered: Some(hovered), - ..Default::default() - }, - |ui| { - egui::Label::new(port_name) - //.text_color(theme.text_color) - .ui(ui) - }, - ); - } - crate::pipewire_impl::PortType::Unknown => {} - } - } - } } diff --git a/src/ui/id.rs b/src/ui/id.rs new file mode 100644 index 0000000..0ce9128 --- /dev/null +++ b/src/ui/id.rs @@ -0,0 +1,19 @@ +use std::hash::Hash; + +use egui::epaint::ahash::AHasher; +use std::hash::Hasher; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub struct Id(u64); + +impl Id { + pub fn new(data: impl Hash) -> Self { + let mut hasher = AHasher::new_with_keys(123, 456); + data.hash(&mut hasher); + Id(hasher.finish()) + } + #[inline] + pub fn value(&self) -> u64 { + self.0 + } +} diff --git a/src/ui/link.rs b/src/ui/link.rs index 34687de..fe3c1a9 100644 --- a/src/ui/link.rs +++ b/src/ui/link.rs @@ -1,9 +1,18 @@ +use super::Id; + #[derive(Debug)] pub struct Link { pub id: u32, - pub from_node: u32, - pub to_node: u32, + pub from_node: Id, + pub to_node: Id, + pub from_port: u32, pub to_port: u32, pub active: bool, } + +impl Link { + pub fn is_self_link(&self) -> bool { + self.from_node == self.to_node + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index df0463c..522676f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,4 +1,5 @@ mod graph; +mod id; mod link; mod node; mod port; @@ -10,8 +11,7 @@ use serde::{Deserialize, Serialize}; use std::sync::mpsc::Receiver; use graph::Graph; -use link::Link; -use node::Node; +use id::Id; use port::Port; pub const INITIAL_WIDTH: u32 = 1280; @@ -19,13 +19,8 @@ pub const INITIAL_HEIGHT: u32 = 720; #[derive(Debug)] pub enum UiMessage { - AddLink { - from_node: u32, - to_node: u32, - from_port: u32, - to_port: u32, - }, RemoveLink(u32), + AddLink { from_port: u32, to_port: u32 }, Exit, } @@ -41,6 +36,9 @@ pub struct Theme { video_port_hovered: egui::Color32, text_color: egui::Color32, + + node_background: egui::Color32, + node_background_hovered: egui::Color32, } impl Default for Theme { @@ -56,6 +54,8 @@ impl Default for Theme { video_port_hovered: egui::Color32::from_rgba_unmultiplied(148, 96, 182, 255), text_color: egui::Color32::WHITE, + node_background: egui::Color32::from_rgba_unmultiplied(50, 50, 50, 255), + node_background_hovered: egui::Color32::from_rgba_unmultiplied(75, 75, 75, 255), } } } @@ -117,6 +117,14 @@ impl GraphUI { ui.color_edit_button_srgba(&mut theme.video_port_hovered); ui.end_row(); + ui.label("Node background"); + ui.color_edit_button_srgba(&mut theme.node_background); + ui.end_row(); + + ui.label("Node background hovered"); + ui.color_edit_button_srgba(&mut theme.node_background_hovered); + ui.end_row(); + ui.label("Text color"); ui.color_edit_button_srgba(&mut theme.text_color); ui.end_row(); @@ -136,7 +144,7 @@ impl GraphUI { .open(&mut self.show_about) .resizable(false) .show(ctx, |ui| { - egui::Grid::new("theme_grid").show(ui, |ui| { + egui::Grid::new("about_grid").show(ui, |ui| { ui.label(env!("CARGO_PKG_NAME")); ui.end_row(); @@ -144,8 +152,8 @@ impl GraphUI { ui.label(env!("CARGO_PKG_VERSION")); ui.end_row(); - ui.label("Author:"); - ui.hyperlink("https://github.com/Ax9D"); + ui.label("Project page"); + ui.hyperlink("https://github.com/Ax9D/pw-viz"); ui.end_row(); }) }); @@ -156,7 +164,7 @@ impl GraphUI { .open(&mut self.show_controls) .resizable(false) .show(ctx, |ui| { - egui::Grid::new("theme_grid").show(ui, |ui| { + egui::Grid::new("controls_grid").show(ui, |ui| { ui.label("Left Click + Drag"); ui.label("Move nodes, create links between nodes"); ui.end_row(); @@ -178,57 +186,47 @@ impl GraphUI { /// Update the graph ui based on the message sent by the pipewire thread fn process_message(&mut self, message: PipewireMessage) { - let _graph = &mut self.graph; - match message { PipewireMessage::NodeAdded { id, name, + description, media_type, } => { - self.graph.add_node(Node::new(id, name, media_type)); + self.graph.add_node(name, id, description, media_type); } - PipewireMessage::NodeRemoved { id } => { - self.graph.remove_node(id); + PipewireMessage::NodeRemoved { name, id } => { + self.graph.remove_node(&name, id); } PipewireMessage::PortAdded { + node_name, node_id, id, name, port_type, } => { - self.graph - .get_node_mut(node_id) - .expect("Port with provided id doesn't exist") - .add_port(Port { - id, - name, - port_type, - }); - } - PipewireMessage::PortRemoved { node_id, id } => { - self.graph - .get_node_mut(node_id) - .expect("Port with provided id doesn't exist") - .remove_port(id); + let port = Port::new(id, name, port_type); + + self.graph.add_port(node_name, node_id, port); } PipewireMessage::LinkAdded { id, - from_node, - to_node, + from_node_name, + to_node_name, from_port, to_port, } => { - self.graph.add_link(Link { - id, - from_node, - to_node, - from_port, - to_port, - active: true, - }); + self.graph + .add_link(id, from_node_name, to_node_name, from_port, to_port); + } + PipewireMessage::PortRemoved { + node_name, + node_id, + id, + } => { + self.graph.remove_port(&node_name, node_id, id); } PipewireMessage::LinkRemoved { id } => { self.graph.remove_link(id); @@ -293,12 +291,12 @@ impl epi::App for GraphUI { } }); egui::menu::menu(ui, "Help", |ui| { - if ui.button("About").clicked() { - self.show_about = true; - } if ui.button("Controls").clicked() { self.show_controls = true; } + if ui.button("About").clicked() { + self.show_about = true; + } }); }); }); @@ -310,16 +308,11 @@ impl epi::App for GraphUI { graph::LinkUpdate::Created { from_port, to_port, - from_node, - to_node, + from_node: _, + to_node: _, } => { self.pipewire_sender - .send(UiMessage::AddLink { - from_port, - to_port, - from_node, - to_node, - }) + .send(UiMessage::AddLink { from_port, to_port }) .expect("Failed to send ui message"); } graph::LinkUpdate::Removed(link_id) => { diff --git a/src/ui/node.rs b/src/ui/node.rs index 40e1c9f..d652067 100644 --- a/src/ui/node.rs +++ b/src/ui/node.rs @@ -1,32 +1,219 @@ use std::collections::HashMap; -use super::port::Port; +use egui::Widget; +use egui_nodes::{NodeConstructor, PinArgs}; + use crate::pipewire_impl::MediaType; +use super::{port::Port, Id, Theme}; + #[derive(Debug)] pub struct Node { - pub id: u32, - pub name: String, - pub media_type: Option, - pub ports: HashMap, // Port id to Port - pub position: Option, + id: Id, + name: String, + pw_nodes: Vec, + pub(super) position: Option, } impl Node { - pub fn new(id: u32, name: String, media_type: Option) -> Self { + pub fn new(id: Id, name: String) -> Self { Self { id, name, + pw_nodes: Vec::new(), + position: None, + } + } + pub fn name(&self) -> &str { + &self.name + } + pub fn id(&self) -> Id { + self.id + } + + pub(super) fn add_pw_node( + &mut self, + id: u32, + description: Option, + media_type: Option, + ) { + self.pw_nodes.push(PwNode { + id, + description, media_type, ports: HashMap::new(), - position: None, + }); + } + //TODO: Use pooling + pub(super) fn remove_pw_node(&mut self, id: u32) -> bool { + self.pw_nodes.retain(|node| node.id != id); + + self.pw_nodes.is_empty() + } + + #[inline] + fn get_pw_node(&mut self, id: u32) -> Option<&mut PwNode> { + self.pw_nodes.iter_mut().find(|node| node.id == id) + } + pub fn add_port(&mut self, node_id: u32, port: Port) { + let pw_node = self.get_pw_node(node_id); + + pw_node + .expect(&format!( + "Couldn't find pipewire node with id {}", + port.id() + )) + .ports + .insert(port.id(), port); + } + pub fn remove_port(&mut self, node_id: u32, port_id: u32) { + if let Some(pw_node) = self.get_pw_node(node_id) { + pw_node.ports.remove(&port_id); + } else { + log::error!("Pipewire node with id: {} was never added", node_id); } } + fn draw_ports<'graph, 'node>( + ui_node: &'graph mut NodeConstructor<'node>, + node: &'node PwNode, + theme: &'node Theme, + debug: bool, + ) { + let mut ports = node.ports.values().collect::>(); + + //Sorts ports based on alphabetical ordering + ports.sort_by(|a, b| a.name().cmp(b.name())); + + for (ix, port) in ports.iter().enumerate() { + let (background, hovered) = match &node.media_type { + Some(MediaType::Audio) => (theme.audio_port, theme.audio_port_hovered), + Some(MediaType::Video) => (theme.video_port, theme.video_port_hovered), + Some(MediaType::Midi) => (egui::Color32::RED, egui::Color32::LIGHT_RED), + None => (egui::Color32::GRAY, egui::Color32::LIGHT_GRAY), + }; + let port_name = { + if debug { + format!("{} [{}]", port.name(), port.id()) + } else { + format!("{} ", port.name()) + } + }; + + let first = debug && ix == 0; - pub fn add_port(&mut self, port: Port) { - self.ports.insert(port.id, port); + let node_desc_str = if let Some(desc) = &node.description { + desc + } else { + "" + }; + + let node_desc = format!("{} [{}]", node_desc_str, node.id); + + match port.port_type() { + crate::pipewire_impl::PortType::Input => { + if first { + ui_node.with_input_attribute( + port.id() as usize, + PinArgs { + background: Some(background), + hovered: Some(hovered), + ..Default::default() + }, + move |ui| { + ui.add( + egui::Label::new(node_desc).text_color(egui::Color32::WHITE), + ); + ui.label(port_name) + }, + ); + } else { + ui_node.with_input_attribute( + port.id() as usize, + PinArgs { + background: Some(background), + hovered: Some(hovered), + ..Default::default() + }, + |ui| ui.label(port_name), + ); + } + } + crate::pipewire_impl::PortType::Output => { + if first { + ui_node.with_output_attribute( + port.id() as usize, + PinArgs { + background: Some(background), + hovered: Some(hovered), + ..Default::default() + }, + move |ui| { + ui.add( + egui::Label::new(node_desc).text_color(egui::Color32::WHITE), + ); + ui.label(port_name) + }, + ); + } else { + ui_node.with_output_attribute( + port.id() as usize, + PinArgs { + background: Some(background), + hovered: Some(hovered), + ..Default::default() + }, + |ui| ui.label(port_name), + ); + } + } + crate::pipewire_impl::PortType::Unknown => {} + } + } } - pub fn remove_port(&mut self, port_id: u32) { - self.ports.remove(&port_id); + + pub fn draw<'graph, 'node>( + &'node self, + ui_node: &'graph mut NodeConstructor<'node>, + theme: &'node Theme, + debug_view: bool, + ) { + // let media_type = node.media_type; + // let media_emoji = match media_type { + // Some(MediaType::Audio) => "🔉", + // Some(MediaType::Video) => "💻", + // Some(MediaType::Midi) => "🎹", + // None => "", + // }; + let mut media_type = String::new(); + for node in self.pw_nodes.iter() { + let media_emoji = match node.media_type { + Some(MediaType::Audio) => "🔉", + Some(MediaType::Video) => "💻", + Some(MediaType::Midi) => "🎹", + None => "", + }; + + if !media_type.contains(media_emoji) { + media_type.push_str(&format!(" {}", media_emoji)); + } + } + + ui_node.with_title(move |ui| { + egui::Label::new(&format!("{} {}", self.name(), media_type)) + .text_color(theme.text_color) + .ui(ui) + }); + + for node in self.pw_nodes.iter() { + Self::draw_ports(ui_node, node, theme, debug_view); + } } } + +#[derive(Debug)] +struct PwNode { + id: u32, //Pipewire id of the node + description: Option, + media_type: Option, + ports: HashMap, +} diff --git a/src/ui/port.rs b/src/ui/port.rs index 0820b3b..57355ce 100644 --- a/src/ui/port.rs +++ b/src/ui/port.rs @@ -6,3 +6,21 @@ pub struct Port { pub name: String, pub port_type: PortType, } +impl Port { + pub fn new(id: u32, name: String, port_type: PortType) -> Self { + Self { + id, + name, + port_type, + } + } + pub fn id(&self) -> u32 { + self.id + } + pub fn name(&self) -> &str { + &self.name + } + pub fn port_type(&self) -> PortType { + self.port_type + } +}