1use std::collections::HashMap;
6use std::future::Future;
7
8use dioxus::prelude::*;
9
10use crate::anim::{bump_epoch, tween};
11use crate::layout::{compute_layout, LayoutNode, LayoutOptions};
12use crate::types::{
13 side_point, Edge, HandleGeom, HandleKey, HandleKind, Id, NodeGeom, Point, Rect, Side, Viewport,
14};
15
16#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
20pub enum Interaction {
21 #[default]
22 None,
23 Pan,
25 DragNode,
27 Connect,
29 PanePressed,
32 Pressed,
34}
35
36#[derive(Clone, Debug, Default)]
39pub struct DragState {
40 pub pointer_id: Option<i32>,
43 pub origin_client: Point,
46 pub last_client: Point,
47 pub moved: bool,
48 pub suppress_click: bool,
51 pub grabs: Vec<(Id, Point)>,
54}
55
56#[derive(Clone, PartialEq, Debug)]
58pub struct SnapTarget {
59 pub key: HandleKey,
60 pub point: Point,
61 pub side: Side,
62}
63
64#[derive(Clone, PartialEq, Debug)]
66pub struct ConnectionState {
67 pub from: HandleKey,
68 pub cursor: Point,
69 pub snap: Option<SnapTarget>,
70}
71
72#[derive(Clone, Copy, PartialEq, Debug)]
74pub struct FlowConfig {
75 pub min_zoom: f64,
76 pub max_zoom: f64,
77 pub pan_on_drag: bool,
78 pub zoom_on_scroll: bool,
79 pub pan_on_scroll: bool,
83 pub nodes_draggable: bool,
84 pub drag_threshold: f64,
88 pub connection_radius: f64,
90 pub fit_view_padding: f64,
91}
92
93impl Default for FlowConfig {
94 fn default() -> Self {
95 Self {
96 min_zoom: 0.25,
99 max_zoom: 4.0,
100 pan_on_drag: true,
101 zoom_on_scroll: true,
102 pan_on_scroll: true,
103 nodes_draggable: true,
104 drag_threshold: 0.0,
105 connection_radius: 28.0,
106 fit_view_padding: 0.12,
107 }
108 }
109}
110
111#[derive(Clone, Copy)]
115pub struct FlowCore {
116 pub iid: usize,
118 pub viewport: Signal<Viewport>,
119 pub container: Signal<Rect>,
121 pub interaction: Signal<Interaction>,
122 pub connection: Signal<Option<ConnectionState>>,
123 pub handles: Signal<HashMap<HandleKey, HandleGeom>>,
124 pub edges: Signal<Vec<Edge>>,
125 pub geoms: Memo<Vec<NodeGeom>>,
127 pub config: Signal<FlowConfig>,
128 pub(crate) drag: Signal<DragState>,
129 pub(crate) epoch: Signal<u64>,
130 pub(crate) snap_key: Memo<Option<HandleKey>>,
133 pub(crate) connect_from: Memo<Option<HandleKey>>,
135 pub(crate) deselect_nodes: Callback<()>,
138 pub(crate) overlay_insets: Signal<HashMap<usize, (Side, f64)>>,
141 pub(crate) pending_sizes: Signal<Vec<(Id, crate::types::Size)>>,
146 pub(crate) size_flush_queued: Signal<bool>,
148 pub(crate) pending_handles: Signal<Vec<(HandleKey, Option<HandleGeom>)>>,
153 pub(crate) handle_flush_queued: Signal<bool>,
155 pub(crate) on_connect_start: Option<EventHandler<HandleKey>>,
159 pub(crate) valid_connection: Option<Callback<crate::types::Connection, bool>>,
163}
164
165impl PartialEq for FlowCore {
166 fn eq(&self, other: &Self) -> bool {
167 self.iid == other.iid
168 }
169}
170
171pub fn use_flow() -> FlowCore {
174 use_context::<FlowCore>()
175}
176
177static NEXT_OVERLAY_KEY: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
178
179pub fn use_overlay_inset(side: Side, thickness: f64) {
184 let core = use_context::<FlowCore>();
185 let key = use_hook(|| NEXT_OVERLAY_KEY.fetch_add(1, std::sync::atomic::Ordering::Relaxed));
186 let mut insets = core.overlay_insets;
187 if insets.peek().get(&key) != Some(&(side, thickness)) {
188 insets.write().insert(key, (side, thickness));
189 }
190 use_drop(move || {
191 core.overlay_insets.clone().write().remove(&key);
192 });
193}
194
195impl FlowCore {
196 pub(crate) fn spawn(&self, future: impl Future<Output = ()> + 'static) -> dioxus::core::Task {
198 dioxus::core::Runtime::current().in_scope(self.viewport.origin_scope(), || spawn(future))
199 }
200
201 pub(crate) fn queue_handle_write(&self, key: HandleKey, geom: Option<HandleGeom>) {
204 self.pending_handles.clone().write().push((key, geom));
205 let mut queued = self.handle_flush_queued;
206 if *queued.peek() {
207 return;
208 }
209 queued.set(true);
210 let core = *self;
211 self.spawn(async move {
214 crate::anim::sleep_ms(0).await;
215 core.handle_flush_queued.clone().set(false);
216 let pending = std::mem::take(&mut *core.pending_handles.clone().write());
217 if pending.is_empty() {
218 return;
219 }
220 let mut handles = core.handles;
221 let changed = {
222 let current = handles.peek();
223 pending.iter().any(|(key, geom)| match geom {
224 Some(geom) => current.get(key) != Some(geom),
225 None => current.contains_key(key),
226 })
227 };
228 if !changed {
229 return;
230 }
231 let mut current = handles.write();
232 for (key, geom) in pending {
233 match geom {
234 Some(geom) => {
235 current.insert(key, geom);
236 }
237 None => {
238 current.remove(&key);
239 }
240 }
241 }
242 });
243 }
244
245 pub fn claim_pointer(&self) -> bool {
254 let mut interaction = self.interaction;
255 if *interaction.peek() != Interaction::None {
256 return false;
257 }
258 interaction.set(Interaction::Pressed);
259 true
260 }
261
262 pub fn release_pointer(&self) {
264 let mut interaction = self.interaction;
265 if *interaction.peek() == Interaction::Pressed {
266 interaction.set(Interaction::None);
267 }
268 }
269
270 pub fn begin_pan(&self, pointer_id: i32, client: Point) -> bool {
276 let mut interaction = self.interaction;
277 if *interaction.peek() != Interaction::None {
278 return false;
279 }
280 self.cancel_animations();
281 {
282 let mut drag = self.drag;
283 let mut state = drag.write();
284 *state = DragState {
285 pointer_id: Some(pointer_id),
286 origin_client: client,
287 last_client: client,
288 moved: false,
289 suppress_click: true,
290 grabs: Vec::new(),
291 };
292 }
293 interaction.set(Interaction::Pan);
294 true
295 }
296
297 pub fn client_to_flow(&self, client: Point) -> Point {
299 let rect = *self.container.peek();
300 self.viewport.peek().screen_to_flow(client - rect.origin())
301 }
302
303 pub fn flow_to_client(&self, flow: Point) -> Point {
305 let rect = *self.container.peek();
306 self.viewport.peek().flow_to_screen(flow) + rect.origin()
307 }
308
309 pub fn cancel_animations(&self) {
311 bump_epoch(self.epoch);
312 }
313
314 pub fn nodes_bounds(&self) -> Option<Rect> {
316 let geoms = self.geoms.peek();
317 let mut iter = geoms.iter();
318 let first = iter.next()?.rect;
319 Some(iter.fold(first, |acc, geom| acc.union(&geom.rect)))
320 }
321
322 pub fn set_viewport(&self, target: Viewport, duration_ms: u64) {
324 let mut viewport = self.viewport;
325 if duration_ms == 0 {
326 self.cancel_animations();
327 viewport.set(target);
328 return;
329 }
330 let from = *viewport.peek();
331 tween(self.epoch, duration_ms, move |t| {
332 viewport.set(from.lerp(&target, t));
333 });
334 }
335
336 pub fn zoom_by(&self, factor: f64, anchor_client: Option<Point>, duration_ms: u64) {
339 let config = *self.config.peek();
340 let rect = *self.container.peek();
341 let vp = *self.viewport.peek();
342 let anchor = anchor_client
343 .map(|c| c - rect.origin())
344 .unwrap_or_else(|| Point::new(rect.width / 2.0, rect.height / 2.0));
345 let target = vp.zoom_about(vp.zoom * factor, anchor, config.min_zoom, config.max_zoom);
346 self.set_viewport(target, duration_ms);
347 }
348
349 pub fn zoom_in(&self, duration_ms: u64) {
350 self.zoom_by(1.25, None, duration_ms);
351 }
352
353 pub fn zoom_out(&self, duration_ms: u64) {
354 self.zoom_by(0.8, None, duration_ms);
355 }
356
357 pub fn fit_bounds(&self, bounds: Rect, padding: f64, duration_ms: u64) {
359 if let Some(target) = fit_viewport(self, bounds, padding) {
360 self.set_viewport(target, duration_ms);
361 }
362 }
363
364 pub fn fit_view(&self, duration_ms: u64) {
366 let padding = self.config.peek().fit_view_padding;
367 if let Some(bounds) = self.nodes_bounds() {
368 self.fit_bounds(bounds, padding, duration_ms);
369 }
370 }
371
372 pub fn center_on(&self, flow: Point, duration_ms: u64) {
374 let rect = *self.container.peek();
375 let zoom = self.viewport.peek().zoom;
376 let target = Viewport::new(
377 rect.width / 2.0 - flow.x * zoom,
378 rect.height / 2.0 - flow.y * zoom,
379 zoom,
380 );
381 self.set_viewport(target, duration_ms);
382 }
383
384 pub(crate) fn resolve_anchor(
389 &self,
390 handles: &HashMap<HandleKey, HandleGeom>,
391 geom: &NodeGeom,
392 kind: HandleKind,
393 handle_id: &Option<Id>,
394 ) -> (Point, Side, bool) {
395 let key = HandleKey {
396 node: geom.id.clone(),
397 kind,
398 id: handle_id.clone().unwrap_or_default(),
399 };
400 anchor_from_geom(handles.get(&key), geom, kind)
401 }
402
403 pub(crate) fn anchor_of(&self, key: &HandleKey) -> Option<(Point, Side)> {
405 let handles = self.handles.peek();
406 let geoms = self.geoms.peek();
407 let geom = geoms.iter().find(|geom| geom.id == key.node)?;
408 let id = (!key.id.is_empty()).then(|| key.id.clone());
409 let (point, side, _) = self.resolve_anchor(&handles, geom, key.kind, &id);
410 Some((point, side))
411 }
412
413 pub(crate) fn find_snap(&self, from: &HandleKey, cursor: Point) -> Option<SnapTarget> {
416 let radius = self.config.peek().connection_radius / self.viewport.peek().zoom.max(1e-6);
417 let handles = self.handles.peek();
418 let geoms = self.geoms.peek();
419 let geom_by_id: HashMap<&str, &NodeGeom> =
420 geoms.iter().map(|geom| (geom.id.as_str(), geom)).collect();
421
422 let mut best: Option<(f64, SnapTarget)> = None;
423 for (key, hg) in handles.iter() {
424 if key.kind == from.kind || key.node == from.node {
425 continue;
426 }
427 let Some(geom) = geom_by_id.get(key.node.as_str()) else {
428 continue;
429 };
430 if let Some(valid) = &self.valid_connection {
433 if !valid.call(orient_connection(from, key)) {
434 continue;
435 }
436 }
437 let point = side_point(&geom.rect, hg.side, hg.offset);
438 let d2 = point.distance_sq(cursor);
439 if d2 <= radius * radius && best.as_ref().map(|(bd, _)| d2 < *bd).unwrap_or(true) {
440 best = Some((
441 d2,
442 SnapTarget {
443 key: key.clone(),
444 point,
445 side: hg.side,
446 },
447 ));
448 }
449 }
450 best.map(|(_, target)| target)
451 }
452}
453
454pub(crate) fn anchor_from_geom(
458 handle: Option<&HandleGeom>,
459 geom: &NodeGeom,
460 kind: HandleKind,
461) -> (Point, Side, bool) {
462 if let Some(hg) = handle {
463 return (side_point(&geom.rect, hg.side, hg.offset), hg.side, true);
464 }
465 let side = match kind {
466 HandleKind::Source => geom.source_side,
467 HandleKind::Target => geom.target_side,
468 };
469 (side_point(&geom.rect, side, 0.5), side, false)
470}
471
472pub(crate) fn orient_connection(from: &HandleKey, to: &HandleKey) -> crate::types::Connection {
475 let (source, target) = match from.kind {
476 HandleKind::Source => (from, to),
477 HandleKind::Target => (to, from),
478 };
479 crate::types::Connection {
480 source: source.node.clone(),
481 target: target.node.clone(),
482 source_handle: (!source.id.is_empty()).then(|| source.id.clone()),
483 target_handle: (!target.id.is_empty()).then(|| target.id.clone()),
484 }
485}
486
487pub struct FlowApi<T: 'static> {
489 pub core: FlowCore,
490 pub nodes: Signal<Vec<crate::types::Node<T>>>,
491}
492
493impl<T> Clone for FlowApi<T> {
494 fn clone(&self) -> Self {
495 *self
496 }
497}
498impl<T> Copy for FlowApi<T> {}
499
500pub struct FlowHandle<T: 'static = ()> {
510 pub(crate) inner: Signal<Option<FlowApi<T>>>,
511}
512
513impl<T> Clone for FlowHandle<T> {
514 fn clone(&self) -> Self {
515 *self
516 }
517}
518impl<T> Copy for FlowHandle<T> {}
519
520impl<T> PartialEq for FlowHandle<T> {
521 fn eq(&self, _other: &Self) -> bool {
522 true
523 }
524}
525
526pub fn use_flow_handle<T: 'static>() -> FlowHandle<T> {
528 FlowHandle {
529 inner: use_signal(|| None),
530 }
531}
532
533impl<T: Clone + PartialEq + 'static> FlowHandle<T> {
534 fn api(&self) -> Option<FlowApi<T>> {
535 *self.inner.peek()
536 }
537
538 fn with_api<R>(&self, action: impl FnOnce(FlowApi<T>) -> R) -> Option<R> {
539 let api = self.api()?;
540 Some(
541 dioxus::core::Runtime::current()
542 .in_scope(api.core.viewport.origin_scope(), || action(api)),
543 )
544 }
545
546 pub fn core(&self) -> Option<FlowCore> {
548 self.api().map(|api| api.core)
549 }
550
551 pub fn viewport(&self) -> Option<Viewport> {
553 self.with_api(|api| *api.core.viewport.peek())
554 }
555
556 pub fn set_viewport(&self, viewport: Viewport, duration_ms: u64) {
557 self.with_api(|api| api.core.set_viewport(viewport, duration_ms));
558 }
559
560 pub fn fit_view(&self, duration_ms: u64) {
561 self.with_api(|api| api.core.fit_view(duration_ms));
562 }
563
564 pub fn zoom_in(&self, duration_ms: u64) {
565 self.with_api(|api| api.core.zoom_in(duration_ms));
566 }
567
568 pub fn zoom_out(&self, duration_ms: u64) {
569 self.with_api(|api| api.core.zoom_out(duration_ms));
570 }
571
572 pub fn client_to_flow(&self, client: Point) -> Option<Point> {
575 self.with_api(|api| api.core.client_to_flow(client))
576 }
577
578 pub fn delete_selected(&self) {
582 self.with_api(|api| crate::flow::delete_selected(api.nodes, api.core.edges));
583 }
584
585 pub fn auto_layout(&self, opts: &LayoutOptions) {
589 self.with_api(|api| api.auto_layout(opts));
590 }
591}
592
593impl<T: Clone + PartialEq + 'static> FlowApi<T> {
594 fn auto_layout(&self, opts: &LayoutOptions) {
595 let mut nodes = self.nodes;
596 let core = self.core;
597
598 let layout_nodes: Vec<LayoutNode> = nodes
599 .peek()
600 .iter()
601 .map(|node| LayoutNode {
602 id: node.id.clone(),
603 size: node.rect().size(),
604 })
605 .collect();
606 let edge_pairs: Vec<(Id, Id)> = core
607 .edges
608 .peek()
609 .iter()
610 .map(|edge| (edge.source.clone(), edge.target.clone()))
611 .collect();
612 let targets = compute_layout(&layout_nodes, &edge_pairs, opts);
613
614 if opts.update_handle_sides {
615 let (target_side, source_side) = opts.direction.handle_sides();
616 nodes.with_mut(|nodes| {
617 for node in nodes.iter_mut() {
618 node.target_side = target_side;
619 node.source_side = source_side;
620 }
621 });
622 }
623
624 let starts: HashMap<Id, Point> = nodes
625 .peek()
626 .iter()
627 .map(|node| (node.id.clone(), node.position))
628 .collect();
629
630 let mut bounds: Option<Rect> = None;
632 for layout_node in &layout_nodes {
633 if let Some(pos) = targets.get(&layout_node.id) {
634 let rect = Rect::from_points(*pos, layout_node.size);
635 bounds = Some(bounds.map(|b| b.union(&rect)).unwrap_or(rect));
636 }
637 }
638
639 tween(core.epoch, 420, move |t| {
640 nodes.with_mut(|nodes| {
641 for node in nodes.iter_mut() {
642 if let (Some(start), Some(end)) = (starts.get(&node.id), targets.get(&node.id))
643 {
644 node.position = start.lerp(*end, t);
645 }
646 }
647 });
648 });
649
650 if let Some(bounds) = bounds {
652 let padding = core.config.peek().fit_view_padding;
653 fit_bounds_without_cancel(core, bounds, padding);
654 }
655 }
656}
657
658fn fit_viewport(core: &FlowCore, bounds: Rect, padding: f64) -> Option<Viewport> {
662 let rect = *core.container.peek();
663 if rect.width <= 0.0 || rect.height <= 0.0 || (bounds.width <= 0.0 && bounds.height <= 0.0) {
664 return None;
665 }
666 let (mut left, mut right, mut top, mut bottom) = (0.0f64, 0.0f64, 0.0f64, 0.0f64);
667 for (side, thickness) in core.overlay_insets.peek().values() {
668 match side {
669 Side::Left => left = left.max(*thickness),
670 Side::Right => right = right.max(*thickness),
671 Side::Top => top = top.max(*thickness),
672 Side::Bottom => bottom = bottom.max(*thickness),
673 }
674 }
675 let cap_x = rect.width * 0.35;
676 let cap_y = rect.height * 0.35;
677 let (left, right) = (left.min(cap_x), right.min(cap_x));
678 let (top, bottom) = (top.min(cap_y), bottom.min(cap_y));
679 let free_w = rect.width - left - right;
680 let free_h = rect.height - top - bottom;
681
682 let config = *core.config.peek();
683 let zoom_x = free_w / bounds.width.max(1.0);
684 let zoom_y = free_h / bounds.height.max(1.0);
685 let zoom =
686 (zoom_x.min(zoom_y) * (1.0 - padding).max(0.05)).clamp(config.min_zoom, config.max_zoom);
687 let center = bounds.center();
688 Some(Viewport::new(
689 left + free_w / 2.0 - center.x * zoom,
690 top + free_h / 2.0 - center.y * zoom,
691 zoom,
692 ))
693}
694
695fn fit_bounds_without_cancel(core: FlowCore, bounds: Rect, padding: f64) {
698 let Some(target) = fit_viewport(&core, bounds, padding) else {
699 return;
700 };
701 let mut viewport = core.viewport;
702 let from = *viewport.peek();
703 let epoch = core.epoch;
704 let my_epoch = *epoch.peek();
705 spawn(async move {
706 let start = web_time::Instant::now();
707 loop {
708 crate::anim::sleep_ms(16).await;
709 if *epoch.peek() != my_epoch {
710 return;
711 }
712 let t = (start.elapsed().as_secs_f64() * 1000.0 / 420.0).min(1.0);
713 viewport.set(from.lerp(&target, crate::anim::ease_in_out_cubic(t)));
714 if t >= 1.0 {
715 return;
716 }
717 }
718 });
719}