Skip to main content

dioxus_flow/
state.rs

1//! Shared flow state: the non-generic core handed to every child component
2//! via context, plus the public [`FlowHandle`] for programmatic control from
3//! outside the flow.
4
5use 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/// Coarse interaction state. Per-frame details (drag offsets, last pointer
16/// position) live in the non-reactive [`DragState`] so pointer-move frames
17/// don't invalidate subscribers of this signal.
18#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
19pub enum Interaction {
20    #[default]
21    None,
22    /// Panning the canvas.
23    Pan,
24    /// Dragging one or more nodes.
25    DragNode,
26    /// Dragging a new connection out of a handle.
27    Connect,
28    /// The pane was pressed but panning is disabled; a release without
29    /// movement still counts as a pane click.
30    PanePressed,
31    /// A child claimed the pointer (e.g. edge click); no pan/drag behavior.
32    Pressed,
33}
34
35/// Per-gesture scratch state. Only ever accessed with `peek`/`write` from
36/// event handlers, so writes don't trigger renders.
37#[derive(Clone, Debug, Default)]
38pub struct DragState {
39    /// The pointer that owns the current gesture; other pointers' moves and
40    /// releases are ignored while it runs.
41    pub pointer_id: Option<i32>,
42    /// Where the press went down, in client coordinates: what a drag
43    /// threshold measures against.
44    pub origin_client: Point,
45    pub last_client: Point,
46    pub moved: bool,
47    /// A release without movement is normally a pane click; a gesture begun
48    /// with [`FlowCore::begin_pan`] can ask for it not to be.
49    pub suppress_click: bool,
50    /// Nodes being dragged: `(id, grab offset)` where
51    /// `position = cursor_flow - grab`.
52    pub grabs: Vec<(Id, Point)>,
53}
54
55/// A snap candidate for the in-progress connection.
56#[derive(Clone, PartialEq, Debug)]
57pub struct SnapTarget {
58    pub key: HandleKey,
59    pub point: Point,
60    pub side: Side,
61}
62
63/// The in-progress connection gesture.
64#[derive(Clone, PartialEq, Debug)]
65pub struct ConnectionState {
66    pub from: HandleKey,
67    pub cursor: Point,
68    pub snap: Option<SnapTarget>,
69}
70
71/// Static-ish configuration mirrored from `Flow` props.
72#[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    /// Scrolling pans instead of zooming; ctrl/meta (a trackpad pinch
79    /// included) zooms about the pointer. On by default, so a two-finger
80    /// trackpad drag pans. Takes precedence over `zoom_on_scroll`.
81    pub pan_on_scroll: bool,
82    pub nodes_draggable: bool,
83    /// How far (screen px) a press on a node must travel before it moves the
84    /// node. Zero moves on the first pixel; a few pixels keep sloppy clicks
85    /// from nudging nodes.
86    pub drag_threshold: f64,
87    /// Snap radius for completing connections, in screen pixels.
88    pub connection_radius: f64,
89    pub fit_view_padding: f64,
90}
91
92impl Default for FlowConfig {
93    fn default() -> Self {
94        Self {
95            // Low enough to overview a large graph, high enough that a
96            // stray zoom-out never strands the user on an unreadable speck.
97            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/// The non-generic heart of a flow, shared through context with every child
111/// (layers, handles, `Background`, `Controls`, `MiniMap`, and user
112/// components). All fields are `Copy` handles to reactive state.
113#[derive(Clone, Copy)]
114pub struct FlowCore {
115    /// Unique per-flow-instance id, used to namespace SVG defs.
116    pub iid: usize,
117    pub viewport: Signal<Viewport>,
118    /// Container rect in client (page) coordinates.
119    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    /// Geometry snapshot of all nodes, derived from the node list.
125    pub geoms: Memo<Vec<NodeGeom>>,
126    pub config: Signal<FlowConfig>,
127    pub(crate) drag: Signal<DragState>,
128    pub(crate) epoch: Signal<u64>,
129    /// Key of the current snap target — a narrow memo so handles don't
130    /// re-render on every connection cursor move.
131    pub(crate) snap_key: Memo<Option<HandleKey>>,
132    /// The handle a connection is being dragged from, if any.
133    pub(crate) connect_from: Memo<Option<HandleKey>>,
134    /// Type-erased "deselect all nodes", so non-generic components (edges,
135    /// pane) can clear node selection.
136    pub(crate) deselect_nodes: Callback<()>,
137    /// Screen-space bands reserved by overlay panels (minimap, controls…),
138    /// keyed per overlay instance: fit-view keeps the graph clear of them.
139    pub(crate) overlay_insets: Signal<HashMap<usize, (Side, f64)>>,
140    /// Measured node sizes awaiting a batched write into `nodes`. Only ever
141    /// peeked/written, never subscribed to: per-node resize events land here
142    /// so N nodes mounting costs one re-render wave instead of N (which made
143    /// mounting quadratic).
144    pub(crate) pending_sizes: Signal<Vec<(Id, crate::types::Size)>>,
145    /// Whether a size flush is already scheduled for this frame.
146    pub(crate) size_flush_queued: Signal<bool>,
147    /// Handle registrations/removals awaiting a batched write into `handles`
148    /// (`None` = remove). Same coalescing rationale as `pending_sizes`: every
149    /// handle registers in its own effect, and letting each registration
150    /// re-render the edge layer made mounting N connected nodes O(N²).
151    pub(crate) pending_handles: Signal<Vec<(HandleKey, Option<HandleGeom>)>>,
152    /// Whether a handle flush is already scheduled for this frame.
153    pub(crate) handle_flush_queued: Signal<bool>,
154    /// Fired when a connection drag leaves a handle (the gesture starting,
155    /// not completing). Stored here because the gesture starts inside
156    /// [`crate::Handle`], which only has the core.
157    pub(crate) on_connect_start: Option<EventHandler<HandleKey>>,
158    /// The application's say over which connections may complete: snap
159    /// targets that fail it are never offered, and a release on one adds
160    /// nothing.
161    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
170/// Access the surrounding flow's state from any component rendered inside a
171/// [`crate::Flow`] (custom nodes, edges, controls, overlays…).
172pub 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
178/// Reserve a screen-space band of `thickness` pixels along `side` of the
179/// container, from inside an overlay component (the built-in [`crate::Controls`]
180/// and [`crate::MiniMap`] do this). Fit-view centers the graph in the
181/// remaining area so nodes don't land underneath overlay panels.
182pub 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    /// Queue a handle registration (`Some(geom)`) or removal (`None`) for a
196    /// batched write into the `handles` registry at the end of the frame.
197    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        // Detached from the enqueuing handle's scope so its unmount can't
206        // strand `handle_flush_queued`; the flow itself may also unmount
207        // before this runs, so every access is fallible.
208        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    /// Claim the pointer for an application-level gesture that started on
253    /// content inside the canvas, so the pane neither pans nor reports a pane
254    /// click for this press. Call from a `pointerdown` handler (which runs
255    /// before the pane's, while the event bubbles); release the claim with
256    /// [`release_pointer`](Self::release_pointer) when the gesture ends —
257    /// though a `pointerup` reaching the pane releases it too.
258    ///
259    /// Returns `false` when some other gesture already owns the pointer.
260    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    /// Release a claim taken with [`claim_pointer`](Self::claim_pointer).
270    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    /// Begin a canvas pan from an application handler — e.g. a press on an
278    /// edge that selects it and then lets the canvas pan underneath. The
279    /// press was on content, so the release does not count as a pane click.
280    ///
281    /// `client` is the press position in client (page) coordinates.
282    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    /// Convert client (page) coordinates to flow coordinates.
305    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    /// Convert flow coordinates to client (page) coordinates.
311    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    /// Cancel any in-flight animation.
317    pub fn cancel_animations(&self) {
318        bump_epoch(self.epoch);
319    }
320
321    /// Bounding box of all nodes in flow coordinates, if any.
322    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    /// Animate (or jump, with `duration_ms == 0`) to the given viewport.
330    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    /// Zoom by `factor` keeping `anchor_client` (client coordinates, defaults
344    /// to the container center) stationary.
345    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    /// Fit the given flow-space bounds into the container.
365    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    /// Fit all nodes into view.
372    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    /// Center the given flow point in the container, keeping the zoom.
380    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    /// Resolve the anchor point and side of an edge endpoint on `geom`,
392    /// preferring a registered handle and falling back to the node's default
393    /// side for that kind. The final `bool` says whether a real handle was
394    /// found (so callers can offset the path to the handle's rim).
395    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    /// Resolve a registered handle key to its anchor point and side.
411    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    /// Find the closest compatible handle within the snap radius of `cursor`
421    /// (flow coordinates).
422    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            // A target the application would refuse is never offered: a snap
438            // that highlights and then does nothing on release is a lie.
439            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
461/// Anchor point/side for an edge endpoint: the handle's position when one is
462/// registered, otherwise the center of the node's default side for `kind`.
463/// The `bool` reports whether a real handle was found.
464pub(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
479/// Orient a completed connection gesture into a `source -> target`
480/// [`crate::Connection`], regardless of which end the drag started from.
481pub(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
494/// The typed API attached to a [`FlowHandle`] once the flow mounts.
495pub 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
507/// A handle for controlling a [`crate::Flow`] from the component that owns it.
508///
509/// ```ignore
510/// let flow = use_flow_handle();
511/// rsx! {
512///     button { onclick: move |_| flow.auto_layout(&LayoutOptions::default()), "Layout" }
513///     Flow { nodes, edges, handle: flow }
514/// }
515/// ```
516pub 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
533/// Create a [`FlowHandle`] to pass to a [`crate::Flow`]'s `handle` prop.
534pub 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    /// The flow's shared core, once mounted.
546    pub fn core(&self) -> Option<FlowCore> {
547        self.api().map(|api| api.core)
548    }
549
550    /// Current viewport (non-reactive read).
551    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    /// Convert client (page) coordinates to flow coordinates, e.g. for
580    /// placing a node at a click position.
581    pub fn client_to_flow(&self, client: Point) -> Option<Point> {
582        self.api().map(|api| api.core.client_to_flow(client))
583    }
584
585    /// Delete the selected nodes (with their edges) and selected edges — the
586    /// same cascade the Delete key performs by default. Call this from an
587    /// `on_delete` handler after confirming or snapshotting for undo.
588    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    /// Re-layout the graph with animated node movement, then fit it into
595    /// view. Handle sides follow the layout direction when
596    /// `opts.update_handle_sides` is set.
597    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        // Final bounds of the layout, for the parallel fit-view tween below.
635        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        // Fit the final layout into view, in parallel with the node tween.
655        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
662/// The viewport that fits `bounds` into the container, centered in the area
663/// left free by overlay insets (each side capped so overlays can never
664/// squeeze the fit area away entirely).
665fn 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
699/// Like `FlowCore::fit_bounds`, but rides the same epoch as a concurrently
700/// running tween instead of cancelling it.
701fn 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}