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}