// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A rectangle given by its corner and its size: the shape a layout hands out.
///
/// A layout thinks in "here, this big", not in two opposite corners, which is
/// why this is not `Aabb2` — the two describe the same set of points and answer
/// different questions. `origin` is the corner with the smaller coordinates and
/// `size` the extent from it, so a `size` with a negative component describes
/// the same points as the equivalent positive one, with the corners swapped.
///
/// Values are immutable.
#derive(Eq)
pub(all) struct Rect {
/// The corner with the smallest coordinate on each axis.
origin : Vec2
/// How far it extends along each axis; never negative.
size : Vec2
}
///|
pub extend Rect with Eq::{equal, not_equal}
///|
/// The rectangle at `origin` extending `size`.
pub fn Rect::new(origin : Vec2, size : Vec2) -> Rect {
{ origin, size, }
}
///|
/// The rectangle with its top-left corner at `x`, `y` and the given extent.
///
/// The usual spelling for a layout node; which way is "top" is the caller's
/// convention, as everywhere else here.
pub fn Rect::from_xywh(
x : Float,
y : Float,
width : Float,
height : Float,
) -> Rect {
{ origin: { x, y, }, size: { x: width, y: height, }, }
}
///|
/// The rectangle spanning two opposite corners, in either order.
pub fn Rect::from_edges(
left : Float,
top : Float,
right : Float,
bottom : Float,
) -> Rect {
{
origin: { x: left.min(right), y: top.min(bottom), },
size: { x: (right - left).abs(), y: (bottom - top).abs(), },
}
}
///|
/// The corner with the smaller coordinates.
pub fn Rect::min(self : Rect) -> Vec2 {
{
x: self.origin.x.min(self.origin.x + self.size.x),
y: self.origin.y.min(self.origin.y + self.size.y),
}
}
///|
/// The corner with the larger coordinates.
pub fn Rect::max(self : Rect) -> Vec2 {
{
x: self.origin.x.max(self.origin.x + self.size.x),
y: self.origin.y.max(self.origin.y + self.size.y),
}
}
///|
/// The point in the middle.
pub fn Rect::center(self : Rect) -> Vec2 {
let half : Float = 0.5
self.origin + self.size.scale(half)
}
///|
/// The extent along `x`, always non-negative.
pub fn Rect::width(self : Rect) -> Float {
self.size.x.abs()
}
///|
/// The extent along `y`, always non-negative.
pub fn Rect::height(self : Rect) -> Float {
self.size.y.abs()
}
///|
/// The area the rectangle covers.
pub fn Rect::area(self : Rect) -> Float {
self.width() * self.height()
}
///|
/// Whether the rectangle covers no area at all.
///
/// A zero width or height is empty: it is a line or a point, and nothing inside
/// it can be drawn or hit.
pub fn Rect::is_empty(self : Rect) -> Bool {
self.width() == 0.0 || self.height() == 0.0
}
///|
/// Whether the point is inside or on the edge.
pub fn Rect::contains(self : Rect, point : Vec2) -> Bool {
let low = self.min()
let high = self.max()
point.x >= low.x && point.x <= high.x && point.y >= low.y && point.y <= high.y
}
///|
/// Whether the two rectangles share any area.
///
/// Touching edges share no area and do not count; an empty rectangle intersects
/// nothing.
pub fn Rect::intersects(self : Rect, other : Rect) -> Bool {
if self.is_empty() || other.is_empty() {
false
} else {
let a0 = self.min()
let a1 = self.max()
let b0 = other.min()
let b1 = other.max()
a0.x < b1.x && b0.x < a1.x && a0.y < b1.y && b0.y < a1.y
}
}
///|
/// The area the two share, or `None` when they share none.
pub fn Rect::intersection(self : Rect, other : Rect) -> Rect? {
let a0 = self.min()
let a1 = self.max()
let b0 = other.min()
let b1 = other.max()
let low : Vec2 = { x: a0.x.max(b0.x), y: a0.y.max(b0.y), }
let high : Vec2 = { x: a1.x.min(b1.x), y: a1.y.min(b1.y), }
if high.x <= low.x || high.y <= low.y {
None
} else {
Some({ origin: low, size: high - low, })
}
}
///|
/// The smallest rectangle containing both.
pub fn Rect::union(self : Rect, other : Rect) -> Rect {
let low = self.min().min(other.min())
let high = self.max().max(other.max())
{ origin: low, size: high - low, }
}
///|
/// The same rectangle with every edge moved in by `amount`.
///
/// A positive amount shrinks it, a negative one grows it; shrinking past the
/// middle leaves an empty rectangle at the centre rather than a negative one.
pub fn Rect::inset(self : Rect, amount : Float) -> Rect {
let twice : Float = 2.0
let width = self.width() - twice * amount
let height = self.height() - twice * amount
let center = self.center()
let size : Vec2 = { x: width.max(0.0), y: height.max(0.0), }
let half : Float = 0.5
{ origin: center - size.scale(half), size, }
}
///|
/// The same rectangle moved by `offset`.
pub fn Rect::translate(self : Rect, offset : Vec2) -> Rect {
{ origin: self.origin + offset, size: self.size, }
}
///|
/// The same points as an axis-aligned box.
pub fn Rect::to_aabb(self : Rect) -> Aabb2 {
{ min: self.min(), max: self.max(), }
}
///|
/// The four corners, starting at `origin` and going counter-clockwise.
pub fn Rect::corners(self : Rect) -> FixedArray[Vec2] {
let low = self.min()
let high = self.max()
[low, { x: high.x, y: low.y, }, high, { x: low.x, y: high.y, }]
}
///|
/// Whether the two rectangles cover the same points to within `eps`.
pub fn Rect::approx(self : Rect, other : Rect, eps? : Float) -> Bool {
self.min().approx(other.min(), eps?) && self.max().approx(other.max(), eps?)
}
///|
pub impl Show for Rect with fn output(self, logger) {
logger.write_string("Rect(\{self.origin}, \{self.size})")
}
///|
pub extend Rect with Show::{output, to_string}
///|
pub impl Debug for Rect with fn to_repr(self) {
@debug.Repr::ctor("Rect", [
(Some("origin"), self.origin.to_repr()),
(Some("size"), self.size.to_repr()),
])
}
///|
pub extend Rect with Debug::{to_repr}