ratatui_core/layout/
rect.rs

1#![warn(missing_docs)]
2use core::array::TryFromSliceError;
3use core::cmp::{max, min};
4use core::fmt;
5
6pub use self::iter::{Columns, Positions, Rows};
7use crate::layout::{Margin, Offset, Position, Size};
8
9mod iter;
10mod ops;
11
12use super::{Constraint, Flex, Layout};
13
14/// A rectangular area in the terminal.
15///
16/// A `Rect` represents a rectangular region in the terminal coordinate system, defined by its
17/// top-left corner position and dimensions. This is the fundamental building block for all layout
18/// operations and widget rendering in Ratatui.
19///
20/// Rectangles are used throughout the layout system to define areas where widgets can be rendered.
21/// They are typically created by [`Layout`] operations that divide terminal space, but can also be
22/// manually constructed for specific positioning needs.
23///
24/// The coordinate system uses the top-left corner as the origin (0, 0), with x increasing to the
25/// right and y increasing downward. All measurements are in character cells.
26///
27/// # Construction and Conversion
28///
29/// - [`new`](Self::new) - Create a new rectangle from coordinates and dimensions
30/// - [`as_position`](Self::as_position) - Convert to a position at the top-left corner
31/// - [`as_size`](Self::as_size) - Convert to a size representing the dimensions
32/// - [`from((Position, Size))`](Self::from) - Create from `(Position, Size)` tuple
33/// - [`from(((u16, u16), (u16, u16)))`](Self::from) - Create from `((u16, u16), (u16, u16))`
34///   coordinate and dimension tuples
35/// - [`into((Position, Size))`] - Convert to `(Position, Size)` tuple
36/// - [`default`](Self::default) - Create a zero-sized rectangle at origin
37///
38/// # Geometry and Properties
39///
40/// - [`area`](Self::area) - Calculate the total area in character cells
41/// - [`is_empty`](Self::is_empty) - Check if the rectangle has zero area
42/// - [`left`](Self::left), [`right`](Self::right), [`top`](Self::top), [`bottom`](Self::bottom) -
43///   Get edge coordinates
44///
45/// # Spatial Operations
46///
47/// - [`inner`](Self::inner), [`outer`](Self::outer) - Apply margins to shrink or expand
48/// - [`offset`](Self::offset) - Move the rectangle by a relative amount
49/// - [`resize`](Self::resize) - Change the rectangle size while keeping the bottom/right in range
50/// - [`union`](Self::union) - Combine with another rectangle to create a bounding box
51/// - [`intersection`](Self::intersection) - Find the overlapping area with another rectangle
52/// - [`clamp`](Self::clamp) - Constrain the rectangle to fit within another
53///
54/// # Positioning and Centering
55///
56/// - [`centered_horizontally`](Self::centered_horizontally) - Center horizontally within a
57///   constraint
58/// - [`centered_vertically`](Self::centered_vertically) - Center vertically within a constraint
59/// - [`centered`](Self::centered) - Center both horizontally and vertically
60///
61/// # Testing and Iteration
62///
63/// - [`contains`](Self::contains) - Check if a position is within the rectangle
64/// - [`intersects`](Self::intersects) - Check if it overlaps with another rectangle
65/// - [`rows`](Self::rows) - Iterate over horizontal rows within the rectangle
66/// - [`columns`](Self::columns) - Iterate over vertical columns within the rectangle
67/// - [`positions`](Self::positions) - Iterate over all positions within the rectangle
68///
69/// # Examples
70///
71/// To create a new `Rect`, use [`Rect::new`]. The size of the `Rect` will be clamped to keep the
72/// right and bottom coordinates within `u16`. Note that this clamping does not occur when creating
73/// a `Rect` directly.
74///
75/// ```rust
76/// use ratatui_core::layout::Rect;
77///
78/// let rect = Rect::new(1, 2, 3, 4);
79/// assert_eq!(
80///     rect,
81///     Rect {
82///         x: 1,
83///         y: 2,
84///         width: 3,
85///         height: 4
86///     }
87/// );
88/// ```
89///
90/// You can also create a `Rect` from a [`Position`] and a [`Size`].
91///
92/// ```rust
93/// use ratatui_core::layout::{Position, Rect, Size};
94///
95/// let position = Position::new(1, 2);
96/// let size = Size::new(3, 4);
97/// let rect = Rect::from((position, size));
98/// assert_eq!(
99///     rect,
100///     Rect {
101///         x: 1,
102///         y: 2,
103///         width: 3,
104///         height: 4
105///     }
106/// );
107/// ```
108///
109/// To move a `Rect` without modifying its size, add or subtract an [`Offset`] to it.
110///
111/// ```rust
112/// use ratatui_core::layout::{Offset, Rect};
113///
114/// let rect = Rect::new(1, 2, 3, 4);
115/// let offset = Offset::new(5, 6);
116/// let moved_rect = rect + offset;
117/// assert_eq!(moved_rect, Rect::new(6, 8, 3, 4));
118/// ```
119///
120/// To resize a `Rect` while ensuring it stays within bounds, use [`Rect::resize`]. The size is
121/// clamped so that `right()` and `bottom()` do not exceed `u16::MAX`.
122///
123/// ```rust
124/// use ratatui_core::layout::{Rect, Size};
125///
126/// let rect = Rect::new(u16::MAX - 1, u16::MAX - 1, 1, 1).resize(Size::new(10, 10));
127/// assert_eq!(rect, Rect::new(u16::MAX - 1, u16::MAX - 1, 1, 1));
128/// ```
129///
130/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
131
132#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
133#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134pub struct Rect {
135    /// The x coordinate of the top left corner of the `Rect`.
136    pub x: u16,
137    /// The y coordinate of the top left corner of the `Rect`.
138    pub y: u16,
139    /// The width of the `Rect`.
140    pub width: u16,
141    /// The height of the `Rect`.
142    pub height: u16,
143}
144
145impl fmt::Display for Rect {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
148    }
149}
150
151impl Rect {
152    /// A zero sized Rect at position 0,0
153    pub const ZERO: Self = Self {
154        x: 0,
155        y: 0,
156        width: 0,
157        height: 0,
158    };
159
160    /// The minimum possible Rect
161    pub const MIN: Self = Self::ZERO;
162
163    /// The maximum possible Rect
164    pub const MAX: Self = Self::new(0, 0, u16::MAX, u16::MAX);
165
166    /// Creates a new `Rect`, with width and height limited to keep both bounds within `u16`.
167    ///
168    /// If the width or height would cause the right or bottom coordinate to be larger than the
169    /// maximum value of `u16`, the width or height will be clamped to keep the right or bottom
170    /// coordinate within `u16`.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// use ratatui_core::layout::Rect;
176    ///
177    /// let rect = Rect::new(1, 2, 3, 4);
178    /// ```
179    pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
180        let width = x.saturating_add(width) - x;
181        let height = y.saturating_add(height) - y;
182        Self {
183            x,
184            y,
185            width,
186            height,
187        }
188    }
189
190    /// The area of the `Rect`.
191    pub const fn area(self) -> u32 {
192        (self.width as u32) * (self.height as u32)
193    }
194
195    /// Returns true if the `Rect` has no area.
196    pub const fn is_empty(self) -> bool {
197        self.width == 0 || self.height == 0
198    }
199
200    /// Returns the left coordinate of the `Rect`.
201    pub const fn left(self) -> u16 {
202        self.x
203    }
204
205    /// Returns the right coordinate of the `Rect`. This is the first coordinate outside of the
206    /// `Rect`.
207    ///
208    /// If the right coordinate is larger than the maximum value of u16, it will be clamped to
209    /// `u16::MAX`.
210    pub const fn right(self) -> u16 {
211        self.x.saturating_add(self.width)
212    }
213
214    /// Returns the top coordinate of the `Rect`.
215    pub const fn top(self) -> u16 {
216        self.y
217    }
218
219    /// Returns the bottom coordinate of the `Rect`. This is the first coordinate outside of the
220    /// `Rect`.
221    ///
222    /// If the bottom coordinate is larger than the maximum value of u16, it will be clamped to
223    /// `u16::MAX`.
224    pub const fn bottom(self) -> u16 {
225        self.y.saturating_add(self.height)
226    }
227
228    /// Returns a new `Rect` inside the current one, with the given margin on each side.
229    ///
230    /// If the margin is larger than the `Rect`, the returned `Rect` will have no area.
231    #[must_use = "method returns the modified value"]
232    pub const fn inner(self, margin: Margin) -> Self {
233        let doubled_margin_horizontal = margin.horizontal.saturating_mul(2);
234        let doubled_margin_vertical = margin.vertical.saturating_mul(2);
235
236        if self.width < doubled_margin_horizontal || self.height < doubled_margin_vertical {
237            Self::ZERO
238        } else {
239            Self {
240                x: self.x.saturating_add(margin.horizontal),
241                y: self.y.saturating_add(margin.vertical),
242                width: self.width.saturating_sub(doubled_margin_horizontal),
243                height: self.height.saturating_sub(doubled_margin_vertical),
244            }
245        }
246    }
247
248    /// Returns a new `Rect` outside the current one, with the given margin applied on each side.
249    ///
250    /// If the margin causes the `Rect`'s bounds to be outside the range of a `u16`, the `Rect` will
251    /// be truncated to keep the bounds within `u16`. This will cause the size of the `Rect` to
252    /// change.
253    ///
254    /// The generated `Rect` may not fit inside the buffer or containing area, so it consider
255    /// constraining the resulting `Rect` with [`Rect::clamp`] before using it.
256    #[must_use = "method returns the modified value"]
257    pub const fn outer(self, margin: Margin) -> Self {
258        let x = self.x.saturating_sub(margin.horizontal);
259        let y = self.y.saturating_sub(margin.vertical);
260        let width = self
261            .right()
262            .saturating_add(margin.horizontal)
263            .saturating_sub(x);
264        let height = self
265            .bottom()
266            .saturating_add(margin.vertical)
267            .saturating_sub(y);
268        Self {
269            x,
270            y,
271            width,
272            height,
273        }
274    }
275
276    /// Moves the `Rect` without modifying its size.
277    ///
278    /// Moves the `Rect` according to the given offset without modifying its [`width`](Rect::width)
279    /// or [`height`](Rect::height).
280    /// - Positive `x` moves the whole `Rect` to the right, negative to the left.
281    /// - Positive `y` moves the whole `Rect` to the bottom, negative to the top.
282    ///
283    /// See [`Offset`] for details.
284    #[must_use = "method returns the modified value"]
285    pub fn offset(self, offset: Offset) -> Self {
286        self + offset
287    }
288
289    /// Resizes the `Rect`, clamping to keep the right and bottom within `u16::MAX`.
290    ///
291    /// The position is preserved. If the requested size would push the `Rect` beyond the bounds of
292    /// `u16`, the width or height is reduced so that [`right`](Self::right) and
293    /// [`bottom`](Self::bottom) remain within range.
294    #[must_use = "method returns the modified value"]
295    pub const fn resize(self, size: Size) -> Self {
296        Self {
297            width: self.x.saturating_add(size.width).saturating_sub(self.x),
298            height: self.y.saturating_add(size.height).saturating_sub(self.y),
299            ..self
300        }
301    }
302
303    /// Returns a new `Rect` that contains both the current one and the given one.
304    #[must_use = "method returns the modified value"]
305    pub fn union(self, other: Self) -> Self {
306        let x1 = min(self.x, other.x);
307        let y1 = min(self.y, other.y);
308        let x2 = max(self.right(), other.right());
309        let y2 = max(self.bottom(), other.bottom());
310        Self {
311            x: x1,
312            y: y1,
313            width: x2.saturating_sub(x1),
314            height: y2.saturating_sub(y1),
315        }
316    }
317
318    /// Returns a new `Rect` that is the intersection of the current one and the given one.
319    ///
320    /// If the two `Rect`s do not intersect, the returned `Rect` will have no area.
321    #[must_use = "method returns the modified value"]
322    pub fn intersection(self, other: Self) -> Self {
323        let x1 = max(self.x, other.x);
324        let y1 = max(self.y, other.y);
325        let x2 = min(self.right(), other.right());
326        let y2 = min(self.bottom(), other.bottom());
327        Self {
328            x: x1,
329            y: y1,
330            width: x2.saturating_sub(x1),
331            height: y2.saturating_sub(y1),
332        }
333    }
334
335    /// Returns true if the two `Rect`s intersect.
336    pub const fn intersects(self, other: Self) -> bool {
337        self.x < other.right()
338            && self.right() > other.x
339            && self.y < other.bottom()
340            && self.bottom() > other.y
341    }
342
343    /// Returns true if the given position is inside the `Rect`.
344    ///
345    /// The position is considered inside the `Rect` if it is on the `Rect`'s border.
346    ///
347    /// # Examples
348    ///
349    /// ```rust
350    /// use ratatui_core::layout::{Position, Rect};
351    ///
352    /// let rect = Rect::new(1, 2, 3, 4);
353    /// assert!(rect.contains(Position { x: 1, y: 2 }));
354    /// ````
355    pub const fn contains(self, position: Position) -> bool {
356        position.x >= self.x
357            && position.x < self.right()
358            && position.y >= self.y
359            && position.y < self.bottom()
360    }
361
362    /// Clamp this `Rect` to fit inside the other `Rect`.
363    ///
364    /// If the width or height of this `Rect` is larger than the other `Rect`, it will be clamped to
365    /// the other `Rect`'s width or height.
366    ///
367    /// If the left or top coordinate of this `Rect` is smaller than the other `Rect`, it will be
368    /// clamped to the other `Rect`'s left or top coordinate.
369    ///
370    /// If the right or bottom coordinate of this `Rect` is larger than the other `Rect`, it will be
371    /// clamped to the other `Rect`'s right or bottom coordinate.
372    ///
373    /// This is different from [`Rect::intersection`] because it will move this `Rect` to fit inside
374    /// the other `Rect`, while [`Rect::intersection`] instead would keep this `Rect`'s position and
375    /// truncate its size to only that which is inside the other `Rect`.
376    ///
377    /// # Examples
378    ///
379    /// ```rust
380    /// use ratatui_core::layout::Rect;
381    ///
382    /// let area = Rect::new(0, 0, 100, 100);
383    /// let rect = Rect::new(80, 80, 30, 30).clamp(area);
384    /// assert_eq!(rect, Rect::new(70, 70, 30, 30));
385    /// ```
386    #[must_use = "method returns the modified value"]
387    pub fn clamp(self, other: Self) -> Self {
388        let width = self.width.min(other.width);
389        let height = self.height.min(other.height);
390        let x = self.x.clamp(other.x, other.right().saturating_sub(width));
391        let y = self.y.clamp(other.y, other.bottom().saturating_sub(height));
392        Self::new(x, y, width, height)
393    }
394
395    /// An iterator over rows within the `Rect`.
396    ///
397    /// Each row is a full `Rect` region with height 1 that can be used for rendering widgets
398    /// or as input to further layout methods.
399    ///
400    /// # Example
401    ///
402    /// ```
403    /// use ratatui_core::buffer::Buffer;
404    /// use ratatui_core::layout::{Constraint, Layout, Rect};
405    /// use ratatui_core::widgets::Widget;
406    ///
407    /// fn render_list(area: Rect, buf: &mut Buffer) {
408    ///     // Renders "Item 0", "Item 1", etc. in each row
409    ///     for (i, row) in area.rows().enumerate() {
410    ///         format!("Item {i}").render(row, buf);
411    ///     }
412    /// }
413    ///
414    /// fn render_with_nested_layout(area: Rect, buf: &mut Buffer) {
415    ///     // Splits each row into left/right areas and renders labels and content
416    ///     for (i, row) in area.rows().take(3).enumerate() {
417    ///         let [left, right] =
418    ///             Layout::horizontal([Constraint::Percentage(30), Constraint::Fill(1)]).areas(row);
419    ///
420    ///         format!("{i}:").render(left, buf);
421    ///         "Content".render(right, buf);
422    ///     }
423    /// }
424    /// ```
425    pub const fn rows(self) -> Rows {
426        Rows::new(self)
427    }
428
429    /// An iterator over columns within the `Rect`.
430    ///
431    /// Each column is a full `Rect` region with width 1 that can be used for rendering widgets
432    /// or as input to further layout methods.
433    ///
434    /// # Example
435    ///
436    /// ```
437    /// use ratatui_core::buffer::Buffer;
438    /// use ratatui_core::layout::Rect;
439    /// use ratatui_core::widgets::Widget;
440    ///
441    /// fn render_columns(area: Rect, buf: &mut Buffer) {
442    ///     // Renders column indices (0-9 repeating) in each column
443    ///     for (i, column) in area.columns().enumerate() {
444    ///         format!("{}", i % 10).render(column, buf);
445    ///     }
446    /// }
447    /// ```
448    pub const fn columns(self) -> Columns {
449        Columns::new(self)
450    }
451
452    /// An iterator over the positions within the `Rect`.
453    ///
454    /// The positions are returned in a row-major order (left-to-right, top-to-bottom).
455    /// Each position is a `Position` that represents a single cell coordinate.
456    ///
457    /// # Example
458    ///
459    /// ```
460    /// use ratatui_core::buffer::Buffer;
461    /// use ratatui_core::layout::{Position, Rect};
462    /// use ratatui_core::widgets::Widget;
463    ///
464    /// fn render_positions(area: Rect, buf: &mut Buffer) {
465    ///     // Renders position indices (0-9 repeating) at each cell position
466    ///     for (i, position) in area.positions().enumerate() {
467    ///         buf[position].set_symbol(&format!("{}", i % 10));
468    ///     }
469    /// }
470    /// ```
471    pub const fn positions(self) -> Positions {
472        Positions::new(self)
473    }
474
475    /// Returns a [`Position`] with the same coordinates as this `Rect`.
476    ///
477    /// # Examples
478    ///
479    /// ```
480    /// use ratatui_core::layout::Rect;
481    ///
482    /// let rect = Rect::new(1, 2, 3, 4);
483    /// let position = rect.as_position();
484    /// ````
485    pub const fn as_position(self) -> Position {
486        Position {
487            x: self.x,
488            y: self.y,
489        }
490    }
491
492    /// Converts the `Rect` into a size struct.
493    pub const fn as_size(self) -> Size {
494        Size {
495            width: self.width,
496            height: self.height,
497        }
498    }
499
500    /// Returns a new Rect, centered horizontally based on the provided constraint.
501    ///
502    /// # Examples
503    ///
504    /// ```
505    /// use ratatui_core::layout::Constraint;
506    /// use ratatui_core::terminal::Frame;
507    ///
508    /// fn render(frame: &mut Frame) {
509    ///     let area = frame.area().centered_horizontally(Constraint::Ratio(1, 2));
510    /// }
511    /// ```
512    #[must_use]
513    pub fn centered_horizontally(self, constraint: Constraint) -> Self {
514        let [area] = self.layout(&Layout::horizontal([constraint]).flex(Flex::Center));
515        area
516    }
517
518    /// Returns a new Rect, centered vertically based on the provided constraint.
519    ///
520    /// # Examples
521    ///
522    /// ```
523    /// use ratatui_core::layout::Constraint;
524    /// use ratatui_core::terminal::Frame;
525    ///
526    /// fn render(frame: &mut Frame) {
527    ///     let area = frame.area().centered_vertically(Constraint::Ratio(1, 2));
528    /// }
529    /// ```
530    #[must_use]
531    pub fn centered_vertically(self, constraint: Constraint) -> Self {
532        let [area] = self.layout(&Layout::vertical([constraint]).flex(Flex::Center));
533        area
534    }
535
536    /// Returns a new Rect, centered horizontally and vertically based on the provided constraints.
537    ///
538    /// # Examples
539    ///
540    /// ```
541    /// use ratatui_core::layout::Constraint;
542    /// use ratatui_core::terminal::Frame;
543    ///
544    /// fn render(frame: &mut Frame) {
545    ///     let area = frame
546    ///         .area()
547    ///         .centered(Constraint::Ratio(1, 2), Constraint::Ratio(1, 3));
548    /// }
549    /// ```
550    #[must_use]
551    pub fn centered(
552        self,
553        horizontal_constraint: Constraint,
554        vertical_constraint: Constraint,
555    ) -> Self {
556        self.centered_horizontally(horizontal_constraint)
557            .centered_vertically(vertical_constraint)
558    }
559
560    /// Split the rect into a number of sub-rects according to the given [`Layout`].
561    ///
562    /// An ergonomic wrapper around [`Layout::split`] that returns an array of `Rect`s instead of
563    /// `Rc<[Rect]>`.
564    ///
565    /// This method requires the number of constraints to be known at compile time. If you don't
566    /// know the number of constraints at compile time, use [`Layout::split`] instead.
567    ///
568    /// # Panics
569    ///
570    /// Panics if the number of constraints is not equal to the length of the returned array.
571    ///
572    /// # Examples
573    ///
574    /// ```
575    /// use ratatui_core::layout::{Constraint, Layout, Rect};
576    ///
577    /// let area = Rect::new(0, 0, 10, 10);
578    /// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
579    /// let [top, main] = area.layout(&layout);
580    /// assert_eq!(top, Rect::new(0, 0, 10, 1));
581    /// assert_eq!(main, Rect::new(0, 1, 10, 9));
582    ///
583    /// // or explicitly specify the number of constraints:
584    /// let areas = area.layout::<2>(&layout);
585    /// assert_eq!(areas, [Rect::new(0, 0, 10, 1), Rect::new(0, 1, 10, 9),]);
586    /// ```
587    #[must_use]
588    pub fn layout<const N: usize>(self, layout: &Layout) -> [Self; N] {
589        let areas = layout.split(self);
590        areas.as_ref().try_into().unwrap_or_else(|_| {
591            panic!(
592                "invalid number of rects: expected {N}, found {}",
593                areas.len()
594            )
595        })
596    }
597
598    /// Split the rect into a number of sub-rects according to the given [`Layout`].
599    ///
600    /// An ergonomic wrapper around [`Layout::split`] that returns a [`Vec`] of `Rect`s instead of
601    /// `Rc<[Rect]>`.
602    ///
603    /// # Examples
604    ///
605    /// ```
606    /// use ratatui_core::layout::{Constraint, Layout, Rect};
607    ///
608    /// let area = Rect::new(0, 0, 10, 10);
609    /// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
610    /// let areas = area.layout_vec(&layout);
611    /// assert_eq!(areas, vec![Rect::new(0, 0, 10, 1), Rect::new(0, 1, 10, 9),]);
612    /// ```
613    ///
614    /// [`Vec`]: alloc::vec::Vec
615    #[must_use]
616    pub fn layout_vec(self, layout: &Layout) -> alloc::vec::Vec<Self> {
617        layout.split(self).as_ref().to_vec()
618    }
619
620    /// Try to split the rect into a number of sub-rects according to the given [`Layout`].
621    ///
622    /// An ergonomic wrapper around [`Layout::split`] that returns an array of `Rect`s instead of
623    /// `Rc<[Rect]>`.
624    ///
625    /// # Errors
626    ///
627    /// Returns an error if the number of constraints is not equal to the length of the returned
628    /// array.
629    ///
630    /// # Examples
631    ///
632    /// ```
633    /// use ratatui_core::layout::{Constraint, Layout, Rect};
634    ///
635    /// let area = Rect::new(0, 0, 10, 10);
636    /// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
637    /// let [top, main] = area.try_layout(&layout)?;
638    /// assert_eq!(top, Rect::new(0, 0, 10, 1));
639    /// assert_eq!(main, Rect::new(0, 1, 10, 9));
640    ///
641    /// // or explicitly specify the number of constraints:
642    /// let areas = area.try_layout::<2>(&layout)?;
643    /// assert_eq!(areas, [Rect::new(0, 0, 10, 1), Rect::new(0, 1, 10, 9),]);
644    /// # Ok::<(), core::array::TryFromSliceError>(())
645    /// ``````
646    pub fn try_layout<const N: usize>(
647        self,
648        layout: &Layout,
649    ) -> Result<[Self; N], TryFromSliceError> {
650        layout.split(self).as_ref().try_into()
651    }
652
653    /// indents the x value of the `Rect` by a given `offset`
654    ///
655    /// This is pub(crate) for now as we need to stabilize the naming / design of this API.
656    #[must_use]
657    pub(crate) const fn indent_x(self, offset: u16) -> Self {
658        Self {
659            x: self.x.saturating_add(offset),
660            width: self.width.saturating_sub(offset),
661            ..self
662        }
663    }
664}
665
666impl From<(Position, Size)> for Rect {
667    fn from((position, size): (Position, Size)) -> Self {
668        Self {
669            x: position.x,
670            y: position.y,
671            width: size.width,
672            height: size.height,
673        }
674    }
675}
676
677impl From<Size> for Rect {
678    /// Creates a new `Rect` with the given size at [`Position::ORIGIN`] (0, 0).
679    fn from(size: Size) -> Self {
680        Self {
681            x: 0,
682            y: 0,
683            width: size.width,
684            height: size.height,
685        }
686    }
687}
688
689#[cfg(test)]
690mod tests {
691    use alloc::string::ToString;
692    use alloc::vec;
693    use alloc::vec::Vec;
694
695    use pretty_assertions::assert_eq;
696    use rstest::rstest;
697
698    use super::*;
699    use crate::layout::{Constraint, Layout};
700
701    #[test]
702    fn to_string() {
703        assert_eq!(Rect::new(1, 2, 3, 4).to_string(), "3x4+1+2");
704    }
705
706    #[test]
707    fn new() {
708        assert_eq!(
709            Rect::new(1, 2, 3, 4),
710            Rect {
711                x: 1,
712                y: 2,
713                width: 3,
714                height: 4
715            }
716        );
717    }
718
719    #[test]
720    fn area() {
721        assert_eq!(Rect::new(1, 2, 3, 4).area(), 12);
722    }
723
724    #[test]
725    fn is_empty() {
726        assert!(!Rect::new(1, 2, 3, 4).is_empty());
727        assert!(Rect::new(1, 2, 0, 4).is_empty());
728        assert!(Rect::new(1, 2, 3, 0).is_empty());
729    }
730
731    #[test]
732    fn left() {
733        assert_eq!(Rect::new(1, 2, 3, 4).left(), 1);
734    }
735
736    #[test]
737    fn right() {
738        assert_eq!(Rect::new(1, 2, 3, 4).right(), 4);
739    }
740
741    #[test]
742    fn top() {
743        assert_eq!(Rect::new(1, 2, 3, 4).top(), 2);
744    }
745
746    #[test]
747    fn bottom() {
748        assert_eq!(Rect::new(1, 2, 3, 4).bottom(), 6);
749    }
750
751    #[test]
752    fn inner() {
753        assert_eq!(
754            Rect::new(1, 2, 3, 4).inner(Margin::new(1, 2)),
755            Rect::new(2, 4, 1, 0)
756        );
757    }
758
759    #[test]
760    fn outer() {
761        // enough space to grow on all sides
762        assert_eq!(
763            Rect::new(100, 200, 10, 20).outer(Margin::new(20, 30)),
764            Rect::new(80, 170, 50, 80)
765        );
766
767        // left / top saturation should truncate the size (10 less on left / top)
768        assert_eq!(
769            Rect::new(10, 20, 10, 20).outer(Margin::new(20, 30)),
770            Rect::new(0, 0, 40, 70),
771        );
772
773        // right / bottom saturation should truncate the size (10 less on bottom / right)
774        assert_eq!(
775            Rect::new(u16::MAX - 20, u16::MAX - 40, 10, 20).outer(Margin::new(20, 30)),
776            Rect::new(u16::MAX - 40, u16::MAX - 70, 40, 70),
777        );
778    }
779
780    #[test]
781    fn offset() {
782        assert_eq!(
783            Rect::new(1, 2, 3, 4).offset(Offset { x: 5, y: 6 }),
784            Rect::new(6, 8, 3, 4),
785        );
786    }
787
788    #[test]
789    fn negative_offset() {
790        assert_eq!(
791            Rect::new(4, 3, 3, 4).offset(Offset { x: -2, y: -1 }),
792            Rect::new(2, 2, 3, 4),
793        );
794    }
795
796    #[test]
797    fn negative_offset_saturate() {
798        assert_eq!(
799            Rect::new(1, 2, 3, 4).offset(Offset { x: -5, y: -6 }),
800            Rect::new(0, 0, 3, 4),
801        );
802    }
803
804    /// Offsets a [`Rect`] making it go outside [`u16::MAX`], it should keep its size.
805    #[test]
806    fn offset_saturate_max() {
807        assert_eq!(
808            Rect::new(u16::MAX - 500, u16::MAX - 500, 100, 100).offset(Offset { x: 1000, y: 1000 }),
809            Rect::new(u16::MAX - 100, u16::MAX - 100, 100, 100),
810        );
811    }
812
813    #[test]
814    fn union() {
815        assert_eq!(
816            Rect::new(1, 2, 3, 4).union(Rect::new(2, 3, 4, 5)),
817            Rect::new(1, 2, 5, 6)
818        );
819    }
820
821    #[test]
822    fn intersection() {
823        assert_eq!(
824            Rect::new(1, 2, 3, 4).intersection(Rect::new(2, 3, 4, 5)),
825            Rect::new(2, 3, 2, 3)
826        );
827    }
828
829    #[test]
830    fn intersection_underflow() {
831        assert_eq!(
832            Rect::new(1, 1, 2, 2).intersection(Rect::new(4, 4, 2, 2)),
833            Rect::new(4, 4, 0, 0)
834        );
835    }
836
837    #[test]
838    fn intersects() {
839        assert!(Rect::new(1, 2, 3, 4).intersects(Rect::new(2, 3, 4, 5)));
840        assert!(!Rect::new(1, 2, 3, 4).intersects(Rect::new(5, 6, 7, 8)));
841    }
842
843    #[rstest]
844    #[case::corner(Rect::new(0, 0, 10, 10), Rect::new(10, 10, 20, 20))]
845    #[case::edge(Rect::new(0, 0, 10, 10), Rect::new(10, 0, 20, 10))]
846    #[case::no_intersect(Rect::new(0, 0, 10, 10), Rect::new(11, 11, 20, 20))]
847    #[case::contains(Rect::new(0, 0, 20, 20), Rect::new(5, 5, 10, 10))]
848    fn mutual_intersect(#[case] rect0: Rect, #[case] rect1: Rect) {
849        assert_eq!(rect0.intersection(rect1), rect1.intersection(rect0));
850        assert_eq!(rect0.intersects(rect1), rect1.intersects(rect0));
851    }
852
853    // the bounds of this rect are x: [1..=3], y: [2..=5]
854    #[rstest]
855    #[case::inside_top_left(Rect::new(1, 2, 3, 4), Position { x: 1, y: 2 }, true)]
856    #[case::inside_top_right(Rect::new(1, 2, 3, 4), Position { x: 3, y: 2 }, true)]
857    #[case::inside_bottom_left(Rect::new(1, 2, 3, 4), Position { x: 1, y: 5 }, true)]
858    #[case::inside_bottom_right(Rect::new(1, 2, 3, 4), Position { x: 3, y: 5 }, true)]
859    #[case::outside_left(Rect::new(1, 2, 3, 4), Position { x: 0, y: 2 }, false)]
860    #[case::outside_right(Rect::new(1, 2, 3, 4), Position { x: 4, y: 2 }, false)]
861    #[case::outside_top(Rect::new(1, 2, 3, 4), Position { x: 1, y: 1 }, false)]
862    #[case::outside_bottom(Rect::new(1, 2, 3, 4), Position { x: 1, y: 6 }, false)]
863    #[case::outside_top_left(Rect::new(1, 2, 3, 4), Position { x: 0, y: 1 }, false)]
864    #[case::outside_bottom_right(Rect::new(1, 2, 3, 4), Position { x: 4, y: 6 }, false)]
865    fn contains(#[case] rect: Rect, #[case] position: Position, #[case] expected: bool) {
866        assert_eq!(
867            rect.contains(position),
868            expected,
869            "rect: {rect:?}, position: {position:?}",
870        );
871    }
872
873    #[test]
874    fn size_truncation() {
875        assert_eq!(
876            Rect::new(u16::MAX - 100, u16::MAX - 1000, 200, 2000),
877            Rect {
878                x: u16::MAX - 100,
879                y: u16::MAX - 1000,
880                width: 100,
881                height: 1000
882            }
883        );
884    }
885
886    #[test]
887    fn size_preservation() {
888        assert_eq!(
889            Rect::new(u16::MAX - 100, u16::MAX - 1000, 100, 1000),
890            Rect {
891                x: u16::MAX - 100,
892                y: u16::MAX - 1000,
893                width: 100,
894                height: 1000
895            }
896        );
897    }
898
899    #[test]
900    fn resize_updates_size() {
901        let rect = Rect::new(10, 20, 5, 5).resize(Size::new(30, 40));
902        assert_eq!(rect, Rect::new(10, 20, 30, 40));
903    }
904
905    #[test]
906    fn resize_clamps_at_bounds() {
907        let rect = Rect::new(u16::MAX - 2, u16::MAX - 3, 1, 1).resize(Size::new(10, 10));
908        assert_eq!(rect, Rect::new(u16::MAX - 2, u16::MAX - 3, 2, 3));
909    }
910
911    #[test]
912    fn can_be_const() {
913        const RECT: Rect = Rect {
914            x: 0,
915            y: 0,
916            width: 10,
917            height: 10,
918        };
919        const _AREA: u32 = RECT.area();
920        const _LEFT: u16 = RECT.left();
921        const _RIGHT: u16 = RECT.right();
922        const _TOP: u16 = RECT.top();
923        const _BOTTOM: u16 = RECT.bottom();
924        assert!(RECT.intersects(RECT));
925    }
926
927    #[test]
928    fn split() {
929        let [a, b] = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
930            .areas(Rect::new(0, 0, 2, 1));
931        assert_eq!(a, Rect::new(0, 0, 1, 1));
932        assert_eq!(b, Rect::new(1, 0, 1, 1));
933    }
934
935    #[test]
936    #[should_panic(expected = "invalid number of rects")]
937    fn split_invalid_number_of_recs() {
938        let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
939        let [_a, _b, _c] = layout.areas(Rect::new(0, 0, 2, 1));
940    }
941
942    #[rstest]
943    #[case::inside(Rect::new(20, 20, 10, 10), Rect::new(20, 20, 10, 10))]
944    #[case::up_left(Rect::new(5, 5, 10, 10), Rect::new(10, 10, 10, 10))]
945    #[case::up(Rect::new(20, 5, 10, 10), Rect::new(20, 10, 10, 10))]
946    #[case::up_right(Rect::new(105, 5, 10, 10), Rect::new(100, 10, 10, 10))]
947    #[case::left(Rect::new(5, 20, 10, 10), Rect::new(10, 20, 10, 10))]
948    #[case::right(Rect::new(105, 20, 10, 10), Rect::new(100, 20, 10, 10))]
949    #[case::down_left(Rect::new(5, 105, 10, 10), Rect::new(10, 100, 10, 10))]
950    #[case::down(Rect::new(20, 105, 10, 10), Rect::new(20, 100, 10, 10))]
951    #[case::down_right(Rect::new(105, 105, 10, 10), Rect::new(100, 100, 10, 10))]
952    #[case::too_wide(Rect::new(5, 20, 200, 10), Rect::new(10, 20, 100, 10))]
953    #[case::too_tall(Rect::new(20, 5, 10, 200), Rect::new(20, 10, 10, 100))]
954    #[case::too_large(Rect::new(0, 0, 200, 200), Rect::new(10, 10, 100, 100))]
955    fn clamp(#[case] rect: Rect, #[case] expected: Rect) {
956        let other = Rect::new(10, 10, 100, 100);
957        assert_eq!(rect.clamp(other), expected);
958    }
959
960    #[test]
961    fn rows() {
962        let area = Rect::new(0, 0, 3, 2);
963        let rows: Vec<Rect> = area.rows().collect();
964
965        let expected_rows: Vec<Rect> = vec![Rect::new(0, 0, 3, 1), Rect::new(0, 1, 3, 1)];
966
967        assert_eq!(rows, expected_rows);
968    }
969
970    #[test]
971    fn columns() {
972        let area = Rect::new(0, 0, 3, 2);
973        let columns: Vec<Rect> = area.columns().collect();
974
975        let expected_columns: Vec<Rect> = vec![
976            Rect::new(0, 0, 1, 2),
977            Rect::new(1, 0, 1, 2),
978            Rect::new(2, 0, 1, 2),
979        ];
980
981        assert_eq!(columns, expected_columns);
982    }
983
984    #[test]
985    fn as_position() {
986        let rect = Rect::new(1, 2, 3, 4);
987        let position = rect.as_position();
988        assert_eq!(position.x, 1);
989        assert_eq!(position.y, 2);
990    }
991
992    #[test]
993    fn as_size() {
994        assert_eq!(
995            Rect::new(1, 2, 3, 4).as_size(),
996            Size {
997                width: 3,
998                height: 4
999            }
1000        );
1001    }
1002
1003    #[test]
1004    fn from_position_and_size() {
1005        let position = Position { x: 1, y: 2 };
1006        let size = Size {
1007            width: 3,
1008            height: 4,
1009        };
1010        assert_eq!(
1011            Rect::from((position, size)),
1012            Rect {
1013                x: 1,
1014                y: 2,
1015                width: 3,
1016                height: 4
1017            }
1018        );
1019    }
1020
1021    #[test]
1022    fn from_size() {
1023        let size = Size {
1024            width: 3,
1025            height: 4,
1026        };
1027        assert_eq!(
1028            Rect::from(size),
1029            Rect {
1030                x: 0,
1031                y: 0,
1032                width: 3,
1033                height: 4
1034            }
1035        );
1036    }
1037
1038    #[test]
1039    fn centered_horizontally() {
1040        let rect = Rect::new(0, 0, 5, 5);
1041        assert_eq!(
1042            rect.centered_horizontally(Constraint::Length(3)),
1043            Rect::new(1, 0, 3, 5)
1044        );
1045    }
1046
1047    #[test]
1048    fn centered_vertically() {
1049        let rect = Rect::new(0, 0, 5, 5);
1050        assert_eq!(
1051            rect.centered_vertically(Constraint::Length(1)),
1052            Rect::new(0, 2, 5, 1)
1053        );
1054    }
1055
1056    #[test]
1057    fn centered() {
1058        let rect = Rect::new(0, 0, 5, 5);
1059        assert_eq!(
1060            rect.centered(Constraint::Length(3), Constraint::Length(1)),
1061            Rect::new(1, 2, 3, 1)
1062        );
1063    }
1064
1065    #[test]
1066    fn layout() {
1067        let layout = Layout::horizontal([Constraint::Length(3), Constraint::Min(0)]);
1068
1069        let [a, b] = Rect::new(0, 0, 10, 10).layout(&layout);
1070        assert_eq!(a, Rect::new(0, 0, 3, 10));
1071        assert_eq!(b, Rect::new(3, 0, 7, 10));
1072
1073        let areas = Rect::new(0, 0, 10, 10).layout::<2>(&layout);
1074        assert_eq!(areas[0], Rect::new(0, 0, 3, 10));
1075        assert_eq!(areas[1], Rect::new(3, 0, 7, 10));
1076    }
1077
1078    #[test]
1079    #[should_panic(expected = "invalid number of rects: expected 3, found 1")]
1080    fn layout_invalid_number_of_rects() {
1081        let layout = Layout::horizontal([Constraint::Length(1)]);
1082        let [_, _, _] = Rect::new(0, 0, 10, 10).layout(&layout);
1083    }
1084
1085    #[test]
1086    fn layout_vec() {
1087        let layout = Layout::horizontal([Constraint::Length(3), Constraint::Min(0)]);
1088
1089        let areas = Rect::new(0, 0, 10, 10).layout_vec(&layout);
1090        assert_eq!(areas[0], Rect::new(0, 0, 3, 10));
1091        assert_eq!(areas[1], Rect::new(3, 0, 7, 10));
1092    }
1093
1094    #[test]
1095    fn try_layout() {
1096        let layout = Layout::horizontal([Constraint::Length(3), Constraint::Min(0)]);
1097
1098        let [a, b] = Rect::new(0, 0, 10, 10).try_layout(&layout).unwrap();
1099        assert_eq!(a, Rect::new(0, 0, 3, 10));
1100        assert_eq!(b, Rect::new(3, 0, 7, 10));
1101
1102        let areas = Rect::new(0, 0, 10, 10).try_layout::<2>(&layout).unwrap();
1103        assert_eq!(areas[0], Rect::new(0, 0, 3, 10));
1104        assert_eq!(areas[1], Rect::new(3, 0, 7, 10));
1105    }
1106
1107    #[test]
1108    fn try_layout_invalid_number_of_rects() {
1109        let layout = Layout::horizontal([Constraint::Length(1)]);
1110        Rect::new(0, 0, 10, 10)
1111            .try_layout::<3>(&layout)
1112            .unwrap_err();
1113    }
1114}