use super::*;
use crate::node_graph::camera::Camera2D;
use crate::node_graph::euclid::{WorldRect, WorldSize, WorldVector};
use crate::node_graph::input::KeyAction;
use crate::node_graph::state::AnchorGeometry;
use crate::node_graph::{EDGE_CUT_THRESHOLD, FocusOptions, FocusTarget, PIN_CLICK_THRESHOLD};
use euclid::Vector2D;
use iced_widget::core::{Padding, touch, window};
use std::collections::HashSet;
const SNAP_THRESHOLD: f32 = 10.0; const UNSNAP_THRESHOLD: f32 = 15.0;
const TOUCH_TAP_TRAVEL: f32 = 8.0;
const TOUCH_TAP_MAX_SECS: f32 = 0.3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum CableZone {
End { edge: usize, at_start: bool },
Wrap { edge: usize, anchor: usize },
Run { edge: usize },
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct CableHit {
pub zone: CableZone,
pub window: (f32, f32),
}
struct UpdateCtx<'a, 'b, 'm, Message> {
tree: &'a mut Tree,
layout: Layout<'b>,
event: &'a Event,
world_cursor: mouse::Cursor,
screen_cursor: mouse::Cursor,
shell: &'a mut Shell<'m, Message>,
}
impl<I, Message, Theme, Renderer> NodeGraph<'_, I, Message, Theme, Renderer>
where
I: Ids,
Theme: Catalog,
Renderer: iced_wgpu::core::renderer::Renderer + iced_wgpu::primitive::Renderer,
{
#[allow(clippy::too_many_arguments)]
pub(super) fn update_impl(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
screen_cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
let state = tree.state.downcast_mut::<NodeGraphState>();
if let Some(camera) = self.camera
&& state.last_synced_camera != Some(camera)
{
let (position, zoom) = camera;
state.camera =
Camera2D::with_zoom_and_position(zoom, WorldPoint::new(position.x, position.y));
state.last_synced_camera = Some(camera);
state.camera_tween = None;
}
state.camera = state.camera_for(layout);
let host_selection = self.host_selection();
if state.selection_baseline.as_ref() != Some(&host_selection) {
state.pending_selection = None;
state.selection_baseline = Some(host_selection);
}
state.ensure_z_entries(self.nodes.len());
let selection = self.resolved_selection(state);
let z_indices = z_render_indices(
state,
self.nodes.len(),
|i| selection.contains(&i),
|i| self.nodes[i].frame,
);
if let Some((world_aabb, opts)) = state.pending_focus.take() {
self.begin_focus(state, world_aabb, layout.bounds().size(), &opts, shell);
}
let now = Instant::now();
state.time = state.animation_time(now);
state.last_update = Some(now);
if let Event::Window(window::Event::RedrawRequested(redraw_at)) = event {
if state.sdf_animated.get() {
shell.request_redraw();
}
if let Some(handler) = self.on_info.as_ref() {
if let Some(info) = state.last_info.borrow_mut().take() {
shell.publish(handler(info));
}
shell.request_redraw();
}
let is_reentrant_redraw = state.last_redraw == Some(*redraw_at);
let redraw_delta = state
.last_redraw
.map(|last| redraw_at.duration_since(last).as_secs_f32().min(0.1))
.unwrap_or(0.0);
state.last_redraw = Some(*redraw_at);
if is_reentrant_redraw {
if state.camera_tween.is_some() {
shell.request_redraw();
}
} else if let Some(tween) = state.camera_tween.as_mut() {
tween.elapsed += redraw_delta;
let t = if tween.duration > 0.0 {
(tween.elapsed / tween.duration).clamp(0.0, 1.0)
} else {
1.0
};
let e = tween.easing.apply(t);
let (center, zoom) = Camera2D::tween_step(
tween.start_center,
tween.start_zoom,
tween.end_center,
tween.end_zoom,
e,
);
let position =
Camera2D::position_for_center(center, zoom, tween.viewport, tween.padding);
let viewport_origin = state.camera.viewport_origin();
state.camera = Camera2D::with_zoom_and_position(zoom, position)
.with_viewport_origin(viewport_origin);
let camera = (Point::new(position.x, position.y), zoom);
if let Some(handler) = self.on_camera.as_ref() {
shell.publish(handler(camera.0, camera.1));
}
state.last_synced_camera = Some(camera);
if t < 1.0 {
shell.request_redraw();
} else {
state.camera_tween = None;
}
}
}
if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event {
state.modifiers = *modifiers;
}
let synthesized = if let Event::Touch(touch_event) = event {
self.apply_touch(state, touch_event, shell)
} else {
None
};
let (event, screen_cursor) = match &synthesized {
Some((event, cursor)) => (event, *cursor),
None => (event, screen_cursor),
};
let graph_move_offset = if let Dragging::Graph(origin) = state.dragging {
screen_cursor.position().map(|cursor_position| {
let cursor_world: WorldPoint = state
.camera
.screen_to_world()
.transform_point(cursor_position.into_euclid());
(cursor_world - origin).into_iced()
})
} else {
None
}
.unwrap_or(Vector::ZERO);
let clipped_viewport = layout
.bounds()
.intersection(viewport)
.unwrap_or(Rectangle::new(layout.bounds().position(), Size::ZERO));
state
.camera
.move_by(graph_move_offset.into_euclid())
.update_with(
&clipped_viewport,
screen_cursor,
|viewport, world_cursor| {
let mut ctx = UpdateCtx {
tree,
layout,
event,
world_cursor,
screen_cursor,
shell,
};
let state = ctx.tree.state.downcast_mut::<NodeGraphState>();
if state.dragging != Dragging::None
&& let Event::Mouse(mouse::Event::CursorMoved { .. }) = event
{
if let Some(cursor_position) = world_cursor.position()
&& let Some(handler) = self.on_drag_update.as_ref()
{
ctx.shell.publish(handler(cursor_position));
}
ctx.shell.capture_event();
ctx.shell.request_redraw();
}
match state.dragging.clone() {
Dragging::None => self.refresh_hover(&mut ctx),
Dragging::EdgeCutting { .. } => self.handle_edge_cutting(&mut ctx),
Dragging::Graph(origin) => self.handle_graph_pan(&mut ctx, origin),
Dragging::Node {
node: node_index,
followers,
..
} => self.handle_node_drag(&mut ctx, node_index, &followers),
Dragging::Anchor { anchor, origin } => {
self.handle_anchor_drag(&mut ctx, anchor, origin)
}
Dragging::Resize {
node: node_index,
origin,
start,
} => self.handle_resize(&mut ctx, node_index, origin, start),
Dragging::Edge {
from_node,
from_pin,
origin: _,
} => self.handle_edge_drag(&mut ctx, from_node, from_pin),
Dragging::EdgeOver {
from_node,
from_pin,
to_node,
to_pin,
} => self.handle_edge_over(&mut ctx, from_node, from_pin, to_node, to_pin),
Dragging::Route { edge, detached } => {
self.handle_route_drag(&mut ctx, edge, detached)
}
Dragging::RouteOver { edge, anchor, .. } => {
self.handle_route_over(&mut ctx, edge, anchor)
}
Dragging::PressPending {
origin_world,
origin_screen,
target,
} => {
self.handle_press_pending(&mut ctx, origin_world, origin_screen, target)
}
Dragging::SelectionBox(start, _current) => {
self.handle_selection_box(&mut ctx, start)
}
Dragging::GroupMove {
anchor, followers, ..
} => self.handle_group_move(&mut ctx, anchor, &followers),
Dragging::Minimap => self.handle_minimap_drag(&mut ctx),
}
let pre_captured = ctx.shell.is_event_captured();
for &node_index in z_indices.iter().rev() {
let Some(node) = self.nodes.get_mut(node_index) else {
continue;
};
let element = &mut node.element;
let Some(child_tree) = ctx.tree.children.get_mut(node_index) else {
continue;
};
let Some(child_layout) = layout.children().nth(node_index) else {
continue;
};
element.as_widget_mut().update(
child_tree,
event,
child_layout,
world_cursor,
renderer,
clipboard,
ctx.shell,
viewport,
);
if !pre_captured && ctx.shell.is_event_captured() {
break;
}
}
if ctx.shell.is_event_captured() {
return;
}
if let Event::Keyboard(keyboard::Event::KeyPressed {
key,
physical_key,
modifiers,
..
}) = event
&& self.keymap.key_action(key, *physical_key, *modifiers)
== Some(KeyAction::DeleteSelection)
&& !selection.is_empty()
&& self.on_delete.as_ref().is_some()
{
if let Some(handler) = self.on_delete.as_ref() {
ctx.shell.publish(handler(self.selection_ids(&selection)));
}
ctx.shell.capture_event();
ctx.shell.request_redraw();
}
if let Event::Keyboard(keyboard::Event::KeyPressed {
key,
physical_key,
modifiers,
..
}) = event
{
let state = ctx.tree.state.downcast_mut::<NodeGraphState>();
match self.keymap.key_action(key, *physical_key, *modifiers) {
Some(KeyAction::CloneSelection)
if !selection.is_empty() && self.on_clone.as_ref().is_some() =>
{
let node_ids = self.selection_ids(&selection);
if let Some(handler) = self.on_clone.as_ref() {
ctx.shell.publish(handler(node_ids));
}
ctx.shell.capture_event();
}
Some(KeyAction::SelectAll) => {
state.pending_selection = Some((0..self.nodes.len()).collect());
let selected: Vec<I::NodeId> =
self.nodes.iter().map(|node| node.id.clone()).collect();
if let Some(handler) = self.on_select.as_ref() {
ctx.shell.publish(handler(selected));
}
ctx.shell.capture_event();
ctx.shell.request_redraw();
}
Some(KeyAction::ClearSelection) if !selection.is_empty() => {
state.pending_selection = Some(HashSet::new());
if let Some(handler) = self.on_select.as_ref() {
ctx.shell.publish(handler(vec![]));
}
ctx.shell.capture_event();
ctx.shell.request_redraw();
}
_ => {}
}
}
if let Event::Keyboard(keyboard::Event::KeyPressed {
key,
physical_key,
modifiers,
..
}) = event
&& self.on_camera.as_ref().is_some()
{
let frame_target =
match self.keymap.key_action(key, *physical_key, *modifiers) {
Some(KeyAction::FrameAll) => Some(FocusTarget::All),
Some(KeyAction::FrameSelection) => Some(FocusTarget::Selection),
_ => None,
};
let state = ctx.tree.state.downcast_mut::<NodeGraphState>();
if let Some(target) = frame_target
&& let Some(world_aabb) =
resolve_focus_target(self, layout, state, &target)
{
self.begin_focus(
state,
world_aabb,
layout.bounds().size(),
&FocusOptions::default(),
ctx.shell,
);
ctx.shell.capture_event();
}
}
if !screen_cursor.is_over(layout.bounds()) {
return;
}
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
self.handle_left_press(&mut ctx, &z_indices)
}
Event::Mouse(mouse::Event::ButtonPressed(button))
if *button == self.keymap.pan_button =>
{
self.handle_pan_press(&mut ctx)
}
Event::Mouse(mouse::Event::WheelScrolled { delta, .. }) => {
self.handle_wheel(&mut ctx, delta)
}
_ => {}
}
},
);
}
fn apply_touch(
&self,
state: &mut NodeGraphState,
event: &touch::Event,
shell: &mut Shell<'_, Message>,
) -> Option<(Event, mouse::Cursor)> {
match *event {
touch::Event::FingerPressed { id, position } => {
if let Some(entry) = state.fingers.iter_mut().find(|(f, _)| *f == id) {
entry.1 = position;
return None;
}
state.fingers.push((id, position));
match state.fingers.len() {
1 => {
state.touch_tap = Some((id, position, state.time));
Some((
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
mouse::Cursor::Available(position),
))
}
2 => {
state.touch_tap = None;
if state.dragging != Dragging::None {
state.dragging = Dragging::None;
if let Some(handler) = self.on_drag_end.as_ref() {
shell.publish(handler());
}
shell.request_redraw();
}
None
}
_ => None,
}
}
touch::Event::FingerMoved { id, position } => {
let index = state.fingers.iter().position(|(f, _)| *f == id)?;
if state.fingers.len() == 1 {
state.fingers[0].1 = position;
if let Some((_, start, _)) = state.touch_tap
&& start.distance(position) > TOUCH_TAP_TRAVEL
{
state.touch_tap = None;
}
return Some((
Event::Mouse(mouse::Event::CursorMoved { position }),
mouse::Cursor::Available(position),
));
}
if index < 2 {
let prev = (state.fingers[0].1, state.fingers[1].1);
state.fingers[index].1 = position;
let next = (state.fingers[0].1, state.fingers[1].1);
let prev_distance = prev.0.distance(prev.1);
let next_distance = next.0.distance(next.1);
let prev_mid =
Point::new((prev.0.x + prev.1.x) / 2.0, (prev.0.y + prev.1.y) / 2.0);
let next_mid =
Point::new((next.0.x + next.1.x) / 2.0, (next.0.y + next.1.y) / 2.0);
state.camera_tween = None;
if prev_distance > 1.0 && next_distance > 1.0 {
let zoom_delta =
(next_distance / prev_distance - 1.0) * state.camera.zoom();
let mid: ScreenPoint = next_mid.into_euclid();
state.camera = state.camera.zoom_at(mid, zoom_delta);
}
let zoom = state.camera.zoom();
let pan = WorldPoint::new(next_mid.x / zoom, next_mid.y / zoom)
- WorldPoint::new(prev_mid.x / zoom, prev_mid.y / zoom);
state.camera = state.camera.move_by(pan);
if let Some(handler) = self.on_camera.as_ref() {
let pos = state.camera.position();
shell.publish(handler(Point::new(pos.x, pos.y), state.camera.zoom()));
}
shell.capture_event();
shell.request_redraw();
} else {
state.fingers[index].1 = position;
}
None
}
touch::Event::FingerLifted { id, position }
| touch::Event::FingerLost { id, position } => {
state.fingers.retain(|(f, _)| *f != id);
if !state.fingers.is_empty() {
return None;
}
let lost = matches!(event, touch::Event::FingerLost { .. });
if let Some((tap_id, _, pressed_at)) = state.touch_tap.take()
&& tap_id == id
&& !lost
&& state.time - pressed_at <= TOUCH_TAP_MAX_SECS
&& matches!(state.dragging, Dragging::Graph(_))
&& !self.resolved_selection(state).is_empty()
{
state.pending_selection = Some(HashSet::new());
if let Some(handler) = self.on_select.as_ref() {
shell.publish(handler(vec![]));
}
shell.request_redraw();
}
let button = if matches!(state.dragging, Dragging::Graph(_)) {
self.keymap.pan_button
} else {
mouse::Button::Left
};
Some((
Event::Mouse(mouse::Event::ButtonReleased(button)),
mouse::Cursor::Available(position),
))
}
}
}
fn handle_edge_cutting(&self, ctx: &mut UpdateCtx<'_, '_, '_, Message>) {
match ctx.event {
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if let Some(cursor_position) = ctx.world_cursor.position() {
let cursor_position: LayoutPoint = cursor_position.into_euclid();
let cables = self.cable_geometry(ctx.tree, ctx.layout);
let state = ctx.tree.state.downcast_mut::<NodeGraphState>();
if let Dragging::EdgeCutting {
ref mut trail,
ref mut pending_cuts,
} = state.dragging
{
trail.push(cursor_position);
let cut_start = trail.first().copied().unwrap_or(cursor_position);
let from = [cut_start.x, cut_start.y];
let to = [cursor_position.x, cursor_position.y];
pending_cuts.clear();
for (geometry, built) in &cables {
if built.path.intersects(from, to) {
pending_cuts.insert(geometry.edge);
}
}
}
}
ctx.shell.request_redraw();
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
let state = ctx.tree.state.downcast_mut::<NodeGraphState>();
if let Dragging::EdgeCutting { pending_cuts, .. } = &state.dragging {
let mut cut_ids = Vec::new();
for &edge_idx in pending_cuts.iter() {
if let Some(Edge { id, from, to, .. }) = self.edges.get(edge_idx) {
if let Some(handler) = self.on_disconnect.as_ref() {
ctx.shell.publish(handler(from.clone(), to.clone()));
}
cut_ids.push(id.clone());
}
}
if let Some(handler) = self.on_edge_delete.as_ref()
&& !cut_ids.is_empty()
{
ctx.shell.publish(handler(cut_ids));
}
}
ctx.tree.state.downcast_mut::<NodeGraphState>().dragging = Dragging::None;
if let Some(handler) = self.on_drag_end.as_ref() {
ctx.shell.publish(handler());
}
ctx.shell.capture_event();
ctx.shell.request_redraw();
}
_ => {}
}
}
fn handle_graph_pan(&self, ctx: &mut UpdateCtx<'_, '_, '_, Message>, origin: WorldPoint) {
let UpdateCtx {
tree,
event,
screen_cursor,
shell,
..
} = &mut *ctx;
let state = tree.state.downcast_mut::<NodeGraphState>();
if let Event::Mouse(mouse::Event::ButtonReleased(button)) = event
&& *button == self.keymap.pan_button
{
if let Some(cursor_position) = screen_cursor.position() {
let screen_to_world = state.camera.screen_to_world();
let cursor_position: ScreenPoint = cursor_position.into_euclid();
let cursor_position: WorldPoint = screen_to_world.transform_point(cursor_position);
let offset = cursor_position - origin;
state.camera = state.camera.move_by(offset);
if let Some(handler) = self.on_camera.as_ref() {
let pos = state.camera.position();
shell.publish(handler(Point::new(pos.x, pos.y), state.camera.zoom()));
}
}
state.dragging = Dragging::None;
shell.capture_event();
shell.request_redraw();
}
}
fn handle_node_drag(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
node_index: usize,
followers: &[usize],
) {
let UpdateCtx {
tree,
event,
world_cursor,
shell,
..
} = &mut *ctx;
if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) = event {
let moved_indices = merged_indices(&[node_index], followers);
if let Some(cursor_position) = world_cursor.position() {
let state = tree.state.downcast_ref::<NodeGraphState>();
let offset = drag_offset(state, self, node_index, cursor_position.into_euclid());
let moved = offset.x.abs() > f32::EPSILON || offset.y.abs() > f32::EPSILON;
let node_ids = self.node_ids_at(&moved_indices);
if moved
&& !node_ids.is_empty()
&& let Some(handler) = self.on_move.as_ref()
{
shell.publish(handler(offset.into_iced(), node_ids));
}
}
let state = tree.state.downcast_mut::<NodeGraphState>();
state.promote_z_many(&moved_indices);
state.dragging = Dragging::None;
if let Some(handler) = self.on_drag_end.as_ref() {
shell.publish(handler());
}
shell.capture_event();
shell.invalidate_layout();
shell.request_redraw();
}
}
fn handle_anchor_drag(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
anchor_index: usize,
origin: WorldPoint,
) {
let UpdateCtx {
tree,
event,
world_cursor,
shell,
..
} = &mut *ctx;
if !matches!(
event,
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
) {
return;
}
if let Some(cursor_position) = world_cursor.position() {
let state = tree.state.downcast_ref::<NodeGraphState>();
let cursor = state.camera.layout_to_world(cursor_position.into_euclid());
let offset = anchor_drag_offset(state, self, anchor_index, cursor - origin);
let moved = offset.x.abs() > f32::EPSILON || offset.y.abs() > f32::EPSILON;
if let Some(anchor) = self.anchors.get(anchor_index)
&& let Some(handler) = self.on_anchor_move.as_ref()
&& moved
{
shell.publish(handler(
anchor.id.clone(),
Point::new(anchor.position.x + offset.x, anchor.position.y + offset.y),
));
}
}
tree.state.downcast_mut::<NodeGraphState>().dragging = Dragging::None;
if let Some(handler) = self.on_drag_end.as_ref() {
shell.publish(handler());
}
shell.capture_event();
shell.request_redraw();
}
fn handle_resize(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
node_index: usize,
origin: LayoutPoint,
start: Size,
) {
let UpdateCtx {
tree,
event,
world_cursor,
shell,
..
} = &mut *ctx;
match event {
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
let Some(cursor_position) = world_cursor.position() else {
return;
};
let offset = cursor_position.into_euclid() - origin;
let Some(node_id) = self.node_id_at(node_index).cloned() else {
return;
};
if let Some(handler) = self.on_resize.as_ref() {
let state = tree.state.downcast_ref::<NodeGraphState>();
let extent = snapped_delta(
state,
self,
self.nodes[node_index].position,
LayoutVector::new(start.width + offset.x, start.height + offset.y),
);
let size = Size::new(
extent.x.max(MIN_NODE_SIZE.width),
extent.y.max(MIN_NODE_SIZE.height),
);
shell.publish(handler(node_id, size));
}
shell.capture_event();
shell.request_redraw();
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
let state = tree.state.downcast_mut::<NodeGraphState>();
state.dragging = Dragging::None;
if let Some(handler) = self.on_drag_end.as_ref() {
shell.publish(handler());
}
shell.capture_event();
shell.invalidate_layout();
shell.request_redraw();
}
_ => {}
}
}
fn handle_edge_drag(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
from_node: usize,
from_pin: usize,
) {
let UpdateCtx {
tree,
layout,
event,
world_cursor,
shell,
..
} = &mut *ctx;
let state = tree.state.downcast_mut::<NodeGraphState>();
match event {
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if let Some(cursor_position) = world_cursor.position() {
let valid_targets = state.valid_drop_targets.clone();
let snap_threshold = SNAP_THRESHOLD / state.camera.zoom();
let mut from_pin_id: Option<I::PinId> = None;
let mut from_dir: Option<PinDirection> = None;
let mut target_info: Option<(usize, usize, I::PinId, PinDirection)> = None;
for (node_index, (node_layout, node_tree)) in
layout.children().zip(&tree.children).enumerate()
{
for (pin_index, pin_state, (a, b)) in find_pins::<I>(node_tree, node_layout)
{
if node_index == from_node && pin_index == from_pin {
from_pin_id = Some(pin_state.pin_id.clone());
from_dir = Some(pin_state.direction);
}
let distance =
a.distance(cursor_position).min(b.distance(cursor_position));
if distance < snap_threshold && target_info.is_none() {
if valid_targets.contains(&(node_index, pin_index)) {
target_info = Some((
node_index,
pin_index,
pin_state.pin_id.clone(),
pin_state.direction,
));
}
}
}
}
if let Some((to_node, to_pin, to_pin_id, to_dir)) = target_info {
let from_node_id = self.node_id_at(from_node).cloned();
let to_node_id = self.node_id_at(to_node).cloned();
if let (Some(from_nid), Some(to_nid), Some(from_pid)) =
(from_node_id, to_node_id, from_pin_id)
{
let (from_ref, to_ref) = orient_connection(
from_dir.unwrap_or(PinDirection::Both),
to_dir,
PinRef::new(from_nid.clone(), from_pid),
PinRef::new(to_nid.clone(), to_pin_id),
);
if let Some(handler) = self.on_connect.as_ref() {
shell.publish(handler(from_ref, to_ref));
}
}
state.dragging = Dragging::EdgeOver {
from_node,
from_pin,
to_node,
to_pin,
};
}
}
shell.request_redraw();
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
if let Some(handler) = self.on_connect_refused.as_ref()
&& let Some(cursor) = world_cursor.position()
{
let threshold = SNAP_THRESHOLD / state.camera.zoom();
let valid = state.valid_drop_targets.clone();
if let Some((from_ref, to_ref)) = refused_drop(
self,
tree,
*layout,
(from_node, from_pin),
&valid,
cursor,
threshold,
) {
shell.publish(handler(from_ref, to_ref));
}
}
let state = tree.state.downcast_mut::<NodeGraphState>();
state.dragging = Dragging::None;
if let Some(handler) = self.on_drag_end.as_ref() {
shell.publish(handler());
}
shell.capture_event();
shell.request_redraw();
}
_ => {}
}
}
fn handle_edge_over(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
from_node: usize,
from_pin: usize,
to_node: usize,
to_pin: usize,
) {
let UpdateCtx {
tree,
layout,
event,
world_cursor,
shell,
..
} = &mut *ctx;
let state = tree.state.downcast_mut::<NodeGraphState>();
match event {
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if let Some(cursor_position) = world_cursor.position() {
let unsnap_threshold = UNSNAP_THRESHOLD / state.camera.zoom();
let mut still_over_pin = false;
let mut from_pin_id: Option<I::PinId> = None;
let mut to_pin_id: Option<I::PinId> = None;
let mut from_dir: Option<PinDirection> = None;
let mut to_dir: Option<PinDirection> = None;
for (node_index, (node_layout, node_tree)) in
layout.children().zip(&tree.children).enumerate()
{
for (pin_index, pin_state, (a, b)) in find_pins::<I>(node_tree, node_layout)
{
if node_index == from_node && pin_index == from_pin {
from_pin_id = Some(pin_state.pin_id.clone());
from_dir = Some(pin_state.direction);
}
if node_index == to_node && pin_index == to_pin {
to_pin_id = Some(pin_state.pin_id.clone());
to_dir = Some(pin_state.direction);
let distance =
a.distance(cursor_position).min(b.distance(cursor_position));
still_over_pin = distance < unsnap_threshold;
}
}
}
if !still_over_pin {
let from_node_id = self.node_id_at(from_node).cloned();
let to_node_id = self.node_id_at(to_node).cloned();
if let (Some(from_nid), Some(to_nid), Some(from_pid), Some(to_pid)) =
(from_node_id, to_node_id, from_pin_id, to_pin_id)
{
let (from_ref, to_ref) = orient_connection(
from_dir.unwrap_or(PinDirection::Both),
to_dir.unwrap_or(PinDirection::Both),
PinRef::new(from_nid.clone(), from_pid),
PinRef::new(to_nid.clone(), to_pid),
);
if let Some(handler) = self.on_disconnect.as_ref() {
shell.publish(handler(from_ref, to_ref));
}
}
state.dragging = Dragging::Edge {
from_node,
from_pin,
origin: cursor_position.into_euclid(),
};
}
}
shell.request_redraw();
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
state.dragging = Dragging::None;
if let Some(handler) = self.on_drag_end.as_ref() {
shell.publish(handler());
}
shell.capture_event();
shell.request_redraw();
}
_ => {}
}
}
fn handle_route_drag(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
edge: usize,
detached: Option<usize>,
) {
match ctx.event {
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if let Some(cursor_position) = ctx.world_cursor.position()
&& let Some(anchor) =
self.route_snap_target(ctx.tree, edge, detached, cursor_position)
{
if let Some(handler) = self.on_route_attach.as_ref()
&& let Some(edge_id) = self.edges.get(edge).map(|edge| edge.id.clone())
&& let Some(anchor_id) =
self.anchors.get(anchor).map(|anchor| anchor.id.clone())
{
ctx.shell.publish(handler(edge_id, anchor_id));
}
ctx.tree.state.downcast_mut::<NodeGraphState>().dragging =
Dragging::RouteOver {
edge,
anchor,
detached: detached.filter(|held| *held != anchor),
};
}
ctx.shell.request_redraw();
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
if let Some(cursor_position) = ctx.world_cursor.position()
&& let Some(handler) = self.on_anchor_create.as_ref()
&& let Some(edge_id) = self.edges.get(edge).map(|edge| edge.id.clone())
{
let at = ctx
.tree
.state
.downcast_ref::<NodeGraphState>()
.camera
.layout_to_world(cursor_position.into_euclid());
ctx.shell.publish(handler(edge_id, Point::new(at.x, at.y)));
}
ctx.tree.state.downcast_mut::<NodeGraphState>().dragging = Dragging::None;
if let Some(handler) = self.on_drag_end.as_ref() {
ctx.shell.publish(handler());
}
ctx.shell.capture_event();
ctx.shell.request_redraw();
}
_ => {}
}
}
fn handle_route_over(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
edge: usize,
anchor: usize,
) {
match ctx.event {
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
let Some(cursor_position) = ctx.world_cursor.position() else {
return;
};
let unsnap = UNSNAP_THRESHOLD
/ ctx
.tree
.state
.downcast_ref::<NodeGraphState>()
.camera
.zoom();
let ring = self.anchor_reach(
ctx.tree,
anchor,
Some(PendingRoute {
edge,
attach: Some(anchor),
detach: None,
}),
);
let left = match ring {
Some(ring) => {
Point::new(ring.center[0], ring.center[1]).distance(cursor_position)
> ring.radius + unsnap
}
None => true,
};
if left {
if let Some(handler) = self.on_route_detach.as_ref()
&& let Some(edge_id) = self.edges.get(edge).map(|edge| edge.id.clone())
&& let Some(anchor_id) =
self.anchors.get(anchor).map(|anchor| anchor.id.clone())
{
ctx.shell.publish(handler(edge_id, anchor_id));
}
ctx.tree.state.downcast_mut::<NodeGraphState>().dragging = Dragging::Route {
edge,
detached: Some(anchor),
};
}
ctx.shell.request_redraw();
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
ctx.tree.state.downcast_mut::<NodeGraphState>().dragging = Dragging::None;
if let Some(handler) = self.on_drag_end.as_ref() {
ctx.shell.publish(handler());
}
ctx.shell.capture_event();
ctx.shell.request_redraw();
}
_ => {}
}
}
fn route_snap_target(
&self,
tree: &Tree,
edge: usize,
detached: Option<usize>,
cursor: Point,
) -> Option<usize> {
let snap = SNAP_THRESHOLD / tree.state.downcast_ref::<NodeGraphState>().camera.zoom();
let pending = PendingRoute {
edge,
attach: None,
detach: detached,
};
let mut best: Option<(usize, f32)> = None;
for anchor in self.route_snap_eligible(edge, detached) {
let Some(ring) = self.anchor_reach(
tree,
anchor,
Some(PendingRoute {
attach: Some(anchor),
..pending
}),
) else {
continue;
};
let distance = Point::new(ring.center[0], ring.center[1]).distance(cursor);
if distance > ring.radius + snap {
continue;
}
match best {
Some((_, held)) if held <= distance => {}
_ => best = Some((anchor, distance)),
}
}
best.map(|(anchor, _)| anchor)
}
fn handle_selection_box(&self, ctx: &mut UpdateCtx<'_, '_, '_, Message>, start: LayoutPoint) {
let UpdateCtx {
tree,
layout,
event,
world_cursor,
shell,
..
} = &mut *ctx;
let state = tree.state.downcast_mut::<NodeGraphState>();
match event {
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if let Some(cursor_position) = world_cursor.position() {
state.dragging = Dragging::SelectionBox(start, cursor_position.into_euclid());
}
shell.request_redraw();
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
if let Some(cursor_position) = world_cursor.position() {
let end: LayoutPoint = cursor_position.into_euclid();
let selection_rect = selection_rect_from_points(start, end);
let additive = state.modifiers.contains(self.keymap.multi_select_modifiers);
let mut selected: HashSet<usize> = if additive {
self.resolved_selection(state)
} else {
HashSet::new()
};
for (node_index, node_layout) in layout.children().enumerate() {
if rects_intersect(&selection_rect, &node_layout.bounds()) {
selected.insert(node_index);
}
}
if let Some(handler) = self.on_select.as_ref() {
shell.publish(handler(self.selection_ids(&selected)));
}
state.pending_selection = Some(selected);
}
state.dragging = Dragging::None;
if let Some(handler) = self.on_drag_end.as_ref() {
shell.publish(handler());
}
shell.capture_event();
shell.request_redraw();
}
_ => {}
}
}
fn handle_group_move(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
anchor: usize,
followers: &[usize],
) {
let UpdateCtx {
tree,
event,
world_cursor,
shell,
..
} = &mut *ctx;
match event {
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
shell.request_redraw();
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
let state = tree.state.downcast_ref::<NodeGraphState>();
let selected = Self::selection_indices(&self.resolved_selection(state));
let indices = merged_indices(&selected, followers);
if let Some(cursor_position) = world_cursor.position() {
let offset = drag_offset(state, self, anchor, cursor_position.into_euclid());
let node_ids = self.node_ids_at(&indices);
if let Some(handler) = self.on_move.as_ref() {
shell.publish(handler(offset.into_iced(), node_ids));
}
}
let state = tree.state.downcast_mut::<NodeGraphState>();
state.promote_z_many(&indices);
state.dragging = Dragging::None;
if let Some(handler) = self.on_drag_end.as_ref() {
shell.publish(handler());
}
shell.capture_event();
shell.invalidate_layout();
shell.request_redraw();
}
_ => {}
}
}
fn handle_minimap_drag(&self, ctx: &mut UpdateCtx<'_, '_, '_, Message>) {
match ctx.event {
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if let Some(cursor) = ctx.screen_cursor.position() {
self.center_camera_from_minimap(ctx, cursor);
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
ctx.tree.state.downcast_mut::<NodeGraphState>().dragging = Dragging::None;
ctx.shell.capture_event();
ctx.shell.request_redraw();
}
_ => {}
}
}
pub(super) fn anchor_ring_rects(&self, state: &NodeGraphState) -> Vec<WorldRect> {
let anchor_geometry = state.anchor_geometry.borrow();
let rings = self.anchor_rings(None);
self.anchors
.iter()
.enumerate()
.map(|(index, anchor)| {
let orbit = u8::try_from(rings.get(index).copied().unwrap_or(0).saturating_sub(1))
.unwrap_or(u8::MAX);
let radius = anchor_geometry
.get(index)
.copied()
.unwrap_or_default()
.orbit_radius(orbit);
let center = anchor.position;
WorldRect::new(
WorldPoint::new(center.x - radius, center.y - radius),
WorldSize::new(radius * 2.0, radius * 2.0),
)
})
.collect()
}
fn center_camera_from_minimap(&self, ctx: &mut UpdateCtx<'_, '_, '_, Message>, cursor: Point) {
let Some(minimap) = self.minimap.as_ref() else {
return;
};
let UpdateCtx {
tree,
layout,
shell,
..
} = &mut *ctx;
let state = tree.state.downcast_mut::<NodeGraphState>();
let bounds = layout.bounds();
let map = minimap::rect(minimap, bounds);
let visible = state.camera.visible_world_rect(bounds);
let world = minimap::world_bounds(
layout
.children()
.map(|child| {
let b = child.bounds();
WorldRect::new(
state.camera.layout_to_world(LayoutPoint::new(b.x, b.y)),
WorldSize::new(b.width, b.height),
)
})
.chain(self.anchor_ring_rects(state)),
visible,
);
let at = Point::new(
cursor.x.clamp(map.x, map.x + map.width),
cursor.y.clamp(map.y, map.y + map.height),
);
let target = Camera2D::position_for_center(
minimap::Projection::new(map, world).map_to_world(at),
state.camera.zoom(),
bounds.size(),
Padding::ZERO,
);
state.camera_tween = None;
state.camera = state.camera.move_by(target - state.camera.position());
if let Some(handler) = self.on_camera.as_ref() {
shell.publish(handler(Point::new(target.x, target.y), state.camera.zoom()));
}
shell.request_redraw();
}
fn handle_left_press(&self, ctx: &mut UpdateCtx<'_, '_, '_, Message>, z_indices: &[usize]) {
if ctx.tree.state.downcast_ref::<NodeGraphState>().dragging != Dragging::None {
return;
}
if self.try_press_minimap(ctx) {
return;
}
let state = ctx.tree.state.downcast_ref::<NodeGraphState>();
let multi_select_held = state.modifiers.contains(self.keymap.multi_select_modifiers);
let edge_cut_held = state.modifiers.contains(self.keymap.edge_cut_modifiers);
if edge_cut_held && self.try_cut_edge_at_cursor(ctx) {
return;
}
if let Some(cursor_position) = ctx.world_cursor.position() {
for &node_index in z_indices.iter().rev() {
if self.try_press_node(ctx, node_index, cursor_position, multi_select_held) {
return;
}
}
if self.try_start_anchor_drag(ctx, cursor_position.into_euclid())
|| self.try_press_cable(ctx, cursor_position)
{
return;
}
}
self.start_selection_box_or_cut(ctx);
}
fn try_press_minimap(&self, ctx: &mut UpdateCtx<'_, '_, '_, Message>) -> bool {
let Some(minimap) = self.minimap.as_ref() else {
return false;
};
let Some(cursor) = ctx.screen_cursor.position() else {
return false;
};
if !minimap::rect(minimap, ctx.layout.bounds()).contains(cursor) {
return false;
}
self.center_camera_from_minimap(ctx, cursor);
ctx.tree.state.downcast_mut::<NodeGraphState>().dragging = Dragging::Minimap;
ctx.shell.capture_event();
true
}
fn try_cut_edge_at_cursor(&self, ctx: &mut UpdateCtx<'_, '_, '_, Message>) -> bool {
let Some(cursor_position) = ctx.world_cursor.position() else {
return false;
};
let cut_threshold = EDGE_CUT_THRESHOLD
/ ctx
.tree
.state
.downcast_ref::<NodeGraphState>()
.camera
.zoom();
let at = [cursor_position.x, cursor_position.y];
let cut = self
.cable_geometry(ctx.tree, ctx.layout)
.into_iter()
.find(|(_, built)| built.path.distance(at) < cut_threshold)
.map(|(geometry, _)| geometry.edge);
let Some(Edge { id, from, to, .. }) = cut.and_then(|edge| self.edges.get(edge)) else {
return false;
};
if let Some(handler) = self.on_disconnect.as_ref() {
ctx.shell.publish(handler(from.clone(), to.clone()));
}
if let Some(handler) = self.on_edge_delete.as_ref() {
ctx.shell.publish(handler(vec![id.clone()]));
}
ctx.shell.capture_event();
ctx.shell.request_redraw();
true
}
fn try_press_node(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
node_index: usize,
cursor_position: Point,
multi_select_held: bool,
) -> bool {
let Some(node_layout) = ctx.layout.children().nth(node_index) else {
return false;
};
let Some(node_tree) = ctx.tree.children.get(node_index) else {
return false;
};
let pins: Vec<(usize, I::PinId, bool, (Point, Point))> =
find_pins::<I>(node_tree, node_layout)
.into_iter()
.map(|(i, s, pos)| (i, s.pin_id.clone(), s.interactions_disabled, pos))
.collect();
let Some(current_node_id) = self.node_id_at(node_index).cloned() else {
return false;
};
let click_threshold = PIN_CLICK_THRESHOLD
/ ctx
.tree
.state
.downcast_ref::<NodeGraphState>()
.camera
.zoom();
for (pin_index, pin_id, disabled, (a, b)) in pins {
let distance = a.distance(cursor_position).min(b.distance(cursor_position));
if distance < click_threshold && !disabled {
if !multi_select_held {
for Edge {
from: from_ref,
to: to_ref,
..
} in &self.edges
{
let anchor =
if from_ref.node_id == current_node_id && from_ref.pin_id == pin_id {
to_ref
} else if to_ref.node_id == current_node_id && to_ref.pin_id == pin_id {
from_ref
} else {
continue;
};
if self.try_start_unplug(
ctx,
anchor,
(from_ref, to_ref),
(node_index, pin_index),
) {
return true;
}
}
}
if self.try_start_edge_drag(
ctx,
node_index,
pin_index,
&pin_id,
¤t_node_id,
cursor_position,
) {
return true;
}
}
}
if ctx.world_cursor.is_over(node_layout.bounds()) {
if self.try_start_resize(ctx, node_index, cursor_position, node_layout.bounds()) {
return true;
}
self.select_or_drag_node(ctx, node_index, cursor_position);
return true;
}
false
}
fn try_start_unplug(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
anchor: &PinRef<I>,
edge: (&PinRef<I>, &PinRef<I>),
grabbed: (usize, usize),
) -> bool {
let Some(anchor_node_idx) = self.node_index(&anchor.node_id) else {
return false;
};
let Some(anchor_pin_idx) = pin_by_id::<I>(
&ctx.tree.children,
ctx.layout,
anchor_node_idx,
&anchor.pin_id,
)
.map(|(index, ..)| index) else {
return false;
};
let valid_targets = compute_valid_targets(
self,
ctx.tree,
ctx.layout,
anchor_node_idx,
anchor_pin_idx,
Some(edge),
);
let state = ctx.tree.state.downcast_mut::<NodeGraphState>();
state.valid_drop_targets = valid_targets;
state.dragging = Dragging::EdgeOver {
from_node: anchor_node_idx,
from_pin: anchor_pin_idx,
to_node: grabbed.0,
to_pin: grabbed.1,
};
ctx.shell.capture_event();
true
}
fn try_start_edge_drag(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
node_index: usize,
pin_index: usize,
pin_id: &I::PinId,
node_id: &I::NodeId,
cursor_position: Point,
) -> bool {
if self.on_connect.as_ref().is_none() {
return false;
}
let valid_targets =
compute_valid_targets(self, ctx.tree, ctx.layout, node_index, pin_index, None);
let state = ctx.tree.state.downcast_mut::<NodeGraphState>();
state.valid_drop_targets = valid_targets;
state.dragging = Dragging::Edge {
from_node: node_index,
from_pin: pin_index,
origin: cursor_position.into_euclid(),
};
if let Some(handler) = self.on_drag_start.as_ref() {
ctx.shell.publish(handler(DragInfo::Edge {
from_node: node_id.clone(),
from_pin: pin_id.clone(),
}));
}
ctx.shell.capture_event();
true
}
fn try_start_resize(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
node_index: usize,
cursor_position: Point,
bounds: Rectangle,
) -> bool {
if self.on_resize.is_none() || !self.nodes[node_index].resizable {
return false;
}
let state = ctx.tree.state.downcast_mut::<NodeGraphState>();
if !resize_grip_zone(bounds, state.camera.zoom()).contains(cursor_position) {
return false;
}
state.dragging = Dragging::Resize {
node: node_index,
origin: cursor_position.into_euclid(),
start: bounds.size(),
};
if let Some(handler) = self.on_drag_start.as_ref()
&& let Some(node_id) = self.node_id_at(node_index).cloned()
{
ctx.shell.publish(handler(DragInfo::Resize { node_id }));
}
ctx.shell.capture_event();
true
}
fn frame_followers(
&self,
layout: Layout<'_>,
pressed: &[usize],
already_moving: impl Fn(usize) -> bool,
) -> Vec<usize> {
let mut followers: Vec<usize> = Vec::new();
for &frame_index in pressed {
if !self.nodes.get(frame_index).is_some_and(|node| node.frame) {
continue;
}
let Some(frame_bounds) = layout.children().nth(frame_index).map(|l| l.bounds()) else {
continue;
};
for (index, node_layout) in layout.children().enumerate() {
if index == frame_index
|| already_moving(index)
|| followers.contains(&index)
|| !contains_rect(frame_bounds, node_layout.bounds())
{
continue;
}
followers.push(index);
}
}
followers.sort_unstable();
followers
}
fn select_or_drag_node(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
node_index: usize,
cursor_position: Point,
) {
let UpdateCtx {
tree,
layout,
shell,
..
} = &mut *ctx;
let layout = *layout;
let state = tree.state.downcast_mut::<NodeGraphState>();
let resolved = self.resolved_selection(state);
let already_selected = resolved.contains(&node_index);
let current: Vec<usize> = Self::selection_indices(&resolved);
let modifiers = state.modifiers;
let selection_changed;
let new_selection: Vec<usize> = if modifiers.contains(self.keymap.multi_select_modifiers) {
selection_changed = true;
if already_selected {
current
.iter()
.copied()
.filter(|&i| i != node_index)
.collect()
} else {
let mut next = current.clone();
next.push(node_index);
next
}
} else if !already_selected {
selection_changed = true;
vec![node_index]
} else {
selection_changed = false;
current.clone()
};
if self.on_move.as_ref().is_some() {
if current.len() > 1 && already_selected {
let selected = current.clone();
let followers = self.frame_followers(layout, &selected, |i| resolved.contains(&i));
state.dragging = Dragging::GroupMove {
origin: cursor_position.into_euclid(),
anchor: node_index,
followers,
};
if let Some(handler) = self.on_drag_start.as_ref() {
shell.publish(handler(DragInfo::Group {
node_ids: self.node_ids_at(&selected),
}));
}
} else {
let followers = self.frame_followers(layout, &[node_index], |_| false);
state.dragging = Dragging::Node {
node: node_index,
origin: cursor_position.into_euclid(),
followers,
};
if let Some(handler) = self.on_drag_start.as_ref()
&& let Some(node_id) = self.node_id_at(node_index).cloned()
{
shell.publish(handler(DragInfo::Node { node_id }));
}
}
}
if selection_changed {
let selected = self.node_ids_at(&new_selection);
if let Some(handler) = self.on_select.as_ref() {
shell.publish(handler(selected));
}
let state = tree.state.downcast_mut::<NodeGraphState>();
state.pending_selection = Some(new_selection.into_iter().collect());
}
shell.capture_event();
}
fn start_selection_box_or_cut(&self, ctx: &mut UpdateCtx<'_, '_, '_, Message>) {
let UpdateCtx {
tree,
world_cursor,
shell,
..
} = &mut *ctx;
if let Some(cursor_position) = world_cursor.position() {
let cursor_position: LayoutPoint = cursor_position.into_euclid();
let state = tree.state.downcast_mut::<NodeGraphState>();
if state.modifiers.contains(self.keymap.edge_cut_modifiers) {
state.dragging = Dragging::EdgeCutting {
trail: vec![cursor_position],
pending_cuts: std::collections::HashSet::new(),
};
if let Some(handler) = self.on_drag_start.as_ref() {
shell.publish(handler(DragInfo::EdgeCut));
}
shell.capture_event();
return;
}
if !state.fingers.is_empty() {
state.camera_tween = None;
state.dragging = Dragging::Graph(state.camera.layout_to_world(cursor_position));
shell.capture_event();
return;
}
state.dragging = Dragging::SelectionBox(cursor_position, cursor_position);
if let Some(handler) = self.on_drag_start.as_ref() {
shell.publish(handler(DragInfo::SelectionBox {
start_x: cursor_position.x,
start_y: cursor_position.y,
}));
}
shell.capture_event();
}
}
fn handle_pan_press(&self, ctx: &mut UpdateCtx<'_, '_, '_, Message>) {
let state = ctx.tree.state.downcast_ref::<NodeGraphState>();
if state.dragging != Dragging::None {
return;
}
let Some(cursor_position) = ctx.screen_cursor.position() else {
return;
};
let origin_world: WorldPoint = state
.camera
.screen_to_world()
.transform_point(cursor_position.into_euclid());
if let Some(target) = self.press_target_at(ctx) {
let state = ctx.tree.state.downcast_mut::<NodeGraphState>();
state.dragging = Dragging::PressPending {
origin_world,
origin_screen: cursor_position,
target,
};
ctx.shell.capture_event();
return;
}
let state = ctx.tree.state.downcast_mut::<NodeGraphState>();
state.camera_tween = None;
state.dragging = Dragging::Graph(origin_world);
ctx.shell.capture_event();
}
fn handle_wheel(&self, ctx: &mut UpdateCtx<'_, '_, '_, Message>, delta: &mouse::ScrollDelta) {
let Some(cursor_pos) = ctx.screen_cursor.position_over(ctx.layout.bounds()) else {
return;
};
let cursor_pos: ScreenPoint = cursor_pos.into_euclid();
let scroll_amount = match delta {
mouse::ScrollDelta::Pixels { y, .. } => *y,
mouse::ScrollDelta::Lines { y, .. } => *y * 10.0,
};
let state = ctx.tree.state.downcast_mut::<NodeGraphState>();
#[cfg(target_arch = "wasm32")]
let zoom_delta = scroll_amount * 0.001 * state.camera.zoom();
#[cfg(not(target_arch = "wasm32"))]
let zoom_delta = scroll_amount * 0.01 * state.camera.zoom();
state.camera_tween = None;
state.camera = state.camera.zoom_at(cursor_pos, zoom_delta);
if let Some(handler) = self.on_camera.as_ref() {
let pos = state.camera.position();
ctx.shell
.publish(handler(Point::new(pos.x, pos.y), state.camera.zoom()));
}
ctx.shell.capture_event();
ctx.shell.request_redraw();
}
fn press_target_at(&self, ctx: &UpdateCtx<'_, '_, '_, Message>) -> Option<PressTarget> {
let cursor_position = ctx.world_cursor.position()?;
if ctx
.layout
.children()
.any(|node| node.bounds().contains(cursor_position))
{
return None;
}
if self.on_anchor_delete.is_some()
&& let Some(anchor) = self.core_at(ctx.tree, cursor_position.into_euclid())
{
return Some(PressTarget::AnchorCore { anchor });
}
if self.on_route_detach.is_some()
&& let Some(CableHit {
zone: CableZone::Wrap { edge, anchor },
..
}) = self.cable_zone_at(ctx.tree, ctx.layout, cursor_position.into_euclid())
{
return Some(PressTarget::Wrap { edge, anchor });
}
None
}
fn handle_press_pending(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
origin_world: WorldPoint,
origin_screen: Point,
target: PressTarget,
) {
match ctx.event {
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
let Some(cursor_position) = ctx.screen_cursor.position() else {
return;
};
if origin_screen.distance(cursor_position) <= TOUCH_TAP_TRAVEL {
return;
}
let state = ctx.tree.state.downcast_mut::<NodeGraphState>();
state.camera_tween = None;
state.dragging = Dragging::Graph(origin_world);
ctx.shell.capture_event();
ctx.shell.request_redraw();
}
Event::Mouse(mouse::Event::ButtonReleased(button))
if *button == self.keymap.pan_button =>
{
match target {
PressTarget::AnchorCore { anchor } => {
if let Some(handler) = self.on_anchor_delete.as_ref()
&& let Some(id) = self.anchors.get(anchor).map(|a| a.id.clone())
{
ctx.shell.publish(handler(id));
}
}
PressTarget::Wrap { edge, anchor } => {
if let Some(handler) = self.on_route_detach.as_ref()
&& let Some(edge_id) = self.edges.get(edge).map(|e| e.id.clone())
&& let Some(anchor_id) = self.anchors.get(anchor).map(|a| a.id.clone())
{
ctx.shell.publish(handler(edge_id, anchor_id));
}
}
}
ctx.tree.state.downcast_mut::<NodeGraphState>().dragging = Dragging::None;
if let Some(handler) = self.on_drag_end.as_ref() {
ctx.shell.publish(handler());
}
ctx.shell.capture_event();
ctx.shell.request_redraw();
}
_ => {}
}
}
fn cable_geometry(
&self,
tree: &Tree,
layout: Layout<'_>,
) -> Vec<(CableGeometry<'_, I>, edge_path::Built)> {
let pin = |pin: &PinRef<I>| -> Option<Station> {
let node_index = self.node_index(&pin.node_id)?;
let (_, (near, far), side, direction) =
pin_by_id::<I>(&tree.children, layout, node_index, &pin.pin_id)?;
Some(Station::for_pin(
side,
([near.x, near.y], [far.x, far.y]),
direction,
))
};
let ring = |anchor: usize, orbit: u8| self.orbit_ring(tree, anchor, orbit);
let curves = tree
.state
.downcast_ref::<NodeGraphState>()
.edge_curves
.borrow();
let curve = |edge: usize| curves.get(edge).copied().unwrap_or_default();
self.edge_hops(&pin, &ring, &curve, None)
.into_iter()
.map(|geometry| {
let built = edge_path::build(&geometry.hops, &curve(geometry.edge));
(geometry, built)
})
.collect()
}
fn oriented_ends(
&self,
tree: &Tree,
layout: Layout<'_>,
edge: usize,
) -> Option<(&PinRef<I>, &PinRef<I>)> {
self.cable_geometry(tree, layout)
.into_iter()
.find(|(geometry, _)| geometry.edge == edge)
.map(|(geometry, _)| geometry.ends)
}
pub(super) fn cable_hit_at(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: LayoutPoint,
) -> Option<CableHit> {
let hit = self.cable_zone_at(tree, layout, cursor)?;
let wired = match hit.zone {
CableZone::End { .. } => self.on_connect.is_some(),
CableZone::Wrap { .. } | CableZone::Run { .. } => {
self.on_anchor_create.is_some()
&& self.on_route_attach.is_some()
&& self.on_route_detach.is_some()
}
};
wired.then_some(hit)
}
fn cable_zone_at(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: LayoutPoint,
) -> Option<CableHit> {
let cables = self.cable_geometry(tree, layout);
let mut best: Option<(CableHit, f32)> = None;
for (geometry, built) in &cables {
let Some(candidate) = self.cable_zone_of(tree, geometry, built, cursor) else {
continue;
};
match best {
Some((_, held)) if held <= candidate.1 => {}
_ => best = Some(candidate),
}
}
best.map(|(hit, _)| hit)
}
fn cable_zone_of(
&self,
tree: &Tree,
geometry: &CableGeometry<'_, I>,
built: &edge_path::Built,
cursor: LayoutPoint,
) -> Option<(CableHit, f32)> {
let zoom = tree.state.downcast_ref::<NodeGraphState>().camera.zoom();
let grab = EDGE_GRAB_THRESHOLD / zoom;
let ring_grab = ANCHOR_GRAB_THRESHOLD / zoom;
let at = [cursor.x, cursor.y];
let near = built.path.nearest(at);
let edge = geometry.edge;
let total = built.path.total_len();
let end_zone = (EDGE_END_GRAB_LENGTH / zoom).min(total / 3.0);
let from_start = near.arc_len;
let from_end = total - near.arc_len;
if near.distance <= grab && from_start.min(from_end) <= end_zone {
let at_start = from_start <= from_end;
let window = if at_start {
(0.0, end_zone.min(total))
} else {
((total - end_zone).max(0.0), total)
};
return Some((
CableHit {
zone: CableZone::End { edge, at_start },
window,
},
near.distance,
));
}
for touch in &built.touches {
let Some(&(_, (anchor, orbit))) =
geometry.rings.iter().find(|(hop, _)| *hop == touch.hop)
else {
continue;
};
let Some(ring) = self.orbit_ring(tree, anchor, orbit) else {
continue;
};
let ring_grab = ring_grab.min(ring.radius * 0.5);
if near.arc_len < touch.span.0 - ring_grab || near.arc_len > touch.span.1 + ring_grab {
continue;
}
let off_ring = ring.ring_distance(at);
if off_ring > ring_grab {
continue;
}
return Some((
CableHit {
zone: CableZone::Wrap { edge, anchor },
window: touch.span,
},
off_ring,
));
}
if near.distance <= grab {
let half = end_zone / 2.0;
return Some((
CableHit {
zone: CableZone::Run { edge },
window: (
(near.arc_len - half).max(0.0),
(near.arc_len + half).min(total),
),
},
near.distance,
));
}
None
}
fn anchor_layout_point(&self, tree: &Tree, anchor: usize) -> Option<LayoutPoint> {
let position = self.anchors.get(anchor)?.position;
Some(
tree.state
.downcast_ref::<NodeGraphState>()
.camera
.world_to_layout(position.into_euclid()),
)
}
pub(super) fn orbit_ring(
&self,
tree: &Tree,
anchor: usize,
orbit: u8,
) -> Option<edge_path::Orbit> {
let center = self.anchor_layout_point(tree, anchor)?;
let radius = tree
.state
.downcast_ref::<NodeGraphState>()
.anchor_geometry
.borrow()
.get(anchor)
.copied()
.unwrap_or_default()
.orbit_radius(orbit);
Some(edge_path::Orbit {
center: [center.x, center.y],
radius,
})
}
fn anchor_reach(
&self,
tree: &Tree,
anchor: usize,
pending: Option<PendingRoute>,
) -> Option<edge_path::Orbit> {
let rings = self.anchor_rings(pending).get(anchor).copied()?;
let outermost = u8::try_from(rings.saturating_sub(1)).unwrap_or(u8::MAX);
self.orbit_ring(tree, anchor, outermost)
}
fn try_start_anchor_drag(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
cursor_position: LayoutPoint,
) -> bool {
let Some(anchor_index) = self.anchor_core_at(ctx.tree, cursor_position) else {
return false;
};
let state = ctx.tree.state.downcast_mut::<NodeGraphState>();
let origin = state.camera.layout_to_world(cursor_position);
state.dragging = Dragging::Anchor {
anchor: anchor_index,
origin,
};
if let Some(handler) = self.on_drag_start.as_ref() {
ctx.shell.publish(handler(DragInfo::Anchor {
anchor_id: self.anchors[anchor_index].id.clone(),
}));
}
ctx.shell.capture_event();
ctx.shell.request_redraw();
true
}
pub(super) fn anchor_core_at(&self, tree: &Tree, cursor: LayoutPoint) -> Option<usize> {
self.on_anchor_move.as_ref()?;
self.core_at(tree, cursor)
}
fn core_at(&self, tree: &Tree, cursor: LayoutPoint) -> Option<usize> {
let state = tree.state.downcast_ref::<NodeGraphState>();
let zoom = state.camera.zoom();
let geometry = state.anchor_geometry.borrow();
(0..self.anchors.len()).find(|&anchor| {
let half = core_grab_half(geometry.get(anchor).copied().unwrap_or_default(), zoom);
self.anchor_layout_point(tree, anchor).is_some_and(|core| {
(cursor.x - core.x).abs() <= half && (cursor.y - core.y).abs() <= half
})
})
}
fn try_press_cable(
&self,
ctx: &mut UpdateCtx<'_, '_, '_, Message>,
cursor_position: Point,
) -> bool {
let Some(hit) = self.cable_hit_at(ctx.tree, ctx.layout, cursor_position.into_euclid())
else {
return false;
};
match hit.zone {
CableZone::End { edge, at_start } => {
let Some(cable) = self.edges.get(edge) else {
return false;
};
let Some((output, input)) = self.oriented_ends(ctx.tree, ctx.layout, edge) else {
return false;
};
let (near, far) = if at_start {
(output, input)
} else {
(input, output)
};
let Some(near_node) = self.node_index(&near.node_id) else {
return false;
};
let Some((near_pin, ..)) =
pin_by_id::<I>(&ctx.tree.children, ctx.layout, near_node, &near.pin_id)
else {
return false;
};
self.try_start_unplug(ctx, far, (&cable.from, &cable.to), (near_node, near_pin))
}
CableZone::Wrap { edge, anchor } => {
let state = ctx.tree.state.downcast_mut::<NodeGraphState>();
state.dragging = Dragging::RouteOver {
edge,
anchor,
detached: None,
};
self.publish_route_start(ctx, edge);
true
}
CableZone::Run { edge } => {
let state = ctx.tree.state.downcast_mut::<NodeGraphState>();
state.dragging = Dragging::Route {
edge,
detached: None,
};
self.publish_route_start(ctx, edge);
true
}
}
}
fn publish_route_start(&self, ctx: &mut UpdateCtx<'_, '_, '_, Message>, edge: usize) {
if let Some(handler) = self.on_drag_start.as_ref() {
ctx.shell.publish(handler(DragInfo::Route {
edge_id: self.edges[edge].id.clone(),
}));
}
ctx.shell.capture_event();
ctx.shell.request_redraw();
}
fn refresh_hover(&self, ctx: &mut UpdateCtx<'_, '_, '_, Message>) {
if !matches!(ctx.event, Event::Mouse(mouse::Event::CursorMoved { .. })) {
return;
}
let inside = ctx
.world_cursor
.position()
.is_some_and(|at| self.hover_zone(ctx.tree, ctx.layout, at));
let state = ctx.tree.state.downcast_ref::<NodeGraphState>();
if inside || state.hover_zone.get() {
ctx.shell.request_redraw();
}
state.hover_zone.set(inside);
}
fn hover_zone(&self, tree: &Tree, layout: Layout<'_>, cursor: Point) -> bool {
self.anchor_core_at(tree, cursor.into_euclid()).is_some()
|| self
.cable_hit_at(tree, layout, cursor.into_euclid())
.is_some()
}
fn begin_focus(
&self,
state: &mut NodeGraphState,
world_aabb: WorldRect,
viewport: Size,
opts: &FocusOptions,
shell: &mut Shell<'_, Message>,
) {
let (end_position, end_zoom) = Camera2D::fit(world_aabb, viewport, opts);
let viewport_origin = state.camera.viewport_origin();
let jump = match opts.animation {
None => true,
Some(anim) => anim.duration.as_secs_f32() <= 0.0,
};
if jump {
state.camera_tween = None;
state.camera = Camera2D::with_zoom_and_position(end_zoom, end_position)
.with_viewport_origin(viewport_origin);
if let Some(handler) = self.on_camera.as_ref() {
shell.publish(handler(
Point::new(end_position.x, end_position.y),
end_zoom,
));
}
} else if let Some(anim) = opts.animation {
let start_center = Camera2D::center_for_position(
state.camera.position(),
state.camera.zoom(),
viewport,
opts.padding,
);
state.camera_tween = Some(CameraTween {
start_center,
start_zoom: state.camera.zoom(),
end_center: world_aabb.center(),
end_zoom,
viewport,
padding: opts.padding,
elapsed: 0.0,
duration: anim.duration.as_secs_f32(),
easing: anim.easing,
});
}
shell.request_redraw();
}
}
fn core_grab_half(geometry: AnchorGeometry, zoom: f32) -> f32 {
(ANCHOR_GRAB_THRESHOLD / zoom)
.max(geometry.core_half())
.min(geometry.orbit_radius(0) * std::f32::consts::FRAC_1_SQRT_2)
}
pub(super) fn resolve_focus_target<I, Message, Theme, Renderer>(
graph: &NodeGraph<'_, I, Message, Theme, Renderer>,
layout: Layout<'_>,
state: &NodeGraphState,
target: &FocusTarget<I>,
) -> Option<WorldRect>
where
I: Ids,
Theme: Catalog,
Renderer: iced_wgpu::core::renderer::Renderer + iced_wgpu::primitive::Renderer,
{
let node_rect = |index: usize| -> Option<WorldRect> {
let b = layout.children().nth(index)?.bounds();
let origin = state.camera.layout_to_world(LayoutPoint::new(b.x, b.y));
Some(WorldRect::new(origin, WorldSize::new(b.width, b.height)))
};
let union_of = |rects: &mut dyn Iterator<Item = WorldRect>| rects.reduce(|a, b| a.union(&b));
let anchor_rects = graph.anchor_ring_rects(state);
let anchor_rect = |index: usize| -> Option<WorldRect> { anchor_rects.get(index).copied() };
let by_node =
|id: &I::NodeId| -> Option<WorldRect> { graph.node_index(id).and_then(node_rect) };
let by_anchor =
|id: &I::AnchorId| -> Option<WorldRect> { graph.anchor_index(id).and_then(&anchor_rect) };
let edge_rect = |id: &I::EdgeId| -> Option<WorldRect> {
let index = graph.edges.iter().position(|edge| edge.id == *id)?;
let edge = &graph.edges[index];
let a = node_rect(graph.node_index(&edge.from.node_id)?)?;
let b = node_rect(graph.node_index(&edge.to.node_id)?)?;
Some(
graph
.resolved_route(index)
.into_iter()
.filter_map(&anchor_rect)
.fold(a.union(&b), |whole, ring| whole.union(&ring)),
)
};
match target {
FocusTarget::All => union_of(
&mut (0..graph.nodes.len())
.filter_map(node_rect)
.chain((0..graph.anchors.len()).filter_map(&anchor_rect)),
),
FocusTarget::Selection => union_of(
&mut graph
.resolved_selection(state)
.into_iter()
.filter_map(node_rect),
),
FocusTarget::Node(id) => by_node(id),
FocusTarget::Nodes(ids) => union_of(&mut ids.iter().filter_map(&by_node)),
FocusTarget::Anchor(id) => by_anchor(id),
FocusTarget::Anchors(ids) => union_of(&mut ids.iter().filter_map(&by_anchor)),
FocusTarget::Edge(id) => edge_rect(id),
FocusTarget::Edges(ids) => union_of(&mut ids.iter().filter_map(edge_rect)),
FocusTarget::Rect(rect) => Some((*rect).into_euclid()),
}
}
fn compute_valid_targets<I, Message, Theme, Renderer>(
graph: &NodeGraph<'_, I, Message, Theme, Renderer>,
tree: &Tree,
layout: Layout<'_>,
from_node: usize,
from_pin: usize,
excluded_edge: Option<(&PinRef<I>, &PinRef<I>)>,
) -> std::collections::HashSet<(usize, usize)>
where
I: Ids,
Theme: Catalog,
Renderer: iced_wgpu::core::renderer::Renderer + iced_wgpu::primitive::Renderer,
{
let mut valid_targets = std::collections::HashSet::new();
let from_pin_state = tree.children.get(from_node).and_then(|node_tree| {
layout.children().nth(from_node).and_then(|node_layout| {
find_pins::<I>(node_tree, node_layout)
.into_iter()
.nth(from_pin)
.map(|(_, state, _)| state.clone())
})
});
let Some(from_state) = from_pin_state else {
return valid_targets;
};
let from_node_id = graph.node_id_at(from_node);
let occupied: std::collections::HashSet<(&I::NodeId, &I::PinId)> = graph
.edges
.iter()
.filter(|edge| excluded_edge != Some((&edge.from, &edge.to)))
.flat_map(|edge| {
[
(&edge.from.node_id, &edge.from.pin_id),
(&edge.to.node_id, &edge.to.pin_id),
]
})
.collect();
let is_occupied =
|node_id: &I::NodeId, pin_id: &I::PinId| occupied.contains(&(node_id, pin_id));
for (node_index, (node_layout, node_tree)) in layout.children().zip(&tree.children).enumerate()
{
for (pin_index, pin_state, _) in find_pins::<I>(node_tree, node_layout) {
if node_index == from_node && pin_index == from_pin {
continue;
}
if pin_state.interactions_disabled {
continue;
}
let (Some(fid), Some(tid)) = (from_node_id, graph.node_id_at(node_index)) else {
continue;
};
let from_end = PinEnd::new(
fid,
&from_state.pin_id,
from_state.direction,
&from_state.user_info,
is_occupied(fid, &from_state.pin_id),
);
let to_end = PinEnd::new(
tid,
&pin_state.pin_id,
pin_state.direction,
&pin_state.user_info,
is_occupied(tid, &pin_state.pin_id),
);
let accepted = match &graph.can_connect {
Some(can_connect) => can_connect(from_end, to_end),
None => crate::connection::default_can_connect(from_end, to_end),
};
if !accepted {
continue;
}
valid_targets.insert((node_index, pin_index));
}
}
valid_targets
}
fn refused_drop<I, Message, Theme, Renderer>(
graph: &NodeGraph<'_, I, Message, Theme, Renderer>,
tree: &Tree,
layout: Layout<'_>,
from: (usize, usize),
valid: &std::collections::HashSet<(usize, usize)>,
cursor: Point,
threshold: f32,
) -> Option<(PinRef<I>, PinRef<I>)>
where
I: Ids,
Theme: Catalog,
Renderer: iced_wgpu::core::renderer::Renderer + iced_wgpu::primitive::Renderer,
{
let mut nearest: Option<(f32, usize, I::PinId)> = None;
for (node_index, (node_layout, node_tree)) in layout.children().zip(&tree.children).enumerate()
{
for (pin_index, pin_state, (a, b)) in find_pins::<I>(node_tree, node_layout) {
if (node_index, pin_index) == from
|| pin_state.interactions_disabled
|| valid.contains(&(node_index, pin_index))
{
continue;
}
let distance = a.distance(cursor).min(b.distance(cursor));
if distance >= threshold {
continue;
}
if nearest
.as_ref()
.is_none_or(|(closest, ..)| distance < *closest)
{
nearest = Some((distance, node_index, pin_state.pin_id.clone()));
}
}
}
let (_, to_node, to_pin_id) = nearest?;
let (_, from_pin_state, _) =
find_pins::<I>(tree.children.get(from.0)?, layout.children().nth(from.0)?)
.into_iter()
.nth(from.1)?;
Some((
PinRef::new(
graph.node_id_at(from.0)?.clone(),
from_pin_state.pin_id.clone(),
),
PinRef::new(graph.node_id_at(to_node)?.clone(), to_pin_id),
))
}
fn pin_by_id<I: Ids>(
node_trees: &[Tree],
layout: Layout<'_>,
node_index: usize,
pin_id: &I::PinId,
) -> Option<(usize, (Point, Point), PinSide, PinDirection)> {
let node_tree = node_trees.get(node_index)?;
let node_layout = layout.children().nth(node_index)?;
find_pins::<I>(node_tree, node_layout)
.iter()
.find(|(_, state, _)| state.pin_id == *pin_id)
.map(|(index, state, anchors)| (*index, *anchors, state.side, state.direction))
}
fn selection_rect_from_points(a: LayoutPoint, b: LayoutPoint) -> Rectangle {
let min_x = a.x.min(b.x);
let min_y = a.y.min(b.y);
let max_x = a.x.max(b.x);
let max_y = a.y.max(b.y);
Rectangle {
x: min_x,
y: min_y,
width: max_x - min_x,
height: max_y - min_y,
}
}
fn rects_intersect(a: &Rectangle, b: &Rectangle) -> bool {
a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y
}
fn contains_rect(outer: Rectangle, inner: Rectangle) -> bool {
inner.x >= outer.x
&& inner.y >= outer.y
&& inner.x + inner.width <= outer.x + outer.width
&& inner.y + inner.height <= outer.y + outer.height
}
fn merged_indices(a: &[usize], b: &[usize]) -> Vec<usize> {
let mut merged = Vec::with_capacity(a.len() + b.len());
merged.extend_from_slice(a);
merged.extend_from_slice(b);
merged.sort_unstable();
merged.dedup();
merged
}
pub(super) fn drag_carries(
state: &NodeGraphState,
node_index: usize,
is_selected: impl Fn(usize) -> bool,
) -> bool {
match &state.dragging {
Dragging::Node {
node, followers, ..
} => *node == node_index || followers.contains(&node_index),
Dragging::GroupMove { followers, .. } => {
is_selected(node_index) || followers.contains(&node_index)
}
_ => false,
}
}
pub(super) fn drag_delta<I, Message, Theme, Renderer>(
state: &NodeGraphState,
graph: &NodeGraph<'_, I, Message, Theme, Renderer>,
cursor_layout: LayoutPoint,
) -> Option<LayoutVector>
where
I: Ids,
Theme: Catalog,
{
let (origin, anchor) = match &state.dragging {
Dragging::Node { node, origin, .. } => (*origin, *node),
Dragging::GroupMove { origin, anchor, .. } => (*origin, *anchor),
_ => return None,
};
let origin_world = graph.nodes.get(anchor)?.position;
Some(snapped_delta(
state,
graph,
origin_world,
cursor_layout - origin,
))
}
pub(super) fn drag_offset<I, Message, Theme, Renderer>(
state: &NodeGraphState,
graph: &NodeGraph<'_, I, Message, Theme, Renderer>,
node_index: usize,
cursor_layout: LayoutPoint,
) -> LayoutVector
where
I: Ids,
Theme: Catalog,
{
let Some(delta) = drag_delta(state, graph, cursor_layout) else {
return LayoutVector::zero();
};
let selection = match state.dragging {
Dragging::GroupMove { .. } => graph.resolved_selection(state),
_ => HashSet::new(),
};
if drag_carries(state, node_index, |i| selection.contains(&i)) {
delta
} else {
LayoutVector::zero()
}
}
pub(super) fn anchor_drag_offset<I, Message, Theme, Renderer>(
state: &NodeGraphState,
graph: &NodeGraph<'_, I, Message, Theme, Renderer>,
anchor_index: usize,
raw: WorldVector,
) -> WorldVector
where
I: Ids,
Theme: Catalog,
{
let Some(origin) = graph
.anchors
.get(anchor_index)
.map(|anchor| anchor.position)
else {
return raw;
};
snapped_delta(state, graph, origin, raw)
}
pub(super) fn snapped_delta<I, Message, Theme, Renderer, U>(
state: &NodeGraphState,
graph: &NodeGraph<'_, I, Message, Theme, Renderer>,
origin: Point,
raw: Vector2D<f32, U>,
) -> Vector2D<f32, U>
where
I: Ids,
Theme: Catalog,
{
let Some(spacing) = graph.snap_grid.filter(|s| *s > 0.0 && s.is_finite()) else {
return raw;
};
if state.modifiers.contains(graph.keymap.snap_override) {
return raw;
}
let snap = |from: f32, delta: f32| ((from + delta) / spacing).round() * spacing - from;
Vector2D::new(snap(origin.x, raw.x), snap(origin.y, raw.y))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::node_graph::DEFAULT_ORBIT_OFFSET;
use std::f32::consts::{FRAC_1_SQRT_2, SQRT_2};
const ZOOMS: [f32; 9] = [0.1, 0.5, 0.9, 1.0, 2.0, 7.0 / 3.0, 3.0, 5.0, 10.0];
#[test]
fn the_core_grab_box_covers_the_painted_core() {
let geometry = AnchorGeometry::default();
for zoom in ZOOMS {
let half = core_grab_half(geometry, zoom);
assert!(
half >= geometry.core_half(),
"at zoom {zoom} the core grab box reaches {half} against a core \
reaching {}: a press inside the dot would open a selection box",
geometry.core_half(),
);
}
}
#[test]
fn the_core_grab_box_never_reaches_orbit_zero() {
for zoom in ZOOMS {
for orbit_offset in [4.0, DEFAULT_ORBIT_OFFSET, 40.0] {
let geometry = AnchorGeometry {
orbit_offset,
..AnchorGeometry::default()
};
let corner = core_grab_half(geometry, zoom) * SQRT_2;
assert!(
corner <= orbit_offset + 1e-4,
"at zoom {zoom} with orbit 0 at {orbit_offset} the core grab \
box reaches {corner}: a press on the innermost wrap would \
grab the core instead",
);
}
}
}
#[test]
fn the_orbit_cap_outranks_the_core_floor() {
let geometry = AnchorGeometry {
core_size: 40.0,
..AnchorGeometry::default()
};
assert!(geometry.core_half() > geometry.orbit_radius(0) * FRAC_1_SQRT_2);
for zoom in ZOOMS {
let half = core_grab_half(geometry, zoom);
assert!(
half * SQRT_2 <= geometry.orbit_radius(0) + 1e-4,
"at zoom {zoom} a core overlapping its own orbit 0 pushed the \
grab box corner to {}, past the ring at {}",
half * SQRT_2,
geometry.orbit_radius(0),
);
assert!(
half < geometry.core_half(),
"at zoom {zoom} the floor at {} beat the cap",
geometry.core_half(),
);
}
}
}