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 dioxus::core::spawn_forever(async move {
232 crate::anim::sleep_ms(0).await;
233 let mut queued_signal = core.size_flush_queued;
234 let Ok(mut queued) = queued_signal.try_write() else {
235 return;
236 };
237 *queued = false;
238 drop(queued);
239 let mut pending_signal = core.pending_sizes;
240 let Ok(mut pending) = pending_signal.try_write() else {
241 return;
242 };
243 let sizes = std::mem::take(&mut *pending);
244 drop(pending);
245 if sizes.is_empty() {
246 return;
247 }
248 let map: std::collections::HashMap<Id, Size> = sizes.into_iter().collect();
249 let Ok(mut nodes) = nodes.try_write() else {
250 return;
251 };
252 for node in nodes.iter_mut() {
253 if let Some(size) = map.get(&node.id) {
254 node.measured = Some(*size);
255 }
256 }
257 });
258}
259
260#[allow(clippy::too_many_arguments)]
261fn node_pointer_down<T: Clone + PartialEq + 'static>(
262 core: FlowCore,
263 mut nodes: Signal<Vec<Node<T>>>,
264 id: &Id,
265 draggable: bool,
266 selectable: bool,
267 on_node_click: &Option<EventHandler<Id>>,
268 evt: Event<PointerData>,
269) {
270 if *core.interaction.peek() != Interaction::None {
272 return;
273 }
274 core.cancel_animations();
275 let shift = evt.modifiers().shift();
276
277 if selectable {
278 let already_selected = nodes
279 .peek()
280 .iter()
281 .find(|n| &n.id == id)
282 .map(|n| n.selected)
283 .unwrap_or(false);
284 if shift {
285 nodes.with_mut(|nodes| {
286 if let Some(node) = nodes.iter_mut().find(|n| &n.id == id) {
287 node.selected = !node.selected;
288 }
289 });
290 } else if !already_selected {
291 nodes.with_mut(|nodes| {
292 for node in nodes.iter_mut() {
293 node.selected = &node.id == id;
294 }
295 });
296 crate::flow::deselect_edges(core.edges);
297 }
298 }
299 if let Some(handler) = on_node_click {
300 handler.call(id.clone());
301 }
302
303 let config = *core.config.peek();
304 if !(draggable && config.nodes_draggable) {
305 core.interaction.clone().set(Interaction::Pressed);
306 return;
307 }
308
309 let cursor_flow = core.client_to_flow(client_point(evt.client_coordinates()));
310 let grabs: Vec<(Id, Point)> = nodes
313 .peek()
314 .iter()
315 .filter(|n| (n.selected && n.draggable) || &n.id == id)
316 .map(|n| (n.id.clone(), cursor_flow - n.position))
317 .collect();
318 let client = client_point(evt.client_coordinates());
319 let mut drag = core.drag;
320 {
321 let mut state = drag.write();
322 *state = DragState {
323 pointer_id: Some(evt.pointer_id()),
324 origin_client: client,
325 last_client: client,
326 moved: false,
327 suppress_click: false,
328 grabs,
329 };
330 }
331 core.interaction.clone().set(Interaction::DragNode);
332}
333
334#[component]
337pub fn Handle(
338 kind: HandleKind,
340 position: ReadSignal<Side>,
342 id: Option<String>,
346 #[props(default = 0.5)]
348 offset: f64,
349 class: Option<String>,
351) -> Element {
352 let core = use_context::<FlowCore>();
353 let scope = use_context::<NodeScope>();
354 let key = use_hook(|| HandleKey {
355 node: scope.id.clone(),
356 kind,
357 id: id.clone().unwrap_or_default(),
358 });
359
360 {
363 let key = key.clone();
364 use_effect(move || {
365 let geom = HandleGeom {
366 side: *position.read(),
367 offset,
368 };
369 if core.handles.peek().get(&key) != Some(&geom) {
370 core.queue_handle_write(key.clone(), Some(geom));
371 }
372 });
373 }
374 {
375 let key = key.clone();
376 use_drop(move || {
377 core.queue_handle_write(key, None);
378 });
379 }
380
381 let connect_from = core.connect_from.read();
384 let is_snap_target = core.snap_key.read().as_ref() == Some(&key);
385 let is_connect_source = connect_from.as_ref() == Some(&key);
386 let is_valid_target = connect_from
387 .as_ref()
388 .map(|from| from.kind != kind && from.node != key.node)
389 .unwrap_or(false);
390
391 let side = *position.read();
392 let class = format!(
393 "df-handle df-handle-{} df-handle-{}{}{}{}{}",
394 match kind {
395 HandleKind::Source => "source",
396 HandleKind::Target => "target",
397 },
398 side.class_name(),
399 if is_connect_source {
400 " df-connecting-from"
401 } else {
402 ""
403 },
404 if is_valid_target {
405 " df-valid-target"
406 } else {
407 ""
408 },
409 if is_snap_target { " df-snap" } else { "" },
410 class
411 .as_deref()
412 .map(|c| format!(" {c}"))
413 .unwrap_or_default(),
414 );
415 let pct = offset * 100.0;
416 let style = match side {
417 Side::Top => format!("left:{pct}%;top:0;"),
418 Side::Bottom => format!("left:{pct}%;top:100%;"),
419 Side::Left => format!("left:0;top:{pct}%;"),
420 Side::Right => format!("left:100%;top:{pct}%;"),
421 };
422
423 let key_for_down = key.clone();
424 rsx! {
425 div {
426 class,
427 style,
428 onpointerdown: move |evt| {
429 if *core.interaction.peek() != Interaction::None {
430 return;
431 }
432 core.cancel_animations();
433 let client = client_point(evt.client_coordinates());
434 let cursor = core.client_to_flow(client);
435 {
436 let mut drag = core.drag;
437 let mut state = drag.write();
438 *state = DragState {
439 pointer_id: Some(evt.pointer_id()),
440 origin_client: client,
441 last_client: client,
442 moved: false,
443 suppress_click: false,
444 grabs: Vec::new(),
445 };
446 }
447 core.connection.clone().set(Some(ConnectionState {
448 from: key_for_down.clone(),
449 cursor,
450 snap: None,
451 }));
452 core.interaction.clone().set(Interaction::Connect);
453 if let Some(handler) = &core.on_connect_start {
454 handler.call(key_for_down.clone());
455 }
456 },
457 }
458 }
459}
460
461#[component]
467pub fn DefaultNodeView<T: Clone + PartialEq + 'static>(ctx: NodeViewCtx<T>) -> Element {
468 let node = &ctx.node;
469 let is_input = node.node_type.as_deref() == Some("input");
470 let is_output = node.node_type.as_deref() == Some("output");
471 rsx! {
472 div { class: "df-node-default",
473 if !is_input {
474 Handle { kind: HandleKind::Target, position: node.target_side }
475 }
476 span { class: "df-node-label", "{node.label}" }
477 if !is_output {
478 Handle { kind: HandleKind::Source, position: node.source_side }
479 }
480 }
481 }
482}