use std::collections::HashMap;
use std::rc::Rc;
use std::sync::atomic::{AtomicUsize, Ordering};
use dioxus::html::geometry::WheelDelta;
use dioxus::html::input_data::MouseButton;
use dioxus::prelude::*;
use crate::edge::{EdgeItem, EdgeMarkers, EdgeViewCtx, HANDLE_RIM};
use crate::node::{NodeItem, NodeViewCtx};
use crate::path::connection_path;
use crate::state::{
orient_connection, ConnectionState, DragState, FlowApi, FlowConfig, FlowCore, FlowHandle,
Interaction,
};
use crate::types::{
AnchorMode, ConnectEnd, Connection, DeleteRequest, Edge, HandleKey, HandleKind, Id, Node,
NodeGeom, Point, Rect, Viewport,
};
pub static STYLE: &str = include_str!("style.css");
static NEXT_IID: AtomicUsize = AtomicUsize::new(0);
fn client_point(coords: dioxus::html::geometry::ClientPoint) -> Point {
Point::new(coords.x, coords.y)
}
fn wheel_pixels(delta: WheelDelta, page: f64) -> Point {
match delta {
WheelDelta::Pixels(v) => Point::new(v.x, v.y),
WheelDelta::Lines(v) => Point::new(v.x * 16.0, v.y * 16.0),
WheelDelta::Pages(v) => {
let scale = page.max(240.0);
Point::new(v.x * scale, v.y * scale)
}
}
}
const PINCH_ZOOM_SENSITIVITY: f64 = 0.0025;
#[derive(Clone, PartialEq, Props)]
pub struct CanvasProps {
#[props(default = 0.25)]
pub min_zoom: f64,
#[props(default = 4.0)]
pub max_zoom: f64,
#[props(default = true)]
pub pan_on_drag: bool,
#[props(default = true)]
pub zoom_on_scroll: bool,
#[props(default = true)]
pub pan_on_scroll: bool,
#[props(default = true)]
pub nodes_draggable: bool,
#[props(default = 0.0)]
pub drag_threshold: f64,
#[props(default = 28.0)]
pub connection_radius: f64,
#[props(default = 0.12)]
pub fit_view_padding: f64,
pub id: Option<String>,
#[props(default = "Node graph".to_string())]
pub aria_label: String,
pub class: Option<String>,
pub on_pane_press: Option<Callback<Event<PointerData>>>,
pub edges: Option<Signal<Vec<Edge>>>,
pub geoms: Option<Memo<Vec<NodeGeom>>>,
pub deselect_nodes: Option<Callback<()>>,
pub on_connect: Option<EventHandler<Connection>>,
pub on_connect_start: Option<EventHandler<HandleKey>>,
pub on_connect_end: Option<EventHandler<ConnectEnd>>,
pub is_valid_connection: Option<Callback<Connection, bool>>,
pub on_node_drag_start: Option<EventHandler<Vec<Id>>>,
pub on_node_drag_stop: Option<EventHandler<Vec<Id>>>,
pub on_pane_click: Option<EventHandler<Point>>,
pub on_pane_double_click: Option<EventHandler<Point>>,
pub on_canvas_key_down: Option<Callback<Event<KeyboardData>>>,
pub on_drag_move: Option<Callback<Point>>,
pub world: Option<Element>,
pub children: Element,
}
#[allow(non_snake_case)]
pub fn Canvas(props: CanvasProps) -> Element {
let core = props.use_core();
props.render(core)
}
impl CanvasProps {
fn use_core(&self) -> FlowCore {
let viewport = use_signal(Viewport::default);
let container = use_signal(|| Rect::ZERO);
let interaction = use_signal(Interaction::default);
let connection = use_signal(|| None::<ConnectionState>);
let handles = use_signal(HashMap::new);
let config = use_signal(FlowConfig::default);
let drag = use_signal(DragState::default);
let epoch = use_signal(|| 0u64);
let pending_sizes = use_signal(Vec::new);
let size_flush_queued = use_signal(|| false);
let pending_handles = use_signal(Vec::new);
let handle_flush_queued = use_signal(|| false);
let snap_key = use_memo(move || {
connection
.read()
.as_ref()
.and_then(|c| c.snap.as_ref())
.map(|s| s.key.clone())
});
let connect_from = use_memo(move || connection.read().as_ref().map(|c| c.from.clone()));
let overlay_insets = use_signal(HashMap::new);
let own_edges = use_signal(Vec::new);
let edges = self.edges.unwrap_or(own_edges);
let empty_geoms = use_memo(Vec::new);
let geoms = self.geoms.unwrap_or(empty_geoms);
let noop_deselect = use_callback(move |_: ()| {});
let deselect_nodes = self.deselect_nodes.unwrap_or(noop_deselect);
let core = use_hook(|| FlowCore {
iid: NEXT_IID.fetch_add(1, Ordering::Relaxed),
viewport,
container,
interaction,
connection,
handles,
edges,
geoms,
config,
drag,
epoch,
snap_key,
connect_from,
deselect_nodes,
overlay_insets,
pending_sizes,
size_flush_queued,
pending_handles,
handle_flush_queued,
on_connect_start: self.on_connect_start,
valid_connection: self.is_valid_connection,
});
use_context_provider(|| core);
core
}
fn render(self, core: FlowCore) -> Element {
let Self {
min_zoom,
max_zoom,
fit_view_padding,
pan_on_drag,
zoom_on_scroll,
pan_on_scroll,
nodes_draggable,
drag_threshold,
connection_radius,
id,
aria_label,
class,
on_pane_press,
on_connect,
on_connect_end,
on_node_drag_start,
on_node_drag_stop,
on_pane_click,
on_pane_double_click,
on_canvas_key_down,
on_drag_move,
world,
children,
..
} = self;
let FlowCore {
viewport,
container,
interaction,
connection,
mut config,
drag,
edges,
deselect_nodes,
..
} = core;
let cfg = FlowConfig {
min_zoom,
max_zoom,
pan_on_drag,
zoom_on_scroll,
pan_on_scroll,
nodes_draggable,
drag_threshold,
connection_radius,
fit_view_padding,
};
if *config.peek() != cfg {
config.set(cfg);
}
let mounted: Signal<Option<Rc<MountedData>>> = use_signal(|| None);
let refresh_rect = use_callback(move |_: ()| {
let element = mounted.peek().clone();
let mut container = container;
if let Some(element) = element {
spawn(async move {
if let Ok(rect) = element.get_client_rect().await {
let rect =
Rect::new(rect.origin.x, rect.origin.y, rect.width(), rect.height());
if *container.peek() != rect {
container.set(rect);
}
}
});
}
});
let end_gesture = use_callback(move |_: ()| {
let mut interaction = interaction;
let mut connection = connection;
if *interaction.peek() != Interaction::None {
interaction.set(Interaction::None);
}
if connection.peek().is_some() {
connection.set(None);
}
let mut drag = drag;
let mut state = drag.write();
state.pointer_id = None;
state.suppress_click = false;
});
let on_pointer_down = move |evt: Event<PointerData>| {
refresh_rect.call(());
core.cancel_animations();
if *interaction.peek() != Interaction::None {
return;
}
if evt.trigger_button() != Some(MouseButton::Primary) {
return;
}
if let Some(handler) = &on_pane_press {
handler.call(evt.clone());
if *interaction.peek() != Interaction::None {
return;
}
}
let client = client_point(evt.client_coordinates());
{
let mut drag = drag;
let mut state = drag.write();
state.pointer_id = Some(evt.pointer_id());
state.origin_client = client;
state.last_client = client;
state.moved = false;
state.suppress_click = false;
state.grabs.clear();
}
let mut interaction = interaction;
if pan_on_drag {
interaction.set(Interaction::Pan);
} else {
interaction.set(Interaction::PanePressed);
}
};
let on_pointer_move = move |evt: Event<PointerData>| {
let current = *interaction.peek();
if current == Interaction::None {
return;
}
if drag
.peek()
.pointer_id
.is_some_and(|id| id != evt.pointer_id())
{
return;
}
if evt.held_buttons().is_empty() {
end_gesture.call(());
return;
}
let client = client_point(evt.client_coordinates());
match current {
Interaction::Pan => {
let delta = {
let mut drag = drag;
let mut state = drag.write();
let delta = client - state.last_client;
state.last_client = client;
state.moved = true;
delta
};
let mut viewport = viewport;
let vp = *viewport.peek();
viewport.set(vp.panned(delta));
}
Interaction::DragNode => {
let began = {
let mut drag = drag;
let mut state = drag.write();
state.last_client = client;
let travelled = state.origin_client.distance(client);
let passed = state.moved || travelled >= config.peek().drag_threshold;
let began = passed && !state.moved;
if passed {
state.moved = true;
}
if !passed {
return;
}
began
};
if began {
if let Some(handler) = &on_node_drag_start {
let ids: Vec<Id> =
drag.peek().grabs.iter().map(|(id, _)| id.clone()).collect();
handler.call(ids);
}
}
let flow = core.client_to_flow(client);
if let Some(handler) = &on_drag_move {
handler.call(flow);
}
}
Interaction::Connect => {
let flow = core.client_to_flow(client);
let mut connection = connection;
let from = connection.peek().as_ref().map(|c| c.from.clone());
if let Some(from) = from {
let snap = core.find_snap(&from, flow);
connection.set(Some(ConnectionState {
from,
cursor: flow,
snap,
}));
}
}
_ => {}
}
};
let on_pointer_up = move |evt: Event<PointerData>| {
if drag
.peek()
.pointer_id
.is_some_and(|id| id != evt.pointer_id())
{
return;
}
let current = *interaction.peek();
match current {
Interaction::Pan | Interaction::PanePressed => {
let state = drag.peek().clone();
let is_click = (current == Interaction::PanePressed || !state.moved)
&& !state.suppress_click;
if is_click {
let client = client_point(evt.client_coordinates());
let flow = core.client_to_flow(client);
if !evt.modifiers().shift() {
deselect_nodes.call(());
deselect_edges(edges);
}
if let Some(handler) = &on_pane_click {
handler.call(flow);
}
}
}
Interaction::Connect => {
let done = connection.peek().clone();
if let Some(done) = done {
let completed = done
.snap
.as_ref()
.map(|snap| orient_connection(&done.from, &snap.key));
if let Some(conn) = completed.clone() {
match &on_connect {
Some(handler) => handler.call(conn),
None => add_edge_for_connection(edges, conn),
}
}
if let Some(handler) = &on_connect_end {
let client = client_point(evt.client_coordinates());
handler.call(ConnectEnd {
point: core.client_to_flow(client),
connection: completed,
});
}
}
}
Interaction::DragNode if drag.peek().moved => {
if let Some(handler) = &on_node_drag_stop {
let ids: Vec<Id> =
drag.peek().grabs.iter().map(|(id, _)| id.clone()).collect();
handler.call(ids);
}
}
_ => {}
}
end_gesture.call(());
};
let on_wheel = move |evt: Event<WheelData>| {
let config = *config.peek();
if !config.pan_on_scroll && !config.zoom_on_scroll {
return;
}
evt.prevent_default();
core.cancel_animations();
let client = client_point(evt.client_coordinates());
let page = container.peek().height;
let delta = wheel_pixels(evt.delta(), page);
let modifiers = evt.modifiers();
if config.pan_on_scroll {
if modifiers.ctrl() || modifiers.meta() {
if delta.y != 0.0 {
let factor = (-delta.y * PINCH_ZOOM_SENSITIVITY).exp();
core.zoom_by(factor, Some(client), 0);
}
return;
}
let mut viewport = viewport;
let vp = *viewport.peek();
let by = if modifiers.shift() && delta.x == 0.0 {
Point::new(-delta.y, 0.0)
} else {
Point::new(-delta.x, -delta.y)
};
viewport.set(vp.panned(by));
return;
}
if delta.y == 0.0 {
return;
}
let factor = (-delta.y * 0.0022).exp().clamp(0.5, 2.0);
core.zoom_by(factor, Some(client), 0);
};
let on_key_down = move |evt: Event<KeyboardData>| {
if evt.key() == Key::Escape {
end_gesture.call(());
}
if let Some(handler) = &on_canvas_key_down {
handler.call(evt);
}
};
let gesture = *interaction.read();
let root_class = format!(
"dioxus-flow{}{}",
match gesture {
Interaction::Pan => " df-panning",
Interaction::Connect => " df-connecting",
_ => "",
},
class
.as_deref()
.map(|c| format!(" {c}"))
.unwrap_or_default()
);
rsx! {
FlowStyles {}
div {
id,
class: root_class,
tabindex: "0",
role: "application",
aria_label,
onmounted: move |evt| {
let mut mounted = mounted;
mounted.set(Some(evt.data()));
refresh_rect.call(());
},
onresize: move |_| refresh_rect.call(()),
onpointerdown: on_pointer_down,
onpointermove: on_pointer_move,
onpointerup: on_pointer_up,
onpointercancel: move |evt: Event<PointerData>| {
let owner = drag.peek().pointer_id;
if owner.is_none() || owner == Some(evt.pointer_id()) {
end_gesture.call(());
}
},
onwheel: on_wheel,
ondoubleclick: move |evt: Event<MouseData>| {
if let Some(handler) = &on_pane_double_click {
let client = client_point(evt.client_coordinates());
handler.call(core.client_to_flow(client));
}
},
onkeydown: on_key_down,
ViewportPane { {world} }
{children}
}
}
}
}
#[component]
fn FlowStyles() -> Element {
rsx! { document::Style { "{STYLE}" } }
}
#[component]
pub fn Flow<T: Clone + PartialEq + 'static>(
nodes: Signal<Vec<Node<T>>>,
edges: Signal<Vec<Edge>>,
#[props(default)]
anchor: AnchorMode,
#[props(default = 0.25)] min_zoom: f64,
#[props(default = 4.0)] max_zoom: f64,
#[props(default = true)]
pan_on_drag: bool,
#[props(default = true)]
zoom_on_scroll: bool,
#[props(default = true)]
pan_on_scroll: bool,
#[props(default = true)]
nodes_draggable: bool,
#[props(default = 0.0)]
drag_threshold: f64,
#[props(default = 28.0)]
connection_radius: f64,
#[props(default = false)]
fit_view: bool,
#[props(default = 0.12)] fit_view_padding: f64,
#[props(default = true)]
delete_key: bool,
id: Option<String>,
class: Option<String>,
node_view: Option<Callback<NodeViewCtx<T>, Element>>,
edge_view: Option<Callback<EdgeViewCtx, Element>>,
on_connect: Option<EventHandler<Connection>>,
on_connect_start: Option<EventHandler<HandleKey>>,
on_connect_end: Option<EventHandler<ConnectEnd>>,
is_valid_connection: Option<Callback<Connection, bool>>,
on_node_drag_start: Option<EventHandler<Vec<Id>>>,
on_node_drag_stop: Option<EventHandler<Vec<Id>>>,
on_delete: Option<EventHandler<DeleteRequest>>,
on_node_click: Option<EventHandler<Id>>,
on_edge_click: Option<EventHandler<Id>>,
on_pane_click: Option<EventHandler<Point>>,
on_pane_double_click: Option<EventHandler<Point>>,
handle: Option<FlowHandle<T>>,
children: Element,
) -> Element {
let geoms = use_memo(move || {
nodes
.read()
.iter()
.map(|node| NodeGeom {
id: node.id.clone(),
rect: node.rect(),
selected: node.selected,
source_side: node.source_side,
target_side: node.target_side,
measured: node.size.is_some() || node.measured.is_some(),
})
.collect::<Vec<_>>()
});
let deselect_nodes = use_callback(move |_: ()| {
if nodes.peek().iter().any(|n| n.selected) {
nodes.clone().with_mut(|nodes| {
for node in nodes.iter_mut() {
node.selected = false;
}
});
}
});
let mut canvas = CanvasProps::builder()
.min_zoom(min_zoom)
.max_zoom(max_zoom)
.pan_on_drag(pan_on_drag)
.zoom_on_scroll(zoom_on_scroll)
.pan_on_scroll(pan_on_scroll)
.nodes_draggable(nodes_draggable)
.drag_threshold(drag_threshold)
.connection_radius(connection_radius)
.fit_view_padding(fit_view_padding)
.id(id)
.class(class)
.edges(edges)
.geoms(geoms)
.deselect_nodes(deselect_nodes)
.on_connect(on_connect)
.on_connect_start(on_connect_start)
.on_connect_end(on_connect_end)
.is_valid_connection(is_valid_connection)
.on_node_drag_start(on_node_drag_start)
.on_node_drag_stop(on_node_drag_stop)
.on_pane_click(on_pane_click)
.on_pane_double_click(on_pane_double_click)
.world(rsx! {
match anchor {
AnchorMode::Handles => rsx! {
EdgesLayer { edge_view, on_edge_click }
NodesLayer { nodes, node_view, on_node_click }
},
AnchorMode::Seats => rsx! {
SeatGraphLayers {
nodes,
node_view,
on_node_click,
edge_view,
on_edge_click,
}
},
}
ConnectionLine {}
})
.children(children)
.build();
let core = canvas.inner.use_core();
use_effect(move || {
if let Some(handle) = handle {
let mut inner = handle.inner;
if inner.peek().is_none() {
inner.set(Some(FlowApi { core, nodes }));
}
}
});
use_drop(move || {
if let Some(handle) = handle {
let mut inner = handle.inner;
if inner.peek().is_some_and(|api| api.core == core) {
inner.set(None);
}
}
});
let mut fits = use_signal(|| 0u8);
let mut fitted: Signal<Option<(Rect, Rect)>> = use_signal(|| None);
let mut touched = use_signal(|| false);
use_effect(move || {
if *core.interaction.read() != Interaction::None {
touched.set(true);
}
});
use_effect(move || {
if !fit_view || *touched.peek() {
return;
}
let container = *core.container.read();
let bounds = geoms
.read()
.iter()
.map(|geom| geom.rect)
.reduce(|acc, rect| acc.union(&rect));
let Some(bounds) = bounds else { return };
if container.width <= 0.0 || container.height <= 0.0 {
return;
}
let same = |a: Rect, b: Rect| {
(a.x - b.x).abs() < 1.0
&& (a.y - b.y).abs() < 1.0
&& (a.width - b.width).abs() < 1.0
&& (a.height - b.height).abs() < 1.0
};
let settled = fitted
.peek()
.is_some_and(|(b, c)| same(b, bounds) && same(c, container));
if settled || *fits.peek() >= 12 {
return;
}
let done = *fits.peek();
fitted.set(Some((bounds, container)));
fits.set(done + 1);
core.fit_view(0);
});
let on_drag_move = use_callback(move |flow: Point| {
let grabs = core.drag.peek().grabs.clone();
let mut nodes = nodes;
nodes.with_mut(|nodes| {
for (id, grab) in &grabs {
if let Some(node) = nodes.iter_mut().find(|n| &n.id == id) {
node.position = flow - *grab;
}
}
});
});
let on_canvas_key_down = use_callback(move |evt: Event<KeyboardData>| match evt.key() {
Key::Delete | Key::Backspace if delete_key => {
let request = delete_request(nodes, core.edges);
if request.nodes.is_empty() && request.edges.is_empty() {
return;
}
match &on_delete {
Some(handler) => handler.call(request),
None => delete_selected(nodes, core.edges),
}
}
_ => {}
});
canvas.inner.on_drag_move = Some(on_drag_move);
canvas.inner.on_canvas_key_down = Some(on_canvas_key_down);
canvas.inner.render(core)
}
#[component]
fn SeatGraphLayers<T: Clone + PartialEq + 'static>(
nodes: Signal<Vec<Node<T>>>,
node_view: Option<Callback<NodeViewCtx<T>, Element>>,
on_node_click: Option<EventHandler<Id>>,
edge_view: Option<Callback<EdgeViewCtx, Element>>,
on_edge_click: Option<EventHandler<Id>>,
) -> Element {
let core = use_context::<FlowCore>();
let anchors = use_memo(move || {
let geoms = core.geoms.read();
let frames: std::collections::BTreeMap<Id, Rect> = geoms
.iter()
.map(|geom| (geom.id.clone(), geom.rect))
.collect();
let links: Vec<crate::ports::Link> = core
.edges
.read()
.iter()
.map(|edge| crate::ports::Link {
id: edge.id.clone(),
start: crate::ports::Terminal::Node(edge.source.clone()),
end: crate::ports::Terminal::Node(edge.target.clone()),
start_seat: edge.source_seat,
end_seat: edge.target_seat,
})
.collect();
crate::ports::solve_ports(&frames, &links)
});
let adapted_edge_view = edge_view.map(|view| {
Callback::new(move |ctx: crate::edge::SeatEdgeViewCtx| {
view.call(EdgeViewCtx {
edge: ctx.edge.clone(),
source: ctx.anchors.start.point(),
source_side: ctx.anchors.start.side(),
target: ctx.anchors.end.point(),
target_side: ctx.anchors.end.side(),
path: crate::path::EdgePath {
d: ctx.geometry.path.clone(),
label: ctx.geometry.label,
},
marker_end: None,
})
})
});
let edges = core.edges;
let solved = anchors.read();
rsx! {
crate::edge::SeatEdges {
edges,
anchors,
edge_view: adapted_edge_view,
on_edge_click,
}
crate::edge::SeatEdgeLabels { edges, anchors }
NodesLayer { nodes, node_view, on_node_click }
svg { class: "df-edges df-ports", "aria-hidden": "true",
for edge in edges.read().iter() {
if let Some(pair) = solved.get(&edge.id) {
g {
key: "{edge.id}",
class: if edge.selected { "df-selected" },
circle {
class: "df-port",
cx: pair.start.x,
cy: pair.start.y,
r: crate::ports::PORT_RADIUS,
}
circle {
class: "df-port",
cx: pair.end.x,
cy: pair.end.y,
r: crate::ports::PORT_RADIUS,
}
}
}
}
}
}
}
#[component]
fn ViewportPane(children: Element) -> Element {
let core = use_context::<FlowCore>();
let vp = *core.viewport.read();
rsx! {
div {
class: "df-viewport",
style: "transform: translate({vp.x}px, {vp.y}px) scale({vp.zoom});",
{children}
}
}
}
#[component]
pub fn WorldLayer(class: Option<String>, children: Element) -> Element {
let core = use_context::<FlowCore>();
let vp = *core.viewport.read();
let class = format!(
"df-world-layer{}",
class
.as_deref()
.map(|c| format!(" {c}"))
.unwrap_or_default()
);
rsx! {
div {
class,
style: "transform: translate({vp.x}px, {vp.y}px) scale({vp.zoom});",
{children}
}
}
}
#[component]
fn NodesLayer<T: Clone + PartialEq + 'static>(
nodes: Signal<Vec<Node<T>>>,
node_view: Option<Callback<NodeViewCtx<T>, Element>>,
on_node_click: Option<EventHandler<Id>>,
) -> Element {
let core = use_context::<FlowCore>();
let tiles = use_memo(move || {
let nodes = nodes.read();
let grabbed = (*core.interaction.read() == Interaction::DragNode)
.then(|| core.drag.peek().grabs.clone());
crate::tile::tiles(nodes.iter().map(|node| {
let raised = node.selected
|| grabbed
.as_ref()
.is_some_and(|grabs| grabs.iter().any(|(id, _)| id == &node.id));
(node.rect(), raised)
}))
});
let all = nodes.read();
rsx! {
div { class: "df-nodes",
for tile in tiles.read().iter() {
div {
key: "{tile.cell.0},{tile.cell.1}",
class: if tile.raised { "df-tile df-raised" } else { "df-tile" },
style: tile_style(tile),
for node in tile.members.iter().filter_map(|&i| all.get(i)) {
NodeItem::<T> {
key: "{node.id}",
nodes,
node: node.clone(),
origin: tile.origin(),
node_view,
on_node_click,
}
}
}
}
}
}
}
fn tile_style(tile: &crate::tile::Tile) -> String {
format!(
"transform:translate({}px,{}px);width:{}px;height:{}px;\
contain-intrinsic-size:{}px {}px;",
tile.rect.x,
tile.rect.y,
tile.rect.width,
tile.rect.height,
tile.rect.width,
tile.rect.height,
)
}
#[component]
fn EdgesLayer(
edge_view: Option<Callback<EdgeViewCtx, Element>>,
on_edge_click: Option<EventHandler<Id>>,
) -> Element {
let core = use_context::<FlowCore>();
let edges = core.edges.read();
let geoms = core.geoms.read();
let handles = core.handles.read();
let geom_by_id: HashMap<&str, &NodeGeom> =
geoms.iter().map(|geom| (geom.id.as_str(), geom)).collect();
let handle_idx: HashMap<(&str, HandleKind, &str), &crate::types::HandleGeom> = handles
.iter()
.map(|(key, geom)| ((key.node.as_str(), key.kind, key.id.as_str()), geom))
.collect();
let anchor = |geom: &NodeGeom, kind: HandleKind, handle_id: &Option<Id>| {
let key = (geom.id.as_str(), kind, handle_id.as_deref().unwrap_or(""));
crate::state::anchor_from_geom(handle_idx.get(&key).copied(), geom, kind)
};
let items: Vec<_> = edges
.iter()
.filter_map(|edge| {
let source_geom = geom_by_id.get(edge.source.as_str())?;
let target_geom = geom_by_id.get(edge.target.as_str())?;
let (source, source_side, source_on_handle) =
anchor(source_geom, HandleKind::Source, &edge.source_handle);
let (target, target_side, target_on_handle) =
anchor(target_geom, HandleKind::Target, &edge.target_handle);
let source = if source_on_handle {
source + source_side.normal() * HANDLE_RIM
} else {
source
};
let target = if target_on_handle {
target + target_side.normal() * HANDLE_RIM
} else {
target
};
Some((
edge.clone(),
source,
source_side,
target,
target_side,
source_geom.rect,
target_geom.rect,
))
})
.collect();
let tiles = crate::tile::tiles(items.iter().map(|item| {
let (edge, source, source_side, target, target_side, source_rect, target_rect) = item;
let geo = crate::path::EdgeGeometry::new(*source, *source_side, *target, *target_side)
.with_rects(*source_rect, *target_rect);
(crate::path::edge_bounds(edge.kind, &geo), edge.selected)
}));
rsx! {
div { class: "df-edges", "aria-hidden": "true",
svg { class: "df-edge-defs",
defs { EdgeMarkers { iid: core.iid } }
}
for tile in tiles.iter() {
svg {
key: "{tile.cell.0},{tile.cell.1}",
class: if tile.raised { "df-edge-tile df-raised" } else { "df-edge-tile" },
view_box: "{tile.rect.x} {tile.rect.y} {tile.rect.width} {tile.rect.height}",
style: tile_style(tile),
for &i in tile.members.iter() {
if let Some((edge, source, source_side, target, target_side, source_rect, target_rect)) = items.get(i) {
EdgeItem {
key: "{edge.id}",
edge: edge.clone(),
source: *source,
source_side: *source_side,
target: *target,
target_side: *target_side,
source_rect: *source_rect,
target_rect: *target_rect,
edge_view,
on_edge_click,
}
}
}
}
}
}
}
}
#[component]
fn ConnectionLine() -> Element {
let core = use_context::<FlowCore>();
let connection = core.connection.read();
let Some(conn) = connection.as_ref() else {
return rsx! {};
};
let Some((from, from_side)) = core.anchor_of(&conn.from) else {
return rsx! {};
};
let (to, to_side) = match &conn.snap {
Some(snap) => (snap.point, Some(snap.side)),
None => (conn.cursor, None),
};
let d = connection_path(from, from_side, to, to_side);
rsx! {
svg { class: "df-connection",
path { class: "df-connection-path", d }
}
}
}
pub(crate) fn deselect_edges(mut edges: Signal<Vec<Edge>>) {
if edges.peek().iter().any(|e| e.selected) {
edges.with_mut(|edges| {
for edge in edges.iter_mut() {
edge.selected = false;
}
});
}
}
fn add_edge_for_connection(mut edges: Signal<Vec<Edge>>, conn: Connection) {
let duplicate = edges.peek().iter().any(|e| {
e.source == conn.source
&& e.target == conn.target
&& e.source_handle == conn.source_handle
&& e.target_handle == conn.target_handle
});
if duplicate {
return;
}
let mut edge = conn.into_edge();
let base = edge.id.clone();
let mut n = 2;
while edges.peek().iter().any(|e| e.id == edge.id) {
edge.id = format!("{base}-{n}");
n += 1;
}
edges.with_mut(|edges| edges.push(edge));
}
fn delete_request<T: Clone + PartialEq + 'static>(
nodes: Signal<Vec<Node<T>>>,
edges: Signal<Vec<Edge>>,
) -> DeleteRequest {
let removed: std::collections::HashSet<Id> = nodes
.peek()
.iter()
.filter(|n| n.selected)
.map(|n| n.id.clone())
.collect();
let edge_ids = edges
.peek()
.iter()
.filter(|e| e.selected || removed.contains(&e.source) || removed.contains(&e.target))
.map(|e| e.id.clone())
.collect();
DeleteRequest {
nodes: removed.into_iter().collect(),
edges: edge_ids,
}
}
pub(crate) fn delete_selected<T: Clone + PartialEq + 'static>(
mut nodes: Signal<Vec<Node<T>>>,
mut edges: Signal<Vec<Edge>>,
) {
let removed: std::collections::HashSet<Id> = nodes
.peek()
.iter()
.filter(|n| n.selected)
.map(|n| n.id.clone())
.collect();
let any_edges = edges
.peek()
.iter()
.any(|e| e.selected || removed.contains(&e.source) || removed.contains(&e.target));
if !removed.is_empty() {
nodes.with_mut(|nodes| nodes.retain(|n| !n.selected));
}
if any_edges {
edges.with_mut(|edges| {
edges.retain(|e| {
!e.selected && !removed.contains(&e.source) && !removed.contains(&e.target)
})
});
}
}