Color links and ports according to their formats

Format params for links and ports are now being watched for in the pipewire connection code.

The parsed media type is then set on the port widget / link object
and they are colored accordingly.

For ports, which were already colored before, this new method of determining the media type
should be more reliable and accurate as this uses the real Format/EnumFormat
params instead of parsing optional properties.
This commit is contained in:
Tom A. Wagner
2023-07-30 12:09:08 +02:00
parent ba73d8cdcc
commit 7f754b207c
8 changed files with 362 additions and 148 deletions

View File

@@ -56,9 +56,13 @@ mod imp {
move |msg| { move |msg| {
match msg { match msg {
PipewireMessage::NodeAdded { id, name, node_type } => imp.add_node(id, name.as_str(), node_type), 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::PortAdded { id, node_id, name, direction } => imp.add_port(id, name.as_str(), node_id, direction),
PipewireMessage::LinkAdded{ id, port_from, port_to, active} => imp.add_link(id, port_from, port_to, active), 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::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::NodeRemoved { id } => imp.remove_node(id),
PipewireMessage::PortRemoved { id, node_id } => imp.remove_port(id, node_id), PipewireMessage::PortRemoved { id, node_id } => imp.remove_port(id, node_id),
PipewireMessage::LinkRemoved { id } => imp.remove_link(id) PipewireMessage::LinkRemoved { id } => imp.remove_link(id)
@@ -96,14 +100,7 @@ mod imp {
} }
/// Add a new port to the view. /// Add a new port to the view.
fn add_port( fn add_port(&self, id: u32, name: &str, node_id: u32, direction: pipewire::spa::Direction) {
&self,
id: u32,
name: &str,
node_id: u32,
direction: pipewire::spa::Direction,
media_type: Option<MediaType>,
) {
log::info!("Adding port to graph: id {}", id); log::info!("Adding port to graph: id {}", id);
let mut items = self.items.borrow_mut(); let mut items = self.items.borrow_mut();
@@ -117,7 +114,7 @@ mod imp {
return; 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. // Create or delete a link if the widget emits the "port-toggled" signal.
port.connect_local( port.connect_local(
@@ -139,6 +136,21 @@ mod imp {
node.add_port(port); 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::<graph::Port>() 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` /// Remove the port with the id `id` from the node with the id `node_id`
/// from the view. /// from the view.
fn remove_port(&self, id: u32, node_id: u32) { fn remove_port(&self, id: u32, node_id: u32) {
@@ -167,7 +179,14 @@ mod imp {
} }
/// Add a new link to the view. /// 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); log::info!("Adding link to graph: id {}", id);
let mut items = self.items.borrow_mut(); let mut items = self.items.borrow_mut();
@@ -193,6 +212,7 @@ mod imp {
link.set_output_port(Some(&output_port)); link.set_output_port(Some(&output_port));
link.set_input_port(Some(&input_port)); link.set_input_port(Some(&input_port));
link.set_active(active); link.set_active(active);
link.set_media_type(media_type);
items.insert(id, link.clone().upcast()); items.insert(id, link.clone().upcast());
@@ -223,6 +243,20 @@ mod imp {
link.set_active(active); 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::<graph::Link>() 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. // Toggle a link between the two specified ports on the remote pipewire server.
fn toggle_link(&self, port_from: u32, port_to: u32) { fn toggle_link(&self, port_from: u32, port_to: u32) {
let sender = self.pw_sender.get().expect("pw_sender shoud be set"); let sender = self.pw_sender.get().expect("pw_sender shoud be set");

View File

@@ -20,7 +20,7 @@ mod pipewire_connection;
mod ui; mod ui;
use gtk::prelude::*; use gtk::prelude::*;
use pipewire::spa::Direction; use pipewire::spa::{format::MediaType, Direction};
/// Messages sent by the GTK thread to notify the pipewire thread. /// Messages sent by the GTK thread to notify the pipewire thread.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -44,18 +44,26 @@ pub enum PipewireMessage {
node_id: u32, node_id: u32,
name: String, name: String,
direction: Direction, direction: Direction,
media_type: Option<MediaType>, },
PortFormatChanged {
id: u32,
media_type: MediaType,
}, },
LinkAdded { LinkAdded {
id: u32, id: u32,
port_from: u32, port_from: u32,
port_to: u32, port_to: u32,
active: bool, active: bool,
media_type: MediaType,
}, },
LinkStateChanged { LinkStateChanged {
id: u32, id: u32,
active: bool, active: bool,
}, },
LinkFormatChanged {
id: u32,
media_type: MediaType,
},
NodeRemoved { NodeRemoved {
id: u32, id: u32,
}, },
@@ -74,13 +82,6 @@ pub enum NodeType {
Output, Output,
} }
#[derive(Debug, Copy, Clone)]
pub enum MediaType {
Audio,
Video,
Midi,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PipewireLink { pub struct PipewireLink {
pub node_from: u32, pub node_from: u32,

View File

@@ -21,11 +21,15 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc};
use gtk::glib::{self, clone}; use gtk::glib::{self, clone};
use log::{debug, info, warn}; use log::{debug, info, warn};
use pipewire::{ use pipewire::{
link::{Link, LinkChangeMask, LinkListener, LinkState}, link::{Link, LinkChangeMask, LinkInfo, LinkListener, LinkState},
port::{Port, PortChangeMask, PortInfo, PortListener},
prelude::*, prelude::*,
properties, properties,
registry::{GlobalObject, Registry}, registry::{GlobalObject, Registry},
spa::{Direction, ForeignDict}, spa::{
param::{ParamInfoFlags, ParamType},
ForeignDict,
},
types::ObjectType, types::ObjectType,
Context, Core, MainLoop, Context, Core, MainLoop,
}; };
@@ -34,6 +38,10 @@ use crate::{GtkMessage, MediaType, NodeType, PipewireMessage};
use state::{Item, State}; use state::{Item, State};
enum ProxyItem { enum ProxyItem {
Port {
proxy: Port,
_listener: PortListener,
},
Link { Link {
_proxy: Link, _proxy: Link,
_listener: LinkListener, _listener: LinkListener,
@@ -67,7 +75,7 @@ pub(super) fn thread_main(
.global(clone!(@strong gtk_sender, @weak registry, @strong proxies, @strong state => .global(clone!(@strong gtk_sender, @weak registry, @strong proxies, @strong state =>
move |global| match global.type_ { move |global| match global.type_ {
ObjectType::Node => handle_node(global, &gtk_sender, &state), ObjectType::Node => handle_node(global, &gtk_sender, &state),
ObjectType::Port => handle_port(global, &gtk_sender, &state), ObjectType::Port => handle_port(global, &gtk_sender, &registry, &proxies, &state),
ObjectType::Link => handle_link(global, &gtk_sender, &registry, &proxies, &state), ObjectType::Link => handle_link(global, &gtk_sender, &registry, &proxies, &state),
_ => { _ => {
// Other objects are not interesting to us // Other objects are not interesting to us
@@ -115,19 +123,6 @@ fn handle_node(
.unwrap_or_default(), .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| { let media_class = |class: &str| {
if class.contains("Sink") || class.contains("Input") { if class.contains("Sink") || class.contains("Input") {
Some(NodeType::Input) Some(NodeType::Input)
@@ -149,13 +144,7 @@ fn handle_node(
}) })
.or_else(|| props.get("media.class").and_then(media_class)); .or_else(|| props.get("media.class").and_then(media_class));
state.borrow_mut().insert( state.borrow_mut().insert(node.id, Item::Node);
node.id,
Item::Node {
// widget: node_widget,
media_type,
},
);
sender sender
.send(PipewireMessage::NodeAdded { .send(PipewireMessage::NodeAdded {
@@ -170,45 +159,107 @@ fn handle_node(
fn handle_port( fn handle_port(
port: &GlobalObject<ForeignDict>, port: &GlobalObject<ForeignDict>,
sender: &glib::Sender<PipewireMessage>, sender: &glib::Sender<PipewireMessage>,
registry: &Rc<Registry>,
proxies: &Rc<RefCell<HashMap<u32, ProxyItem>>>,
state: &Rc<RefCell<State>>, state: &Rc<RefCell<State>>,
) { ) {
let props = port let port_id = port.id;
.props let proxy: Port = registry.bind(port).expect("Failed to bind to port proxy");
.as_ref() let listener = proxy
.expect("Port object is missing properties"); .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<RefCell<HashMap<u32, ProxyItem>>>,
state: &Rc<RefCell<State>>,
sender: &glib::Sender<PipewireMessage>,
) {
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;
};
let mut state = state.borrow_mut();
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 name = props.get("port.name").unwrap_or_default().to_string();
let node_id: u32 = props let node_id: u32 = props
.get("node.id") .get("node.id")
.expect("Port has no node.id property!") .expect("Port has no node.id property!")
.parse() .parse()
.expect("Could not parse node.id property"); .expect("Could not parse node.id property");
let direction = if matches!(props.get("port.direction"), Some("in")) {
Direction::Input
} else {
Direction::Output
};
// Find out the nodes media type so that the port can be colored. state.insert(id, Item::Port { node_id });
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
};
// Save node_id so we can delete this port easily. let params = info.params();
state.borrow_mut().insert(port.id, Item::Port { node_id }); 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 sender
.send(PipewireMessage::PortAdded { .send(PipewireMessage::PortAdded {
id: port.id, id,
node_id, node_id,
name, name,
direction, direction: info.direction(),
media_type,
}) })
.expect("Failed to send message"); .expect("Failed to send message");
} }
}
fn handle_port_enum_format(
port_id: u32,
param: Option<&pipewire::spa::pod::Pod>,
sender: &glib::Sender<PipewireMessage>,
) {
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::PortFormatChanged {
id: port_id,
media_type,
})
.expect("Failed to send message")
}
/// Handle a new link being added /// Handle a new link being added
fn handle_link( fn handle_link(
@@ -227,38 +278,7 @@ fn handle_link(
let listener = proxy let listener = proxy
.add_listener_local() .add_listener_local()
.info(clone!(@strong state, @strong sender => move |info| { .info(clone!(@strong state, @strong sender => move |info| {
debug!("Received link info: {:?}", info); handle_link_info(info, &state, &sender);
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"
);
}
})) }))
.register(); .register();
@@ -271,6 +291,53 @@ fn handle_link(
); );
} }
fn handle_link_info(
info: &LinkInfo,
state: &Rc<RefCell<State>>,
sender: &glib::Sender<PipewireMessage>,
) {
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. /// Toggle a link between the two specified ports.
fn toggle_link( fn toggle_link(
port_from: u32, 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
}

View File

@@ -16,15 +16,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::MediaType;
/// Any pipewire item we need to keep track of. /// Any pipewire item we need to keep track of.
/// These will be saved in the `State` struct associated with their id. /// These will be saved in the `State` struct associated with their id.
pub(super) enum Item { pub(super) enum Item {
Node { Node,
// Keep track of the nodes media type to color ports on it.
media_type: Option<MediaType>,
},
Port { Port {
// Save the id of the node this is on so we can remove the port from it // Save the id of the node this is on so we can remove the port from it
// when it is deleted. // when it is deleted.

View File

@@ -15,23 +15,23 @@
SPDX-License-Identifier: GPL-3.0-only SPDX-License-Identifier: GPL-3.0-only
*/ */
@define-color audio rgb(50,100,240); @define-color media-type-audio rgb( 50, 100, 240);
@define-color video rgb(200,200,0); @define-color media-type-video rgb(200, 200, 0);
@define-color midi rgb(200,0,50); @define-color media-type-midi rgb(200, 0, 50);
@define-color graphview-link #808080; @define-color media-type-unknown rgb(128, 128, 128);
.audio { .audio {
background: @audio; background: @media-type-audio;
color: black; color: black;
} }
.video { .video {
background: @video; background: @media-type-video;
color: black; color: black;
} }
.midi { .midi {
background: @midi; background: @media-type-midi;
color: black; color: black;
} }

View File

@@ -43,8 +43,27 @@ mod imp {
}; };
use log::warn; use log::warn;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use pipewire::spa::format::MediaType;
use pipewire::spa::Direction; 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 { pub struct DragState {
node: glib::WeakRef<Node>, node: glib::WeakRef<Node>,
/// This stores the offset of the pointer to the origin of the node, /// This stores the offset of the pointer to the origin of the node,
@@ -510,6 +529,7 @@ mod imp {
output_anchor: &Point, output_anchor: &Point,
input_anchor: &Point, input_anchor: &Point,
active: bool, active: bool,
color: &gdk::RGBA,
) { ) {
let output_x: f64 = output_anchor.x().into(); let output_x: f64 = output_anchor.x().into();
let output_y: f64 = output_anchor.y().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_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 // 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 // a similar y coordinate, apply a y offset to the control points
// so that the curve sticks out a bit. // 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 { let Some(port_anchor) = port.compute_point(&*self.obj(), &port.link_anchor()) else {
return; return;
}; };
@@ -579,7 +606,9 @@ mod imp {
_ => unreachable!(), _ => 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: &gtk::Snapshot) { fn snapshot_links(&self, widget: &super::GraphView, snapshot: &gtk::Snapshot) {
@@ -594,30 +623,45 @@ mod imp {
link_cr.set_line_width(2.0 * self.zoom_factor.get()); link_cr.set_line_width(2.0 * self.zoom_factor.get());
let rgba = widget let colors = Colors {
audio: widget
.style_context() .style_context()
.lookup_color("graphview-link") .lookup_color("media-type-audio")
.unwrap_or(gtk::gdk::RGBA::BLACK); .expect("color not found"),
video: widget
link_cr.set_source_rgba( .style_context()
rgba.red().into(), .lookup_color("media-type-video")
rgba.green().into(), .expect("color not found"),
rgba.blue().into(), midi: widget
rgba.alpha().into(), .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() { 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 // TODO: Do not draw links when they are outside the view
let Some((output_anchor, input_anchor)) = self.get_link_coordinates(link) else { let Some((output_anchor, input_anchor)) = self.get_link_coordinates(link) else {
warn!("Could not get allocation of ports of link: {:?}", link); warn!("Could not get allocation of ports of link: {:?}", link);
continue; 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() { 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(); 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.imp().links.borrow_mut().insert(link);
self.queue_draw(); self.queue_draw();
} }

View File

@@ -15,6 +15,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use gtk::{glib, prelude::*, subclass::prelude::*}; use gtk::{glib, prelude::*, subclass::prelude::*};
use pipewire::spa::format::MediaType;
use super::Port; use super::Port;
@@ -25,11 +26,22 @@ mod imp {
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
#[derive(Default)]
pub struct Link { pub struct Link {
pub output_port: glib::WeakRef<Port>, pub output_port: glib::WeakRef<Port>,
pub input_port: glib::WeakRef<Port>, pub input_port: glib::WeakRef<Port>,
pub active: Cell<bool>, pub active: Cell<bool>,
pub media_type: Cell<MediaType>,
}
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] #[glib::object_subclass]
@@ -53,6 +65,10 @@ mod imp {
.default_value(false) .default_value(false)
.flags(glib::ParamFlags::READWRITE) .flags(glib::ParamFlags::READWRITE)
.build(), .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(), "output-port" => self.output_port.upgrade().to_value(),
"input-port" => self.input_port.upgrade().to_value(), "input-port" => self.input_port.upgrade().to_value(),
"active" => self.active.get().to_value(), "active" => self.active.get().to_value(),
"media-type" => self.media_type.get().as_raw().to_value(),
_ => unimplemented!(), _ => unimplemented!(),
} }
} }
@@ -73,6 +90,9 @@ mod imp {
"output-port" => self.output_port.set(value.get().unwrap()), "output-port" => self.output_port.set(value.get().unwrap()),
"input-port" => self.input_port.set(value.get().unwrap()), "input-port" => self.input_port.set(value.get().unwrap()),
"active" => self.active.set(value.get().unwrap()), "active" => self.active.set(value.get().unwrap()),
"media-type" => self
.media_type
.set(MediaType::from_raw(value.get().unwrap())),
_ => unimplemented!(), _ => unimplemented!(),
} }
} }
@@ -111,6 +131,14 @@ impl Link {
pub fn set_active(&self, active: bool) { pub fn set_active(&self, active: bool) {
self.set_property("active", active); 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 { impl Default for Link {

View File

@@ -23,20 +23,26 @@ use gtk::{
}; };
use pipewire::spa::Direction; use pipewire::spa::Direction;
use crate::MediaType;
mod imp { mod imp {
use super::*; use super::*;
use std::cell::Cell;
use once_cell::{sync::Lazy, unsync::OnceCell}; use once_cell::{sync::Lazy, unsync::OnceCell};
use pipewire::spa::Direction; use pipewire::spa::{format::MediaType, Direction};
/// Graphical representation of a pipewire port. /// Graphical representation of a pipewire port.
#[derive(Default, glib::Properties)] #[derive(glib::Properties)]
#[properties(wrapper_type = super::Port)] #[properties(wrapper_type = super::Port)]
pub struct Port { pub struct Port {
#[property(get, set, construct_only)] #[property(get, set, construct_only)]
pub(super) pipewire_id: OnceCell<u32>, pub(super) pipewire_id: OnceCell<u32>,
#[property(
type = u32,
get = |_| self.media_type.get().as_raw(),
set = Self::set_media_type
)]
pub(super) media_type: Cell<MediaType>,
#[property( #[property(
name = "name", type = String, name = "name", type = String,
get = |this: &Self| this.label.text().to_string(), get = |this: &Self| this.label.text().to_string(),
@@ -49,6 +55,17 @@ mod imp {
pub(super) direction: OnceCell<Direction>, pub(super) direction: OnceCell<Direction>,
} }
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] #[glib::object_subclass]
impl ObjectSubclass for Port { impl ObjectSubclass for Port {
const NAME: &'static str = "HelvumPort"; const NAME: &'static str = "HelvumPort";
@@ -184,6 +201,26 @@ mod imp {
obj.add_controller(drop_target); 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! { glib::wrapper! {
@@ -192,7 +229,7 @@ glib::wrapper! {
} }
impl Port { impl Port {
pub fn new(id: u32, name: &str, direction: Direction, media_type: Option<MediaType>) -> Self { pub fn new(id: u32, name: &str, direction: Direction) -> Self {
// Create the widget and initialize needed fields // Create the widget and initialize needed fields
let res: Self = glib::Object::builder() let res: Self = glib::Object::builder()
.property("pipewire-id", id) .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. // 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()); 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 res
} }