1use std::collections::HashMap;
6
7use dioxus::prelude::*;
8
9use crate::anim::{bump_epoch, tween};
10use crate::layout::{compute_layout, LayoutNode, LayoutOptions};
11use crate::types::{
12 side_point, Edge, HandleGeom, HandleKey, HandleKind, Id, NodeGeom, Point, Rect, Side, Viewport,
13};
14
15#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
19pub enum Interaction {
20 #[default]
21 None,
22 Pan,
24 DragNode,
26 Connect,
28 PanePressed,
31 Pressed,
33}
34
35#[derive(Clone, Debug, Default)]
38pub struct DragState {
39 pub pointer_id: Option<i32>,
42 pub origin_client: Point,
45 pub last_client: Point,
46 pub moved: bool,
47 pub suppress_click: bool,
50 pub grabs: Vec<(Id, Point)>,
53}
54
55#[derive(Clone, PartialEq, Debug)]
57pub struct SnapTarget {
58 pub key: HandleKey,
59 pub point: Point,
60 pub side: Side,
61}
62
63#[derive(Clone, PartialEq, Debug)]
65pub struct ConnectionState {
66 pub from: HandleKey,
67 pub cursor: Point,
68 pub snap: Option<SnapTarget>,
69}
70
71#[derive(Clone, Copy, PartialEq, Debug)]
73pub struct FlowConfig {
74 pub min_zoom: f64,
75 pub max_zoom: f64,
76 pub pan_on_drag: bool,
77 pub zoom_on_scroll: bool,
78 pub pan_on_scroll: bool,
82 pub nodes_draggable: bool,
83 pub drag_threshold: f64,
87 pub connection_radius: f64,
89 pub fit_view_padding: f64,
90}
91
92impl Default for FlowConfig {
93 fn default() -> Self {
94 Self {
95 min_zoom: 0.25,
98 max_zoom: 4.0,
99 pan_on_drag: true,
100 zoom_on_scroll: true,
101 pan_on_scroll: true,
102 nodes_draggable: true,
103 drag_threshold: 0.0,
104 connection_radius: 28.0,
105 fit_view_padding: 0.12,
106 }
107 }
108}
109
110#[derive(Clone, Copy)]
114pub struct FlowCore {
115 pub iid: usize,
117 pub viewport: Signal<Viewport>,
118 pub container: Signal<Rect>,
120 pub interaction: Signal<Interaction>,
121 pub connection: Signal<Option<ConnectionState>>,
122 pub handles: Signal<HashMap<HandleKey, HandleGeom>>,
123 pub edges: Signal<Vec<Edge>>,
124 pub geoms: Memo<Vec<NodeGeom>>,
126 pub config: Signal<FlowConfig>,
127 pub(crate) drag: Signal<DragState>,
128 pub(crate) epoch: Signal<u64>,
129 pub(crate) snap_key: Memo<Option<HandleKey>>,
132 pub(crate) connect_from: Memo<Option<HandleKey>>,
134 pub(crate) deselect_nodes: Callback<()>,
137 pub(crate) overlay_insets: Signal<HashMap<usize, (Side, f64)>>,
140 pub(crate) pending_sizes: Signal<Vec<(Id, crate::types::Size)>>,
145 pub(crate) size_flush_queued: Signal<bool>,
147 pub(crate) pending_handles: Signal<Vec<(HandleKey, Option<HandleGeom>)>>,
152 pub(crate) handle_flush_queued: Signal<bool>,
154 pub(crate) on_connect_start: Option<EventHandler<HandleKey>>,
158 pub(crate) valid_connection: Option<Callback<crate::types::Connection, bool>>,
162}
163
164impl PartialEq for FlowCore {
165 fn eq(&self, other: &Self) -> bool {
166 self.iid == other.iid
167 }
168}
169
170pub fn use_flow() -> FlowCore {
173 use_context::<FlowCore>()
174}
175
176static NEXT_OVERLAY_KEY: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
177
178pub fn use_overlay_inset(side: Side, thickness: f64) {
183 let core = use_context::<FlowCore>();
184 let key = use_hook(|| NEXT_OVERLAY_KEY.fetch_add(1, std::sync::atomic::Ordering::Relaxed));
185 let mut insets = core.overlay_insets;
186 if insets.peek().get(&key) != Some(&(side, thickness)) {
187 insets.write().insert(key, (side, thickness));
188 }
189 use_drop(move || {
190 core.overlay_insets.clone().write().remove(&key);
191 });
192}
193
194impl FlowCore {
195 pub(crate) fn queue_handle_write(&self, key: HandleKey, geom: Option<HandleGeom>) {
198 self.pending_handles.clone().write().push((key, geom));
199 let mut queued = self.handle_flush_queued;
200 if *queued.peek() {
201 return;
202 }
203 queued.set(true);
204 let core = *self;
205 dioxus::core::spawn_forever(async move {
209 crate::anim::sleep_ms(0).await;
210 let mut queued_signal = core.handle_flush_queued;
211 let Ok(mut queued) = queued_signal.try_write() else {
212 return;
213 };
214 *queued = false;
215 drop(queued);
216 let mut pending_signal = core.pending_handles;
217 let Ok(mut pending_ref) = pending_signal.try_write() else {
218 return;
219 };
220 let pending = std::mem::take(&mut *pending_ref);
221 drop(pending_ref);
222 if pending.is_empty() {
223 return;
224 }
225 let mut handles = core.handles;
226 let changed = match handles.try_peek() {
227 Ok(current) => pending.iter().any(|(key, geom)| match geom {
228 Some(geom) => current.get(key) != Some(geom),
229 None => current.contains_key(key),
230 }),
231 Err(_) => return,
232 };
233 if !changed {
234 return;
235 }
236 let Ok(mut current) = handles.try_write() else {
237 return;
238 };
239 for (key, geom) in pending {
240 match geom {
241 Some(geom) => {
242 current.insert(key, geom);
243 }
244 None => {
245 current.remove(&key);
246 }
247 }
248 }
249 });
250 }
251
252 pub fn claim_pointer(&self) -> bool {
261 let mut interaction = self.interaction;
262 if *interaction.peek() != Interaction::None {
263 return false;
264 }
265 interaction.set(Interaction::Pressed);
266 true
267 }
268
269 pub fn release_pointer(&self) {
271 let mut interaction = self.interaction;
272 if *interaction.peek() == Interaction::Pressed {
273 interaction.set(Interaction::None);
274 }
275 }
276
277 pub fn begin_pan(&self, pointer_id: i32, client: Point) -> bool {
283 let mut interaction = self.interaction;
284 if *interaction.peek() != Interaction::None {
285 return false;
286 }
287 self.cancel_animations();
288 {
289 let mut drag = self.drag;
290 let mut state = drag.write();
291 *state = DragState {
292 pointer_id: Some(pointer_id),
293 origin_client: client,
294 last_client: client,
295 moved: false,
296 suppress_click: true,
297 grabs: Vec::new(),
298 };
299 }
300 interaction.set(Interaction::Pan);
301 true
302 }
303
304 pub fn client_to_flow(&self, client: Point) -> Point {
306 let rect = *self.container.peek();
307 self.viewport.peek().screen_to_flow(client - rect.origin())
308 }
309
310 pub fn flow_to_client(&self, flow: Point) -> Point {
312 let rect = *self.container.peek();
313 self.viewport.peek().flow_to_screen(flow) + rect.origin()
314 }
315
316 pub fn cancel_animations(&self) {
318 bump_epoch(self.epoch);
319 }
320
321 pub fn nodes_bounds(&self) -> Option<Rect> {
323 let geoms = self.geoms.peek();
324 let mut iter = geoms.iter();
325 let first = iter.next()?.rect;
326 Some(iter.fold(first, |acc, geom| acc.union(&geom.rect)))
327 }
328
329 pub fn set_viewport(&self, target: Viewport, duration_ms: u64) {
331 let mut viewport = self.viewport;
332 if duration_ms == 0 {
333 self.cancel_animations();
334 viewport.set(target);
335 return;
336 }
337 let from = *viewport.peek();
338 tween(self.epoch, duration_ms, move |t| {
339 viewport.set(from.lerp(&target, t));
340 });
341 }
342
343 pub fn zoom_by(&self, factor: f64, anchor_client: Option<Point>, duration_ms: u64) {
346 let config = *self.config.peek();
347 let rect = *self.container.peek();
348 let vp = *self.viewport.peek();
349 let anchor = anchor_client
350 .map(|c| c - rect.origin())
351 .unwrap_or_else(|| Point::new(rect.width / 2.0, rect.height / 2.0));
352 let target = vp.zoom_about(vp.zoom * factor, anchor, config.min_zoom, config.max_zoom);
353 self.set_viewport(target, duration_ms);
354 }
355
356 pub fn zoom_in(&self, duration_ms: u64) {
357 self.zoom_by(1.25, None, duration_ms);
358 }
359
360 pub fn zoom_out(&self, duration_ms: u64) {
361 self.zoom_by(0.8, None, duration_ms);
362 }
363
364 pub fn fit_bounds(&self, bounds: Rect, padding: f64, duration_ms: u64) {
366 if let Some(target) = fit_viewport(self, bounds, padding) {
367 self.set_viewport(target, duration_ms);
368 }
369 }
370
371 pub fn fit_view(&self, duration_ms: u64) {
373 let padding = self.config.peek().fit_view_padding;
374 if let Some(bounds) = self.nodes_bounds() {
375 self.fit_bounds(bounds, padding, duration_ms);
376 }
377 }
378
379 pub fn center_on(&self, flow: Point, duration_ms: u64) {
381 let rect = *self.container.peek();
382 let zoom = self.viewport.peek().zoom;
383 let target = Viewport::new(
384 rect.width / 2.0 - flow.x * zoom,
385 rect.height / 2.0 - flow.y * zoom,
386 zoom,
387 );
388 self.set_viewport(target, duration_ms);
389 }
390
391 pub(crate) fn resolve_anchor(
396 &self,
397 handles: &HashMap<HandleKey, HandleGeom>,
398 geom: &NodeGeom,
399 kind: HandleKind,
400 handle_id: &Option<Id>,
401 ) -> (Point, Side, bool) {
402 let key = HandleKey {
403 node: geom.id.clone(),
404 kind,
405 id: handle_id.clone().unwrap_or_default(),
406 };
407 anchor_from_geom(handles.get(&key), geom, kind)
408 }
409
410 pub(crate) fn anchor_of(&self, key: &HandleKey) -> Option<(Point, Side)> {
412 let handles = self.handles.peek();
413 let geoms = self.geoms.peek();
414 let geom = geoms.iter().find(|geom| geom.id == key.node)?;
415 let id = (!key.id.is_empty()).then(|| key.id.clone());
416 let (point, side, _) = self.resolve_anchor(&handles, geom, key.kind, &id);
417 Some((point, side))
418 }
419
420 pub(crate) fn find_snap(&self, from: &HandleKey, cursor: Point) -> Option<SnapTarget> {
423 let radius = self.config.peek().connection_radius / self.viewport.peek().zoom.max(1e-6);
424 let handles = self.handles.peek();
425 let geoms = self.geoms.peek();
426 let geom_by_id: HashMap<&str, &NodeGeom> =
427 geoms.iter().map(|geom| (geom.id.as_str(), geom)).collect();
428
429 let mut best: Option<(f64, SnapTarget)> = None;
430 for (key, hg) in handles.iter() {
431 if key.kind == from.kind || key.node == from.node {
432 continue;
433 }
434 let Some(geom) = geom_by_id.get(key.node.as_str()) else {
435 continue;
436 };
437 if let Some(valid) = &self.valid_connection {
440 if !valid.call(orient_connection(from, key)) {
441 continue;
442 }
443 }
444 let point = side_point(&geom.rect, hg.side, hg.offset);
445 let d2 = point.distance_sq(cursor);
446 if d2 <= radius * radius && best.as_ref().map(|(bd, _)| d2 < *bd).unwrap_or(true) {
447 best = Some((
448 d2,
449 SnapTarget {
450 key: key.clone(),
451 point,
452 side: hg.side,
453 },
454 ));
455 }
456 }
457 best.map(|(_, target)| target)
458 }
459}
460
461pub(crate) fn anchor_from_geom(
465 handle: Option<&HandleGeom>,
466 geom: &NodeGeom,
467 kind: HandleKind,
468) -> (Point, Side, bool) {
469 if let Some(hg) = handle {
470 return (side_point(&geom.rect, hg.side, hg.offset), hg.side, true);
471 }
472 let side = match kind {
473 HandleKind::Source => geom.source_side,
474 HandleKind::Target => geom.target_side,
475 };
476 (side_point(&geom.rect, side, 0.5), side, false)
477}
478
479pub(crate) fn orient_connection(from: &HandleKey, to: &HandleKey) -> crate::types::Connection {
482 let (source, target) = match from.kind {
483 HandleKind::Source => (from, to),
484 HandleKind::Target => (to, from),
485 };
486 crate::types::Connection {
487 source: source.node.clone(),
488 target: target.node.clone(),
489 source_handle: (!source.id.is_empty()).then(|| source.id.clone()),
490 target_handle: (!target.id.is_empty()).then(|| target.id.clone()),
491 }
492}
493
494pub struct FlowApi<T: 'static> {
496 pub core: FlowCore,
497 pub nodes: Signal<Vec<crate::types::Node<T>>>,
498}
499
500impl<T> Clone for FlowApi<T> {
501 fn clone(&self) -> Self {
502 *self
503 }
504}
505impl<T> Copy for FlowApi<T> {}
506
507pub struct FlowHandle<T: 'static = ()> {
517 pub(crate) inner: Signal<Option<FlowApi<T>>>,
518}
519
520impl<T> Clone for FlowHandle<T> {
521 fn clone(&self) -> Self {
522 *self
523 }
524}
525impl<T> Copy for FlowHandle<T> {}
526
527impl<T> PartialEq for FlowHandle<T> {
528 fn eq(&self, _other: &Self) -> bool {
529 true
530 }
531}
532
533pub fn use_flow_handle<T: 'static>() -> FlowHandle<T> {
535 FlowHandle {
536 inner: use_signal(|| None),
537 }
538}
539
540impl<T: Clone + PartialEq + 'static> FlowHandle<T> {
541 fn api(&self) -> Option<FlowApi<T>> {
542 *self.inner.peek()
543 }
544
545 pub fn core(&self) -> Option<FlowCore> {
547 self.api().map(|api| api.core)
548 }
549
550 pub fn viewport(&self) -> Option<Viewport> {
552 self.api().map(|api| *api.core.viewport.peek())
553 }
554
555 pub fn set_viewport(&self, viewport: Viewport, duration_ms: u64) {
556 if let Some(api) = self.api() {
557 api.core.set_viewport(viewport, duration_ms);
558 }
559 }
560
561 pub fn fit_view(&self, duration_ms: u64) {
562 if let Some(api) = self.api() {
563 api.core.fit_view(duration_ms);
564 }
565 }
566
567 pub fn zoom_in(&self, duration_ms: u64) {
568 if let Some(api) = self.api() {
569 api.core.zoom_in(duration_ms);
570 }
571 }
572
573 pub fn zoom_out(&self, duration_ms: u64) {
574 if let Some(api) = self.api() {
575 api.core.zoom_out(duration_ms);
576 }
577 }
578
579 pub fn client_to_flow(&self, client: Point) -> Option<Point> {
582 self.api().map(|api| api.core.client_to_flow(client))
583 }
584
585 pub fn delete_selected(&self) {
589 if let Some(api) = self.api() {
590 crate::flow::delete_selected(api.nodes, api.core.edges);
591 }
592 }
593
594 pub fn auto_layout(&self, opts: &LayoutOptions) {
598 let Some(api) = self.api() else { return };
599 let mut nodes = api.nodes;
600 let core = api.core;
601
602 let layout_nodes: Vec<LayoutNode> = nodes
603 .peek()
604 .iter()
605 .map(|node| LayoutNode {
606 id: node.id.clone(),
607 size: node.rect().size(),
608 })
609 .collect();
610 let edge_pairs: Vec<(Id, Id)> = core
611 .edges
612 .peek()
613 .iter()
614 .map(|edge| (edge.source.clone(), edge.target.clone()))
615 .collect();
616 let targets = compute_layout(&layout_nodes, &edge_pairs, opts);
617
618 if opts.update_handle_sides {
619 let (target_side, source_side) = opts.direction.handle_sides();
620 nodes.with_mut(|nodes| {
621 for node in nodes.iter_mut() {
622 node.target_side = target_side;
623 node.source_side = source_side;
624 }
625 });
626 }
627
628 let starts: HashMap<Id, Point> = nodes
629 .peek()
630 .iter()
631 .map(|node| (node.id.clone(), node.position))
632 .collect();
633
634 let mut bounds: Option<Rect> = None;
636 for layout_node in &layout_nodes {
637 if let Some(pos) = targets.get(&layout_node.id) {
638 let rect = Rect::from_points(*pos, layout_node.size);
639 bounds = Some(bounds.map(|b| b.union(&rect)).unwrap_or(rect));
640 }
641 }
642
643 tween(core.epoch, 420, move |t| {
644 nodes.with_mut(|nodes| {
645 for node in nodes.iter_mut() {
646 if let (Some(start), Some(end)) = (starts.get(&node.id), targets.get(&node.id))
647 {
648 node.position = start.lerp(*end, t);
649 }
650 }
651 });
652 });
653
654 if let Some(bounds) = bounds {
656 let padding = core.config.peek().fit_view_padding;
657 fit_bounds_without_cancel(core, bounds, padding);
658 }
659 }
660}
661
662fn fit_viewport(core: &FlowCore, bounds: Rect, padding: f64) -> Option<Viewport> {
666 let rect = *core.container.peek();
667 if rect.width <= 0.0 || rect.height <= 0.0 || (bounds.width <= 0.0 && bounds.height <= 0.0) {
668 return None;
669 }
670 let (mut left, mut right, mut top, mut bottom) = (0.0f64, 0.0f64, 0.0f64, 0.0f64);
671 for (side, thickness) in core.overlay_insets.peek().values() {
672 match side {
673 Side::Left => left = left.max(*thickness),
674 Side::Right => right = right.max(*thickness),
675 Side::Top => top = top.max(*thickness),
676 Side::Bottom => bottom = bottom.max(*thickness),
677 }
678 }
679 let cap_x = rect.width * 0.35;
680 let cap_y = rect.height * 0.35;
681 let (left, right) = (left.min(cap_x), right.min(cap_x));
682 let (top, bottom) = (top.min(cap_y), bottom.min(cap_y));
683 let free_w = rect.width - left - right;
684 let free_h = rect.height - top - bottom;
685
686 let config = *core.config.peek();
687 let zoom_x = free_w / bounds.width.max(1.0);
688 let zoom_y = free_h / bounds.height.max(1.0);
689 let zoom =
690 (zoom_x.min(zoom_y) * (1.0 - padding).max(0.05)).clamp(config.min_zoom, config.max_zoom);
691 let center = bounds.center();
692 Some(Viewport::new(
693 left + free_w / 2.0 - center.x * zoom,
694 top + free_h / 2.0 - center.y * zoom,
695 zoom,
696 ))
697}
698
699fn fit_bounds_without_cancel(core: FlowCore, bounds: Rect, padding: f64) {
702 let Some(target) = fit_viewport(&core, bounds, padding) else {
703 return;
704 };
705 let mut viewport = core.viewport;
706 let from = *viewport.peek();
707 let epoch = core.epoch;
708 let my_epoch = *epoch.peek();
709 spawn(async move {
710 let start = web_time::Instant::now();
711 loop {
712 crate::anim::sleep_ms(16).await;
713 if *epoch.peek() != my_epoch {
714 return;
715 }
716 let t = (start.elapsed().as_secs_f64() * 1000.0 / 420.0).min(1.0);
717 viewport.set(from.lerp(&target, crate::anim::ease_in_out_cubic(t)));
718 if t >= 1.0 {
719 return;
720 }
721 }
722 });
723}