Compare commits

...

1 Commits

Author SHA1 Message Date
Josh McKinney
22f56ac43a feat(layout): support per-side insets via Rect::inner
- introduce Inset for side-specific shrinking with symmetric/horizontal/vertical helpers and Margin conversion
- update Rect::inner to accept Into<Inset>
2025-12-06 14:36:57 -08:00
5 changed files with 266 additions and 12 deletions

View File

@@ -20,6 +20,7 @@ This is a quick summary of the sections below:
- 'layout::Alignment' is renamed to 'layout::HorizontalAlignment'
- MSRV is now 1.86.0
- `Backend` now requires an associated `Error` type and `clear_region` method
- `Rect::inner` now takes `Into<Inset>` instead of `Margin`
- `TestBackend` now uses `core::convert::Infallible` for error handling instead of `std::io::Error`
- Disabling `default-features` will now disable layout cache, which can have a negative impact on performance
- `Layout::init_cache` and `Layout::DEFAULT_CACHE_SIZE` are now only available if `layout-cache`
@@ -126,6 +127,13 @@ behavior can be achieved by using `Flex::SpaceEvenly` instead.
+ let rects = Layout::horizontal([Length(1), Length(2)]).flex(Flex::SpaceEvenly).split(area);
```
### `Rect::inner` now takes `Into<Inset>` instead of `Margin`
`Rect::inner` accepts any type that converts into `Inset` (for example `Inset` or `Margin`). Calls
that relied on inference for a `Margin` conversion may now need an explicit `Margin::new` (or
`Inset::trbl`) to make type inference succeed. This change also removes `const` support for
`Rect::inner`, so calls in const contexts need to move to runtime code.
### `block::Title` no longer exists ([#1926])
[#1926]: https://github.com/ratatui/ratatui/pull/1926

View File

@@ -62,6 +62,7 @@
//! - [`Position`] - Represents a point in the terminal coordinate system
//! - [`Size`] - Represents dimensions (width and height)
//! - [`Margin`] - Defines spacing around rectangular areas
//! - [`Inset`] - Defines side-specific spacing inside a rectangle
//! - [`Offset`] - Represents relative movement in the coordinate system
//! - [`Spacing`] - Controls spacing or overlap between layout segments
//!
@@ -314,6 +315,7 @@ mod alignment;
mod constraint;
mod direction;
mod flex;
mod inset;
mod layout;
mod margin;
mod offset;
@@ -325,6 +327,7 @@ pub use alignment::{Alignment, HorizontalAlignment, VerticalAlignment};
pub use constraint::Constraint;
pub use direction::Direction;
pub use flex::Flex;
pub use inset::Inset;
pub use layout::{Layout, Spacing};
pub use margin::Margin;
pub use offset::Offset;

View File

@@ -0,0 +1,189 @@
#![warn(missing_docs)]
use core::fmt;
use crate::layout::Margin;
/// Represents side-specific spacing inside rectangular areas.
///
/// `Inset` defines how much space to remove from each side of a rectangle. Unlike [`Margin`], which
/// applies uniform spacing horizontally and vertically, `Inset` lets you specify independent
/// amounts for the top, right, bottom, and left edges. The default constructor order is
/// top-right-bottom-left (often remembered as “trbl” or “trouble”), matching the CSS spec and
/// offering an easy clockwise mnemonic.
///
/// Use `Inset` when you need per-side control; choose [`Margin`](crate::layout::Margin) for the
/// common symmetric case.
///
/// # Construction
///
/// - [`trbl`](Self::trbl) - Create a new inset with top/right/bottom/left values
/// - [`default`](Default::default) - Create with zero inset on all sides
/// - [`symmetric`](Self::symmetric) - Create with shared horizontal and vertical values
/// - [`horizontal`](Self::horizontal) - Create with equal left and right values
/// - [`vertical`](Self::vertical) - Create with equal top and bottom values
///
/// # Examples
///
/// ```rust
/// use ratatui_core::layout::{Inset, Rect};
///
/// let inset = Inset::trbl(1, 2, 3, 4);
/// let rect = Rect::new(0, 0, 10, 10).inner(inset);
/// assert_eq!(rect, Rect::new(4, 1, 4, 6));
/// ```
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Inset {
/// Space to remove from the top edge
pub top: u16,
/// Space to remove from the right edge
pub right: u16,
/// Space to remove from the bottom edge
pub bottom: u16,
/// Space to remove from the left edge
pub left: u16,
}
impl Inset {
/// Creates a new inset with explicit top/right/bottom/left values.
pub const fn trbl(top: u16, right: u16, bottom: u16, left: u16) -> Self {
Self {
top,
right,
bottom,
left,
}
}
/// Creates a new inset with shared horizontal and vertical values.
///
/// The `horizontal` value is applied to `left` and `right`; the `vertical` value is applied to
/// `top` and `bottom`. Note the order is `horizontal, vertical` (x then y), opposite of the CSS
/// ordering; we keep a single ordering across helpers to avoid mixing patterns in code.
pub const fn symmetric(horizontal: u16, vertical: u16) -> Self {
Self {
right: horizontal,
left: horizontal,
top: vertical,
bottom: vertical,
}
}
/// Creates a new inset with equal left and right values.
pub const fn horizontal(horizontal: u16) -> Self {
Self {
top: 0,
right: horizontal,
bottom: 0,
left: horizontal,
}
}
/// Creates a new inset with equal top and bottom values.
pub const fn vertical(vertical: u16) -> Self {
Self {
top: vertical,
right: 0,
bottom: vertical,
left: 0,
}
}
}
impl fmt::Display for Inset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"t{} r{} b{} l{}",
self.top, self.right, self.bottom, self.left
)
}
}
impl From<Margin> for Inset {
fn from(margin: Margin) -> Self {
Self {
top: margin.vertical,
right: margin.horizontal,
bottom: margin.vertical,
left: margin.horizontal,
}
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use super::*;
#[test]
fn new() {
assert_eq!(
Inset::trbl(1, 2, 3, 4),
Inset {
top: 1,
right: 2,
bottom: 3,
left: 4
}
);
}
#[test]
fn display() {
assert_eq!(Inset::trbl(1, 2, 3, 4).to_string(), "t1 r2 b3 l4");
}
#[test]
fn symmetric() {
assert_eq!(
Inset::symmetric(2, 3),
Inset {
top: 3,
right: 2,
bottom: 3,
left: 2
}
);
}
#[test]
fn horizontal() {
assert_eq!(
Inset::horizontal(2),
Inset {
top: 0,
right: 2,
bottom: 0,
left: 2
}
);
}
#[test]
fn vertical() {
assert_eq!(
Inset::vertical(3),
Inset {
top: 3,
right: 0,
bottom: 3,
left: 0
}
);
}
#[test]
fn from_margin() {
assert_eq!(
Inset::from(Margin::new(2, 3)),
Inset {
top: 3,
right: 2,
bottom: 3,
left: 2
}
);
}
}

View File

@@ -11,6 +11,9 @@ use core::fmt;
/// margin, the space is applied to both the left and right sides. For vertical margin, the space
/// is applied to both the top and bottom sides.
///
/// Use [`Inset`](crate::layout::Inset) when you need different values for each side; `Margin`
/// provides the symmetric case.
///
/// # Construction
///
/// - [`new`](Self::new) - Create a new margin with horizontal and vertical spacing

View File

@@ -4,7 +4,7 @@ use core::cmp::{max, min};
use core::fmt;
pub use self::iter::{Columns, Positions, Rows};
use crate::layout::{Margin, Offset, Position, Size};
use crate::layout::{Inset, Margin, Offset, Position, Size};
mod iter;
mod ops;
@@ -44,7 +44,7 @@ use super::{Constraint, Flex, Layout};
///
/// # Spatial Operations
///
/// - [`inner`](Self::inner), [`outer`](Self::outer) - Apply margins to shrink or expand
/// - [`inner`](Self::inner), [`outer`](Self::outer) - Apply insets or margins to shrink or expand
/// - [`offset`](Self::offset) - Move the rectangle by a relative amount
/// - [`resize`](Self::resize) - Change the rectangle size while keeping the bottom/right in range
/// - [`union`](Self::union) - Combine with another rectangle to create a bounding box
@@ -127,6 +127,15 @@ use super::{Constraint, Flex, Layout};
/// assert_eq!(rect, Rect::new(u16::MAX - 1, u16::MAX - 1, 1, 1));
/// ```
///
/// To inset a `Rect` with different values on each side, use [`Rect::inner`] with an [`Inset`].
///
/// ```rust
/// use ratatui_core::layout::{Inset, Rect};
///
/// let rect = Rect::new(0, 0, 10, 10).inner(Inset::trbl(1, 2, 3, 4));
/// assert_eq!(rect, Rect::new(4, 1, 4, 6));
/// ```
///
/// For comprehensive layout documentation and examples, see the [`layout`](crate::layout) module.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
@@ -225,22 +234,48 @@ impl Rect {
self.y.saturating_add(self.height)
}
/// Returns a new `Rect` inside the current one, with the given margin on each side.
/// Returns a new `Rect` inside the current one, with the given inset on each side.
///
/// If the margin is larger than the `Rect`, the returned `Rect` will have no area.
/// Accepts any type that can convert into an [`Inset`], including [`Margin`]. If the inset is
/// larger than the `Rect`, the returned `Rect` will have no area.
///
/// # Examples
///
/// Using a margin (shared horizontal and vertical):
///
/// ```rust
/// use ratatui_core::layout::{Margin, Rect};
///
/// let area = Rect::new(0, 0, 10, 6);
/// let inner = area.inner(Margin::new(1, 2));
///
/// assert_eq!(inner, Rect::new(1, 2, 8, 2));
/// ```
///
/// Using an inset with side-specific values:
///
/// ```rust
/// use ratatui_core::layout::{Inset, Rect};
///
/// let area = Rect::new(0, 0, 10, 6);
/// let inner = area.inner(Inset::trbl(1, 2, 3, 4));
///
/// assert_eq!(inner, Rect::new(4, 1, 4, 2));
/// ```
#[must_use = "method returns the modified value"]
pub const fn inner(self, margin: Margin) -> Self {
let doubled_margin_horizontal = margin.horizontal.saturating_mul(2);
let doubled_margin_vertical = margin.vertical.saturating_mul(2);
pub fn inner<I: Into<Inset>>(self, inset: I) -> Self {
let inset = inset.into();
let total_horizontal = inset.left.saturating_add(inset.right);
let total_vertical = inset.top.saturating_add(inset.bottom);
if self.width < doubled_margin_horizontal || self.height < doubled_margin_vertical {
if self.width < total_horizontal || self.height < total_vertical {
Self::ZERO
} else {
Self {
x: self.x.saturating_add(margin.horizontal),
y: self.y.saturating_add(margin.vertical),
width: self.width.saturating_sub(doubled_margin_horizontal),
height: self.height.saturating_sub(doubled_margin_vertical),
x: self.x.saturating_add(inset.left),
y: self.y.saturating_add(inset.top),
width: self.width.saturating_sub(total_horizontal),
height: self.height.saturating_sub(total_vertical),
}
}
}
@@ -777,6 +812,22 @@ mod tests {
);
}
#[test]
fn inner_supports_inset() {
assert_eq!(
Rect::new(10, 20, 50, 60).inner(Inset::trbl(1, 2, 3, 4)),
Rect::new(14, 21, 44, 56),
);
}
#[test]
fn inner_inset_zero_on_overflow() {
assert_eq!(
Rect::new(0, 0, 2, 2).inner(Inset::trbl(1, 1, 2, 2)),
Rect::ZERO,
);
}
#[test]
fn offset() {
assert_eq!(