Skip to main content

qframe/widgets/
placement.rs

1//! Placing a layer next to the thing it belongs to: dropdowns, popovers, menus, tooltips.
2
3use crate::geometry::{Rect, Size, clamp_u16};
4use crate::motion::steps;
5
6/// The side of its anchor a layer prefers. When the layer does not fit there it flips to the
7/// opposite side, and it is always kept on screen.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum Placement {
10    /// Under the anchor, left edges aligned.
11    #[default]
12    Below,
13    /// Over the anchor, left edges aligned.
14    Above,
15    /// To the right of the anchor, top edges aligned.
16    Right,
17    /// To the left of the anchor, top edges aligned.
18    Left,
19}
20
21impl Placement {
22    /// Every placement.
23    pub const ALL: [Self; 4] = [Self::Below, Self::Above, Self::Right, Self::Left];
24
25    /// A short name, e.g. for settings screens.
26    #[must_use]
27    pub fn name(self) -> &'static str {
28        match self {
29            Self::Below => "below",
30            Self::Above => "above",
31            Self::Right => "right",
32            Self::Left => "left",
33        }
34    }
35
36    /// The other side of the anchor.
37    pub(crate) fn opposite(self) -> Self {
38        match self {
39            Self::Below => Self::Above,
40            Self::Above => Self::Below,
41            Self::Right => Self::Left,
42            Self::Left => Self::Right,
43        }
44    }
45
46    fn vertical(self) -> bool {
47        matches!(self, Self::Below | Self::Above)
48    }
49}
50
51/// Where a layer of `size` goes next to `anchor` on `screen`, and the side it ended up on.
52///
53/// The preferred side is used when the layer fits there, otherwise the opposite side, otherwise
54/// whichever of the two has more room. A side placement (right or left) that fits on neither
55/// side falls back to below or above. Along the other axis the layer is aligned with the anchor
56/// and slid back onto the screen.
57pub(crate) fn place(anchor: Rect, size: Size, screen: Rect, preferred: Placement) -> (Rect, Placement) {
58    let size = size.min(screen.size());
59    let (width, height) = (i32::from(size.width), i32::from(size.height));
60    let room = |side: Placement| match side {
61        Placement::Below => screen.bottom() - anchor.bottom(),
62        Placement::Above => anchor.y - screen.y,
63        Placement::Right => screen.right() - anchor.right(),
64        Placement::Left => anchor.x - screen.x,
65    };
66    let needed = |side: Placement| if side.vertical() { height } else { width };
67    let side = if room(preferred) >= needed(preferred) {
68        preferred
69    } else if room(preferred.opposite()) >= needed(preferred) {
70        preferred.opposite()
71    } else if !preferred.vertical() {
72        return place(anchor, size, screen, Placement::Below);
73    } else if room(preferred.opposite()) > room(preferred) {
74        preferred.opposite()
75    } else {
76        preferred
77    };
78    let clamp_x = |x: i32| x.min(screen.right() - width).max(screen.x);
79    let clamp_y = |y: i32| y.min(screen.bottom() - height).max(screen.y);
80    let (x, y) = match side {
81        Placement::Below => (clamp_x(anchor.x), clamp_y(anchor.bottom())),
82        Placement::Above => (clamp_x(anchor.x), clamp_y(anchor.y - height)),
83        Placement::Right => (clamp_x(anchor.right()), clamp_y(anchor.y)),
84        Placement::Left => (clamp_x(anchor.x - width), clamp_y(anchor.y)),
85    };
86    (Rect::new(x, y, clamp_u16(width), clamp_u16(height)), side)
87}
88
89/// The width of a layer that opens as wide as the widget it belongs to, cut to what is left of
90/// the screen: a popover under a button and a field's suggestion list both take it, so the
91/// layer's own left and right edges stand where the anchor's do.
92pub(crate) fn anchor_width(anchor: Rect, screen: Rect) -> u16 {
93    anchor.width.min(screen.width)
94}
95
96/// The part shown of a layer at `full`, `progress` of the way (0 to 1) through unfolding from
97/// its anchor on `side`: whole rows grow away from an anchor above or below it, whole columns
98/// from an anchor beside it. At least one row or column shows.
99pub(crate) fn unfold(full: Rect, side: Placement, progress: f32) -> Rect {
100    match side {
101        Placement::Below => Rect::new(full.x, full.y, full.width, steps(progress, full.height).max(1)),
102        Placement::Above => {
103            let rows = steps(progress, full.height).max(1);
104            Rect::new(full.x, full.bottom() - i32::from(rows), full.width, rows)
105        }
106        Placement::Right => Rect::new(full.x, full.y, steps(progress, full.width).max(1), full.height),
107        Placement::Left => {
108            let columns = steps(progress, full.width).max(1);
109            Rect::new(full.right() - i32::from(columns), full.y, columns, full.height)
110        }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    const SCREEN: Rect = Rect::new(0, 0, 40, 20);
119
120    #[test]
121    fn uses_the_preferred_side_when_it_fits() {
122        let anchor = Rect::new(5, 3, 10, 1);
123        assert_eq!(
124            place(anchor, Size::new(12, 4), SCREEN, Placement::Below),
125            (Rect::new(5, 4, 12, 4), Placement::Below)
126        );
127        assert_eq!(
128            place(anchor, Size::new(12, 3), SCREEN, Placement::Above),
129            (Rect::new(5, 0, 12, 3), Placement::Above)
130        );
131        assert_eq!(
132            place(anchor, Size::new(8, 3), SCREEN, Placement::Right),
133            (Rect::new(15, 3, 8, 3), Placement::Right)
134        );
135    }
136
137    #[test]
138    fn flips_when_there_is_no_room() {
139        let bottom = Rect::new(5, 18, 10, 1);
140        assert_eq!(place(bottom, Size::new(12, 4), SCREEN, Placement::Below).1, Placement::Above);
141        let top = Rect::new(5, 1, 10, 1);
142        assert_eq!(place(top, Size::new(12, 4), SCREEN, Placement::Above).1, Placement::Below);
143        let right_edge = Rect::new(34, 5, 4, 1);
144        assert_eq!(
145            place(right_edge, Size::new(8, 3), SCREEN, Placement::Right),
146            (Rect::new(26, 5, 8, 3), Placement::Left)
147        );
148    }
149
150    #[test]
151    fn a_layer_matching_its_anchor_takes_the_anchor_width_cut_to_the_screen() {
152        assert_eq!(anchor_width(Rect::new(5, 3, 18, 1), SCREEN), 18);
153        assert_eq!(anchor_width(Rect::new(30, 3, 30, 1), Rect::new(0, 0, 20, 10)), 20, "the screen cuts it");
154    }
155
156    #[test]
157    fn unfolds_away_from_the_anchor_in_whole_cells() {
158        let full = Rect::new(4, 6, 10, 4);
159        assert_eq!(unfold(full, Placement::Below, 0.5), Rect::new(4, 6, 10, 2));
160        assert_eq!(unfold(full, Placement::Above, 0.5), Rect::new(4, 8, 10, 2));
161        assert_eq!(unfold(full, Placement::Right, 0.3), Rect::new(4, 6, 3, 4));
162        assert_eq!(unfold(full, Placement::Left, 0.3), Rect::new(11, 6, 3, 4));
163        assert_eq!(unfold(full, Placement::Below, 0.0), Rect::new(4, 6, 10, 1), "one row shows at once");
164        assert_eq!(unfold(full, Placement::Left, 1.0), full);
165    }
166
167    #[test]
168    fn stays_on_screen() {
169        let corner = Rect::new(36, 10, 4, 1);
170        let (rect, _) = place(corner, Size::new(12, 4), SCREEN, Placement::Below);
171        assert_eq!(rect, Rect::new(28, 11, 12, 4));
172        let (tall, side) = place(Rect::new(2, 8, 4, 1), Size::new(6, 30), SCREEN, Placement::Below);
173        assert_eq!((tall.y, tall.height, side), (0, 20, Placement::Below));
174        let wide = place(Rect::new(18, 8, 4, 1), Size::new(30, 2), SCREEN, Placement::Right);
175        assert_eq!(wide.1, Placement::Below);
176    }
177}