dreg_core/
primitives.rs

1//! Primitive Types
2
3
4
5/// A rectangular area.
6///
7/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
8/// area they are supposed to render to.
9#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
10pub struct Rect {
11    /// The x coordinate of the top left corner of the `Rect`.
12    pub x: u16,
13    /// The y coordinate of the top left corner of the `Rect`.
14    pub y: u16,
15    /// The width of the `Rect`.
16    pub width: u16,
17    /// The height of the `Rect`.
18    pub height: u16,
19}
20
21impl std::fmt::Display for Rect {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
24    }
25}
26
27impl Rect {
28    /// A zero sized Rect at position 0,0
29    pub const ZERO: Self = Self {
30        x: 0,
31        y: 0,
32        width: 0,
33        height: 0,
34    };
35
36    /// Creates a new `Rect`, with width and height limited to keep the area under max `u16`. If
37    /// clipped, aspect ratio will be preserved.
38    pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
39        let max_area = u16::MAX;
40        let (clipped_width, clipped_height) =
41            if u32::from(width) * u32::from(height) > u32::from(max_area) {
42                let aspect_ratio = f64::from(width) / f64::from(height);
43                let max_area_f = f64::from(max_area);
44                let height_f = (max_area_f / aspect_ratio).sqrt();
45                let width_f = height_f * aspect_ratio;
46                (width_f as u16, height_f as u16)
47            } else {
48                (width, height)
49            };
50        Self {
51            x,
52            y,
53            width: clipped_width,
54            height: clipped_height,
55        }
56    }
57
58    /// The area of the `Rect`. If the area is larger than the maximum value of `u16`, it will be
59    /// clamped to `u16::MAX`.
60    pub const fn area(self) -> u16 {
61        self.width.saturating_mul(self.height)
62    }
63
64    /// Returns true if the `Rect` has no area.
65    pub const fn is_empty(self) -> bool {
66        self.width == 0 || self.height == 0
67    }
68
69    /// Returns the left coordinate of the `Rect`.
70    pub const fn left(self) -> u16 {
71        self.x
72    }
73
74    /// Returns the right coordinate of the `Rect`. This is the first coordinate outside of the
75    /// `Rect`.
76    ///
77    /// If the right coordinate is larger than the maximum value of u16, it will be clamped to
78    /// `u16::MAX`.
79    pub const fn right(self) -> u16 {
80        self.x.saturating_add(self.width)
81    }
82
83    /// Returns the top coordinate of the `Rect`.
84    pub const fn top(self) -> u16 {
85        self.y
86    }
87
88    /// Returns the bottom coordinate of the `Rect`. This is the first coordinate outside of the
89    /// `Rect`.
90    ///
91    /// If the bottom coordinate is larger than the maximum value of u16, it will be clamped to
92    /// `u16::MAX`.
93    pub const fn bottom(self) -> u16 {
94        self.y.saturating_add(self.height)
95    }
96
97    /// Returns a new `Rect` inside the current one, with the given margin on each side.
98    ///
99    /// If the margin is larger than the `Rect`, the returned `Rect` will have no area.
100    #[must_use = "method returns the modified value"]
101    pub const fn inner(self, margin_x: u16, margin_y: u16) -> Self {
102        let doubled_margin_horizontal = margin_x.saturating_mul(2);
103        let doubled_margin_vertical = margin_y.saturating_mul(2);
104
105        if self.width < doubled_margin_horizontal || self.height < doubled_margin_vertical {
106            Self::ZERO
107        } else {
108            Self {
109                x: self.x.saturating_add(margin_x),
110                y: self.y.saturating_add(margin_y),
111                width: self.width.saturating_sub(doubled_margin_horizontal),
112                height: self.height.saturating_sub(doubled_margin_vertical),
113            }
114        }
115    }
116
117    /// Moves the `Rect` without modifying its size.
118    ///
119    /// Moves the `Rect` according to the given offset without modifying its [`width`](Rect::width)
120    /// or [`height`](Rect::height).
121    /// - Positive `x` moves the whole `Rect` to the right, negative to the left.
122    /// - Positive `y` moves the whole `Rect` downward, negative upward.
123    #[must_use = "method returns the modified value"]
124    pub fn offset(self, x: i32, y: i32) -> Self {
125        Self {
126            x: i32::from(self.x)
127                .saturating_add(x)
128                .clamp(0, i32::from(u16::MAX - self.width)) as u16,
129            y: i32::from(self.y)
130                .saturating_add(y)
131                .clamp(0, i32::from(u16::MAX - self.height)) as u16,
132            ..self
133        }
134    }
135
136    /// Returns a new `Rect` that contains both the current one and the given one.
137    #[must_use = "method returns the modified value"]
138    pub fn union(self, other: Self) -> Self {
139        let x1 = std::cmp::min(self.x, other.x);
140        let y1 = std::cmp::min(self.y, other.y);
141        let x2 = std::cmp::max(self.right(), other.right());
142        let y2 = std::cmp::max(self.bottom(), other.bottom());
143        Self {
144            x: x1,
145            y: y1,
146            width: x2.saturating_sub(x1),
147            height: y2.saturating_sub(y1),
148        }
149    }
150
151    /// Returns a new `Rect` that is the intersection of the current one and the given one.
152    ///
153    /// If the two `Rect`s do not intersect, the returned `Rect` will have no area.
154    #[must_use = "method returns the modified value"]
155    pub fn intersection(self, other: Self) -> Self {
156        let x1 = std::cmp::max(self.x, other.x);
157        let y1 = std::cmp::max(self.y, other.y);
158        let x2 = std::cmp::min(self.right(), other.right());
159        let y2 = std::cmp::min(self.bottom(), other.bottom());
160        Self {
161            x: x1,
162            y: y1,
163            width: x2.saturating_sub(x1),
164            height: y2.saturating_sub(y1),
165        }
166    }
167
168    /// Returns true if the two `Rect`s intersect.
169    pub const fn intersects(self, other: Self) -> bool {
170        self.x < other.right()
171            && self.right() > other.x
172            && self.y < other.bottom()
173            && self.bottom() > other.y
174    }
175
176    /// Returns true if the given position is inside the `Rect`.
177    ///
178    /// The position is considered inside the `Rect` if it is on the `Rect`'s border.
179    pub const fn contains(self, x: u16, y: u16) -> bool {
180        x >= self.x
181            && x < self.right()
182            && y >= self.y
183            && y < self.bottom()
184    }
185
186    /// Clamp this `Rect` to fit inside the other `Rect`.
187    ///
188    /// If the width or height of this `Rect` is larger than the other `Rect`, it will be clamped
189    /// to the other `Rect`'s width or height.
190    ///
191    /// If the left or top coordinate of this `Rect` is smaller than the other `Rect`, it will be
192    /// clamped to the other `Rect`'s left or top coordinate.
193    ///
194    /// If the right or bottom coordinate of this `Rect` is larger than the other `Rect`, it will
195    /// be clamped to the other `Rect`'s right or bottom coordinate.
196    ///
197    /// This is different from [`Rect::intersection`] because it will move this `Rect` to fit
198    /// inside the other `Rect`, while [`Rect::intersection`] instead would keep this `Rect`'s
199    /// position and truncate its size to only that which is inside the other `Rect`.
200    #[must_use = "method returns the modified value"]
201    pub fn clamp(self, other: Self) -> Self {
202        let width = self.width.min(other.width);
203        let height = self.height.min(other.height);
204        let x = self.x.clamp(other.x, other.right().saturating_sub(width));
205        let y = self.y.clamp(other.y, other.bottom().saturating_sub(height));
206        Self::new(x, y, width, height)
207    }
208}
209
210impl Rect {
211    pub fn hsplit_portion(&self, portion: f32) -> (Self, Self) {
212        let width_a = (self.width as f32 * portion).floor() as u16;
213        let width_b = self.width - width_a;
214        (
215            Rect::new(self.x, self.y, width_a, self.height),
216            Rect::new(self.x + width_a, self.y, width_b, self.height),
217        )
218    }
219
220    pub fn vsplit_portion(&self, portion: f32) -> (Self, Self) {
221        let height_a = (self.height as f32 * portion).floor() as u16;
222        let height_b = self.height - height_a;
223        (
224            Rect::new(self.x, self.y, self.width, height_a),
225            Rect::new(self.x, self.y + height_a, self.width, height_b),
226        )
227    }
228
229    pub fn hsplit_len(&self, length: u16) -> (Self, Self) {
230        if length >= self.width {
231            return (*self, Rect::ZERO);
232        }
233        (
234            Rect::new(self.x, self.y, length, self.height),
235            Rect::new(self.x + length, self.y, self.width - length, self.height),
236        )
237    }
238
239    pub fn vsplit_len(&self, length: u16) -> (Self, Self) {
240        if length >= self.height {
241            return (*self, Rect::ZERO);
242        }
243        (
244            Rect::new(self.x, self.y, self.width, length),
245            Rect::new(self.x, self.y + length, self.width, self.height - length),
246        )
247    }
248
249    pub fn hsplit_inverse_portion(&self, portion: f32) -> (Self, Self) {
250        let width_a = (self.width as f32 * portion).floor() as u16;
251        let width_b = self.width - width_a;
252        (
253            Rect::new(self.x + width_b, self.y, width_a, self.height),
254            Rect::new(self.x, self.y, width_b, self.height),
255        )
256    }
257
258    pub fn vsplit_inverse_portion(&self, portion: f32) -> (Self, Self) {
259        let height_a = (self.height as f32 * portion).floor() as u16;
260        let height_b = self.height - height_a;
261        (
262            Rect::new(self.x, self.y + height_b, self.width, height_a),
263            Rect::new(self.x, self.y, self.width, height_b),
264        )
265    }
266
267    pub fn hsplit_inverse_len(&self, length: u16) -> (Self, Self) {
268        if length >= self.width {
269            return (Rect::ZERO, *self);
270        }
271        (
272            Rect::new(self.x + (self.width - length), self.y, length, self.height),
273            Rect::new(self.x, self.y, self.width - length, self.height),
274        )
275    }
276
277    pub fn vsplit_inverse_len(&self, length: u16) -> (Self, Self) {
278        if length >= self.height {
279            return (Rect::ZERO, *self);
280        }
281        (
282            Rect::new(self.x, self.y + (self.height - length), self.width, length),
283            Rect::new(self.x, self.y, self.width, self.height - length),
284        )
285    }
286
287    pub fn inner_centered(&self, width: u16, height: u16) -> Self {
288        let x = self.x + (self.width.saturating_sub(width) / 2);
289        let y = self.y + (self.height.saturating_sub(height) / 2);
290        Rect::new(x, y, width.min(self.width), height.min(self.height))
291    }
292
293    pub fn rows(&self) -> Vec<Self> {
294        (0..self.height)
295            .into_iter()
296            .map(|row_index| {
297                Rect::new(self.left(), self.top() + row_index, self.width, self.height)
298            })
299            .collect()
300    }
301}
302
303
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn rect_splitting() {
311        let test_rect = Rect::new(0, 0, 10, 20);
312
313        assert_eq!(test_rect.hsplit_len(3), (Rect::new(0, 0, 3, 20), Rect::new(3, 0, 7, 20)));
314        assert_eq!(test_rect.hsplit_len(11), (Rect::new(0, 0, 10, 20), Rect::new(0, 0, 0, 0)));
315        assert_eq!(test_rect.vsplit_len(7), (Rect::new(0, 0, 10, 7), Rect::new(0, 7, 10, 13)));
316        assert_eq!(test_rect.vsplit_len(22), (Rect::new(0, 0, 10, 20), Rect::new(0, 0, 0, 0)));
317
318        assert_eq!(
319            test_rect.hsplit_inverse_len(3),
320            (Rect::new(7, 0, 3, 20), Rect::new(0, 0, 7, 20)),
321        );
322    }
323
324    #[test]
325    fn rect_splitting_with_offset() {
326        let test_rect = Rect::new(1, 1, 10, 20);
327
328        assert_eq!(test_rect.hsplit_len(3), (Rect::new(1, 1, 3, 20), Rect::new(4, 1, 7, 20)));
329        assert_eq!(test_rect.hsplit_len(11), (Rect::new(1, 1, 10, 20), Rect::new(0, 0, 0, 0)));
330        assert_eq!(test_rect.vsplit_len(7), (Rect::new(1, 1, 10, 7), Rect::new(1, 8, 10, 13)));
331        assert_eq!(test_rect.vsplit_len(22), (Rect::new(1, 1, 10, 20), Rect::new(0, 0, 0, 0)));
332
333        assert_eq!(
334            test_rect.hsplit_inverse_len(3),
335            (Rect::new(8, 1, 3, 20), Rect::new(1, 1, 7, 20)),
336        );
337    }
338
339    #[test]
340    fn fixed_issue4() {
341        let rect = Rect::new(3, 5, 7, 11);
342
343        assert_eq!(
344            rect.hsplit_inverse_len(2),
345            (
346                Rect::new(8, 5, 2, 11),
347                Rect::new(3, 5, 5, 11),
348            )
349        )
350    }
351}