mirror of
https://gitlab.freedesktop.org/pipewire/helvum
synced 2026-03-16 03:56:12 +08:00
graphview: Make graph widget zoomable via a zoom-factor property.
This adds a `zoom-factor` property. Changing the property zooms in the entire widget including background, nodes, links, etc.
This commit is contained in:
committed by
Tom Wagner
parent
bcef1300ca
commit
56e73d33c9
@@ -18,7 +18,9 @@ use super::{Node, Port};
|
|||||||
|
|
||||||
use gtk::{
|
use gtk::{
|
||||||
glib::{self, clone},
|
glib::{self, clone},
|
||||||
graphene::{self, Point},
|
graphene,
|
||||||
|
graphene::Point,
|
||||||
|
gsk,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
subclass::prelude::*,
|
subclass::prelude::*,
|
||||||
};
|
};
|
||||||
@@ -33,12 +35,21 @@ const CANVAS_SIZE: f64 = 5000.0;
|
|||||||
mod imp {
|
mod imp {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::{Cell, RefCell};
|
||||||
|
|
||||||
use gtk::{gdk::RGBA, graphene::Rect, gsk::ColorStop};
|
use gtk::{gdk::RGBA, graphene::Rect, gsk::ColorStop};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
pub struct DragState {
|
||||||
|
node: glib::WeakRef<Node>,
|
||||||
|
/// This stores the offset of the pointer to the origin of the node,
|
||||||
|
/// so that we can keep the pointer over the same position when moving the node
|
||||||
|
///
|
||||||
|
/// The offset is normalized to the default zoom-level of 1.0.
|
||||||
|
offset: Point,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GraphView {
|
pub struct GraphView {
|
||||||
/// Stores nodes and their positions.
|
/// Stores nodes and their positions.
|
||||||
@@ -47,8 +58,9 @@ mod imp {
|
|||||||
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 hadjustment: RefCell<Option<gtk::Adjustment>>,
|
||||||
pub vadjustment: 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 zoom_factor: Cell<f64>,
|
||||||
pub drag_state: RefCell<Option<(Node, Point)>>,
|
/// This keeps track of an ongoing node drag operation.
|
||||||
|
pub dragged_node: RefCell<Option<DragState>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
@@ -69,55 +81,7 @@ mod imp {
|
|||||||
|
|
||||||
obj.set_overflow(gtk::Overflow::Hidden);
|
obj.set_overflow(gtk::Overflow::Hidden);
|
||||||
|
|
||||||
let drag_controller = gtk::GestureDrag::new();
|
self.setup_node_dragging();
|
||||||
|
|
||||||
drag_controller.connect_drag_begin(|drag_controller, x, y| {
|
|
||||||
let widget = drag_controller
|
|
||||||
.widget()
|
|
||||||
.dynamic_cast::<Self::Type>()
|
|
||||||
.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.
|
|
||||||
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() {
|
|
||||||
// The user targeted a port, so the dragging should be handled by the Port
|
|
||||||
// component instead of here.
|
|
||||||
None
|
|
||||||
} else if let Some(target) = target.ancestor(Node::static_type()) {
|
|
||||||
// The user targeted a Node without targeting a specific Port.
|
|
||||||
// Drag the Node around the screen.
|
|
||||||
let node = target.dynamic_cast_ref::<Node>().unwrap();
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
drag_controller.connect_drag_update(|drag_controller, x, y| {
|
|
||||||
let widget = drag_controller
|
|
||||||
.widget()
|
|
||||||
.dynamic_cast::<Self::Type>()
|
|
||||||
.expect("drag-update event is not on the GraphView");
|
|
||||||
let drag_state = widget.imp().drag_state.borrow();
|
|
||||||
let hadj = widget.imp().hadjustment.borrow();
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispose(&self, _obj: &Self::Type) {
|
fn dispose(&self, _obj: &Self::Type) {
|
||||||
@@ -134,6 +98,15 @@ mod imp {
|
|||||||
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("vadjustment"),
|
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("vadjustment"),
|
||||||
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("hscroll-policy"),
|
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("hscroll-policy"),
|
||||||
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("vscroll-policy"),
|
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("vscroll-policy"),
|
||||||
|
glib::ParamSpecDouble::new(
|
||||||
|
"zoom-factor",
|
||||||
|
"zoom-factor",
|
||||||
|
"zoom-factor",
|
||||||
|
0.3,
|
||||||
|
4.0,
|
||||||
|
1.0,
|
||||||
|
glib::ParamFlags::CONSTRUCT | glib::ParamFlags::READWRITE,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,6 +118,7 @@ mod imp {
|
|||||||
"hadjustment" => self.hadjustment.borrow().to_value(),
|
"hadjustment" => self.hadjustment.borrow().to_value(),
|
||||||
"vadjustment" => self.vadjustment.borrow().to_value(),
|
"vadjustment" => self.vadjustment.borrow().to_value(),
|
||||||
"hscroll-policy" | "vscroll-policy" => gtk::ScrollablePolicy::Natural.to_value(),
|
"hscroll-policy" | "vscroll-policy" => gtk::ScrollablePolicy::Natural.to_value(),
|
||||||
|
"zoom-factor" => self.zoom_factor.get().to_value(),
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,26 +138,33 @@ mod imp {
|
|||||||
self.set_adjustment(obj, value.get().ok(), gtk::Orientation::Vertical)
|
self.set_adjustment(obj, value.get().ok(), gtk::Orientation::Vertical)
|
||||||
}
|
}
|
||||||
"hscroll-policy" | "vscroll-policy" => {}
|
"hscroll-policy" | "vscroll-policy" => {}
|
||||||
|
"zoom-factor" => {
|
||||||
|
self.zoom_factor.set(value.get().unwrap());
|
||||||
|
obj.queue_allocate();
|
||||||
|
}
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetImpl for GraphView {
|
impl WidgetImpl for GraphView {
|
||||||
fn size_allocate(&self, widget: &Self::Type, _width: i32, _height: i32, _baseline: i32) {
|
fn size_allocate(&self, widget: &Self::Type, _width: i32, _height: i32, baseline: i32) {
|
||||||
|
let zoom_factor = self.zoom_factor.get();
|
||||||
|
|
||||||
for (node, point) in self.nodes.borrow().values() {
|
for (node, point) in self.nodes.borrow().values() {
|
||||||
let (_, natural_size) = node.preferred_size();
|
let (_, natural_size) = node.preferred_size();
|
||||||
node.size_allocate(
|
|
||||||
>k::Allocation::new(
|
let transform = self
|
||||||
(f64::from(point.x()) - self.hadjustment.borrow().as_ref().unwrap().value())
|
.canvas_space_to_screen_space_transform()
|
||||||
as i32,
|
.translate(point)
|
||||||
(f64::from(point.y()) - self.vadjustment.borrow().as_ref().unwrap().value())
|
.unwrap();
|
||||||
as i32,
|
|
||||||
natural_size.width(),
|
node.allocate(
|
||||||
natural_size.height(),
|
(natural_size.width() as f64 / zoom_factor).ceil() as i32,
|
||||||
),
|
(natural_size.height() as f64 / zoom_factor).ceil() as i32,
|
||||||
-1,
|
baseline,
|
||||||
)
|
Some(&transform),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref hadjustment) = *self.hadjustment.borrow() {
|
if let Some(ref hadjustment) = *self.hadjustment.borrow() {
|
||||||
@@ -214,13 +195,114 @@ mod imp {
|
|||||||
impl ScrollableImpl for GraphView {}
|
impl ScrollableImpl for GraphView {}
|
||||||
|
|
||||||
impl GraphView {
|
impl GraphView {
|
||||||
|
/// Returns a [`gsk::Transform`] matrix that can translate from canvas space to screen space.
|
||||||
|
///
|
||||||
|
/// Canvas space is non-zoomed, and (0, 0) is fixed at the middle of the graph. \
|
||||||
|
/// Screen space is zoomed and adjusted for scrolling, (0, 0) is at the top-left corner of the window.
|
||||||
|
///
|
||||||
|
/// This is the inverted form of [`Self::screen_space_to_canvas_space_transform()`].
|
||||||
|
fn canvas_space_to_screen_space_transform(&self) -> gsk::Transform {
|
||||||
|
let hadj = self.hadjustment.borrow().as_ref().unwrap().value();
|
||||||
|
let vadj = self.vadjustment.borrow().as_ref().unwrap().value();
|
||||||
|
let zoom_factor = self.zoom_factor.get();
|
||||||
|
|
||||||
|
gsk::Transform::new()
|
||||||
|
.translate(&Point::new(-hadj as f32, -vadj as f32))
|
||||||
|
.unwrap()
|
||||||
|
.scale(zoom_factor as f32, zoom_factor as f32)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a [`gsk::Transform`] matrix that can translate from screen space to canvas space.
|
||||||
|
///
|
||||||
|
/// This is the inverted form of [`Self::canvas_space_to_screen_space_transform()`], see that function for a more detailed explantion.
|
||||||
|
fn screen_space_to_canvas_space_transform(&self) -> gsk::Transform {
|
||||||
|
self.canvas_space_to_screen_space_transform()
|
||||||
|
.invert()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_node_dragging(&self) {
|
||||||
|
let obj = self.instance();
|
||||||
|
let drag_controller = gtk::GestureDrag::new();
|
||||||
|
|
||||||
|
drag_controller.connect_drag_begin(|drag_controller, x, y| {
|
||||||
|
let widget = drag_controller
|
||||||
|
.widget()
|
||||||
|
.dynamic_cast::<super::GraphView>()
|
||||||
|
.expect("drag-begin event is not on the GraphView");
|
||||||
|
let mut dragged_node = widget.imp().dragged_node.borrow_mut();
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
*dragged_node = if target.ancestor(Port::static_type()).is_some() {
|
||||||
|
// The user targeted a port, so the dragging should be handled by the Port
|
||||||
|
// component instead of here.
|
||||||
|
None
|
||||||
|
} else if let Some(target) = target.ancestor(Node::static_type()) {
|
||||||
|
// The user targeted a Node without targeting a specific Port.
|
||||||
|
// Drag the Node around the screen.
|
||||||
|
let node = target.dynamic_cast_ref::<Node>().unwrap();
|
||||||
|
|
||||||
|
let Some(canvas_node_pos) = widget.node_position(node) else { return };
|
||||||
|
let canvas_cursor_pos = widget
|
||||||
|
.imp()
|
||||||
|
.screen_space_to_canvas_space_transform()
|
||||||
|
.transform_point(&Point::new(x as f32, y as f32));
|
||||||
|
|
||||||
|
Some(DragState {
|
||||||
|
node: node.clone().downgrade(),
|
||||||
|
offset: Point::new(
|
||||||
|
canvas_cursor_pos.x() - canvas_node_pos.x(),
|
||||||
|
canvas_cursor_pos.y() - canvas_node_pos.y(),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
drag_controller.connect_drag_update(|drag_controller, x, y| {
|
||||||
|
let widget = drag_controller
|
||||||
|
.widget()
|
||||||
|
.dynamic_cast::<super::GraphView>()
|
||||||
|
.expect("drag-update event is not on the GraphView");
|
||||||
|
let dragged_node = widget.imp().dragged_node.borrow();
|
||||||
|
let Some(DragState { node, offset }) = dragged_node.as_ref() else { return };
|
||||||
|
let Some(node) = node.upgrade() else { return };
|
||||||
|
|
||||||
|
let (start_x, start_y) = drag_controller
|
||||||
|
.start_point()
|
||||||
|
.expect("Drag has no start point");
|
||||||
|
|
||||||
|
let onscreen_node_origin = Point::new((start_x + x) as f32, (start_y + y) as f32);
|
||||||
|
let transform = widget.imp().screen_space_to_canvas_space_transform();
|
||||||
|
let canvas_node_origin = transform.transform_point(&onscreen_node_origin);
|
||||||
|
|
||||||
|
widget.move_node(
|
||||||
|
&node,
|
||||||
|
&Point::new(
|
||||||
|
canvas_node_origin.x() - offset.x(),
|
||||||
|
canvas_node_origin.y() - offset.y(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
obj.add_controller(&drag_controller);
|
||||||
|
}
|
||||||
|
|
||||||
fn snapshot_background(&self, widget: &super::GraphView, snapshot: >k::Snapshot) {
|
fn snapshot_background(&self, widget: &super::GraphView, snapshot: >k::Snapshot) {
|
||||||
const GRID_SIZE: f32 = 20.0;
|
// Grid size and line width during neutral zoom (factor 1.0).
|
||||||
const GRID_LINE_WIDTH: f32 = 1.0;
|
const NORMAL_GRID_SIZE: f32 = 20.0;
|
||||||
|
const NORMAL_GRID_LINE_WIDTH: f32 = 1.0;
|
||||||
|
|
||||||
|
let zoom_factor = self.zoom_factor.get();
|
||||||
|
let grid_size = NORMAL_GRID_SIZE * zoom_factor as f32;
|
||||||
|
let grid_line_width = NORMAL_GRID_LINE_WIDTH * zoom_factor as f32;
|
||||||
|
|
||||||
let alloc = widget.allocation();
|
let alloc = widget.allocation();
|
||||||
|
|
||||||
// We need to offset the lines between 0 and (excluding) GRID_SIZE so the grid moves with
|
// 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 rest of the view when scrolling.
|
||||||
// The offset is rounded so the grid is always aligned to a row of pixels.
|
// The offset is rounded so the grid is always aligned to a row of pixels.
|
||||||
let hadj = self
|
let hadj = self
|
||||||
@@ -229,22 +311,22 @@ mod imp {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|hadjustment| hadjustment.value())
|
.map(|hadjustment| hadjustment.value())
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
let hoffset = ((GRID_SIZE - (hadj as f32 % GRID_SIZE)) % GRID_SIZE).floor();
|
let hoffset = (grid_size - (hadj as f32 % grid_size)) % grid_size;
|
||||||
let vadj = self
|
let vadj = self
|
||||||
.vadjustment
|
.vadjustment
|
||||||
.borrow()
|
.borrow()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|vadjustment| vadjustment.value())
|
.map(|vadjustment| vadjustment.value())
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
let voffset = ((GRID_SIZE - (vadj as f32 % GRID_SIZE)) % GRID_SIZE).floor();
|
let voffset = (grid_size - (vadj as f32 % grid_size)) % grid_size;
|
||||||
|
|
||||||
snapshot.push_repeat(
|
snapshot.push_repeat(
|
||||||
&Rect::new(0.0, 0.0, alloc.width() as f32, alloc.height() as f32),
|
&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)),
|
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);
|
let grid_color = RGBA::new(0.137, 0.137, 0.137, 1.0);
|
||||||
snapshot.append_linear_gradient(
|
snapshot.append_linear_gradient(
|
||||||
&Rect::new(0.0, voffset, alloc.width() as f32, GRID_LINE_WIDTH),
|
&Rect::new(0.0, voffset, alloc.width() as f32, grid_line_width),
|
||||||
&Point::new(0.0, 0.0),
|
&Point::new(0.0, 0.0),
|
||||||
&Point::new(alloc.width() as f32, 0.0),
|
&Point::new(alloc.width() as f32, 0.0),
|
||||||
&[
|
&[
|
||||||
@@ -256,10 +338,10 @@ mod imp {
|
|||||||
|
|
||||||
snapshot.push_repeat(
|
snapshot.push_repeat(
|
||||||
&Rect::new(0.0, 0.0, alloc.width() as f32, alloc.height() as f32),
|
&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)),
|
Some(&Rect::new(hoffset, 0.0, grid_size, alloc.height() as f32)),
|
||||||
);
|
);
|
||||||
snapshot.append_linear_gradient(
|
snapshot.append_linear_gradient(
|
||||||
&Rect::new(hoffset, 0.0, GRID_LINE_WIDTH, alloc.height() as f32),
|
&Rect::new(hoffset, 0.0, grid_line_width, alloc.height() as f32),
|
||||||
&Point::new(0.0, 0.0),
|
&Point::new(0.0, 0.0),
|
||||||
&Point::new(0.0, alloc.height() as f32),
|
&Point::new(0.0, alloc.height() as f32),
|
||||||
&[
|
&[
|
||||||
@@ -280,7 +362,7 @@ mod imp {
|
|||||||
alloc.height() as f32,
|
alloc.height() as f32,
|
||||||
));
|
));
|
||||||
|
|
||||||
link_cr.set_line_width(2.0);
|
link_cr.set_line_width(2.0 * self.zoom_factor.get());
|
||||||
|
|
||||||
let rgba = widget
|
let rgba = widget
|
||||||
.style_context()
|
.style_context()
|
||||||
@@ -344,30 +426,29 @@ mod imp {
|
|||||||
fn get_link_coordinates(&self, link: &crate::PipewireLink) -> Option<(f64, f64, f64, f64)> {
|
fn get_link_coordinates(&self, link: &crate::PipewireLink) -> Option<(f64, f64, f64, f64)> {
|
||||||
let nodes = self.nodes.borrow();
|
let nodes = self.nodes.borrow();
|
||||||
|
|
||||||
// For some reason, gtk4::WidgetExt::translate_coordinates gives me incorrect values,
|
let output_port = &nodes.get(&link.node_from)?.0.get_port(link.port_from)?;
|
||||||
// so we manually calculate the needed offsets here.
|
|
||||||
|
|
||||||
let from_port = &nodes.get(&link.node_from)?.0.get_port(link.port_from)?;
|
let output_port_padding =
|
||||||
let from_node = from_port
|
(output_port.allocated_width() - output_port.width()) as f64 / 2.0;
|
||||||
.ancestor(Node::static_type())
|
|
||||||
.expect("Port is not a child of a node");
|
|
||||||
let from_x = from_node.allocation().x()
|
|
||||||
+ from_port.allocation().x()
|
|
||||||
+ from_port.allocation().width();
|
|
||||||
let from_y = from_node.allocation().y()
|
|
||||||
+ from_port.allocation().y()
|
|
||||||
+ (from_port.allocation().height() / 2);
|
|
||||||
|
|
||||||
let to_port = &nodes.get(&link.node_to)?.0.get_port(link.port_to)?;
|
let (from_x, from_y) = output_port.translate_coordinates(
|
||||||
let to_node = to_port
|
&self.instance(),
|
||||||
.ancestor(Node::static_type())
|
output_port.width() as f64 + output_port_padding,
|
||||||
.expect("Port is not a child of a node");
|
(output_port.height() / 2) as f64,
|
||||||
let to_x = to_node.allocation().x() + to_port.allocation().x();
|
)?;
|
||||||
let to_y = to_node.allocation().y()
|
|
||||||
+ to_port.allocation().y()
|
|
||||||
+ (to_port.allocation().height() / 2);
|
|
||||||
|
|
||||||
Some((from_x.into(), from_y.into(), to_x.into(), to_y.into()))
|
let input_port = &nodes.get(&link.node_to)?.0.get_port(link.port_to)?;
|
||||||
|
|
||||||
|
let input_port_padding =
|
||||||
|
(input_port.allocated_width() - input_port.width()) as f64 / 2.0;
|
||||||
|
|
||||||
|
let (to_x, to_y) = input_port.translate_coordinates(
|
||||||
|
&self.instance(),
|
||||||
|
-input_port_padding,
|
||||||
|
(input_port.height() / 2) as f64,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Some((from_x, from_y, to_x, to_y))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_adjustment(
|
fn set_adjustment(
|
||||||
@@ -401,14 +482,15 @@ mod imp {
|
|||||||
gtk::Orientation::Vertical => obj.height(),
|
gtk::Orientation::Vertical => obj.height(),
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
};
|
};
|
||||||
|
let zoom_factor = self.zoom_factor.get();
|
||||||
|
|
||||||
adjustment.configure(
|
adjustment.configure(
|
||||||
adjustment.value(),
|
adjustment.value(),
|
||||||
-(CANVAS_SIZE / 2.0),
|
-(CANVAS_SIZE / 2.0) * zoom_factor,
|
||||||
CANVAS_SIZE / 2.0,
|
(CANVAS_SIZE / 2.0) * zoom_factor,
|
||||||
f64::from(size) * 0.1,
|
(f64::from(size) * 0.1) * zoom_factor,
|
||||||
f64::from(size) * 0.9,
|
(f64::from(size) * 0.9) * zoom_factor,
|
||||||
f64::from(size),
|
f64::from(size) * zoom_factor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,10 +502,27 @@ glib::wrapper! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GraphView {
|
impl GraphView {
|
||||||
|
pub const ZOOM_MIN: f64 = 0.3;
|
||||||
|
pub const ZOOM_MAX: f64 = 4.0;
|
||||||
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
glib::Object::new(&[]).expect("Failed to create GraphView")
|
glib::Object::new(&[]).expect("Failed to create GraphView")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn zoom_factor(&self) -> f64 {
|
||||||
|
self.property("zoom-factor")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the scale factor.
|
||||||
|
///
|
||||||
|
/// A factor of 1.0 is equivalent to 100% zoom, 0.5 to 50% zoom etc.
|
||||||
|
///
|
||||||
|
/// Note that the zoom level is limited to between 30% and 300%.
|
||||||
|
/// See [`ZOOM_MIN`] and [`ZOOM_MAX`].
|
||||||
|
pub fn set_zoom_factor(&self, scale_factor: f64) {
|
||||||
|
self.set_property("zoom-factor", scale_factor)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_node(&self, id: u32, node: Node, node_type: Option<NodeType>) {
|
pub fn add_node(&self, id: u32, node: Node, node_type: Option<NodeType>) {
|
||||||
let private = imp::GraphView::from_instance(self);
|
let private = imp::GraphView::from_instance(self);
|
||||||
node.set_parent(self);
|
node.set_parent(self);
|
||||||
@@ -444,7 +543,8 @@ impl GraphView {
|
|||||||
.values()
|
.values()
|
||||||
.map(|node| {
|
.map(|node| {
|
||||||
// Map nodes to their locations
|
// Map nodes to their locations
|
||||||
self.get_node_position(&node.0.clone().upcast()).unwrap()
|
let point = self.node_position(&node.0.clone().upcast()).unwrap();
|
||||||
|
(point.x(), point.y())
|
||||||
})
|
})
|
||||||
.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
|
||||||
@@ -518,15 +618,17 @@ 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: &Node) -> Option<(f32, f32)> {
|
///
|
||||||
|
/// The returned position is in canvas-space (non-zoomed, (0, 0) fixed in the middle of the canvas).
|
||||||
|
pub(super) fn node_position(&self, node: &Node) -> Option<Point> {
|
||||||
self.imp()
|
self.imp()
|
||||||
.nodes
|
.nodes
|
||||||
.borrow()
|
.borrow()
|
||||||
.get(&node.pipewire_id())
|
.get(&node.pipewire_id())
|
||||||
.map(|(_, point)| (point.x(), point.y()))
|
.map(|(_, point)| *point)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn move_node(&self, widget: &Node, x: f32, y: f32) {
|
pub(super) fn move_node(&self, widget: &Node, point: &Point) {
|
||||||
let mut nodes = self.imp().nodes.borrow_mut();
|
let mut nodes = self.imp().nodes.borrow_mut();
|
||||||
let mut node = nodes
|
let mut node = nodes
|
||||||
.get_mut(&widget.pipewire_id())
|
.get_mut(&widget.pipewire_id())
|
||||||
@@ -534,11 +636,11 @@ impl GraphView {
|
|||||||
|
|
||||||
// Clamp the new position to within the graph, so a node can't be moved outside it and be lost.
|
// Clamp the new position to within the graph, so a node can't be moved outside it and be lost.
|
||||||
node.1 = Point::new(
|
node.1 = Point::new(
|
||||||
x.clamp(
|
point.x().clamp(
|
||||||
-(CANVAS_SIZE / 2.0) as f32,
|
-(CANVAS_SIZE / 2.0) as f32,
|
||||||
(CANVAS_SIZE / 2.0) as f32 - widget.width() as f32,
|
(CANVAS_SIZE / 2.0) as f32 - widget.width() as f32,
|
||||||
),
|
),
|
||||||
y.clamp(
|
point.y().clamp(
|
||||||
-(CANVAS_SIZE / 2.0) as f32,
|
-(CANVAS_SIZE / 2.0) as f32,
|
||||||
(CANVAS_SIZE / 2.0) as f32 - widget.height() as f32,
|
(CANVAS_SIZE / 2.0) as f32 - widget.height() as f32,
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user