use gtk::{
gdk,
glib::{self, clone, subclass::Signal},
prelude::*,
subclass::prelude::*,
};
use log::{trace, warn};
use pipewire::spa::Direction;
use crate::MediaType;
#[derive(Clone, Debug, glib::Boxed)]
#[boxed_type(name = "HelvumForwardLink")]
struct ForwardLink(u32);
#[derive(Clone, Debug, glib::Boxed)]
#[boxed_type(name = "HelvumReversedLink")]
struct ReversedLink(u32);
mod imp {
use glib::ParamFlags;
use once_cell::{sync::Lazy, unsync::OnceCell};
use pipewire::spa::Direction;
use super::*;
#[derive(Default)]
pub struct Port {
pub(super) pipewire_id: OnceCell<u32>,
pub(super) label: gtk::Label,
pub(super) direction: OnceCell<Direction>,
}
#[glib::object_subclass]
impl ObjectSubclass for Port {
const NAME: &'static str = "HelvumPort";
type Type = super::Port;
type ParentType = gtk::Widget;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk::BinLayout>();
klass.set_css_name("button");
}
}
impl ObjectImpl for Port {
fn constructed(&self) {
self.parent_constructed();
self.label.set_parent(&*self.obj());
self.label.set_wrap(true);
self.label.set_lines(2);
self.label.set_max_width_chars(20);
self.label.set_ellipsize(gtk::pango::EllipsizeMode::End);
}
fn dispose(&self) {
self.label.unparent()
}
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecUInt::builder("pipewire-id")
.flags(ParamFlags::READWRITE | ParamFlags::CONSTRUCT_ONLY)
.build(),
glib::ParamSpecString::builder("name").build(),
]
});
PROPERTIES.as_ref()
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"pipewire-id" => self.pipewire_id.get().unwrap().to_value(),
"name" => self.label.text().to_value(),
_ => unimplemented!(),
}
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"name" => {
self.label.set_text(value.get().unwrap());
self.label.set_tooltip_text(value.get().ok());
}
"pipewire-id" => self.pipewire_id.set(value.get().unwrap()).unwrap(),
_ => unimplemented!(),
}
}
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder("port-toggled")
.param_types([<u32>::static_type(), <u32>::static_type()])
.build()]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for Port {}
}
glib::wrapper! {
pub struct Port(ObjectSubclass<imp::Port>)
@extends gtk::Widget;
}
impl Port {
pub fn new(id: u32, name: &str, direction: Direction, media_type: Option<MediaType>) -> Self {
let res: Self = glib::Object::builder()
.property("pipewire-id", &id)
.property("name", &name)
.build();
let imp = res.imp();
imp.direction
.set(direction)
.expect("Port direction already set");
let drag_src = gtk::DragSource::builder()
.content(&gdk::ContentProvider::for_value(&match direction {
Direction::Input => ReversedLink(id).to_value(),
Direction::Output => ForwardLink(id).to_value(),
}))
.build();
drag_src.connect_drag_begin(move |_, _| {
trace!("Drag started from port {}", id);
});
drag_src.connect_drag_cancel(move |_, _, _| {
trace!("Drag from port {} was cancelled", id);
false
});
res.add_controller(drag_src);
let drop_target = gtk::DropTarget::new(
match direction {
Direction::Input => ForwardLink::static_type(),
Direction::Output => ReversedLink::static_type(),
},
gdk::DragAction::COPY,
);
match direction {
Direction::Input => {
drop_target.connect_drop(
clone!(@weak res as this => @default-panic, move |drop_target, val, _, _| {
if let Ok(ForwardLink(source_id)) = val.get::<ForwardLink>() {
drop_target
.widget()
.emit_by_name::<()>("port-toggled", &[&source_id, &this.pipewire_id()]);
} else {
warn!("Invalid type dropped on ingoing port");
}
true
}),
);
}
Direction::Output => {
drop_target.connect_drop(
clone!(@weak res as this => @default-panic, move |drop_target, val, _, _| {
if let Ok(ReversedLink(target_id)) = val.get::<ReversedLink>() {
drop_target
.widget()
.emit_by_name::<()>("port-toggled", &[&this.pipewire_id(), &target_id]);
} else {
warn!("Invalid type dropped on outgoing port");
}
true
}),
);
}
}
res.add_controller(drop_target);
res.set_cursor(gtk::gdk::Cursor::from_name("grab", None).as_ref());
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
}
pub fn pipewire_id(&self) -> u32 {
self.property("pipewire-id")
}
pub fn name(&self) -> String {
self.property("name")
}
pub fn set_name(&self, name: &str) {
self.set_property("name", name);
}
pub fn direction(&self) -> &Direction {
self.imp()
.direction
.get()
.expect("Port direction is not set")
}
}