ratatui/layout/
rect.rs

1#![warn(missing_docs)]
2use std::{
3    cmp::{max, min},
4    fmt,
5};
6
7use crate::layout::{Margin, Position, Size};
8
9mod iter;
10pub use iter::*;
11
12/// A Rectangular area.
13///
14/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
15/// area they are supposed to render to.
16#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub struct Rect {
19    /// The x coordinate of the top left corner of the `Rect`.
20    pub x: u16,
21    /// The y coordinate of the top left corner of the `Rect`.
22    pub y: u16,
23    /// The width of the `Rect`.
24    pub width: u16,
25    /// The height of the `Rect`.
26    pub height: u16,
27}
28
29/// Amounts by which to move a [`Rect`](crate::layout::Rect).
30///
31/// Positive numbers move to the right/bottom and negative to the left/top.
32///
33/// See [`Rect::offset`]
34#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub struct Offset {
37    /// How much to move on the X axis
38    pub x: i32,
39    /// How much to move on the Y axis
40    pub y: i32,
41}
42
43impl fmt::Display for Rect {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
46    }
47}
48
49impl Rect {
50    /// A zero sized Rect at position 0,0
51    pub const ZERO: Self = Self {
52        x: 0,
53        y: 0,
54        width: 0,
55        height: 0,
56    };
57
58    /// Creates a new `Rect`, with width and height limited to keep both bounds within `u16`.
59    ///
60    /// If the width or height would cause the right or bottom coordinate to be larger than the
61    /// maximum value of `u16`, the width or height will be clamped to keep the right or bottom
62    /// coordinate within `u16`.
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use ratatui::layout::Rect;
68    ///
69    /// let rect = Rect::new(1, 2, 3, 4);
70    /// ```
71    pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
72        // these calculations avoid using min so that this function can be const
73        let max_width = u16::MAX - x;
74        let max_height = u16::MAX - y;
75        let width = if width > max_width { max_width } else { width };
76        let height = if height > max_height {
77            max_height
78        } else {
79            height
80        };
81        Self {
82            x,
83            y,
84            width,
85            height,
86        }
87    }
88
89    /// The area of the `Rect`. If the area is larger than the maximum value of `u16`, it will be
90    /// clamped to `u16::MAX`.
91    pub const fn area(self) -> u32 {
92        (self.width as u32) * (self.height as u32)
93    }
94
95    /// Returns true if the `Rect` has no area.
96    pub const fn is_empty(self) -> bool {
97        self.width == 0 || self.height == 0
98    }
99
100    /// Returns the left coordinate of the `Rect`.
101    pub const fn left(self) -> u16 {
102        self.x
103    }
104
105    /// Returns the right coordinate of the `Rect`. This is the first coordinate outside of the
106    /// `Rect`.
107    ///
108    /// If the right coordinate is larger than the maximum value of u16, it will be clamped to
109    /// `u16::MAX`.
110    pub const fn right(self) -> u16 {
111        self.x.saturating_add(self.width)
112    }
113
114    /// Returns the top coordinate of the `Rect`.
115    pub const fn top(self) -> u16 {
116        self.y
117    }
118
119    /// Returns the bottom coordinate of the `Rect`. This is the first coordinate outside of the
120    /// `Rect`.
121    ///
122    /// If the bottom coordinate is larger than the maximum value of u16, it will be clamped to
123    /// `u16::MAX`.
124    pub const fn bottom(self) -> u16 {
125        self.y.saturating_add(self.height)
126    }
127
128    /// Returns a new `Rect` inside the current one, with the given margin on each side.
129    ///
130    /// If the margin is larger than the `Rect`, the returned `Rect` will have no area.
131    #[must_use = "method returns the modified value"]
132    pub const fn inner(self, margin: Margin) -> Self {
133        let doubled_margin_horizontal = margin.horizontal.saturating_mul(2);
134        let doubled_margin_vertical = margin.vertical.saturating_mul(2);
135
136        if self.width < doubled_margin_horizontal || self.height < doubled_margin_vertical {
137            Self::ZERO
138        } else {
139            Self {
140                x: self.x.saturating_add(margin.horizontal),
141                y: self.y.saturating_add(margin.vertical),
142                width: self.width.saturating_sub(doubled_margin_horizontal),
143                height: self.height.saturating_sub(doubled_margin_vertical),
144            }
145        }
146    }
147
148    /// Moves the `Rect` without modifying its size.
149    ///
150    /// Moves the `Rect` according to the given offset without modifying its [`width`](Rect::width)
151    /// or [`height`](Rect::height).
152    /// - Positive `x` moves the whole `Rect` to the right, negative to the left.
153    /// - Positive `y` moves the whole `Rect` to the bottom, negative to the top.
154    ///
155    /// See [`Offset`] for details.
156    #[must_use = "method returns the modified value"]
157    pub fn offset(self, offset: Offset) -> Self {
158        Self {
159            x: i32::from(self.x)
160                .saturating_add(offset.x)
161                .clamp(0, i32::from(u16::MAX - self.width)) as u16,
162            y: i32::from(self.y)
163                .saturating_add(offset.y)
164                .clamp(0, i32::from(u16::MAX - self.height)) as u16,
165            ..self
166        }
167    }
168
169    /// Returns a new `Rect` that contains both the current one and the given one.
170    #[must_use = "method returns the modified value"]
171    pub fn union(self, other: Self) -> Self {
172        let x1 = min(self.x, other.x);
173        let y1 = min(self.y, other.y);
174        let x2 = max(self.right(), other.right());
175        let y2 = max(self.bottom(), other.bottom());
176        Self {
177            x: x1,
178            y: y1,
179            width: x2.saturating_sub(x1),
180            height: y2.saturating_sub(y1),
181        }
182    }
183
184    /// Returns a new `Rect` that is the intersection of the current one and the given one.
185    ///
186    /// If the two `Rect`s do not intersect, the returned `Rect` will have no area.
187    #[must_use = "method returns the modified value"]
188    pub fn intersection(self, other: Self) -> Self {
189        let x1 = max(self.x, other.x);
190        let y1 = max(self.y, other.y);
191        let x2 = min(self.right(), other.right());
192        let y2 = min(self.bottom(), other.bottom());
193        Self {
194            x: x1,
195            y: y1,
196            width: x2.saturating_sub(x1),
197            height: y2.saturating_sub(y1),
198        }
199    }
200
201    /// Returns true if the two `Rect`s intersect.
202    pub const fn intersects(self, other: Self) -> bool {
203        self.x < other.right()
204            && self.right() > other.x
205            && self.y < other.bottom()
206            && self.bottom() > other.y
207    }
208
209    /// Returns true if the given position is inside the `Rect`.
210    ///
211    /// The position is considered inside the `Rect` if it is on the `Rect`'s border.
212    ///
213    /// # Examples
214    ///
215    /// ```rust
216    /// use ratatui::layout::{Position, Rect};
217    ///
218    /// let rect = Rect::new(1, 2, 3, 4);
219    /// assert!(rect.contains(Position { x: 1, y: 2 }));
220    /// ````
221    pub const fn contains(self, position: Position) -> bool {
222        position.x >= self.x
223            && position.x < self.right()
224            && position.y >= self.y
225            && position.y < self.bottom()
226    }
227
228    /// Clamp this `Rect` to fit inside the other `Rect`.
229    ///
230    /// If the width or height of this `Rect` is larger than the other `Rect`, it will be clamped to
231    /// the other `Rect`'s width or height.
232    ///
233    /// If the left or top coordinate of this `Rect` is smaller than the other `Rect`, it will be
234    /// clamped to the other `Rect`'s left or top coordinate.
235    ///
236    /// If the right or bottom coordinate of this `Rect` is larger than the other `Rect`, it will be
237    /// clamped to the other `Rect`'s right or bottom coordinate.
238    ///
239    /// This is different from [`Rect::intersection`] because it will move this `Rect` to fit inside
240    /// the other `Rect`, while [`Rect::intersection`] instead would keep this `Rect`'s position and
241    /// truncate its size to only that which is inside the other `Rect`.
242    ///
243    /// # Examples
244    ///
245    /// ```rust
246    /// use ratatui::{layout::Rect, Frame};
247    ///
248    /// # fn render(frame: &mut Frame) {
249    /// let area = frame.area();
250    /// let rect = Rect::new(0, 0, 100, 100).clamp(area);
251    /// # }
252    /// ```
253    #[must_use = "method returns the modified value"]
254    pub fn clamp(self, other: Self) -> Self {
255        let width = self.width.min(other.width);
256        let height = self.height.min(other.height);
257        let x = self.x.clamp(other.x, other.right().saturating_sub(width));
258        let y = self.y.clamp(other.y, other.bottom().saturating_sub(height));
259        Self::new(x, y, width, height)
260    }
261
262    /// An iterator over rows within the `Rect`.
263    ///
264    /// # Example
265    ///
266    /// ```
267    /// use ratatui::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
268    ///
269    /// fn render(area: Rect, buf: &mut Buffer) {
270    ///     for row in area.rows() {
271    ///         Line::raw("Hello, world!").render(row, buf);
272    ///     }
273    /// }
274    /// ```
275    pub const fn rows(self) -> Rows {
276        Rows::new(self)
277    }
278
279    /// An iterator over columns within the `Rect`.
280    ///
281    /// # Example
282    ///
283    /// ```
284    /// use ratatui::{
285    ///     buffer::Buffer,
286    ///     layout::Rect,
287    ///     widgets::{Block, Borders, Widget},
288    /// };
289    /// fn render(area: Rect, buf: &mut Buffer) {
290    ///     if let Some(left) = area.columns().next() {
291    ///         Block::new().borders(Borders::LEFT).render(left, buf);
292    ///     }
293    /// }
294    /// ```
295    pub const fn columns(self) -> Columns {
296        Columns::new(self)
297    }
298
299    /// An iterator over the positions within the `Rect`.
300    ///
301    /// The positions are returned in a row-major order (left-to-right, top-to-bottom).
302    ///
303    /// # Example
304    ///
305    /// ```
306    /// use ratatui::{buffer::Buffer, layout::Rect};
307    ///
308    /// fn render(area: Rect, buf: &mut Buffer) {
309    ///     for position in area.positions() {
310    ///         buf[(position.x, position.y)].set_symbol("x");
311    ///     }
312    /// }
313    /// ```
314    pub const fn positions(self) -> Positions {
315        Positions::new(self)
316    }
317
318    /// Returns a [`Position`] with the same coordinates as this `Rect`.
319    ///
320    /// # Examples
321    ///
322    /// ```
323    /// use ratatui::layout::Rect;
324    ///
325    /// let rect = Rect::new(1, 2, 3, 4);
326    /// let position = rect.as_position();
327    /// ````
328    pub const fn as_position(self) -> Position {
329        Position {
330            x: self.x,
331            y: self.y,
332        }
333    }
334
335    /// Converts the `Rect` into a size struct.
336    pub const fn as_size(self) -> Size {
337        Size {
338            width: self.width,
339            height: self.height,
340        }
341    }
342
343    /// indents the x value of the `Rect` by a given `offset`
344    ///
345    /// This is pub(crate) for now as we need to stabilize the naming / design of this API.
346    #[must_use]
347    pub(crate) const fn indent_x(self, offset: u16) -> Self {
348        Self {
349            x: self.x.saturating_add(offset),
350            width: self.width.saturating_sub(offset),
351            ..self
352        }
353    }
354}
355
356impl From<(Position, Size)> for Rect {
357    fn from((position, size): (Position, Size)) -> Self {
358        Self {
359            x: position.x,
360            y: position.y,
361            width: size.width,
362            height: size.height,
363        }
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use rstest::rstest;
370
371    use super::*;
372    use crate::layout::{Constraint, Layout};
373
374    #[test]
375    fn to_string() {
376        assert_eq!(Rect::new(1, 2, 3, 4).to_string(), "3x4+1+2");
377    }
378
379    #[test]
380    fn new() {
381        assert_eq!(
382            Rect::new(1, 2, 3, 4),
383            Rect {
384                x: 1,
385                y: 2,
386                width: 3,
387                height: 4
388            }
389        );
390    }
391
392    #[test]
393    fn area() {
394        assert_eq!(Rect::new(1, 2, 3, 4).area(), 12);
395    }
396
397    #[test]
398    fn is_empty() {
399        assert!(!Rect::new(1, 2, 3, 4).is_empty());
400        assert!(Rect::new(1, 2, 0, 4).is_empty());
401        assert!(Rect::new(1, 2, 3, 0).is_empty());
402    }
403
404    #[test]
405    fn left() {
406        assert_eq!(Rect::new(1, 2, 3, 4).left(), 1);
407    }
408
409    #[test]
410    fn right() {
411        assert_eq!(Rect::new(1, 2, 3, 4).right(), 4);
412    }
413
414    #[test]
415    fn top() {
416        assert_eq!(Rect::new(1, 2, 3, 4).top(), 2);
417    }
418
419    #[test]
420    fn bottom() {
421        assert_eq!(Rect::new(1, 2, 3, 4).bottom(), 6);
422    }
423
424    #[test]
425    fn inner() {
426        assert_eq!(
427            Rect::new(1, 2, 3, 4).inner(Margin::new(1, 2)),
428            Rect::new(2, 4, 1, 0)
429        );
430    }
431
432    #[test]
433    fn offset() {
434        assert_eq!(
435            Rect::new(1, 2, 3, 4).offset(Offset { x: 5, y: 6 }),
436            Rect::new(6, 8, 3, 4),
437        );
438    }
439
440    #[test]
441    fn negative_offset() {
442        assert_eq!(
443            Rect::new(4, 3, 3, 4).offset(Offset { x: -2, y: -1 }),
444            Rect::new(2, 2, 3, 4),
445        );
446    }
447
448    #[test]
449    fn negative_offset_saturate() {
450        assert_eq!(
451            Rect::new(1, 2, 3, 4).offset(Offset { x: -5, y: -6 }),
452            Rect::new(0, 0, 3, 4),
453        );
454    }
455
456    /// Offsets a [`Rect`] making it go outside [`u16::MAX`], it should keep its size.
457    #[test]
458    fn offset_saturate_max() {
459        assert_eq!(
460            Rect::new(u16::MAX - 500, u16::MAX - 500, 100, 100).offset(Offset { x: 1000, y: 1000 }),
461            Rect::new(u16::MAX - 100, u16::MAX - 100, 100, 100),
462        );
463    }
464
465    #[test]
466    fn union() {
467        assert_eq!(
468            Rect::new(1, 2, 3, 4).union(Rect::new(2, 3, 4, 5)),
469            Rect::new(1, 2, 5, 6)
470        );
471    }
472
473    #[test]
474    fn intersection() {
475        assert_eq!(
476            Rect::new(1, 2, 3, 4).intersection(Rect::new(2, 3, 4, 5)),
477            Rect::new(2, 3, 2, 3)
478        );
479    }
480
481    #[test]
482    fn intersection_underflow() {
483        assert_eq!(
484            Rect::new(1, 1, 2, 2).intersection(Rect::new(4, 4, 2, 2)),
485            Rect::new(4, 4, 0, 0)
486        );
487    }
488
489    #[test]
490    fn intersects() {
491        assert!(Rect::new(1, 2, 3, 4).intersects(Rect::new(2, 3, 4, 5)));
492        assert!(!Rect::new(1, 2, 3, 4).intersects(Rect::new(5, 6, 7, 8)));
493    }
494
495    // the bounds of this rect are x: [1..=3], y: [2..=5]
496    #[rstest]
497    #[case::inside_top_left(Rect::new(1, 2, 3, 4), Position { x: 1, y: 2 }, true)]
498    #[case::inside_top_right(Rect::new(1, 2, 3, 4), Position { x: 3, y: 2 }, true)]
499    #[case::inside_bottom_left(Rect::new(1, 2, 3, 4), Position { x: 1, y: 5 }, true)]
500    #[case::inside_bottom_right(Rect::new(1, 2, 3, 4), Position { x: 3, y: 5 }, true)]
501    #[case::outside_left(Rect::new(1, 2, 3, 4), Position { x: 0, y: 2 }, false)]
502    #[case::outside_right(Rect::new(1, 2, 3, 4), Position { x: 4, y: 2 }, false)]
503    #[case::outside_top(Rect::new(1, 2, 3, 4), Position { x: 1, y: 1 }, false)]
504    #[case::outside_bottom(Rect::new(1, 2, 3, 4), Position { x: 1, y: 6 }, false)]
505    #[case::outside_top_left(Rect::new(1, 2, 3, 4), Position { x: 0, y: 1 }, false)]
506    #[case::outside_bottom_right(Rect::new(1, 2, 3, 4), Position { x: 4, y: 6 }, false)]
507    fn contains(#[case] rect: Rect, #[case] position: Position, #[case] expected: bool) {
508        assert_eq!(
509            rect.contains(position),
510            expected,
511            "rect: {rect:?}, position: {position:?}",
512        );
513    }
514
515    #[test]
516    fn size_truncation() {
517        assert_eq!(
518            Rect::new(u16::MAX - 100, u16::MAX - 1000, 200, 2000),
519            Rect {
520                x: u16::MAX - 100,
521                y: u16::MAX - 1000,
522                width: 100,
523                height: 1000
524            }
525        );
526    }
527
528    #[test]
529    fn size_preservation() {
530        assert_eq!(
531            Rect::new(u16::MAX - 100, u16::MAX - 1000, 100, 1000),
532            Rect {
533                x: u16::MAX - 100,
534                y: u16::MAX - 1000,
535                width: 100,
536                height: 1000
537            }
538        );
539    }
540
541    #[test]
542    fn can_be_const() {
543        const RECT: Rect = Rect {
544            x: 0,
545            y: 0,
546            width: 10,
547            height: 10,
548        };
549        const _AREA: u32 = RECT.area();
550        const _LEFT: u16 = RECT.left();
551        const _RIGHT: u16 = RECT.right();
552        const _TOP: u16 = RECT.top();
553        const _BOTTOM: u16 = RECT.bottom();
554        assert!(RECT.intersects(RECT));
555    }
556
557    #[test]
558    fn split() {
559        let [a, b] = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
560            .areas(Rect::new(0, 0, 2, 1));
561        assert_eq!(a, Rect::new(0, 0, 1, 1));
562        assert_eq!(b, Rect::new(1, 0, 1, 1));
563    }
564
565    #[test]
566    #[should_panic(expected = "invalid number of rects")]
567    fn split_invalid_number_of_recs() {
568        let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
569        let [_a, _b, _c] = layout.areas(Rect::new(0, 0, 2, 1));
570    }
571
572    #[rstest]
573    #[case::inside(Rect::new(20, 20, 10, 10), Rect::new(20, 20, 10, 10))]
574    #[case::up_left(Rect::new(5, 5, 10, 10), Rect::new(10, 10, 10, 10))]
575    #[case::up(Rect::new(20, 5, 10, 10), Rect::new(20, 10, 10, 10))]
576    #[case::up_right(Rect::new(105, 5, 10, 10), Rect::new(100, 10, 10, 10))]
577    #[case::left(Rect::new(5, 20, 10, 10), Rect::new(10, 20, 10, 10))]
578    #[case::right(Rect::new(105, 20, 10, 10), Rect::new(100, 20, 10, 10))]
579    #[case::down_left(Rect::new(5, 105, 10, 10), Rect::new(10, 100, 10, 10))]
580    #[case::down(Rect::new(20, 105, 10, 10), Rect::new(20, 100, 10, 10))]
581    #[case::down_right(Rect::new(105, 105, 10, 10), Rect::new(100, 100, 10, 10))]
582    #[case::too_wide(Rect::new(5, 20, 200, 10), Rect::new(10, 20, 100, 10))]
583    #[case::too_tall(Rect::new(20, 5, 10, 200), Rect::new(20, 10, 10, 100))]
584    #[case::too_large(Rect::new(0, 0, 200, 200), Rect::new(10, 10, 100, 100))]
585    fn clamp(#[case] rect: Rect, #[case] expected: Rect) {
586        let other = Rect::new(10, 10, 100, 100);
587        assert_eq!(rect.clamp(other), expected);
588    }
589
590    #[test]
591    fn rows() {
592        let area = Rect::new(0, 0, 3, 2);
593        let rows: Vec<Rect> = area.rows().collect();
594
595        let expected_rows: Vec<Rect> = vec![Rect::new(0, 0, 3, 1), Rect::new(0, 1, 3, 1)];
596
597        assert_eq!(rows, expected_rows);
598    }
599
600    #[test]
601    fn columns() {
602        let area = Rect::new(0, 0, 3, 2);
603        let columns: Vec<Rect> = area.columns().collect();
604
605        let expected_columns: Vec<Rect> = vec![
606            Rect::new(0, 0, 1, 2),
607            Rect::new(1, 0, 1, 2),
608            Rect::new(2, 0, 1, 2),
609        ];
610
611        assert_eq!(columns, expected_columns);
612    }
613
614    #[test]
615    fn as_position() {
616        let rect = Rect::new(1, 2, 3, 4);
617        let position = rect.as_position();
618        assert_eq!(position.x, 1);
619        assert_eq!(position.y, 2);
620    }
621
622    #[test]
623    fn as_size() {
624        assert_eq!(
625            Rect::new(1, 2, 3, 4).as_size(),
626            Size {
627                width: 3,
628                height: 4
629            }
630        );
631    }
632
633    #[test]
634    fn from_position_and_size() {
635        let position = Position { x: 1, y: 2 };
636        let size = Size {
637            width: 3,
638            height: 4,
639        };
640        assert_eq!(
641            Rect::from((position, size)),
642            Rect {
643                x: 1,
644                y: 2,
645                width: 3,
646                height: 4
647            }
648        );
649    }
650}