1use dioxus::prelude::*;
5
6use crate::state::{ConnectionState, DragState, FlowCore, Interaction};
7use crate::types::{HandleGeom, HandleKey, HandleKind, Id, Node, Point, Side, Size};
8
9#[derive(Clone, PartialEq)]
11pub struct NodeViewCtx<T: Clone + PartialEq + 'static> {
12 pub node: Node<T>,
13 pub dragging: bool,
15}
16
17#[derive(Clone, PartialEq)]
20pub(crate) struct NodeScope {
21 pub id: Id,
22}
23
24fn client_point(coords: dioxus::html::geometry::ClientPoint) -> Point {
25 Point::new(coords.x, coords.y)
26}
27
28#[component]
30pub(crate) fn NodeItem<T: Clone + PartialEq + 'static>(
31 nodes: Signal<Vec<Node<T>>>,
32 node: Node<T>,
33 #[props(default)]
36 origin: Point,
37 node_view: Option<Callback<NodeViewCtx<T>, Element>>,
38 on_node_click: Option<EventHandler<Id>>,
39) -> Element {
40 let core = use_context::<FlowCore>();
41 use_context_provider(|| NodeScope {
42 id: node.id.clone(),
43 });
44
45 let dragging = *core.interaction.read() == Interaction::DragNode
47 && core.drag.peek().grabs.iter().any(|(id, _)| id == &node.id);
48
49 let class = format!(
50 "df-node{}{}{}",
51 if node.selected { " df-selected" } else { "" },
52 if dragging { " df-dragging" } else { "" },
53 node.class
54 .as_deref()
55 .map(|c| format!(" {c}"))
56 .unwrap_or_default(),
57 );
58 let size_style = node
59 .size
60 .map(|s| format!("width:{}px;height:{}px;", s.width, s.height))
61 .unwrap_or_default();
62 let style = format!(
63 "transform:translate({}px,{}px);z-index:{};{}{}",
64 node.position.x - origin.x,
65 node.position.y - origin.y,
66 if dragging || node.selected { 1000 } else { 0 },
67 size_style,
68 node.style.as_deref().unwrap_or_default(),
69 );
70
71 let id_for_drag = node.id.clone();
72 let id_for_resize = node.id.clone();
73 let id_for_keys = node.id.clone();
74 let draggable = node.draggable;
75 let selectable = node.selectable;
76 let label = node.label.clone();
77
78 let content = match node_view {
79 Some(view) => view.call(NodeViewCtx {
80 node: node.clone(),
81 dragging,
82 }),
83 None => rsx! {
84 DefaultNodeView::<T> {
85 ctx: NodeViewCtx { node: node.clone(), dragging },
86 }
87 },
88 };
89
90 rsx! {
91 div {
92 class,
93 style,
94 tabindex: "0",
95 role: "group",
96 aria_label: "{label}",
97 onpointerdown: move |evt| {
98 node_pointer_down(core, nodes, &id_for_drag, draggable, selectable, &on_node_click, evt)
99 },
100 onkeydown: move |evt| {
101 node_key_down(core, nodes, &id_for_keys, draggable, selectable, evt)
102 },
103 onresize: move |evt| {
104 if let Ok(size) = evt.data().get_border_box_size() {
105 store_measured(core, nodes, &id_for_resize, Size::new(size.width, size.height));
106 }
107 },
108 {content}
109 }
110 }
111}
112
113fn node_key_down<T: Clone + PartialEq + 'static>(
117 core: FlowCore,
118 mut nodes: Signal<Vec<Node<T>>>,
119 id: &Id,
120 draggable: bool,
121 selectable: bool,
122 evt: Event<KeyboardData>,
123) {
124 let step = if evt.modifiers().shift() { 1.0 } else { 10.0 };
125 let delta = match evt.key() {
126 Key::ArrowUp => Point::new(0.0, -step),
127 Key::ArrowDown => Point::new(0.0, step),
128 Key::ArrowLeft => Point::new(-step, 0.0),
129 Key::ArrowRight => Point::new(step, 0.0),
130 Key::Enter => {
131 evt.prevent_default();
132 select_node(core, nodes, id, selectable, evt.modifiers().shift());
133 return;
134 }
135 Key::Character(c) if c == " " => {
136 evt.prevent_default();
137 select_node(core, nodes, id, selectable, evt.modifiers().shift());
138 return;
139 }
140 _ => return,
141 };
142 if !(draggable && core.config.peek().nodes_draggable) {
143 return;
144 }
145 evt.prevent_default();
146 core.cancel_animations();
147 let focused_selected = nodes
148 .peek()
149 .iter()
150 .find(|n| &n.id == id)
151 .map(|n| n.selected)
152 .unwrap_or(false);
153 nodes.with_mut(|nodes| {
154 for node in nodes.iter_mut() {
155 let moves = if focused_selected {
156 node.selected && node.draggable
157 } else {
158 &node.id == id
159 };
160 if moves {
161 node.position = node.position + delta;
162 }
163 }
164 });
165}
166
167fn select_node<T: Clone + PartialEq + 'static>(
168 core: FlowCore,
169 mut nodes: Signal<Vec<Node<T>>>,
170 id: &Id,
171 selectable: bool,
172 toggle: bool,
173) {
174 if !selectable {
175 return;
176 }
177 if toggle {
178 nodes.with_mut(|nodes| {
179 if let Some(node) = nodes.iter_mut().find(|n| &n.id == id) {
180 node.selected = !node.selected;
181 }
182 });
183 } else {
184 nodes.with_mut(|nodes| {
185 for node in nodes.iter_mut() {
186 node.selected = &node.id == id;
187 }
188 });
189 crate::flow::deselect_edges(core.edges);
190 }
191}
192
193fn store_measured<T: Clone + PartialEq + 'static>(
197 core: FlowCore,
198 mut nodes: Signal<Vec<Node<T>>>,
199 id: &Id,
200 size: Size,
201) {
202 if !(size.width > 0.0 && size.height > 0.0) {
208 return;
209 }
210 let changed = nodes
211 .peek()
212 .iter()
213 .find(|n| &n.id == id)
214 .map(|n| match n.measured {
215 Some(m) => (m.width - size.width).abs() > 0.5 || (m.height - size.height).abs() > 0.5,
216 None => true,
217 })
218 .unwrap_or(false);
219 if !changed {
220 return;
221 }
222 core.pending_sizes.clone().write().push((id.clone(), size));
223 let mut queued = core.size_flush_queued;
224 if *queued.peek() {
225 return;
226 }
227 queued.set(true);
228 core.spawn(async move {
231 crate::anim::sleep_ms(0).await;
232 core.size_flush_queued.clone().set(false);
233 let sizes = std::mem::take(&mut *core.pending_sizes.clone().write());
234 if sizes.is_empty() {
235 return;
236 }
237 let map: std::collections::HashMap<Id, Size> = sizes.into_iter().collect();
238 let mut nodes = nodes.write();
239 for node in nodes.iter_mut() {
240 if let Some(size) = map.get(&node.id) {
241 node.measured = Some(*size);
242 }
243 }
244 });
245}
246
247#[allow(clippy::too_many_arguments)]
248fn node_pointer_down<T: Clone + PartialEq + 'static>(
249 core: FlowCore,
250 mut nodes: Signal<Vec<Node<T>>>,
251 id: &Id,
252 draggable: bool,
253 selectable: bool,
254 on_node_click: &Option<EventHandler<Id>>,
255 evt: Event<PointerData>,
256) {
257 if *core.interaction.peek() != Interaction::None {
259 return;
260 }
261 core.cancel_animations();
262 let shift = evt.modifiers().shift();
263
264 if selectable {
265 let already_selected = nodes
266 .peek()
267 .iter()
268 .find(|n| &n.id == id)
269 .map(|n| n.selected)
270 .unwrap_or(false);
271 if shift {
272 nodes.with_mut(|nodes| {
273 if let Some(node) = nodes.iter_mut().find(|n| &n.id == id) {
274 node.selected = !node.selected;
275 }
276 });
277 } else if !already_selected {
278 nodes.with_mut(|nodes| {
279 for node in nodes.iter_mut() {
280 node.selected = &node.id == id;
281 }
282 });
283 crate::flow::deselect_edges(core.edges);
284 }
285 }
286 if let Some(handler) = on_node_click {
287 handler.call(id.clone());
288 }
289
290 let config = *core.config.peek();
291 if !(draggable && config.nodes_draggable) {
292 core.interaction.clone().set(Interaction::Pressed);
293 return;
294 }
295
296 let cursor_flow = core.client_to_flow(client_point(evt.client_coordinates()));
297 let grabs: Vec<(Id, Point)> = nodes
300 .peek()
301 .iter()
302 .filter(|n| (n.selected && n.draggable) || &n.id == id)
303 .map(|n| (n.id.clone(), cursor_flow - n.position))
304 .collect();
305 let client = client_point(evt.client_coordinates());
306 let mut drag = core.drag;
307 {
308 let mut state = drag.write();
309 *state = DragState {
310 pointer_id: Some(evt.pointer_id()),
311 origin_client: client,
312 last_client: client,
313 moved: false,
314 suppress_click: false,
315 grabs,
316 };
317 }
318 core.interaction.clone().set(Interaction::DragNode);
319}
320
321#[component]
324pub fn Handle(
325 kind: HandleKind,
327 position: ReadSignal<Side>,
329 id: Option<String>,
333 #[props(default = 0.5)]
335 offset: f64,
336 class: Option<String>,
338) -> Element {
339 let core = use_context::<FlowCore>();
340 let scope = use_context::<NodeScope>();
341 let key = use_hook(|| HandleKey {
342 node: scope.id.clone(),
343 kind,
344 id: id.clone().unwrap_or_default(),
345 });
346
347 {
350 let key = key.clone();
351 use_effect(move || {
352 let geom = HandleGeom {
353 side: *position.read(),
354 offset,
355 };
356 if core.handles.peek().get(&key) != Some(&geom) {
357 core.queue_handle_write(key.clone(), Some(geom));
358 }
359 });
360 }
361 {
362 let key = key.clone();
363 use_drop(move || {
364 core.queue_handle_write(key, None);
365 });
366 }
367
368 let connect_from = core.connect_from.read();
371 let is_snap_target = core.snap_key.read().as_ref() == Some(&key);
372 let is_connect_source = connect_from.as_ref() == Some(&key);
373 let is_valid_target = connect_from
374 .as_ref()
375 .map(|from| from.kind != kind && from.node != key.node)
376 .unwrap_or(false);
377
378 let side = *position.read();
379 let class = format!(
380 "df-handle df-handle-{} df-handle-{}{}{}{}{}",
381 match kind {
382 HandleKind::Source => "source",
383 HandleKind::Target => "target",
384 },
385 side.class_name(),
386 if is_connect_source {
387 " df-connecting-from"
388 } else {
389 ""
390 },
391 if is_valid_target {
392 " df-valid-target"
393 } else {
394 ""
395 },
396 if is_snap_target { " df-snap" } else { "" },
397 class
398 .as_deref()
399 .map(|c| format!(" {c}"))
400 .unwrap_or_default(),
401 );
402 let pct = offset * 100.0;
403 let style = match side {
404 Side::Top => format!("left:{pct}%;top:0;"),
405 Side::Bottom => format!("left:{pct}%;top:100%;"),
406 Side::Left => format!("left:0;top:{pct}%;"),
407 Side::Right => format!("left:100%;top:{pct}%;"),
408 };
409
410 let key_for_down = key.clone();
411 rsx! {
412 div {
413 class,
414 style,
415 onpointerdown: move |evt| {
416 if *core.interaction.peek() != Interaction::None {
417 return;
418 }
419 core.cancel_animations();
420 let client = client_point(evt.client_coordinates());
421 let cursor = core.client_to_flow(client);
422 {
423 let mut drag = core.drag;
424 let mut state = drag.write();
425 *state = DragState {
426 pointer_id: Some(evt.pointer_id()),
427 origin_client: client,
428 last_client: client,
429 moved: false,
430 suppress_click: false,
431 grabs: Vec::new(),
432 };
433 }
434 core.connection.clone().set(Some(ConnectionState {
435 from: key_for_down.clone(),
436 cursor,
437 snap: None,
438 }));
439 core.interaction.clone().set(Interaction::Connect);
440 if let Some(handler) = &core.on_connect_start {
441 handler.call(key_for_down.clone());
442 }
443 },
444 }
445 }
446}
447
448#[component]
454pub fn DefaultNodeView<T: Clone + PartialEq + 'static>(ctx: NodeViewCtx<T>) -> Element {
455 let node = &ctx.node;
456 let is_input = node.node_type.as_deref() == Some("input");
457 let is_output = node.node_type.as_deref() == Some("output");
458 rsx! {
459 div { class: "df-node-default",
460 if !is_input {
461 Handle { kind: HandleKind::Target, position: node.target_side }
462 }
463 span { class: "df-node-label", "{node.label}" }
464 if !is_output {
465 Handle { kind: HandleKind::Source, position: node.source_side }
466 }
467 }
468 }
469}
470
471#[cfg(test)]
472mod lifecycle_tests;