mirror of
https://gitlab.freedesktop.org/pipewire/helvum
synced 2026-03-15 19:46:10 +08:00
view: graph: Implement gtk::Scrollable and do not render content outside the displayed area
The graphview widget now implements the gtk::Scrollable interface, so it is no longer wrapped inside a gtk::Viewport when used in a gtk::ScrollWindow anymore. Instead, it repositions its content itself when scrolled, and also skips rendering any content that is not inside the visible area, which should improve performance when the graph becomes big. This commit also makes the canvas a fixed size, with much space to each side from the starting area. This will hopefully improve user experience, as the view can now be moved around more freely, and nodes can be dragged left and above the starting area.
This commit is contained in:
@@ -18,7 +18,6 @@
|
|||||||
@define-color audio rgb(50,100,240);
|
@define-color audio rgb(50,100,240);
|
||||||
@define-color video rgb(200,200,0);
|
@define-color video rgb(200,200,0);
|
||||||
@define-color midi rgb(200,0,50);
|
@define-color midi rgb(200,0,50);
|
||||||
@define-color graphview-grid rgb(35,35,35);
|
|
||||||
@define-color graphview-link #808080;
|
@define-color graphview-link #808080;
|
||||||
|
|
||||||
.audio {
|
.audio {
|
||||||
@@ -37,8 +36,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
graphview {
|
graphview {
|
||||||
background-image: linear-gradient(@graphview-grid 1px, transparent 1px),
|
|
||||||
linear-gradient(to right, @graphview-grid 1px, transparent 1px);
|
|
||||||
background-size: 20px 20px;
|
|
||||||
background-color: @text_view_bg;
|
background-color: @text_view_bg;
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ use super::{Node, Port};
|
|||||||
|
|
||||||
use gtk::{
|
use gtk::{
|
||||||
glib::{self, clone},
|
glib::{self, clone},
|
||||||
graphene, gsk,
|
graphene::{self, Point},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
subclass::prelude::*,
|
subclass::prelude::*,
|
||||||
};
|
};
|
||||||
@@ -28,18 +28,27 @@ use std::{cmp::Ordering, collections::HashMap};
|
|||||||
|
|
||||||
use crate::NodeType;
|
use crate::NodeType;
|
||||||
|
|
||||||
|
const CANVAS_SIZE: f64 = 5000.0;
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
use gtk::{gdk::RGBA, graphene::Rect, gsk::ColorStop};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GraphView {
|
pub struct GraphView {
|
||||||
pub(super) nodes: RefCell<HashMap<u32, Node>>,
|
/// Stores nodes and their positions.
|
||||||
|
pub(super) nodes: RefCell<HashMap<u32, (Node, Point)>>,
|
||||||
/// Stores the link and whether it is currently active.
|
/// Stores the link and whether it is currently active.
|
||||||
pub(super) links: RefCell<HashMap<u32, (crate::PipewireLink, bool)>>,
|
pub(super) links: RefCell<HashMap<u32, (crate::PipewireLink, bool)>>,
|
||||||
|
pub hadjustment: RefCell<Option<gtk::Adjustment>>,
|
||||||
|
pub vadjustment: RefCell<Option<gtk::Adjustment>>,
|
||||||
|
/// When a node drag is ongoing, this stores the dragged node and the initial coordinates on the widget surface.
|
||||||
|
pub drag_state: RefCell<Option<(Node, Point)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
@@ -47,10 +56,9 @@ mod imp {
|
|||||||
const NAME: &'static str = "GraphView";
|
const NAME: &'static str = "GraphView";
|
||||||
type Type = super::GraphView;
|
type Type = super::GraphView;
|
||||||
type ParentType = gtk::Widget;
|
type ParentType = gtk::Widget;
|
||||||
|
type Interfaces = (gtk::Scrollable,);
|
||||||
|
|
||||||
fn class_init(klass: &mut Self::Class) {
|
fn class_init(klass: &mut Self::Class) {
|
||||||
// The layout manager determines how child widgets are laid out.
|
|
||||||
klass.set_layout_manager_type::<gtk::FixedLayout>();
|
|
||||||
klass.set_css_name("graphview");
|
klass.set_css_name("graphview");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,18 +67,21 @@ mod imp {
|
|||||||
fn constructed(&self, obj: &Self::Type) {
|
fn constructed(&self, obj: &Self::Type) {
|
||||||
self.parent_constructed(obj);
|
self.parent_constructed(obj);
|
||||||
|
|
||||||
let drag_state = Rc::new(RefCell::new(None));
|
obj.set_overflow(gtk::Overflow::Hidden);
|
||||||
|
|
||||||
let drag_controller = gtk::GestureDrag::new();
|
let drag_controller = gtk::GestureDrag::new();
|
||||||
|
|
||||||
drag_controller.connect_drag_begin(
|
drag_controller.connect_drag_begin(|drag_controller, x, y| {
|
||||||
clone!(@strong drag_state => move |drag_controller, x, y| {
|
|
||||||
let mut drag_state = drag_state.borrow_mut();
|
|
||||||
let widget = drag_controller
|
let widget = drag_controller
|
||||||
.widget()
|
.widget()
|
||||||
.dynamic_cast::<Self::Type>()
|
.dynamic_cast::<Self::Type>()
|
||||||
.expect("drag-begin event is not on the GraphView");
|
.expect("drag-begin event is not on the GraphView");
|
||||||
|
let mut drag_state = widget.imp().drag_state.borrow_mut();
|
||||||
|
|
||||||
// pick() should at least return the widget itself.
|
// pick() should at least return the widget itself.
|
||||||
let target = widget.pick(x, y, gtk::PickFlags::DEFAULT).expect("drag-begin pick() did not return a widget");
|
let target = widget
|
||||||
|
.pick(x, y, gtk::PickFlags::DEFAULT)
|
||||||
|
.expect("drag-begin pick() did not return a widget");
|
||||||
*drag_state = if target.ancestor(Port::static_type()).is_some() {
|
*drag_state = if target.ancestor(Port::static_type()).is_some() {
|
||||||
// The user targeted a port, so the dragging should be handled by the Port
|
// The user targeted a port, so the dragging should be handled by the Port
|
||||||
// component instead of here.
|
// component instead of here.
|
||||||
@@ -78,26 +89,34 @@ mod imp {
|
|||||||
} else if let Some(target) = target.ancestor(Node::static_type()) {
|
} else if let Some(target) = target.ancestor(Node::static_type()) {
|
||||||
// The user targeted a Node without targeting a specific Port.
|
// The user targeted a Node without targeting a specific Port.
|
||||||
// Drag the Node around the screen.
|
// Drag the Node around the screen.
|
||||||
let (x, y) = widget.get_node_position(&target);
|
let node = target.dynamic_cast_ref::<Node>().unwrap();
|
||||||
Some((target, x, y))
|
|
||||||
|
// We use the upper-left corner of the widget as the start position instead of the actual
|
||||||
|
// cursor location, this lets us move the node around easier because we don't need to
|
||||||
|
// account for where the cursor is on the node.
|
||||||
|
let alloc = node.allocation();
|
||||||
|
Some((node.clone(), Point::new(alloc.x() as f32, alloc.y() as f32)))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
));
|
drag_controller.connect_drag_update(|drag_controller, x, y| {
|
||||||
drag_controller.connect_drag_update(
|
|
||||||
clone!(@strong drag_state => move |drag_controller, x, y| {
|
|
||||||
let widget = drag_controller
|
let widget = drag_controller
|
||||||
.widget()
|
.widget()
|
||||||
.dynamic_cast::<Self::Type>()
|
.dynamic_cast::<Self::Type>()
|
||||||
.expect("drag-update event is not on the GraphView");
|
.expect("drag-update event is not on the GraphView");
|
||||||
let drag_state = drag_state.borrow();
|
let drag_state = widget.imp().drag_state.borrow();
|
||||||
if let Some((ref node, x1, y1)) = *drag_state {
|
let hadj = widget.imp().hadjustment.borrow();
|
||||||
widget.move_node(node, x1 + x as f32, y1 + y as f32);
|
let vadj = widget.imp().vadjustment.borrow();
|
||||||
}
|
|
||||||
}
|
if let Some((ref node, ref start_point)) = *drag_state {
|
||||||
),
|
widget.move_node(
|
||||||
|
node,
|
||||||
|
start_point.x() + hadj.as_ref().unwrap().value() as f32 + x as f32,
|
||||||
|
start_point.y() + vadj.as_ref().unwrap().value() as f32 + y as f32,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
obj.add_controller(&drag_controller);
|
obj.add_controller(&drag_controller);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,24 +124,155 @@ mod imp {
|
|||||||
self.nodes
|
self.nodes
|
||||||
.borrow()
|
.borrow()
|
||||||
.values()
|
.values()
|
||||||
.for_each(|node| node.unparent())
|
.for_each(|(node, _)| node.unparent())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn properties() -> &'static [glib::ParamSpec] {
|
||||||
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||||
|
vec![
|
||||||
|
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("hadjustment"),
|
||||||
|
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("vadjustment"),
|
||||||
|
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("hscroll-policy"),
|
||||||
|
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("vscroll-policy"),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
PROPERTIES.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||||
|
match pspec.name() {
|
||||||
|
"hadjustment" => self.hadjustment.borrow().to_value(),
|
||||||
|
"vadjustment" => self.vadjustment.borrow().to_value(),
|
||||||
|
"hscroll-policy" | "vscroll-policy" => gtk::ScrollablePolicy::Natural.to_value(),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_property(
|
||||||
|
&self,
|
||||||
|
obj: &Self::Type,
|
||||||
|
_id: usize,
|
||||||
|
value: &glib::Value,
|
||||||
|
pspec: &glib::ParamSpec,
|
||||||
|
) {
|
||||||
|
match pspec.name() {
|
||||||
|
"hadjustment" => {
|
||||||
|
self.set_adjustment(obj, value.get().ok(), gtk::Orientation::Horizontal)
|
||||||
|
}
|
||||||
|
"vadjustment" => {
|
||||||
|
self.set_adjustment(obj, value.get().ok(), gtk::Orientation::Vertical)
|
||||||
|
}
|
||||||
|
"hscroll-policy" | "vscroll-policy" => {}
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetImpl for GraphView {
|
impl WidgetImpl for GraphView {
|
||||||
fn snapshot(&self, widget: &Self::Type, snapshot: >k::Snapshot) {
|
fn size_allocate(&self, widget: &Self::Type, _width: i32, _height: i32, _baseline: i32) {
|
||||||
/* FIXME: A lot of hardcoded values in here.
|
for (node, point) in self.nodes.borrow().values() {
|
||||||
Try to use relative units (em) and colours from the theme as much as possible. */
|
let (_, natural_size) = node.preferred_size();
|
||||||
|
node.size_allocate(
|
||||||
|
>k::Allocation::new(
|
||||||
|
(f64::from(point.x()) - self.hadjustment.borrow().as_ref().unwrap().value())
|
||||||
|
as i32,
|
||||||
|
(f64::from(point.y()) - self.vadjustment.borrow().as_ref().unwrap().value())
|
||||||
|
as i32,
|
||||||
|
natural_size.width(),
|
||||||
|
natural_size.height(),
|
||||||
|
),
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref hadjustment) = *self.hadjustment.borrow() {
|
||||||
|
self.set_adjustment_values(widget, hadjustment, gtk::Orientation::Horizontal);
|
||||||
|
}
|
||||||
|
if let Some(ref vadjustment) = *self.vadjustment.borrow() {
|
||||||
|
self.set_adjustment_values(widget, vadjustment, gtk::Orientation::Vertical);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot(&self, widget: &Self::Type, snapshot: >k::Snapshot) {
|
||||||
let alloc = widget.allocation();
|
let alloc = widget.allocation();
|
||||||
|
|
||||||
// Draw all children
|
self.snapshot_background(widget, snapshot);
|
||||||
|
|
||||||
|
// Draw all visible children
|
||||||
self.nodes
|
self.nodes
|
||||||
.borrow()
|
.borrow()
|
||||||
.values()
|
.values()
|
||||||
.for_each(|node| self.instance().snapshot_child(node, snapshot));
|
// Cull nodes from rendering when they are outside the visible canvas area
|
||||||
|
.filter(|(node, _)| alloc.intersect(&node.allocation()).is_some())
|
||||||
|
.for_each(|(node, _)| self.instance().snapshot_child(node, snapshot));
|
||||||
|
|
||||||
|
self.snapshot_links(widget, snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScrollableImpl for GraphView {}
|
||||||
|
|
||||||
|
impl GraphView {
|
||||||
|
fn snapshot_background(&self, widget: &super::GraphView, snapshot: >k::Snapshot) {
|
||||||
|
const GRID_SIZE: f32 = 20.0;
|
||||||
|
const GRID_LINE_WIDTH: f32 = 1.0;
|
||||||
|
|
||||||
|
let alloc = widget.allocation();
|
||||||
|
|
||||||
|
// We need to offset the lines between 0 and (excluding) GRID_SIZE so the grid moves with
|
||||||
|
// the rest of the view when scrolling.
|
||||||
|
// The offset is rounded so the grid is always aligned to a row of pixels.
|
||||||
|
let hadj = self
|
||||||
|
.hadjustment
|
||||||
|
.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.map(|hadjustment| hadjustment.value())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
let hoffset = ((GRID_SIZE - (hadj as f32 % GRID_SIZE)) % GRID_SIZE).floor();
|
||||||
|
let vadj = self
|
||||||
|
.vadjustment
|
||||||
|
.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.map(|vadjustment| vadjustment.value())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
let voffset = ((GRID_SIZE - (vadj as f32 % GRID_SIZE)) % GRID_SIZE).floor();
|
||||||
|
|
||||||
|
snapshot.push_repeat(
|
||||||
|
&Rect::new(0.0, 0.0, alloc.width() as f32, alloc.height() as f32),
|
||||||
|
Some(&Rect::new(0.0, voffset, alloc.width() as f32, GRID_SIZE)),
|
||||||
|
);
|
||||||
|
let grid_color = RGBA::new(0.137, 0.137, 0.137, 1.0);
|
||||||
|
snapshot.append_linear_gradient(
|
||||||
|
&Rect::new(0.0, voffset, alloc.width() as f32, GRID_LINE_WIDTH),
|
||||||
|
&Point::new(0.0, 0.0),
|
||||||
|
&Point::new(alloc.width() as f32, 0.0),
|
||||||
|
&[
|
||||||
|
ColorStop::new(0.0, grid_color),
|
||||||
|
ColorStop::new(1.0, grid_color),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
snapshot.pop();
|
||||||
|
|
||||||
|
snapshot.push_repeat(
|
||||||
|
&Rect::new(0.0, 0.0, alloc.width() as f32, alloc.height() as f32),
|
||||||
|
Some(&Rect::new(hoffset, 0.0, GRID_SIZE, alloc.height() as f32)),
|
||||||
|
);
|
||||||
|
snapshot.append_linear_gradient(
|
||||||
|
&Rect::new(hoffset, 0.0, GRID_LINE_WIDTH, alloc.height() as f32),
|
||||||
|
&Point::new(0.0, 0.0),
|
||||||
|
&Point::new(0.0, alloc.height() as f32),
|
||||||
|
&[
|
||||||
|
ColorStop::new(0.0, grid_color),
|
||||||
|
ColorStop::new(1.0, grid_color),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
snapshot.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot_links(&self, widget: &super::GraphView, snapshot: >k::Snapshot) {
|
||||||
|
let alloc = widget.allocation();
|
||||||
|
|
||||||
// Draw all links
|
|
||||||
let link_cr = snapshot.append_cairo(&graphene::Rect::new(
|
let link_cr = snapshot.append_cairo(&graphene::Rect::new(
|
||||||
0.0,
|
0.0,
|
||||||
0.0,
|
0.0,
|
||||||
@@ -135,7 +285,7 @@ mod imp {
|
|||||||
let rgba = widget
|
let rgba = widget
|
||||||
.style_context()
|
.style_context()
|
||||||
.lookup_color("graphview-link")
|
.lookup_color("graphview-link")
|
||||||
.unwrap_or_else(|| gtk::gdk::RGBA::new(0.0, 0.0, 0.0, 0.0));
|
.unwrap_or(gtk::gdk::RGBA::BLACK);
|
||||||
|
|
||||||
link_cr.set_source_rgba(
|
link_cr.set_source_rgba(
|
||||||
rgba.red().into(),
|
rgba.red().into(),
|
||||||
@@ -145,6 +295,7 @@ mod imp {
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (link, active) in self.links.borrow().values() {
|
for (link, active) in self.links.borrow().values() {
|
||||||
|
// TODO: Do not draw links when they are outside the view
|
||||||
if let Some((from_x, from_y, to_x, to_y)) = self.get_link_coordinates(link) {
|
if let Some((from_x, from_y, to_x, to_y)) = self.get_link_coordinates(link) {
|
||||||
link_cr.move_to(from_x, from_y);
|
link_cr.move_to(from_x, from_y);
|
||||||
|
|
||||||
@@ -185,9 +336,7 @@ mod imp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphView {
|
|
||||||
/// 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.
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
@@ -198,7 +347,7 @@ mod imp {
|
|||||||
// For some reason, gtk4::WidgetExt::translate_coordinates gives me incorrect values,
|
// For some reason, gtk4::WidgetExt::translate_coordinates gives me incorrect values,
|
||||||
// so we manually calculate the needed offsets here.
|
// so we manually calculate the needed offsets here.
|
||||||
|
|
||||||
let from_port = &nodes.get(&link.node_from)?.get_port(link.port_from)?;
|
let from_port = &nodes.get(&link.node_from)?.0.get_port(link.port_from)?;
|
||||||
let from_node = from_port
|
let from_node = from_port
|
||||||
.ancestor(Node::static_type())
|
.ancestor(Node::static_type())
|
||||||
.expect("Port is not a child of a node");
|
.expect("Port is not a child of a node");
|
||||||
@@ -209,7 +358,7 @@ mod imp {
|
|||||||
+ from_port.allocation().y()
|
+ from_port.allocation().y()
|
||||||
+ (from_port.allocation().height() / 2);
|
+ (from_port.allocation().height() / 2);
|
||||||
|
|
||||||
let to_port = &nodes.get(&link.node_to)?.get_port(link.port_to)?;
|
let to_port = &nodes.get(&link.node_to)?.0.get_port(link.port_to)?;
|
||||||
let to_node = to_port
|
let to_node = to_port
|
||||||
.ancestor(Node::static_type())
|
.ancestor(Node::static_type())
|
||||||
.expect("Port is not a child of a node");
|
.expect("Port is not a child of a node");
|
||||||
@@ -220,6 +369,48 @@ mod imp {
|
|||||||
|
|
||||||
Some((from_x.into(), from_y.into(), to_x.into(), to_y.into()))
|
Some((from_x.into(), from_y.into(), to_x.into(), to_y.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_adjustment(
|
||||||
|
&self,
|
||||||
|
obj: &super::GraphView,
|
||||||
|
adjustment: Option<>k::Adjustment>,
|
||||||
|
orientation: gtk::Orientation,
|
||||||
|
) {
|
||||||
|
match orientation {
|
||||||
|
gtk::Orientation::Horizontal => {
|
||||||
|
*self.hadjustment.borrow_mut() = adjustment.cloned()
|
||||||
|
}
|
||||||
|
gtk::Orientation::Vertical => *self.vadjustment.borrow_mut() = adjustment.cloned(),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(adjustment) = adjustment {
|
||||||
|
adjustment
|
||||||
|
.connect_value_changed(clone!(@weak obj => move |_| obj.queue_allocate() ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_adjustment_values(
|
||||||
|
&self,
|
||||||
|
obj: &super::GraphView,
|
||||||
|
adjustment: >k::Adjustment,
|
||||||
|
orientation: gtk::Orientation,
|
||||||
|
) {
|
||||||
|
let size = match orientation {
|
||||||
|
gtk::Orientation::Horizontal => obj.width(),
|
||||||
|
gtk::Orientation::Vertical => obj.height(),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
adjustment.configure(
|
||||||
|
adjustment.value(),
|
||||||
|
-(CANVAS_SIZE / 2.0),
|
||||||
|
CANVAS_SIZE / 2.0,
|
||||||
|
f64::from(size) * 0.1,
|
||||||
|
f64::from(size) * 0.9,
|
||||||
|
f64::from(size),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +444,7 @@ impl GraphView {
|
|||||||
.values()
|
.values()
|
||||||
.map(|node| {
|
.map(|node| {
|
||||||
// Map nodes to their locations
|
// Map nodes to their locations
|
||||||
self.get_node_position(&node.clone().upcast())
|
self.get_node_position(&node.0.clone().upcast()).unwrap()
|
||||||
})
|
})
|
||||||
.filter(|(x2, _)| {
|
.filter(|(x2, _)| {
|
||||||
// Only look for other nodes that have a similar x coordinate
|
// Only look for other nodes that have a similar x coordinate
|
||||||
@@ -265,15 +456,16 @@ impl GraphView {
|
|||||||
})
|
})
|
||||||
.map_or(20_f32, |(_x, y)| y + 100.0);
|
.map_or(20_f32, |(_x, y)| y + 100.0);
|
||||||
|
|
||||||
self.move_node(&node.clone().upcast(), x, y);
|
private
|
||||||
|
.nodes
|
||||||
private.nodes.borrow_mut().insert(id, node);
|
.borrow_mut()
|
||||||
|
.insert(id, (node, Point::new(x, y)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_node(&self, id: u32) {
|
pub fn remove_node(&self, id: u32) {
|
||||||
let private = imp::GraphView::from_instance(self);
|
let private = imp::GraphView::from_instance(self);
|
||||||
let mut nodes = private.nodes.borrow_mut();
|
let mut nodes = private.nodes.borrow_mut();
|
||||||
if let Some(node) = nodes.remove(&id) {
|
if let Some((node, _)) = nodes.remove(&id) {
|
||||||
node.unparent();
|
node.unparent();
|
||||||
} else {
|
} else {
|
||||||
warn!("Tried to remove non-existant node (id={}) from graph", id);
|
warn!("Tried to remove non-existant node (id={}) from graph", id);
|
||||||
@@ -283,7 +475,7 @@ impl GraphView {
|
|||||||
pub fn add_port(&self, node_id: u32, port_id: u32, port: crate::view::port::Port) {
|
pub fn add_port(&self, node_id: u32, port_id: u32, port: crate::view::port::Port) {
|
||||||
let private = imp::GraphView::from_instance(self);
|
let private = imp::GraphView::from_instance(self);
|
||||||
|
|
||||||
if let Some(node) = private.nodes.borrow_mut().get_mut(&node_id) {
|
if let Some((node, _)) = private.nodes.borrow_mut().get_mut(&node_id) {
|
||||||
node.add_port(port_id, port);
|
node.add_port(port_id, port);
|
||||||
} else {
|
} else {
|
||||||
error!(
|
error!(
|
||||||
@@ -296,7 +488,7 @@ impl GraphView {
|
|||||||
pub fn remove_port(&self, id: u32, node_id: u32) {
|
pub fn remove_port(&self, id: u32, node_id: u32) {
|
||||||
let private = imp::GraphView::from_instance(self);
|
let private = imp::GraphView::from_instance(self);
|
||||||
let nodes = private.nodes.borrow();
|
let nodes = private.nodes.borrow();
|
||||||
if let Some(node) = nodes.get(&node_id) {
|
if let Some((node, _)) = nodes.get(&node_id) {
|
||||||
node.remove_port(id);
|
node.remove_port(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,43 +518,33 @@ impl GraphView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the position of the specified node inside the graphview.
|
/// Get the position of the specified node inside the graphview.
|
||||||
pub(super) fn get_node_position(&self, node: >k::Widget) -> (f32, f32) {
|
pub(super) fn get_node_position(&self, node: &Node) -> Option<(f32, f32)> {
|
||||||
let layout_manager = self
|
self.imp()
|
||||||
.layout_manager()
|
.nodes
|
||||||
.expect("Failed to get layout manager")
|
.borrow()
|
||||||
.dynamic_cast::<gtk::FixedLayout>()
|
.get(&node.pipewire_id())
|
||||||
.expect("Failed to cast to FixedLayout");
|
.map(|(_, point)| (point.x(), point.y()))
|
||||||
|
|
||||||
let node = layout_manager
|
|
||||||
.layout_child(node)
|
|
||||||
.dynamic_cast::<gtk::FixedLayoutChild>()
|
|
||||||
.expect("Could not cast to FixedLayoutChild");
|
|
||||||
node.transform()
|
|
||||||
.expect("Failed to obtain transform from layout child")
|
|
||||||
.to_translate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn move_node(&self, node: >k::Widget, x: f32, y: f32) {
|
pub(super) fn move_node(&self, widget: &Node, x: f32, y: f32) {
|
||||||
let layout_manager = self
|
let mut nodes = self.imp().nodes.borrow_mut();
|
||||||
.layout_manager()
|
let mut node = nodes
|
||||||
.expect("Failed to get layout manager")
|
.get_mut(&widget.pipewire_id())
|
||||||
.dynamic_cast::<gtk::FixedLayout>()
|
.expect("Node is not on the graph");
|
||||||
.expect("Failed to cast to FixedLayout");
|
|
||||||
|
|
||||||
let transform = gsk::Transform::new()
|
// Clamp the new position to within the graph, so a node can't be moved outside it and be lost.
|
||||||
// Nodes should not be able to be dragged out of the view, so we use `max(coordinate, 0.0)` to prevent that.
|
node.1 = Point::new(
|
||||||
.translate(&graphene::Point::new(f32::max(x, 0.0), f32::max(y, 0.0)))
|
x.clamp(
|
||||||
.unwrap();
|
-(CANVAS_SIZE / 2.0) as f32,
|
||||||
|
(CANVAS_SIZE / 2.0) as f32 - widget.width() as f32,
|
||||||
|
),
|
||||||
|
y.clamp(
|
||||||
|
-(CANVAS_SIZE / 2.0) as f32,
|
||||||
|
(CANVAS_SIZE / 2.0) as f32 - widget.height() as f32,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
layout_manager
|
self.queue_allocate();
|
||||||
.layout_child(node)
|
|
||||||
.dynamic_cast::<gtk::FixedLayoutChild>()
|
|
||||||
.expect("Could not cast to FixedLayoutChild")
|
|
||||||
.set_transform(&transform);
|
|
||||||
|
|
||||||
// FIXME: If links become proper widgets,
|
|
||||||
// we don't need to redraw the full graph everytime.
|
|
||||||
self.queue_draw();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user