Skip to main content

dioxus_flow/
types.rs

1//! Core geometry and graph types.
2
3use std::ops::{Add, Div, Mul, Sub};
4
5/// Identifier for nodes, edges and handles.
6pub type Id = String;
7
8/// A point (or vector) in 2D space. Flow coordinates unless stated otherwise.
9#[derive(Clone, Copy, PartialEq, Debug, Default)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct Point {
12    pub x: f64,
13    pub y: f64,
14}
15
16impl Point {
17    pub const ZERO: Point = Point { x: 0.0, y: 0.0 };
18
19    pub const fn new(x: f64, y: f64) -> Self {
20        Self { x, y }
21    }
22
23    pub fn distance_sq(&self, other: Point) -> f64 {
24        let d = *self - other;
25        d.x * d.x + d.y * d.y
26    }
27
28    pub fn distance(&self, other: Point) -> f64 {
29        self.distance_sq(other).sqrt()
30    }
31
32    pub fn lerp(&self, other: Point, t: f64) -> Point {
33        Point::new(
34            self.x + (other.x - self.x) * t,
35            self.y + (other.y - self.y) * t,
36        )
37    }
38}
39
40impl From<(f64, f64)> for Point {
41    fn from((x, y): (f64, f64)) -> Self {
42        Point::new(x, y)
43    }
44}
45
46impl Add for Point {
47    type Output = Point;
48    fn add(self, rhs: Point) -> Point {
49        Point::new(self.x + rhs.x, self.y + rhs.y)
50    }
51}
52
53impl Sub for Point {
54    type Output = Point;
55    fn sub(self, rhs: Point) -> Point {
56        Point::new(self.x - rhs.x, self.y - rhs.y)
57    }
58}
59
60impl Mul<f64> for Point {
61    type Output = Point;
62    fn mul(self, rhs: f64) -> Point {
63        Point::new(self.x * rhs, self.y * rhs)
64    }
65}
66
67impl Div<f64> for Point {
68    type Output = Point;
69    fn div(self, rhs: f64) -> Point {
70        Point::new(self.x / rhs, self.y / rhs)
71    }
72}
73
74/// A width/height pair.
75#[derive(Clone, Copy, PartialEq, Debug, Default)]
76pub struct Size {
77    pub width: f64,
78    pub height: f64,
79}
80
81impl Size {
82    pub fn new(width: f64, height: f64) -> Self {
83        Self { width, height }
84    }
85}
86
87impl From<(f64, f64)> for Size {
88    fn from((width, height): (f64, f64)) -> Self {
89        Size::new(width, height)
90    }
91}
92
93/// An axis-aligned rectangle.
94#[derive(Clone, Copy, PartialEq, Debug, Default)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96pub struct Rect {
97    pub x: f64,
98    pub y: f64,
99    pub width: f64,
100    pub height: f64,
101}
102
103impl Rect {
104    pub const ZERO: Rect = Rect {
105        x: 0.0,
106        y: 0.0,
107        width: 0.0,
108        height: 0.0,
109    };
110
111    pub const fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
112        Self {
113            x,
114            y,
115            width,
116            height,
117        }
118    }
119
120    pub fn from_points(origin: Point, size: Size) -> Self {
121        Self::new(origin.x, origin.y, size.width, size.height)
122    }
123
124    /// The rectangle two corners describe, in any order — a marquee, a
125    /// selection bounds, a drag rectangle.
126    pub fn between(a: Point, b: Point) -> Self {
127        Self {
128            x: a.x.min(b.x),
129            y: a.y.min(b.y),
130            width: (a.x - b.x).abs(),
131            height: (a.y - b.y).abs(),
132        }
133    }
134
135    pub fn origin(&self) -> Point {
136        Point::new(self.x, self.y)
137    }
138
139    pub fn size(&self) -> Size {
140        Size::new(self.width, self.height)
141    }
142
143    pub fn center(&self) -> Point {
144        Point::new(self.x + self.width / 2.0, self.y + self.height / 2.0)
145    }
146
147    pub fn max_x(&self) -> f64 {
148        self.x + self.width
149    }
150
151    pub fn max_y(&self) -> f64 {
152        self.y + self.height
153    }
154
155    pub fn union(&self, other: &Rect) -> Rect {
156        let x = self.x.min(other.x);
157        let y = self.y.min(other.y);
158        Rect::new(
159            x,
160            y,
161            self.max_x().max(other.max_x()) - x,
162            self.max_y().max(other.max_y()) - y,
163        )
164    }
165
166    pub fn contains(&self, p: Point) -> bool {
167        p.x >= self.x && p.x <= self.max_x() && p.y >= self.y && p.y <= self.max_y()
168    }
169
170    /// Whether the rectangles touch or overlap (touching counts).
171    pub fn intersects(&self, other: Rect) -> bool {
172        self.x <= other.max_x()
173            && self.max_x() >= other.x
174            && self.y <= other.max_y()
175            && self.max_y() >= other.y
176    }
177
178    /// The same rectangle with `by` of room added on every side.
179    pub fn expanded(&self, by: f64) -> Rect {
180        Rect::new(
181            self.x - by,
182            self.y - by,
183            self.width + by * 2.0,
184            self.height + by * 2.0,
185        )
186    }
187
188    /// How far a point is from this rectangle's outline: negative inside,
189    /// positive outside, zero exactly on it.
190    ///
191    /// A band around the outline is then one comparison rather than four, and
192    /// it can straddle the line — which is what a border a person can grab has
193    /// to do, because the line they are aiming at has no thickness.
194    pub fn distance_to_edge(&self, point: Point) -> f64 {
195        let dx = (self.x - point.x).max(point.x - self.max_x());
196        let dy = (self.y - point.y).max(point.y - self.max_y());
197        if dx > 0.0 || dy > 0.0 {
198            dx.max(0.0).hypot(dy.max(0.0))
199        } else {
200            dx.max(dy)
201        }
202    }
203
204    /// The rectangle covering all of `rects`, or `None` when there are none.
205    pub fn bounds(rects: impl IntoIterator<Item = Self>) -> Option<Self> {
206        let mut min_x = f64::INFINITY;
207        let mut min_y = f64::INFINITY;
208        let mut max_x = f64::NEG_INFINITY;
209        let mut max_y = f64::NEG_INFINITY;
210        for rect in rects {
211            min_x = min_x.min(rect.x);
212            min_y = min_y.min(rect.y);
213            max_x = max_x.max(rect.max_x());
214            max_y = max_y.max(rect.max_y());
215        }
216        min_x
217            .is_finite()
218            .then_some(Self::new(min_x, min_y, max_x - min_x, max_y - min_y))
219    }
220}
221
222/// The lattice positions come to rest on. Snapping is the plane's business —
223/// the background draws this grid, and a release lands on it — while what may
224/// occupy a cell is the application's.
225#[derive(Clone, Copy, Debug, PartialEq)]
226pub struct Grid {
227    size: f64,
228}
229
230impl Grid {
231    /// A grid of `size` units. A size that is not a positive number would make
232    /// every snap meaningless, so it falls back to one unit.
233    pub const fn new(size: f64) -> Self {
234        Self {
235            size: if size.is_finite() && size > 0.0 {
236                size
237            } else {
238                1.0
239            },
240        }
241    }
242
243    pub const fn size(self) -> f64 {
244        self.size
245    }
246
247    pub fn snap(self, value: f64) -> f64 {
248        (value / self.size).round() * self.size
249    }
250
251    /// Snapped away from zero, for a measurement that must not be cut short —
252    /// a node grown to fit its text, say.
253    pub fn snap_up(self, value: f64) -> f64 {
254        (value / self.size).ceil() * self.size
255    }
256
257    pub fn snap_point(self, point: Point) -> Point {
258        Point::new(self.snap(point.x), self.snap(point.y))
259    }
260
261    pub fn snap_rect(self, rect: Rect) -> Rect {
262        Rect::new(
263            self.snap(rect.x),
264            self.snap(rect.y),
265            self.snap(rect.width),
266            self.snap(rect.height),
267        )
268    }
269}
270
271/// The pan/zoom state of the flow canvas.
272///
273/// `x`/`y` are the screen point (relative to the container) that the flow
274/// origin is drawn at, so a flow-space point `p` appears on screen at
275/// `p * zoom + offset()`. The same shape — and, with the `serde` feature, the
276/// same serialization — as react-flow's viewport.
277#[derive(Clone, Copy, PartialEq, Debug)]
278#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
279pub struct Viewport {
280    /// Horizontal translation, in screen pixels.
281    pub x: f64,
282    /// Vertical translation, in screen pixels.
283    pub y: f64,
284    /// Zoom factor.
285    pub zoom: f64,
286}
287
288impl Default for Viewport {
289    fn default() -> Self {
290        Self {
291            x: 0.0,
292            y: 0.0,
293            zoom: 1.0,
294        }
295    }
296}
297
298impl Viewport {
299    pub const fn new(x: f64, y: f64, zoom: f64) -> Self {
300        Self { x, y, zoom }
301    }
302
303    /// The translation as a point.
304    pub const fn offset(&self) -> Point {
305        Point::new(self.x, self.y)
306    }
307
308    /// The same viewport translated to `offset`.
309    pub const fn with_offset(self, offset: Point) -> Self {
310        Self {
311            x: offset.x,
312            y: offset.y,
313            zoom: self.zoom,
314        }
315    }
316
317    /// Convert a point relative to the container (screen pixels) to flow space.
318    pub fn screen_to_flow(&self, p: Point) -> Point {
319        (p - self.offset()) / self.zoom
320    }
321
322    /// Convert a flow-space point to screen pixels relative to the container.
323    pub fn flow_to_screen(&self, p: Point) -> Point {
324        p * self.zoom + self.offset()
325    }
326
327    /// Zooms about a screen point, so whatever is under it stays under it.
328    ///
329    /// A zoom that is not a number is refused rather than clamped — `f64::clamp`
330    /// keeps a NaN, and a NaN here would spread to every coordinate drawn from
331    /// this viewport, with nothing left on screen to say why.
332    pub fn zoom_about(self, zoom: f64, screen: Point, min_zoom: f64, max_zoom: f64) -> Self {
333        if !zoom.is_finite() {
334            return self;
335        }
336        let zoom = zoom.clamp(min_zoom, max_zoom);
337        let flow = self.screen_to_flow(screen);
338        Self {
339            x: screen.x - flow.x * zoom,
340            y: screen.y - flow.y * zoom,
341            zoom,
342        }
343    }
344
345    /// Pans by a screen-space delta.
346    pub fn panned(self, by: Point) -> Self {
347        Self {
348            x: self.x + by.x,
349            y: self.y + by.y,
350            ..self
351        }
352    }
353
354    /// Whether this could have come from an editor rather than from a corrupt
355    /// or hand-edited file: every coordinate a number, and the zoom within the
356    /// given limits.
357    pub fn is_sane(self, min_zoom: f64, max_zoom: f64) -> bool {
358        self.x.is_finite() && self.y.is_finite() && (min_zoom..=max_zoom).contains(&self.zoom)
359    }
360
361    /// Frames `drawing` inside `safe` — the part of the screen that is actually
362    /// clear, which is not the whole of it when panels float over the edges. An
363    /// empty drawing centres the origin instead, so a fresh canvas opens with
364    /// room on every side. The zoom is clamped to `[min_zoom, max_zoom]`; pass
365    /// a `max_zoom` below the interactive limit so fitting a single small node
366    /// never magnifies it to fill the screen.
367    pub fn fit(drawing: Option<Rect>, safe: Rect, min_zoom: f64, max_zoom: f64) -> Self {
368        let Some(drawing) = drawing else {
369            return Self {
370                x: safe.x + safe.width / 2.0,
371                y: safe.y + safe.height / 2.0,
372                zoom: 1.0,
373            };
374        };
375        let width = drawing.width.max(1.0);
376        let height = drawing.height.max(1.0);
377        let wanted = (safe.width / width).min(safe.height / height);
378        let zoom = if wanted.is_finite() {
379            wanted.clamp(min_zoom, max_zoom)
380        } else {
381            1.0
382        };
383        Self {
384            x: safe.x + (safe.width - width * zoom) / 2.0 - drawing.x * zoom,
385            y: safe.y + (safe.height - height * zoom) / 2.0 - drawing.y * zoom,
386            zoom,
387        }
388    }
389
390    pub fn lerp(&self, other: &Viewport, t: f64) -> Viewport {
391        Viewport {
392            x: self.x + (other.x - self.x) * t,
393            y: self.y + (other.y - self.y) * t,
394            zoom: self.zoom + (other.zoom - self.zoom) * t,
395        }
396    }
397}
398
399/// A side of a node, used for handle placement, seat naming and edge routing.
400///
401/// For port seats: `Top` and `Bottom` count their cells from the left corner,
402/// `Left` and `Right` from the top corner. That common origin is what makes a
403/// seat survive a resize — growing a node rightwards or downwards moves only
404/// the edge being dragged, so every seat that is not on it keeps its place.
405#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Debug)]
406#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
407#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
408pub enum Side {
409    Top,
410    Right,
411    Bottom,
412    Left,
413}
414
415impl Side {
416    /// Outward unit vector of this side.
417    pub fn normal(&self) -> Point {
418        match self {
419            Side::Top => Point::new(0.0, -1.0),
420            Side::Right => Point::new(1.0, 0.0),
421            Side::Bottom => Point::new(0.0, 1.0),
422            Side::Left => Point::new(-1.0, 0.0),
423        }
424    }
425
426    pub fn opposite(&self) -> Side {
427        match self {
428            Side::Top => Side::Bottom,
429            Side::Right => Side::Left,
430            Side::Bottom => Side::Top,
431            Side::Left => Side::Right,
432        }
433    }
434
435    pub fn is_horizontal(&self) -> bool {
436        matches!(self, Side::Left | Side::Right)
437    }
438
439    pub(crate) fn class_name(&self) -> &'static str {
440        match self {
441            Side::Top => "top",
442            Side::Right => "right",
443            Side::Bottom => "bottom",
444            Side::Left => "left",
445        }
446    }
447}
448
449/// The point on the boundary of `rect` at fraction `frac` (0..=1) along `side`.
450pub fn side_point(rect: &Rect, side: Side, frac: f64) -> Point {
451    match side {
452        Side::Top => Point::new(rect.x + rect.width * frac, rect.y),
453        Side::Bottom => Point::new(rect.x + rect.width * frac, rect.max_y()),
454        Side::Left => Point::new(rect.x, rect.y + rect.height * frac),
455        Side::Right => Point::new(rect.max_x(), rect.y + rect.height * frac),
456    }
457}
458
459/// Fallback node size used before a node has been measured.
460pub(crate) const DEFAULT_NODE_SIZE: Size = Size {
461    width: 150.0,
462    height: 40.0,
463};
464
465/// A node in the flow graph.
466///
467/// `T` is a user-defined payload available to custom node renderers.
468#[derive(Clone, PartialEq, Debug)]
469pub struct Node<T = ()> {
470    pub id: Id,
471    /// Position of the node's top-left corner, in flow coordinates.
472    pub position: Point,
473    /// Label rendered by the default node view.
474    pub label: String,
475    /// Custom payload for custom node views.
476    pub data: T,
477    /// Node type tag. The default view recognizes `"input"` (source handle
478    /// only) and `"output"` (target handle only); custom views can match on
479    /// any value.
480    pub node_type: Option<String>,
481    /// Side where outgoing edges leave (used by the default view's source
482    /// handle and as the anchor fallback).
483    pub source_side: Side,
484    /// Side where incoming edges arrive.
485    pub target_side: Side,
486    pub draggable: bool,
487    pub selectable: bool,
488    pub selected: bool,
489    /// Explicit size. When `None` (default) the node sizes to its content and
490    /// is measured automatically.
491    pub size: Option<Size>,
492    /// Measured size, maintained by the framework.
493    pub measured: Option<Size>,
494    /// Extra CSS classes for the node wrapper (e.g. Tailwind utilities).
495    pub class: Option<String>,
496    /// Extra inline CSS for the node wrapper.
497    pub style: Option<String>,
498}
499
500impl Node<()> {
501    /// Create a node with the default (unit) payload.
502    pub fn new(id: impl Into<Id>, label: impl Into<String>, position: impl Into<Point>) -> Self {
503        Self::with_data(id, label, position, ())
504    }
505}
506
507impl<T> Node<T> {
508    /// Create a node carrying a custom payload.
509    pub fn with_data(
510        id: impl Into<Id>,
511        label: impl Into<String>,
512        position: impl Into<Point>,
513        data: T,
514    ) -> Self {
515        Self {
516            id: id.into(),
517            position: position.into(),
518            label: label.into(),
519            data,
520            node_type: None,
521            source_side: Side::Bottom,
522            target_side: Side::Top,
523            draggable: true,
524            selectable: true,
525            selected: false,
526            size: None,
527            measured: None,
528            class: None,
529            style: None,
530        }
531    }
532
533    pub fn node_type(mut self, ty: impl Into<String>) -> Self {
534        self.node_type = Some(ty.into());
535        self
536    }
537
538    pub fn size(mut self, size: impl Into<Size>) -> Self {
539        self.size = Some(size.into());
540        self
541    }
542
543    pub fn class(mut self, class: impl Into<String>) -> Self {
544        self.class = Some(class.into());
545        self
546    }
547
548    pub fn style(mut self, style: impl Into<String>) -> Self {
549        self.style = Some(style.into());
550        self
551    }
552
553    pub fn draggable(mut self, draggable: bool) -> Self {
554        self.draggable = draggable;
555        self
556    }
557
558    pub fn selectable(mut self, selectable: bool) -> Self {
559        self.selectable = selectable;
560        self
561    }
562
563    /// Set both handle sides at once, e.g. for horizontal layouts
564    /// `sides(Side::Left, Side::Right)` (incoming left, outgoing right).
565    pub fn sides(mut self, target: Side, source: Side) -> Self {
566        self.target_side = target;
567        self.source_side = source;
568        self
569    }
570
571    /// The node's current rectangle (explicit size, else measured, else a
572    /// default estimate).
573    pub fn rect(&self) -> Rect {
574        let size = self.size.or(self.measured).unwrap_or(DEFAULT_NODE_SIZE);
575        Rect::from_points(self.position, size)
576    }
577}
578
579/// How an edge's path is drawn.
580#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
581pub enum EdgeKind {
582    /// A cubic bezier curve (the classic react-flow default).
583    #[default]
584    Bezier,
585    /// A straight line.
586    Straight,
587    /// An orthogonal path with rounded corners.
588    SmoothStep,
589}
590
591/// Arrowhead marker at the end of an edge.
592#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
593pub enum MarkerKind {
594    #[default]
595    ArrowClosed,
596    Arrow,
597    None,
598}
599
600/// How a [`crate::Flow`]'s edges find their endpoints on nodes.
601#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
602pub enum AnchorMode {
603    /// Edges attach to [`crate::Handle`]s (or a node's default side when the
604    /// edge names none). The react-flow model, and the default.
605    #[default]
606    Handles,
607    /// Edges attach to *seats* — discrete positions packed around each node's
608    /// rounded rim by [`crate::ports::solve_ports`]. Ends the user pinned
609    /// (via [`Edge::source_seat`]/[`Edge::target_seat`]) stay put; free ends
610    /// are placed nearest their partner, deterministically. Edges are drawn
611    /// with rim-aware curves and a bead where they meet the node.
612    Seats,
613}
614
615/// An edge connecting two nodes.
616#[derive(Clone, PartialEq, Debug)]
617pub struct Edge {
618    pub id: Id,
619    pub source: Id,
620    pub target: Id,
621    /// Optional id of a specific source [`crate::Handle`] on the source node.
622    pub source_handle: Option<Id>,
623    /// Optional id of a specific target [`crate::Handle`] on the target node.
624    pub target_handle: Option<Id>,
625    /// Under [`AnchorMode::Seats`]: the seat this end is pinned to. `None`
626    /// leaves the end to the solver, which is where every connection starts.
627    pub source_seat: Option<crate::ports::PortSeat>,
628    /// See [`Edge::source_seat`].
629    pub target_seat: Option<crate::ports::PortSeat>,
630    pub label: Option<String>,
631    /// Where the label sits along the edge, as a fraction of the curve
632    /// (clamped to the drawable range). Seat-anchored edges honour it; the
633    /// default is the midpoint.
634    pub label_position: f64,
635    /// Stroke emphasis, 1–3. Seat-anchored arrowheads scale with it, so a
636    /// heavier edge carries a proportionate head. Styling the stroke itself
637    /// stays in `style`/CSS.
638    pub weight: u8,
639    pub kind: EdgeKind,
640    /// Animated edges render a marching-dashes effect.
641    pub animated: bool,
642    pub selected: bool,
643    pub selectable: bool,
644    pub marker_start: MarkerKind,
645    pub marker_end: MarkerKind,
646    /// Extra CSS classes for the edge group.
647    pub class: Option<String>,
648    /// Extra inline CSS for the visible edge path (e.g. `stroke: #f43f5e`).
649    pub style: Option<String>,
650}
651
652impl Edge {
653    /// Create an edge from `source` to `target` with an autogenerated id.
654    pub fn new(source: impl Into<Id>, target: impl Into<Id>) -> Self {
655        let source = source.into();
656        let target = target.into();
657        Self {
658            id: format!("{source}->{target}"),
659            source,
660            target,
661            source_handle: None,
662            target_handle: None,
663            source_seat: None,
664            target_seat: None,
665            label: None,
666            label_position: 0.5,
667            weight: 2,
668            kind: EdgeKind::default(),
669            animated: false,
670            selected: false,
671            selectable: true,
672            marker_start: MarkerKind::None,
673            marker_end: MarkerKind::default(),
674            class: None,
675            style: None,
676        }
677    }
678
679    pub fn id(mut self, id: impl Into<Id>) -> Self {
680        self.id = id.into();
681        self
682    }
683
684    pub fn label(mut self, label: impl Into<String>) -> Self {
685        self.label = Some(label.into());
686        self
687    }
688
689    /// Place the label at `position` (0..=1) along the curve.
690    pub fn label_position(mut self, position: f64) -> Self {
691        self.label_position = position;
692        self
693    }
694
695    /// Stroke emphasis, 1–3; seat-anchored arrowheads scale with it.
696    pub fn weight(mut self, weight: u8) -> Self {
697        self.weight = weight;
698        self
699    }
700
701    pub fn kind(mut self, kind: EdgeKind) -> Self {
702        self.kind = kind;
703        self
704    }
705
706    pub fn animated(mut self, animated: bool) -> Self {
707        self.animated = animated;
708        self
709    }
710
711    pub fn marker_start(mut self, marker: MarkerKind) -> Self {
712        self.marker_start = marker;
713        self
714    }
715
716    pub fn marker_end(mut self, marker: MarkerKind) -> Self {
717        self.marker_end = marker;
718        self
719    }
720
721    /// Pin this edge's source end to a seat (used under [`AnchorMode::Seats`]).
722    pub fn source_seat(mut self, seat: crate::ports::PortSeat) -> Self {
723        self.source_seat = Some(seat);
724        self
725    }
726
727    /// Pin this edge's target end to a seat (used under [`AnchorMode::Seats`]).
728    pub fn target_seat(mut self, seat: crate::ports::PortSeat) -> Self {
729        self.target_seat = Some(seat);
730        self
731    }
732
733    pub fn source_handle(mut self, id: impl Into<Id>) -> Self {
734        self.source_handle = Some(id.into());
735        self
736    }
737
738    pub fn target_handle(mut self, id: impl Into<Id>) -> Self {
739        self.target_handle = Some(id.into());
740        self
741    }
742
743    pub fn class(mut self, class: impl Into<String>) -> Self {
744        self.class = Some(class.into());
745        self
746    }
747
748    pub fn style(mut self, style: impl Into<String>) -> Self {
749        self.style = Some(style.into());
750        self
751    }
752}
753
754/// A pending or completed connection between two handles.
755#[derive(Clone, PartialEq, Debug)]
756pub struct Connection {
757    pub source: Id,
758    pub target: Id,
759    pub source_handle: Option<Id>,
760    pub target_handle: Option<Id>,
761}
762
763/// How a connection gesture ended, whatever it ended on. Passed to
764/// `on_connect_end`; `connection` is `None` when the drag was released over
765/// nothing — the hook for "drop on empty canvas to create a node there".
766#[derive(Clone, PartialEq, Debug)]
767pub struct ConnectEnd {
768    /// Where the pointer let go, in flow coordinates.
769    pub point: Point,
770    /// The connection that completed, if the release was on (or snapped to) a
771    /// compatible handle.
772    pub connection: Option<Connection>,
773}
774
775/// What a Delete/Backspace press would remove: the selected nodes, plus
776/// edges that are selected or touch a selected node. Passed to `Flow`'s
777/// `on_delete` so apps can confirm, snapshot for undo, or veto.
778#[derive(Clone, PartialEq, Debug)]
779pub struct DeleteRequest {
780    pub nodes: Vec<Id>,
781    pub edges: Vec<Id>,
782}
783
784impl Connection {
785    /// Build the default edge for this connection.
786    pub fn into_edge(self) -> Edge {
787        let mut edge = Edge::new(self.source, self.target);
788        edge.source_handle = self.source_handle;
789        edge.target_handle = self.target_handle;
790        // Disambiguate the autogenerated id if specific handles are involved.
791        if let Some(h) = &edge.source_handle {
792            edge.id = format!("{}#{}", edge.id, h);
793        }
794        if let Some(h) = &edge.target_handle {
795            edge.id = format!("{}#{}", edge.id, h);
796        }
797        edge
798    }
799}
800
801/// Whether a handle is a source (edges start here) or a target (edges end
802/// here).
803#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
804pub enum HandleKind {
805    Source,
806    Target,
807}
808
809/// Uniquely identifies a handle within a flow.
810#[derive(Clone, PartialEq, Eq, Hash, Debug)]
811pub struct HandleKey {
812    pub node: Id,
813    pub kind: HandleKind,
814    /// User handle id; empty string for the node's default handle of that
815    /// kind.
816    pub id: Id,
817}
818
819/// Geometry of a registered handle, relative to its node.
820#[derive(Clone, Copy, PartialEq, Debug)]
821pub struct HandleGeom {
822    pub side: Side,
823    /// Fraction (0..=1) along the side.
824    pub offset: f64,
825}
826
827/// Lightweight per-node geometry snapshot used by edges, minimap and
828/// fit-view. Derived reactively from the node list.
829#[derive(Clone, PartialEq, Debug)]
830pub struct NodeGeom {
831    pub id: Id,
832    pub rect: Rect,
833    pub selected: bool,
834    pub source_side: Side,
835    pub target_side: Side,
836    /// Whether `rect` reflects a real (measured or explicit) size.
837    pub measured: bool,
838}
839
840#[cfg(test)]
841mod tests {
842    use super::*;
843
844    #[test]
845    fn viewport_roundtrip() {
846        let vp = Viewport::new(13.0, -7.0, 1.7);
847        let p = Point::new(100.0, 250.0);
848        let q = vp.screen_to_flow(vp.flow_to_screen(p));
849        assert!(p.distance(q) < 1e-9);
850    }
851
852    /// The property that makes zooming feel like zooming: the thing under the
853    /// pointer does not move.
854    #[test]
855    fn zooming_holds_the_point_it_is_given() {
856        let vp = Viewport::new(137.0, -42.0, 1.4);
857        let at = Point::new(400.0, 300.0);
858        for zoom in [0.2, 0.5, 1.0, 1.7, 3.0, 12.0] {
859            let before = vp.screen_to_flow(at);
860            let after = vp.zoom_about(zoom, at, 0.2, 3.0).screen_to_flow(at);
861            assert!(
862                before.distance(after) < 1e-9,
863                "zoom {zoom} moved the canvas under the pointer",
864            );
865        }
866    }
867
868    #[test]
869    fn zoom_stays_within_its_limits() {
870        let vp = Viewport::new(137.0, -42.0, 1.4);
871        let at = Point::ZERO;
872        assert_eq!(vp.zoom_about(99.0, at, 0.2, 3.0).zoom, 3.0);
873        assert_eq!(vp.zoom_about(0.0, at, 0.2, 3.0).zoom, 0.2);
874        // Refused, not clamped: a NaN would otherwise survive the clamp.
875        assert_eq!(vp.zoom_about(f64::NAN, at, 0.2, 3.0), vp);
876        assert_eq!(vp.zoom_about(f64::INFINITY, at, 0.2, 3.0), vp);
877    }
878
879    #[test]
880    fn a_viewport_from_outside_is_only_trusted_when_it_makes_sense() {
881        assert!(Viewport::new(137.0, -42.0, 1.4).is_sane(0.2, 3.0));
882        assert!(Viewport::default().is_sane(0.2, 3.0));
883        for broken in [
884            Viewport::new(f64::NAN, 0.0, 1.0),
885            Viewport::new(0.0, f64::INFINITY, 1.0),
886            Viewport::new(0.0, 0.0, 0.0),
887            Viewport::new(0.0, 0.0, 6.0),
888            Viewport::new(0.0, 0.0, f64::NAN),
889        ] {
890            assert!(!broken.is_sane(0.2, 3.0), "{broken:?}");
891        }
892    }
893
894    #[test]
895    fn fitting_puts_the_whole_drawing_inside_the_clear_area() {
896        let safe = Rect::new(32.0, 32.0, 1216.0, 560.0);
897        for drawing in [
898            Rect::new(-400.0, -300.0, 900.0, 600.0),
899            Rect::new(0.0, 0.0, 60.0, 40.0),
900            Rect::new(1000.0, 1000.0, 4000.0, 200.0),
901        ] {
902            let fitted = Viewport::fit(Some(drawing), safe, 0.2, 1.35);
903            let top_left = fitted.flow_to_screen(drawing.origin());
904            let bottom_right = fitted.flow_to_screen(Point::new(drawing.max_x(), drawing.max_y()));
905            let slack = 1e-6;
906            assert!(
907                top_left.x >= safe.x - slack && top_left.y >= safe.y - slack,
908                "{drawing:?} starts outside the clear area",
909            );
910            assert!(
911                bottom_right.x <= safe.max_x() + slack && bottom_right.y <= safe.max_y() + slack,
912                "{drawing:?} runs past the clear area",
913            );
914            assert!((0.2..=1.35).contains(&fitted.zoom));
915        }
916    }
917
918    #[test]
919    fn fitting_a_nonsense_drawing_still_gives_a_usable_view() {
920        let safe = Rect::new(0.0, 0.0, 800.0, 600.0);
921        for drawing in [
922            Rect::new(0.0, 0.0, f64::NAN, 10.0),
923            Rect::new(0.0, 0.0, 0.0, 0.0),
924            Rect::new(f64::NAN, 0.0, 10.0, 10.0),
925        ] {
926            let fitted = Viewport::fit(Some(drawing), safe, 0.2, 1.35);
927            assert!(fitted.zoom.is_finite() && fitted.zoom > 0.0, "{drawing:?}");
928        }
929    }
930
931    #[test]
932    fn fitting_nothing_centres_the_origin() {
933        let safe = Rect::new(0.0, 0.0, 800.0, 600.0);
934        let fitted = Viewport::fit(None, safe, 0.2, 1.35);
935        assert_eq!(fitted.flow_to_screen(Point::ZERO), Point::new(400.0, 300.0));
936        assert_eq!(fitted.zoom, 1.0);
937    }
938
939    #[test]
940    fn a_grid_snaps_both_ways_and_survives_a_nonsense_size() {
941        let grid = Grid::new(12.0);
942        assert_eq!(grid.snap(17.0), 12.0);
943        assert_eq!(grid.snap(19.0), 24.0);
944        assert_eq!(grid.snap(-17.0), -12.0);
945        assert_eq!(grid.snap_up(13.0), 24.0);
946        assert_eq!(grid.snap_up(24.0), 24.0);
947        for size in [0.0, -12.0, f64::NAN, f64::INFINITY] {
948            assert_eq!(Grid::new(size).size(), 1.0, "size {size}");
949        }
950    }
951
952    #[test]
953    fn a_rectangle_between_two_corners_is_the_same_whichever_corner_comes_first() {
954        let a = Point::new(30.0, -10.0);
955        let b = Point::new(-6.0, 22.0);
956        assert_eq!(Rect::between(a, b), Rect::between(b, a));
957        assert_eq!(Rect::between(a, b), Rect::new(-6.0, -10.0, 36.0, 32.0));
958    }
959
960    #[test]
961    fn bounds_covers_every_rect_or_nothing_at_all() {
962        assert_eq!(Rect::bounds([]), None);
963        let rects = [
964            Rect::new(0.0, 0.0, 10.0, 10.0),
965            Rect::new(-5.0, 20.0, 5.0, 5.0),
966        ];
967        assert_eq!(Rect::bounds(rects), Some(Rect::new(-5.0, 0.0, 15.0, 25.0)));
968    }
969
970    #[test]
971    fn distance_to_edge_is_signed() {
972        let r = Rect::new(0.0, 0.0, 100.0, 50.0);
973        assert!(r.distance_to_edge(Point::new(50.0, 25.0)) < 0.0);
974        assert_eq!(r.distance_to_edge(Point::new(0.0, 25.0)), 0.0);
975        assert!(r.distance_to_edge(Point::new(110.0, 25.0)) > 0.0);
976    }
977
978    #[test]
979    fn side_points() {
980        let r = Rect::new(10.0, 20.0, 100.0, 50.0);
981        assert_eq!(side_point(&r, Side::Top, 0.5), Point::new(60.0, 20.0));
982        assert_eq!(side_point(&r, Side::Bottom, 0.5), Point::new(60.0, 70.0));
983        assert_eq!(side_point(&r, Side::Left, 0.5), Point::new(10.0, 45.0));
984        assert_eq!(side_point(&r, Side::Right, 0.25), Point::new(110.0, 32.5));
985    }
986
987    #[test]
988    fn rect_union() {
989        let a = Rect::new(0.0, 0.0, 10.0, 10.0);
990        let b = Rect::new(20.0, -5.0, 10.0, 10.0);
991        let u = a.union(&b);
992        assert_eq!(u, Rect::new(0.0, -5.0, 30.0, 15.0));
993    }
994}