Skip to main content

dioxus_flow/
place.rs

1//! Where something new goes when nothing said exactly where.
2//!
3//! Each step is separate on purpose: a pointer position is honoured exactly, so
4//! only the steps a caller asks for are taken. What size a new thing is, and what
5//! counts as occupied, belong to the document — these take both as arguments.
6
7use crate::types::{Grid, Point, Rect};
8
9/// The frame an item of `width` by `height` occupies centred on `centre`, snapped
10/// to the grid.
11pub fn centred(centre: Point, width: f64, height: f64, grid: Grid) -> Rect {
12    grid.snap_rect(Rect::new(
13        centre.x - width / 2.0,
14        centre.y - height / 2.0,
15        width,
16        height,
17    ))
18}
19
20/// The frame of a new item that a band was just dropped at, sized `width` by
21/// `height` and placed so `at` lands on the edge facing `source`.
22///
23/// Centring it on the drop instead would swallow the pointer inside the new
24/// card and, for anything wider than the drag, shove the card somewhere the
25/// pointer never was. The band is what the pointer is holding, so it is the band
26/// that has to stay put: the card grows away from the drop, in whichever
27/// direction the drag was already going.
28pub fn beside(at: Point, source: Rect, width: f64, height: f64, grid: Grid) -> Rect {
29    let centre = source.center();
30    let away = Point::new(at.x - centre.x, at.y - centre.y);
31    let frame = if away.x.abs() >= away.y.abs() {
32        Rect::new(
33            if away.x >= 0.0 { at.x } else { at.x - width },
34            at.y - height / 2.0,
35            width,
36            height,
37        )
38    } else {
39        Rect::new(
40            at.x - width / 2.0,
41            if away.y >= 0.0 { at.y } else { at.y - height },
42            width,
43            height,
44        )
45    };
46    grid.snap_rect(frame)
47}
48
49/// Pushes `frame` out past `source` along whichever axis `frame` is already
50/// further along, leaving `gap` between them. A frame already clear of `source` is
51/// left alone — this is for something dropped so close to where it came from that
52/// it would land on top of it.
53pub fn pushed_clear(frame: Rect, source: Rect, gap: f64, grid: Grid) -> Rect {
54    if !source.expanded(gap).intersects(frame) {
55        return frame;
56    }
57    let centre = frame.center();
58    let away = Point::new(centre.x - source.center().x, centre.y - source.center().y);
59    let mut frame = frame;
60    if away.x.abs() >= away.y.abs() {
61        frame.x = if away.x >= 0.0 {
62            source.x + source.width + gap
63        } else {
64            source.x - frame.width - gap
65        };
66        frame.y = grid.snap(centre.y - frame.height / 2.0);
67    } else {
68        frame.y = if away.y >= 0.0 {
69            source.y + source.height + gap
70        } else {
71            source.y - frame.height - gap
72        };
73        frame.x = grid.snap(centre.x - frame.width / 2.0);
74    }
75    frame
76}
77
78/// Jumps `frame` down past whatever is covering it until it clears everything in
79/// `occupied`, and reports where it came to rest.
80///
81/// Each pass goes to the far side of every obstacle it is currently touching, so
82/// it retires at least one of them for good — a card taller than the frame's own
83/// row included, which stepping down by that row would never get past. That also
84/// bounds the walk by the number of obstacles rather than by the distance.
85pub fn settled_below(frame: Rect, occupied: &[Rect], gap: f64, grid: Grid) -> Point {
86    let mut frame = frame;
87    for _ in 0..=occupied.len() {
88        let Some(below) = occupied
89            .iter()
90            .filter(|other| other.expanded(gap).intersects(frame))
91            .map(|other| other.y + other.height + gap)
92            .reduce(f64::max)
93        else {
94            break;
95        };
96        // Touching counts as intersecting, so a frame parked exactly `gap` clear
97        // of what is above it still reads as covered. It is already where it
98        // belongs, and moving it would be moving it to where it already is.
99        if below <= frame.y {
100            break;
101        }
102        frame.y = below;
103    }
104    grid.snap_point(Point::new(frame.x, frame.y))
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    const GRID: Grid = Grid::new(12.0);
112
113    #[test]
114    fn a_clear_spot_is_used_exactly() {
115        let frame = centred(Point::new(0.0, 0.0), 216.0, 48.0, GRID);
116        assert_eq!(
117            settled_below(frame, &[], 48.0, GRID),
118            Point::new(frame.x, frame.y)
119        );
120    }
121
122    #[test]
123    fn a_taken_spot_is_stepped_past_and_the_result_clears_everything() {
124        let taken = [
125            Rect::new(-108.0, -24.0, 216.0, 48.0),
126            Rect::new(-108.0, 96.0, 216.0, 48.0),
127        ];
128        let frame = centred(Point::new(0.0, 0.0), 216.0, 48.0, GRID);
129        let at = settled_below(frame, &taken, 48.0, GRID);
130        let placed = Rect::new(at.x, at.y, frame.width, frame.height);
131        assert!(taken.iter().all(|other| !other.intersects(placed)));
132        assert!(at.y > frame.y, "it should have moved down, not up");
133    }
134
135    /// One obstacle taller than the frame's own row used to defeat the walk
136    /// entirely: it stepped down by a row a bounded number of times and gave up
137    /// still inside the card it was trying to clear.
138    #[test]
139    fn a_tall_obstacle_is_cleared_however_much_taller_than_the_new_item_it_is() {
140        let tall = [Rect::new(0.0, 0.0, 216.0, 1200.0)];
141        let frame = centred(Point::new(108.0, 24.0), 216.0, 48.0, GRID);
142        let at = settled_below(frame, &tall, 12.0, GRID);
143        let placed = Rect::new(at.x, at.y, frame.width, frame.height);
144
145        assert!(
146            !tall[0].intersects(placed),
147            "{placed:?} is still inside {:?}",
148            tall[0]
149        );
150    }
151
152    #[test]
153    fn a_drop_beside_its_source_is_pushed_off_the_source() {
154        let source = Rect::new(0.0, 0.0, 216.0, 48.0);
155        let gap = 48.0;
156        for towards in [
157            Point::new(240.0, 10.0),
158            Point::new(-240.0, 10.0),
159            Point::new(10.0, 240.0),
160            Point::new(10.0, -240.0),
161        ] {
162            let frame = centred(towards, 216.0, 48.0, GRID);
163            let pushed = pushed_clear(frame, source, gap, GRID);
164            assert!(
165                !source.expanded(gap - 1.0).intersects(pushed),
166                "a drop towards {towards:?} stayed on its source",
167            );
168        }
169    }
170
171    #[test]
172    fn a_drop_already_clear_of_its_source_is_left_where_it_is() {
173        let source = Rect::new(0.0, 0.0, 216.0, 48.0);
174        let frame = centred(Point::new(600.0, 600.0), 216.0, 48.0, GRID);
175        assert_eq!(pushed_clear(frame, source, 48.0, GRID), frame);
176    }
177
178    /// What the pointer is holding is the band, so the drop point stays on the
179    /// new card's edge and the card grows away from it.
180    #[test]
181    fn a_new_item_grows_away_from_the_drop_rather_than_around_it() {
182        let source = Rect::new(0.0, 0.0, 216.0, 48.0);
183        let (width, height) = (216.0, 48.0);
184        for (at, edge) in [
185            (Point::new(600.0, 24.0), "left"),
186            (Point::new(-600.0, 24.0), "right"),
187            (Point::new(108.0, 600.0), "top"),
188            (Point::new(108.0, -600.0), "bottom"),
189        ] {
190            let frame = beside(at, source, width, height, GRID);
191            let touching = match edge {
192                "left" => frame.x,
193                "right" => frame.x + frame.width,
194                "top" => frame.y,
195                _ => frame.y + frame.height,
196            };
197            let along = if matches!(edge, "left" | "right") {
198                at.x
199            } else {
200                at.y
201            };
202            assert!(
203                (touching - along).abs() <= GRID.size() / 2.0,
204                "dropping at {at:?} put the {edge} edge at {touching}, not {along}"
205            );
206            assert!(
207                !source.intersects(frame),
208                "dropping at {at:?} landed on the card it came from"
209            );
210        }
211    }
212}