use iced_wgpu::core::{
Clipboard, Layout, Shell, layout, mouse, overlay, renderer,
widget::{self, Tree, tree},
};
use iced_widget::core::{Element, Event, Length, Point, Rectangle, Size, Vector, keyboard};
use web_time::Instant;
use super::{
ANCHOR_GRAB_THRESHOLD, Counts, DragInfo, EDGE_END_GRAB_LENGTH, EDGE_GRAB_THRESHOLD, Edge,
GraphInfo, MIN_NODE_SIZE, NodeGraph, OpTiming, RESIZE_GRIP_SIDE,
cable::{CableGeometry, PendingRoute, PhantomKind, RoutePhantom, Station},
edge_path,
euclid::{IntoIced, LayoutVector},
state::{CameraTween, Dragging, NodeGraphState, PressTarget, z_render_indices},
};
use crate::{
PinDirection, PinRef, PinSide,
ids::Ids,
node_graph::euclid::{IntoEuclid, LayoutPoint, ScreenPoint, WorldPoint},
node_graph::focus::FocusRequest,
node_pin::{NodePinState, PinEnd, PinInfo, PinSlot},
style::{
AnchorStatus, AnchorStyle, Catalog, EdgeGeometry, EdgeStatus, EdgeStyle, NodeStatus,
NodeStyle, PinStatus, PinStyle, TilingKind,
},
};
use iced_nodegraph_sdf::{Pattern, SdfPrimitive, Shape, Style, Tiling};
mod camera_overlay;
mod draw;
mod minimap;
pub(crate) mod update;
use camera_overlay::CameraOverlay;
impl<I, Message, Theme, Renderer> iced_wgpu::core::Widget<Message, Theme, Renderer>
for NodeGraph<'_, I, Message, Theme, Renderer>
where
I: Ids,
Theme: Catalog,
Renderer: iced_wgpu::core::renderer::Renderer + iced_wgpu::primitive::Renderer,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<NodeGraphState>()
}
fn state(&self) -> tree::State {
tree::State::new(NodeGraphState::default())
}
fn size(&self) -> Size<Length> {
self.size
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.width(self.size.width).height(self.size.height);
let size = limits.resolve(self.size.width, self.size.height, Size::ZERO);
let node_limits = layout::Limits::new(Size::ZERO, Size::INFINITE);
let nodes = self
.elements_iter_mut()
.zip(&mut tree.children)
.enumerate()
.map(|(node_index, ((position, element), node_tree))| {
let node = element
.as_widget_mut()
.layout(node_tree, renderer, &node_limits);
debug_assert!(
node.size().width.is_finite() && node.size().height.is_finite(),
"node {node_index} body resolved to an infinite size {:?}: a `Length::Fill` inside a node body has nothing to fill; give the element a fixed size",
node.size(),
);
node.move_to(position)
})
.collect();
layout::Node::with_children(size, nodes)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: layout::Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.draw_impl(tree, renderer, theme, style, layout, cursor, viewport);
}
fn size_hint(&self) -> Size<Length> {
self.size()
}
fn children(&self) -> Vec<Tree> {
self.elements_iter()
.map(|(_, element)| Tree::new(element))
.collect()
}
fn diff(&self, tree: &mut Tree) {
let children: Vec<&Element<'_, Message, Theme, Renderer>> =
self.elements_iter().map(|(_, e)| e).collect();
tree.diff_children(&children);
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
let mut slot: Option<FocusRequest<I>> = None;
operation.custom(self.id.as_ref(), layout.bounds(), &mut slot);
if let Some(request) = slot {
let state = tree.state.downcast_mut::<NodeGraphState>();
state.pending_focus =
update::resolve_focus_target(self, layout, state, &request.target)
.map(|world_aabb| (world_aabb, request.options));
}
operation.traverse(&mut |operation| {
for (((_, element), node_tree), node_layout) in self
.elements_iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
{
element
.as_widget_mut()
.operate(node_tree, node_layout, renderer, operation);
}
});
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
let state = tree.state.downcast_ref::<NodeGraphState>();
let camera = state.camera_for(layout);
let translation = camera.overlay_translation() + translation * (1.0 / camera.zoom());
let children: Vec<overlay::Element<'b, Message, Theme, Renderer>> = self
.nodes
.iter_mut()
.map(|node| &mut node.element)
.zip(&mut tree.children)
.zip(layout.children())
.filter_map(|((element, node_tree), node_layout)| {
element.as_widget_mut().overlay(
node_tree,
node_layout,
renderer,
viewport,
translation,
)
})
.collect();
if children.is_empty() {
return None;
}
let content = overlay::Group::with_children(children).overlay();
Some(overlay::Element::new(Box::new(CameraOverlay {
content,
zoom: camera.zoom(),
})))
}
fn update(
&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,
) {
self.update_impl(
tree,
event,
layout,
screen_cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
let state = tree.state.downcast_ref::<NodeGraphState>();
match &state.dragging {
Dragging::Resize { .. } => return mouse::Interaction::ResizingDiagonallyDown,
Dragging::Graph(_)
| Dragging::Node { .. }
| Dragging::GroupMove { .. }
| Dragging::Anchor { .. }
| Dragging::Route { .. }
| Dragging::RouteOver { .. }
| Dragging::PressPending { .. } => return mouse::Interaction::Grabbing,
Dragging::Minimap => return mouse::Interaction::Pointer,
Dragging::Edge { .. }
| Dragging::EdgeOver { .. }
| Dragging::EdgeCutting { .. }
| Dragging::SelectionBox(..) => return mouse::Interaction::Crosshair,
Dragging::None => {}
}
if cursor.position_over(layout.bounds()).is_none() {
return mouse::Interaction::None;
}
if let Some(minimap) = self.minimap.as_ref()
&& cursor
.position_over(minimap::rect(minimap, layout.bounds()))
.is_some()
{
return mouse::Interaction::Pointer;
}
let camera = state.camera_for(layout);
let clipped_viewport = layout
.bounds()
.intersection(viewport)
.unwrap_or(Rectangle::new(layout.bounds().position(), Size::ZERO));
camera.update_with(
&clipped_viewport,
cursor,
|child_viewport, layout_cursor| {
let Some(position) = layout_cursor.position() else {
return mouse::Interaction::None;
};
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,
);
for &node_index in z_indices.iter().rev() {
let Some(node) = self.nodes.get(node_index) else {
continue;
};
let Some(child_tree) = tree.children.get(node_index) else {
continue;
};
let Some(node_layout) = layout.children().nth(node_index) else {
continue;
};
if !node_layout.bounds().contains(position) {
continue;
}
if self.on_resize.is_some()
&& node.resizable
&& resize_grip_zone(node_layout.bounds(), camera.zoom()).contains(position)
{
return mouse::Interaction::ResizingDiagonallyDown;
}
return node.element.as_widget().mouse_interaction(
child_tree,
node_layout,
layout_cursor,
child_viewport,
renderer,
);
}
let at: LayoutPoint = position.into_euclid();
if self.anchor_core_at(tree, at).is_some()
|| self.cable_hit_at(tree, layout, at).is_some()
{
mouse::Interaction::Grab
} else {
mouse::Interaction::None
}
},
)
}
}
impl<'a, I, Message, Theme, Renderer> From<NodeGraph<'a, I, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
I: Ids,
Theme: Catalog + 'a,
Renderer: iced_wgpu::core::renderer::Renderer + 'a + iced_wgpu::primitive::Renderer,
Message: 'static,
{
fn from(graph: NodeGraph<'a, I, Message, Theme, Renderer>) -> Self {
Element::new(graph)
}
}
pub(super) type PinLayout<'a, I> = (
usize,
&'a NodePinState<<I as Ids>::PinId, <I as Ids>::Payload>,
(Point, Point),
);
fn find_pins<'a, I: Ids>(tree: &'a Tree, layout: Layout<'a>) -> Vec<PinLayout<'a, I>> {
let mut flat = Vec::new();
let mut pin_index = 0;
inner_find_pins::<I>(&mut flat, &mut pin_index, layout, tree);
flat
}
fn inner_find_pins<'a, I: Ids>(
flat: &mut Vec<PinLayout<'a, I>>,
pin_index: &mut usize,
node_layout: Layout<'a>,
pin_tree: &'a Tree,
) {
if pin_tree.tag == tree::Tag::of::<NodeGraphState>() {
return;
}
if pin_tree.tag == tree::Tag::of::<PinSlot>() {
let slot = pin_tree.state.downcast_ref::<PinSlot>();
match slot.get::<I::PinId, I::Payload>() {
Some(pin_state) => {
let node_bounds = node_layout.bounds();
let pin_positions = pin_positions(pin_state, node_bounds);
flat.push((*pin_index, pin_state, pin_positions));
*pin_index += 1;
}
None => debug_assert!(
false,
"pin state {} inside a graph over {}: a pin's id and payload types must be the graph's Ids::PinId and Ids::Payload",
slot.type_name(),
std::any::type_name::<NodePinState<I::PinId, I::Payload>>(),
),
}
}
for child_tree in &pin_tree.children {
inner_find_pins::<I>(flat, pin_index, node_layout, child_tree);
}
}
fn orient_connection<I: Ids>(
from_dir: PinDirection,
to_dir: PinDirection,
from: PinRef<I>,
to: PinRef<I>,
) -> (PinRef<I>, PinRef<I>) {
let swap = !matches!(from_dir, PinDirection::Output) && matches!(to_dir, PinDirection::Output);
if swap { (to, from) } else { (from, to) }
}
fn pin_positions<P, UI>(state: &NodePinState<P, UI>, node_bounds: Rectangle) -> (Point, Point) {
let Point { x, y } = state.position;
let (left, right) = (node_bounds.x, node_bounds.x + node_bounds.width);
let (top, bottom) = (node_bounds.y, node_bounds.y + node_bounds.height);
let both = |p: Point| (p, p);
match state.side {
PinSide::Row => (Point::new(left, y), Point::new(right, y)),
PinSide::Left => both(Point::new(left, y)),
PinSide::Right => both(Point::new(right, y)),
PinSide::Top => both(Point::new(x, top)),
PinSide::Bottom => both(Point::new(x, bottom)),
}
}
fn resize_grip_zone(bounds: Rectangle, zoom: f32) -> Rectangle {
let side = (RESIZE_GRIP_SIDE / zoom)
.min(bounds.width * 0.5)
.min(bounds.height * 0.5);
Rectangle {
x: bounds.x + bounds.width - side,
y: bounds.y + bounds.height - side,
width: side,
height: side,
}
}
#[cfg(test)]
mod grip_tests {
use super::{MIN_NODE_SIZE, resize_grip_zone};
use iced_widget::core::{Point, Rectangle, Size};
fn zone(size: Size, zoom: f32) -> Rectangle {
resize_grip_zone(Rectangle::new(Point::new(10.0, 20.0), size), zoom)
}
#[test]
fn sits_in_the_bottom_right_corner() {
assert_eq!(
zone(Size::new(100.0, 60.0), 1.0),
Rectangle::new(Point::new(98.0, 68.0), Size::new(12.0, 12.0)),
);
}
#[test]
fn scales_inversely_with_zoom() {
assert_eq!(zone(Size::new(100.0, 60.0), 2.0).width, 6.0);
assert_eq!(zone(Size::new(100.0, 60.0), 0.5).width, 24.0);
}
#[test]
fn never_covers_more_than_half_the_node() {
assert_eq!(zone(MIN_NODE_SIZE, 1.0).width, 12.0);
assert_eq!(zone(Size::new(100.0, 60.0), 0.25).width, 30.0);
}
}
#[cfg(test)]
mod orient_tests {
use super::orient_connection;
use crate::node_pin::PinDirection;
use crate::{Indexed, PinRef};
#[test]
fn output_to_input_keeps_order() {
let out = PinRef::<Indexed>::new(0, 0);
let inp = PinRef::new(1, 0);
let (from, to) = orient_connection(PinDirection::Output, PinDirection::Input, out, inp);
assert_eq!(from, PinRef::new(0, 0));
assert_eq!(to, PinRef::new(1, 0));
}
#[test]
fn input_to_output_is_flipped() {
let inp = PinRef::<Indexed>::new(1, 0);
let out = PinRef::new(0, 0);
let (from, to) = orient_connection(PinDirection::Input, PinDirection::Output, inp, out);
assert_eq!(from, PinRef::new(0, 0));
assert_eq!(to, PinRef::new(1, 0));
}
#[test]
fn both_keeps_drag_order() {
let a = PinRef::<Indexed>::new(0, 0);
let b = PinRef::new(1, 0);
let (from, to) = orient_connection(PinDirection::Both, PinDirection::Both, a, b);
assert_eq!(from, PinRef::new(0, 0));
assert_eq!(to, PinRef::new(1, 0));
}
}