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