graph: Draw "fake" link during port drag-and-drop to visualize link creating

Cursor movement during port drag-and-drop on the graph is now being tracked
and a link is drawn from the dragged port to the cursor.

If the cursor is hovering a port the source port can link to,
the second end of the link instead attaches to the ports link anchor
so that the link "snaps" to the linkable port.
This commit is contained in:
Tom A. Wagner
2023-08-04 13:46:09 +02:00
parent d99c5e253c
commit 7145c83ae1
2 changed files with 186 additions and 44 deletions

View File

@@ -15,9 +15,9 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use gtk::{ use gtk::{
cairo, gio,
glib::{self, clone}, glib::{self, clone},
graphene, graphene::{self, Point},
graphene::Point,
gsk, gsk,
prelude::*, prelude::*,
subclass::prelude::*, subclass::prelude::*,
@@ -43,6 +43,7 @@ mod imp {
}; };
use log::warn; use log::warn;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use pipewire::spa::Direction;
pub struct DragState { pub struct DragState {
node: glib::WeakRef<Node>, node: glib::WeakRef<Node>,
@@ -53,22 +54,46 @@ mod imp {
offset: Point, offset: Point,
} }
#[derive(Default)]
pub struct GraphView { pub struct GraphView {
/// Stores nodes and their positions. /// Stores nodes and their positions.
pub(super) nodes: RefCell<HashMap<Node, Point>>, pub(super) nodes: RefCell<HashMap<Node, Point>>,
/// Stores the link and whether it is currently active. /// Stores the links and whether they are currently active.
pub(super) links: RefCell<HashSet<Link>>, pub(super) links: RefCell<HashSet<Link>>,
// Properties for zooming and scrolling the hraph
pub hadjustment: RefCell<Option<gtk::Adjustment>>, pub hadjustment: RefCell<Option<gtk::Adjustment>>,
pub vadjustment: RefCell<Option<gtk::Adjustment>>, pub vadjustment: RefCell<Option<gtk::Adjustment>>,
pub zoom_factor: Cell<f64>, pub zoom_factor: Cell<f64>,
/// This keeps track of an ongoing node drag operation. /// This keeps track of an ongoing node drag operation.
pub dragged_node: RefCell<Option<DragState>>, pub dragged_node: RefCell<Option<DragState>>,
// These keep track of an ongoing port drag operation
pub dragged_port: glib::WeakRef<Port>,
pub port_drag_cursor: Cell<Point>,
// Memorized data for an in-progress zoom gesture // Memorized data for an in-progress zoom gesture
pub zoom_gesture_initial_zoom: Cell<Option<f64>>, pub zoom_gesture_initial_zoom: Cell<Option<f64>>,
pub zoom_gesture_anchor: Cell<Option<(f64, f64)>>, pub zoom_gesture_anchor: Cell<Option<(f64, f64)>>,
} }
impl Default for GraphView {
fn default() -> Self {
Self {
nodes: Default::default(),
links: Default::default(),
hadjustment: Default::default(),
vadjustment: Default::default(),
zoom_factor: Default::default(),
dragged_node: Default::default(),
dragged_port: Default::default(),
port_drag_cursor: Cell::new(Point::new(0.0, 0.0)),
zoom_gesture_initial_zoom: Default::default(),
zoom_gesture_anchor: Default::default(),
}
}
}
#[glib::object_subclass] #[glib::object_subclass]
impl ObjectSubclass for GraphView { impl ObjectSubclass for GraphView {
const NAME: &'static str = "GraphView"; const NAME: &'static str = "GraphView";
@@ -88,6 +113,7 @@ mod imp {
self.obj().set_overflow(gtk::Overflow::Hidden); self.obj().set_overflow(gtk::Overflow::Hidden);
self.setup_node_dragging(); self.setup_node_dragging();
self.setup_port_drag_and_drop();
self.setup_scroll_zooming(); self.setup_scroll_zooming();
self.setup_zoom_gesture(); self.setup_zoom_gesture();
} }
@@ -289,6 +315,78 @@ mod imp {
self.obj().add_controller(drag_controller); self.obj().add_controller(drag_controller);
} }
fn setup_port_drag_and_drop(&self) {
let controller = gtk::DropControllerMotion::new();
controller.connect_enter(|controller, x, y| {
let graph = controller
.widget()
.downcast::<super::GraphView>()
.expect("Widget should be a graphview");
graph.imp().port_drag_enter(controller, x, y)
});
controller.connect_motion(|controller, x, y| {
let graph = controller
.widget()
.downcast::<super::GraphView>()
.expect("Widget should be a graphview");
graph.imp().port_drag_motion(x, y)
});
controller.connect_leave(|controller| {
let graph = controller
.widget()
.downcast::<super::GraphView>()
.expect("Widget should be a graphview");
graph.imp().port_drag_leave()
});
self.obj().add_controller(controller);
}
fn port_drag_enter(&self, controller: &gtk::DropControllerMotion, x: f64, y: f64) {
let Some(drop) = controller.drop() else {
return;
};
self.port_drag_cursor.set(Point::new(x as f32, y as f32));
drop.read_value_async(
Port::static_type(),
glib::PRIORITY_DEFAULT,
Option::<&gio::Cancellable>::None,
clone!(@weak self as imp => move|value| {
let Ok(value) = value else {
return;
};
let port: &Port = value.get().expect("Value should contain a port");
imp.dragged_port.set(Some(port));
}),
);
self.obj().queue_draw();
}
fn port_drag_motion(&self, x: f64, y: f64) {
if self.dragged_port.upgrade().is_some() {
self.port_drag_cursor.set(Point::new(x as f32, y as f32));
self.obj().queue_draw();
}
}
fn port_drag_leave(&self) {
if self.dragged_port.upgrade().is_some() {
self.dragged_port.set(None);
self.obj().queue_draw();
}
}
fn setup_scroll_zooming(&self) { fn setup_scroll_zooming(&self) {
// We're only interested in the vertical axis, but for devices like touchpads, // We're only interested in the vertical axis, but for devices like touchpads,
// not capturing a small accidental horizontal move may cause the scroll to be disrupted if a widget // not capturing a small accidental horizontal move may cause the scroll to be disrupted if a widget
@@ -406,44 +504,20 @@ mod imp {
snapshot.pop(); snapshot.pop();
} }
fn snapshot_links(&self, widget: &super::GraphView, snapshot: &gtk::Snapshot) { fn draw_link(
let alloc = widget.allocation(); &self,
link_cr: &cairo::Context,
let link_cr = snapshot.append_cairo(&graphene::Rect::new( output_anchor: &Point,
0.0, input_anchor: &Point,
0.0, active: bool,
alloc.width() as f32, ) {
alloc.height() as f32,
));
link_cr.set_line_width(2.0 * self.zoom_factor.get());
let rgba = widget
.style_context()
.lookup_color("graphview-link")
.unwrap_or(gtk::gdk::RGBA::BLACK);
link_cr.set_source_rgba(
rgba.red().into(),
rgba.green().into(),
rgba.blue().into(),
rgba.alpha().into(),
);
for link in self.links.borrow().iter() {
// TODO: Do not draw links when they are outside the view
let Some((output_anchor, input_anchor)) = self.get_link_coordinates(link) else {
warn!("Could not get allocation of ports of link: {:?}", link);
continue;
};
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();
let input_x: f64 = input_anchor.x().into(); let input_x: f64 = input_anchor.x().into();
let input_y: f64 = input_anchor.y().into(); let input_y: f64 = input_anchor.y().into();
// Use dashed line for inactive links, full line otherwise. // Use dashed line for inactive links, full line otherwise.
if link.active() { if active {
link_cr.set_dash(&[], 0.0); link_cr.set_dash(&[], 0.0);
} else { } else {
link_cr.set_dash(&[10.0, 5.0], 0.0); link_cr.set_dash(&[10.0, 5.0], 0.0);
@@ -476,6 +550,74 @@ mod imp {
warn!("Failed to draw graphview links: {}", e); warn!("Failed to draw graphview links: {}", e);
}; };
} }
fn draw_dragged_link(&self, port: &Port, link_cr: &cairo::Context) {
let Some(port_anchor) = port.compute_point(&*self.obj(), &port.link_anchor()) else {
return;
};
let drag_cursor = self.port_drag_cursor.get();
/* If we can find a linkable port under the cursor, link to its anchor,
* otherwise link to the mouse cursor */
let picked_port = self
.obj()
.pick(
drag_cursor.x().into(),
drag_cursor.y().into(),
gtk::PickFlags::DEFAULT,
)
.and_then(|widget| widget.ancestor(Port::static_type()).and_downcast::<Port>())
.filter(|picked_port| port.is_linkable_to(picked_port));
let picked_port_anchor = picked_port.and_then(|picked_port| {
picked_port.compute_point(&*self.obj(), &picked_port.link_anchor())
});
let other_anchor = picked_port_anchor.unwrap_or(drag_cursor);
let (output_anchor, input_anchor) = match port.direction() {
Direction::Output => (&port_anchor, &other_anchor),
Direction::Input => (&other_anchor, &port_anchor),
};
self.draw_link(link_cr, output_anchor, input_anchor, false);
}
fn snapshot_links(&self, widget: &super::GraphView, snapshot: &gtk::Snapshot) {
let alloc = widget.allocation();
let link_cr = snapshot.append_cairo(&graphene::Rect::new(
0.0,
0.0,
alloc.width() as f32,
alloc.height() as f32,
));
link_cr.set_line_width(2.0 * self.zoom_factor.get());
let rgba = widget
.style_context()
.lookup_color("graphview-link")
.unwrap_or(gtk::gdk::RGBA::BLACK);
link_cr.set_source_rgba(
rgba.red().into(),
rgba.green().into(),
rgba.blue().into(),
rgba.alpha().into(),
);
for link in self.links.borrow().iter() {
// TODO: Do not draw links when they are outside the view
let Some((output_anchor, input_anchor)) = self.get_link_coordinates(link) else {
warn!("Could not get allocation of ports of link: {:?}", link);
continue;
};
self.draw_link(&link_cr, &output_anchor, &input_anchor, link.active());
}
if let Some(port) = self.dragged_port.upgrade() {
self.draw_dragged_link(&port, &link_cr);
}
} }
/// Get coordinates for the drawn link to start at and to end at. /// Get coordinates for the drawn link to start at and to end at.

View File

@@ -119,6 +119,9 @@ mod imp {
let drag_src = gtk::DragSource::builder() let drag_src = gtk::DragSource::builder()
.content(&gdk::ContentProvider::for_value(&obj.to_value())) .content(&gdk::ContentProvider::for_value(&obj.to_value()))
.build(); .build();
// Override the default drag icon with an empty one so that only a grab cursor is shown.
// The graph will render a link from the source port to the cursor to visualize the drag instead.
drag_src.set_icon(Some(&gdk::Paintable::new_empty(0, 0)), 0, 0);
drag_src.connect_drag_begin(|drag_source, _| { drag_src.connect_drag_begin(|drag_source, _| {
let port = drag_source let port = drag_source
.widget() .widget()
@@ -126,9 +129,6 @@ mod imp {
.expect("Widget should be a Port"); .expect("Widget should be a Port");
log::trace!("Drag started from port {}", port.pipewire_id()); log::trace!("Drag started from port {}", port.pipewire_id());
let paintable = gtk::WidgetPaintable::new(Some(&port));
drag_source.set_icon(Some(&paintable), 0, 0);
}); });
drag_src.connect_drag_cancel(|drag_source, _, _| { drag_src.connect_drag_cancel(|drag_source, _, _| {
let port = drag_source let port = drag_source