Skip to main content

dioxus_mind_elixir/plugins/
gesture.rs

1//! Platform-independent pointer hit testing; coordinates are CSS client pixels.
2use crate::{LayoutDirection, MindMapError, Placement, Point};
3
4/// Rectangle measured in client coordinates.
5#[derive(Clone, Copy, Debug, PartialEq)]
6pub struct HitRect {
7    /// Left edge.
8    pub x: f64,
9    /// Top edge.
10    pub y: f64,
11    /// Width.
12    pub width: f64,
13    /// Height.
14    pub height: f64,
15}
16impl HitRect {
17    /// Reject invalid measurements before hit testing.
18    pub fn validate(self) -> Result<(), MindMapError> {
19        if [self.x, self.y, self.width, self.height]
20            .iter()
21            .all(|v| v.is_finite())
22            && self.width > 0.0
23            && self.height > 0.0
24        {
25            Ok(())
26        } else {
27            Err(MindMapError::InvalidGeometry)
28        }
29    }
30    /// Whether the point is inside the rectangle.
31    pub fn contains(self, point: Point) -> bool {
32        point.x >= self.x
33            && point.x <= self.x + self.width
34            && point.y >= self.y
35            && point.y <= self.y + self.height
36    }
37    /// Whether the rectangle intersects a selection area, regardless of direction.
38    pub fn intersects(self, start: Point, end: Point) -> bool {
39        self.x <= start.x.max(end.x)
40            && self.x + self.width >= start.x.min(end.x)
41            && self.y <= start.y.max(end.y)
42            && self.y + self.height >= start.y.min(end.y)
43    }
44}
45/// The upstream five client-pixel threshold avoids moving nodes on clicks.
46pub fn is_drag(start: Point, current: Point) -> bool {
47    (current.x - start.x).hypot(current.y - start.y) >= 5.0
48}
49/// Resolve before/after bands (12 × zoom) and the central child target.
50/// LEFT/RIGHT/SIDE siblings are vertical; DOWN siblings are horizontal.
51pub fn drop_placement(
52    id: &str,
53    rect: HitRect,
54    at: Point,
55    layout: LayoutDirection,
56    scale: f64,
57) -> Result<Placement, MindMapError> {
58    rect.validate()?;
59    if !scale.is_finite() || scale <= 0.0 || !at.x.is_finite() || !at.y.is_finite() {
60        return Err(MindMapError::InvalidGeometry);
61    }
62    let (offset, length) = if layout == LayoutDirection::Down {
63        (at.x - rect.x, rect.width)
64    } else {
65        (at.y - rect.y, rect.height)
66    };
67    let band = (12.0 * scale).min(length / 3.0);
68    Ok(if offset < band {
69        Placement::Before(id.into())
70    } else if offset > length - band {
71        Placement::After(id.into())
72    } else {
73        Placement::In(id.into())
74    })
75}
76/// Per-pointer-step edge pan within a 50px band, independent of document history.
77pub fn edge_pan(bounds: HitRect, at: Point) -> Result<Point, MindMapError> {
78    bounds.validate()?;
79    if !at.x.is_finite() || !at.y.is_finite() {
80        return Err(MindMapError::InvalidGeometry);
81    }
82    let axis = |v: f64, low: f64, length: f64| {
83        if v < low + 50.0 {
84            ((low + 50.0 - v) / 50.0).clamp(0.0, 1.0) * 10.0
85        } else if v > low + length - 50.0 {
86            -((v - (low + length - 50.0)) / 50.0).clamp(0.0, 1.0) * 10.0
87        } else {
88            0.0
89        }
90    };
91    Ok(Point {
92        x: axis(at.x, bounds.x, bounds.width),
93        y: axis(at.y, bounds.y, bounds.height),
94    })
95}
96/// Viewport that fits the canvas-space box `a..b` into a view of `size`,
97/// centred, with a 24px margin and magnification at most 100%.
98pub fn fit(a: Point, b: Point, size: Point) -> Result<crate::Viewport, MindMapError> {
99    let (w, h) = ((b.x - a.x).abs(), (b.y - a.y).abs());
100    if ![a.x, a.y, b.x, b.y, size.x, size.y]
101        .into_iter()
102        .all(f64::is_finite)
103        || size.x <= 0.0
104        || size.y <= 0.0
105    {
106        return Err(MindMapError::InvalidGeometry);
107    }
108    let margin = 24.0;
109    let sx = (size.x - margin * 2.0).max(1.0) / w.max(1.0);
110    let sy = (size.y - margin * 2.0).max(1.0) / h.max(1.0);
111    let mut view = crate::Viewport {
112        translation: Point::default(),
113        scale: sx.min(sy).clamp(0.1, 1.0),
114    };
115    view.center(
116        Point {
117            x: (a.x + b.x) / 2.0,
118            y: (a.y + b.y) / 2.0,
119        },
120        size,
121    )?;
122    Ok(view)
123}