mirror of
https://gitlab.freedesktop.org/pipewire/helvum
synced 2026-03-15 11:36:11 +08:00
Display Nodes/Ports/Links obtained from pipewire server
This commit is contained in:
43
src/main.rs
43
src/main.rs
@@ -1,11 +1,13 @@
|
||||
mod pipewire_connection;
|
||||
mod view;
|
||||
|
||||
use gio::prelude::*;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PipewireLink {
|
||||
pub node_from: u32,
|
||||
pub port_from: u32,
|
||||
@@ -15,33 +17,18 @@ pub struct PipewireLink {
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
gtk::init()?;
|
||||
let mut graphview = view::GraphView::new();
|
||||
let graphview = Rc::new(RefCell::new(view::GraphView::new()));
|
||||
|
||||
// For UI Testing purposes
|
||||
let mut node = view::PipewireNode::new("Test Node");
|
||||
node.add_ingoing_port(10, gtk::Button::with_label("Ingoing Port"));
|
||||
node.add_outgoing_port(11, gtk::Button::with_label("Outgoing Port"));
|
||||
node.add_outgoing_port(12, gtk::Button::with_label("Outgoing Port 2"));
|
||||
|
||||
let mut node2 = view::PipewireNode::new("Test Node 2");
|
||||
node2.add_ingoing_port(13, gtk::Button::with_label("Ingoing Port"));
|
||||
node2.add_outgoing_port(14, gtk::Button::with_label("Outgoing Port"));
|
||||
node2.add_outgoing_port(15, gtk::Button::with_label("Outgoing Port 2"));
|
||||
|
||||
graphview.add_node(0, node);
|
||||
graphview.add_node(1, node2);
|
||||
graphview.add_link(
|
||||
2,
|
||||
PipewireLink {
|
||||
node_from: 0,
|
||||
port_from: 12,
|
||||
node_to: 1,
|
||||
port_to: 13,
|
||||
},
|
||||
);
|
||||
// End UI Testing
|
||||
|
||||
let graphview = Rc::new(graphview);
|
||||
// Create the connection to the pipewire server and do an initial roundtrip before showing the view,
|
||||
// so that the graph is already populated when the window opens.
|
||||
let pw_con = pipewire_connection::PipewireConnection::new(graphview.clone())
|
||||
.expect("Failed to initialize pipewire connection");
|
||||
pw_con.roundtrip();
|
||||
// From now on, call roundtrip() every second.
|
||||
glib::timeout_add_seconds_local(1, move || {
|
||||
pw_con.roundtrip();
|
||||
Continue(true)
|
||||
});
|
||||
|
||||
let app = gtk::Application::new(
|
||||
Some("org.freedesktop.pipewire.graphui"),
|
||||
@@ -53,7 +40,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let window = gtk::ApplicationWindow::new(app);
|
||||
window.set_default_size(800, 600);
|
||||
window.set_title("Pipewire Graph Editor");
|
||||
window.add(&graphview.widget);
|
||||
window.add(&graphview.borrow().widget);
|
||||
window.show_all();
|
||||
}));
|
||||
|
||||
|
||||
155
src/pipewire_connection.rs
Normal file
155
src/pipewire_connection.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use glib::clone;
|
||||
use pipewire as pw;
|
||||
use pw::{port::Direction, registry::ObjectType, PW_ID_CORE};
|
||||
|
||||
use crate::PipewireLink;
|
||||
|
||||
pub struct PipewireConnection {
|
||||
mainloop: pw::MainLoop,
|
||||
_context: pw::Context<pw::MainLoop>,
|
||||
core: pw::Core,
|
||||
_registry: pw::registry::Registry,
|
||||
_reg_listeners: pw::registry::Listener,
|
||||
}
|
||||
|
||||
impl PipewireConnection {
|
||||
pub fn new(graphview: Rc<RefCell<crate::view::GraphView>>) -> Result<Self, String> {
|
||||
pw::init();
|
||||
let mainloop = pw::MainLoop::new().map_err(|_| "Failed to create pipewire mainloop!")?;
|
||||
let context =
|
||||
pw::Context::new(&mainloop).map_err(|_| "Failed to create pipewire context")?;
|
||||
let core = context
|
||||
.connect()
|
||||
.map_err(|_| "Failed to connect to pipewire core")?;
|
||||
let registry = core.get_registry();
|
||||
|
||||
let reg_listeners = registry
|
||||
.add_listener_local()
|
||||
.global(clone!(@weak graphview => @default-panic, move |global| {
|
||||
PipewireConnection::handle_global(graphview, global)
|
||||
}))
|
||||
.global_remove(|_| { /* TODO */ })
|
||||
.register();
|
||||
|
||||
Ok(Self {
|
||||
mainloop,
|
||||
_context: context,
|
||||
core,
|
||||
_registry: registry,
|
||||
_reg_listeners: reg_listeners,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn roundtrip(&self) {
|
||||
let done = Rc::new(Cell::new(false));
|
||||
let pending = self.core.sync(0);
|
||||
|
||||
let done_clone = done.clone();
|
||||
let loop_clone = self.mainloop.clone();
|
||||
|
||||
let _listener = self
|
||||
.core
|
||||
.add_listener_local()
|
||||
.done(move |id, seq| {
|
||||
if id == PW_ID_CORE && seq == pending {
|
||||
done_clone.set(true);
|
||||
loop_clone.quit();
|
||||
}
|
||||
})
|
||||
.register();
|
||||
|
||||
while !done.get() {
|
||||
self.mainloop.run();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_global(
|
||||
graphview: Rc<RefCell<crate::view::GraphView>>,
|
||||
global: pw::registry::GlobalObject,
|
||||
) {
|
||||
match global.type_ {
|
||||
ObjectType::Node => {
|
||||
let node_widget = crate::view::Node::new(&format!(
|
||||
"{}",
|
||||
global
|
||||
.props
|
||||
.map(|dict| String::from(
|
||||
dict.get("node.nick")
|
||||
.or(dict.get("node.description"))
|
||||
.or(dict.get("node.name"))
|
||||
.unwrap_or_default()
|
||||
))
|
||||
.unwrap_or_default()
|
||||
));
|
||||
|
||||
graphview.borrow_mut().add_node(global.id, node_widget);
|
||||
}
|
||||
ObjectType::Port => {
|
||||
let props = global.props.expect("Port object is missing properties");
|
||||
let port_label = format!("{}", props.get("port.name").unwrap_or_default());
|
||||
let node_id: u32 = props
|
||||
.get("node.id")
|
||||
.expect("Port has no node.id property!")
|
||||
.parse()
|
||||
.expect("Could not parse node.id property");
|
||||
let port = crate::view::port::Port::new(
|
||||
global.id,
|
||||
&port_label,
|
||||
if matches!(props.get("port.direction"), Some("in")) {
|
||||
Direction::Input
|
||||
} else {
|
||||
Direction::Output
|
||||
},
|
||||
);
|
||||
|
||||
graphview
|
||||
.borrow_mut()
|
||||
.add_port_to_node(node_id, global.id, port);
|
||||
}
|
||||
ObjectType::Link => {
|
||||
let props = global.props.expect("Link object is missing properties");
|
||||
let input_node: u32 = props
|
||||
.get("link.input.node")
|
||||
.expect("Link has no link.input.node property")
|
||||
.parse()
|
||||
.expect("Could not parse link.input.node property");
|
||||
let input_port: u32 = props
|
||||
.get("link.input.port")
|
||||
.expect("Link has no link.input.port property")
|
||||
.parse()
|
||||
.expect("Could not parse link.input.port property");
|
||||
let output_node: u32 = props
|
||||
.get("link.output.node")
|
||||
.expect("Link has no link.input.node property")
|
||||
.parse()
|
||||
.expect("Could not parse link.input.node property");
|
||||
let output_port: u32 = props
|
||||
.get("link.output.port")
|
||||
.expect("Link has no link.output.port property")
|
||||
.parse()
|
||||
.expect("Could not parse link.output.port property");
|
||||
graphview.borrow_mut().add_link(
|
||||
global.id,
|
||||
PipewireLink {
|
||||
node_from: output_node,
|
||||
port_from: output_port,
|
||||
node_to: input_node,
|
||||
port_to: input_port,
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*impl Drop for PipewireConnection {
|
||||
fn drop(&mut self) {
|
||||
unsafe { pw::deinit() }
|
||||
}
|
||||
}*/
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::PipewireNode;
|
||||
use super::Node;
|
||||
use cairo::Context;
|
||||
use glib::clone;
|
||||
use gtk::{prelude::*, LayoutExt};
|
||||
@@ -6,7 +6,7 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
pub struct GraphView {
|
||||
pub(crate) widget: gtk::Layout,
|
||||
nodes: Rc<RefCell<HashMap<u32, PipewireNode>>>,
|
||||
nodes: Rc<RefCell<HashMap<u32, Node>>>,
|
||||
links: Rc<RefCell<HashMap<u32, crate::PipewireLink>>>,
|
||||
}
|
||||
|
||||
@@ -28,16 +28,29 @@ impl GraphView {
|
||||
result
|
||||
}
|
||||
|
||||
pub fn add_node(&mut self, id: u32, node: PipewireNode) {
|
||||
pub fn add_node(&mut self, id: u32, node: Node) {
|
||||
// TODO: Find a free position to put the widget at.
|
||||
self.widget.put(
|
||||
&node.widget,
|
||||
(self.nodes.borrow().len() % 4 * 400) as i32,
|
||||
(self.nodes.borrow().len() / 4 * 100) as i32,
|
||||
(self.nodes.borrow().len() / 4 * 400) as i32,
|
||||
(self.nodes.borrow().len() % 4 * 100) as i32,
|
||||
);
|
||||
node.widget.show_all();
|
||||
self.nodes.borrow_mut().insert(id, node);
|
||||
}
|
||||
|
||||
pub fn add_port_to_node(&mut self, node_id: u32, port_id: u32, port: super::port::Port) {
|
||||
if let Some(node) = self.nodes.borrow_mut().get_mut(&node_id) {
|
||||
node.add_port(port_id, port);
|
||||
} else {
|
||||
// FIXME: Log this instead
|
||||
eprintln!(
|
||||
"Node with id {} not found when trying to add port with id {} to graph",
|
||||
node_id, port_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a link to the graph.
|
||||
///
|
||||
/// `add_link` takes three arguments: `link_id` is the id of the link as assigned by the pipewire server,
|
||||
@@ -49,7 +62,7 @@ impl GraphView {
|
||||
}
|
||||
|
||||
fn draw(
|
||||
nodes: Rc<RefCell<HashMap<u32, PipewireNode>>>,
|
||||
nodes: Rc<RefCell<HashMap<u32, Node>>>,
|
||||
links: Rc<RefCell<HashMap<u32, crate::PipewireLink>>>,
|
||||
cr: &Context,
|
||||
) {
|
||||
@@ -58,44 +71,40 @@ fn draw(
|
||||
cr.paint();
|
||||
cr.set_source_rgb(0.0, 0.0, 0.0);
|
||||
for link in links.borrow().values() {
|
||||
let (from_alloc, to_alloc) = get_allocs(nodes.clone(), link);
|
||||
if let Some((from_alloc, to_alloc)) = get_allocs(nodes.clone(), link) {
|
||||
let from_x: f64 = (from_alloc.x + from_alloc.width).into();
|
||||
let from_y: f64 = (from_alloc.y + (from_alloc.height / 2)).into();
|
||||
cr.move_to(from_x, from_y);
|
||||
|
||||
let from_x: f64 = (from_alloc.x + from_alloc.width).into();
|
||||
let from_y: f64 = (from_alloc.y + (from_alloc.height / 2)).into();
|
||||
cr.move_to(
|
||||
from_x, from_y
|
||||
);
|
||||
let to_x: f64 = to_alloc.x.into();
|
||||
let to_y: f64 = (to_alloc.y + (to_alloc.height / 2)).into();
|
||||
cr.curve_to(from_x + 75.0, from_y, to_x - 75.0, to_y, to_x, to_y);
|
||||
|
||||
let to_x: f64 = to_alloc.x.into();
|
||||
let to_y: f64 = (to_alloc.y + (to_alloc.height / 2)).into();
|
||||
cr.curve_to(
|
||||
from_x + 75.0, from_y,
|
||||
to_x - 75.0, to_y,
|
||||
to_x, to_y
|
||||
);
|
||||
|
||||
cr.stroke();
|
||||
cr.stroke();
|
||||
} else {
|
||||
eprintln!("Could not get allocation of ports of link: {:?}", link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_allocs(
|
||||
nodes: Rc<RefCell<HashMap<u32, PipewireNode>>>,
|
||||
nodes: Rc<RefCell<HashMap<u32, Node>>>,
|
||||
link: &crate::PipewireLink,
|
||||
) -> (gtk::Allocation, gtk::Allocation) {
|
||||
) -> Option<(gtk::Allocation, gtk::Allocation)> {
|
||||
println!();
|
||||
|
||||
let from_alloc = &nodes
|
||||
.borrow()
|
||||
.get(&link.node_from)
|
||||
.unwrap()
|
||||
.get_outgoing_port(link.port_from)
|
||||
.unwrap()
|
||||
.get(&link.node_from)?
|
||||
.get_port(link.port_from)?
|
||||
.widget
|
||||
.get_allocation();
|
||||
let to_alloc = &nodes
|
||||
.borrow()
|
||||
.get(&link.node_to)
|
||||
.unwrap()
|
||||
.get_ingoing_port(link.port_to)
|
||||
.unwrap()
|
||||
.get(&link.node_to)?
|
||||
.get_port(link.port_to)?
|
||||
.widget
|
||||
.get_allocation();
|
||||
|
||||
(from_alloc.to_owned(), to_alloc.to_owned())
|
||||
Some((from_alloc.to_owned(), to_alloc.to_owned()))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod graph_view;
|
||||
mod pipewire_node;
|
||||
mod node;
|
||||
pub mod port;
|
||||
|
||||
pub use graph_view::GraphView;
|
||||
pub use pipewire_node::PipewireNode;
|
||||
pub use node::Node;
|
||||
|
||||
@@ -1,40 +1,47 @@
|
||||
use gtk::prelude::*;
|
||||
use gdk::prelude::*;
|
||||
use gtk::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct PipewireNode {
|
||||
use pipewire::port::Direction;
|
||||
|
||||
pub struct Node {
|
||||
pub(super) widget: gtk::Grid,
|
||||
label: gtk::Label,
|
||||
label_box: gtk::EventBox,
|
||||
ingoing_ports: HashMap<u32, gtk::Button>,
|
||||
outgoing_ports: HashMap<u32, gtk::Button>,
|
||||
ports: HashMap<u32, super::port::Port>,
|
||||
num_ports_in: u32,
|
||||
num_ports_out: u32,
|
||||
}
|
||||
|
||||
impl PipewireNode {
|
||||
impl Node {
|
||||
pub fn new(name: &str) -> Self {
|
||||
let result = Self {
|
||||
widget: gtk::Grid::new(),
|
||||
label: gtk::Label::new(Some(name)),
|
||||
label_box: gtk::EventBox::new(),
|
||||
ingoing_ports: HashMap::new(),
|
||||
outgoing_ports: HashMap::new(),
|
||||
ports: HashMap::new(),
|
||||
num_ports_in: 0,
|
||||
num_ports_out: 0,
|
||||
};
|
||||
|
||||
result.label_box.add(&result.label);
|
||||
result.widget.attach(&result.label_box, 0, 0, 2, 1);
|
||||
|
||||
// Setup needed events for dragging a node.
|
||||
result
|
||||
.label_box
|
||||
.add_events(gdk::EventMask::BUTTON1_MOTION_MASK);
|
||||
|
||||
// Setup callback for dragging the node.
|
||||
result
|
||||
.label_box
|
||||
.connect_motion_notify_event(|label, event| {
|
||||
let grid = label
|
||||
let node_frame = label
|
||||
.get_ancestor(gtk::Grid::static_type())
|
||||
.unwrap()
|
||||
.dynamic_cast::<gtk::Grid>()
|
||||
.unwrap();
|
||||
let graphview = grid
|
||||
let graphview = node_frame
|
||||
.get_ancestor(gtk::Layout::static_type())
|
||||
.unwrap()
|
||||
.dynamic_cast::<gtk::Layout>()
|
||||
@@ -47,8 +54,8 @@ impl PipewireNode {
|
||||
|
||||
// TODO: Calculate proper values to center the mouse on the label
|
||||
// instead of using hardcoded offsets.
|
||||
graphview.set_child_x(&grid, x as i32 - offset_x - 100);
|
||||
graphview.set_child_y(&grid, y as i32 - offset_y - 50);
|
||||
graphview.set_child_x(&node_frame, x as i32 - offset_x - 100);
|
||||
graphview.set_child_y(&node_frame, y as i32 - offset_y - 50);
|
||||
|
||||
// FIXME: If links become proper widgets,
|
||||
// we don't need to redraw the full graph everytime.
|
||||
@@ -60,23 +67,25 @@ impl PipewireNode {
|
||||
result
|
||||
}
|
||||
|
||||
pub fn add_ingoing_port(&mut self, id: u32, port: gtk::Button) {
|
||||
self.widget
|
||||
.attach(&port, 0, (self.ingoing_ports.len() + 1) as i32, 1, 1);
|
||||
self.ingoing_ports.insert(id, port);
|
||||
pub fn add_port(&mut self, id: u32, port: super::port::Port) {
|
||||
match port.direction {
|
||||
Direction::Input => {
|
||||
self.widget
|
||||
.attach(&port.widget, 0, (self.num_ports_in + 1) as i32, 1, 1);
|
||||
self.num_ports_in += 1;
|
||||
}
|
||||
Direction::Output => {
|
||||
self.widget
|
||||
.attach(&port.widget, 1, (self.num_ports_out + 1) as i32, 1, 1);
|
||||
self.num_ports_out += 1;
|
||||
}
|
||||
}
|
||||
|
||||
port.widget.show_all();
|
||||
self.ports.insert(id, port);
|
||||
}
|
||||
|
||||
pub fn add_outgoing_port(&mut self, id: u32, port: gtk::Button) {
|
||||
self.widget
|
||||
.attach(&port, 1, (self.outgoing_ports.len() + 1) as i32, 1, 1);
|
||||
self.outgoing_ports.insert(id, port);
|
||||
}
|
||||
|
||||
pub fn get_ingoing_port(&self, id: u32) -> Option<>k::Button> {
|
||||
self.ingoing_ports.get(&id)
|
||||
}
|
||||
|
||||
pub fn get_outgoing_port(&self, id: u32) -> Option<>k::Button> {
|
||||
self.outgoing_ports.get(&id)
|
||||
pub fn get_port(&self, id: u32) -> Option<&super::port::Port> {
|
||||
self.ports.get(&id)
|
||||
}
|
||||
}
|
||||
16
src/view/port.rs
Normal file
16
src/view/port.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
/// Graphical representation of a pipewire port.
|
||||
pub struct Port {
|
||||
pub(super) widget: gtk::Button,
|
||||
pub id: u32,
|
||||
pub direction: pipewire::port::Direction,
|
||||
}
|
||||
|
||||
impl Port {
|
||||
pub fn new(id: u32, name: &str, direction: pipewire::port::Direction) -> Self {
|
||||
Self {
|
||||
widget: gtk::Button::with_label(name),
|
||||
id,
|
||||
direction
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user