use crate::style::Overflow;
use crate::tree::LayoutRect;
use astrelis_core::math::Vec2;
#[derive(Debug, Clone, Copy)]
pub struct ClipRect {
pub min: Vec2,
pub max: Vec2,
}
impl ClipRect {
pub fn infinite() -> Self {
Self {
min: Vec2::new(f32::NEG_INFINITY, f32::NEG_INFINITY),
max: Vec2::new(f32::INFINITY, f32::INFINITY),
}
}
pub fn from_bounds(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
min: Vec2::new(x, y),
max: Vec2::new(x + width, y + height),
}
}
pub fn from_layout(layout: &LayoutRect) -> Self {
Self::from_bounds(layout.x, layout.y, layout.width, layout.height)
}
pub fn from_min_max(min: Vec2, max: Vec2) -> Self {
Self { min, max }
}
pub fn is_infinite(&self) -> bool {
self.min.x == f32::NEG_INFINITY
|| self.min.y == f32::NEG_INFINITY
|| self.max.x == f32::INFINITY
|| self.max.y == f32::INFINITY
}
pub fn contains(&self, point: Vec2) -> bool {
point.x >= self.min.x
&& point.x <= self.max.x
&& point.y >= self.min.y
&& point.y <= self.max.y
}
pub fn intersects(&self, other: &ClipRect) -> bool {
self.min.x < other.max.x
&& self.max.x > other.min.x
&& self.min.y < other.max.y
&& self.max.y > other.min.y
}
pub fn intersect(&self, other: &ClipRect) -> ClipRect {
ClipRect {
min: Vec2::new(self.min.x.max(other.min.x), self.min.y.max(other.min.y)),
max: Vec2::new(self.max.x.min(other.max.x), self.max.y.min(other.max.y)),
}
}
pub fn width(&self) -> f32 {
(self.max.x - self.min.x).max(0.0)
}
pub fn height(&self) -> f32 {
(self.max.y - self.min.y).max(0.0)
}
pub fn has_area(&self) -> bool {
self.width() > 0.0 && self.height() > 0.0
}
pub fn to_physical(&self, scale_factor: f64) -> PhysicalClipRect {
let scale = scale_factor as f32;
let x = (self.min.x * scale).round() as u32;
let y = (self.min.y * scale).round() as u32;
let x_max = (self.max.x * scale).floor() as u32;
let y_max = (self.max.y * scale).floor() as u32;
PhysicalClipRect {
x,
y,
width: x_max.saturating_sub(x),
height: y_max.saturating_sub(y),
}
}
}
impl Default for ClipRect {
fn default() -> Self {
Self::infinite()
}
}
impl PartialEq for ClipRect {
fn eq(&self, other: &Self) -> bool {
self.min.x.to_bits() == other.min.x.to_bits()
&& self.min.y.to_bits() == other.min.y.to_bits()
&& self.max.x.to_bits() == other.max.x.to_bits()
&& self.max.y.to_bits() == other.max.y.to_bits()
}
}
impl Eq for ClipRect {}
impl std::hash::Hash for ClipRect {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.min.x.to_bits().hash(state);
self.min.y.to_bits().hash(state);
self.max.x.to_bits().hash(state);
self.max.y.to_bits().hash(state);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PhysicalClipRect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl PhysicalClipRect {
pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
Self {
x,
y,
width,
height,
}
}
pub fn clamp_to_viewport(&self, viewport_width: u32, viewport_height: u32) -> Self {
let x = self.x.min(viewport_width);
let y = self.y.min(viewport_height);
let width = self.width.min(viewport_width.saturating_sub(x));
let height = self.height.min(viewport_height.saturating_sub(y));
Self {
x,
y,
width,
height,
}
}
}
pub fn should_clip(overflow_x: Overflow, overflow_y: Overflow) -> bool {
matches!(
overflow_x,
Overflow::Hidden | Overflow::Scroll | Overflow::Auto
) || matches!(
overflow_y,
Overflow::Hidden | Overflow::Scroll | Overflow::Auto
)
}
pub fn compute_clip_rect(
node_layout: &LayoutRect,
overflow_x: Overflow,
overflow_y: Overflow,
parent_clip: Option<ClipRect>,
) -> ClipRect {
let mut clip = parent_clip.unwrap_or_else(ClipRect::infinite);
if should_clip(overflow_x, overflow_y) {
let node_clip = ClipRect::from_layout(node_layout);
clip = clip.intersect(&node_clip);
}
clip
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clip_rect_from_bounds() {
let clip = ClipRect::from_bounds(10.0, 20.0, 100.0, 50.0);
assert_eq!(clip.min, Vec2::new(10.0, 20.0));
assert_eq!(clip.max, Vec2::new(110.0, 70.0));
assert_eq!(clip.width(), 100.0);
assert_eq!(clip.height(), 50.0);
}
#[test]
fn test_clip_rect_contains() {
let clip = ClipRect::from_bounds(0.0, 0.0, 100.0, 100.0);
assert!(clip.contains(Vec2::new(50.0, 50.0)));
assert!(clip.contains(Vec2::new(0.0, 0.0)));
assert!(clip.contains(Vec2::new(100.0, 100.0)));
assert!(!clip.contains(Vec2::new(101.0, 50.0)));
assert!(!clip.contains(Vec2::new(-1.0, 50.0)));
}
#[test]
fn test_clip_rect_intersect() {
let a = ClipRect::from_bounds(0.0, 0.0, 100.0, 100.0);
let b = ClipRect::from_bounds(50.0, 50.0, 100.0, 100.0);
let intersection = a.intersect(&b);
assert_eq!(intersection.min, Vec2::new(50.0, 50.0));
assert_eq!(intersection.max, Vec2::new(100.0, 100.0));
assert_eq!(intersection.width(), 50.0);
assert_eq!(intersection.height(), 50.0);
}
#[test]
fn test_clip_rect_no_intersection() {
let a = ClipRect::from_bounds(0.0, 0.0, 50.0, 50.0);
let b = ClipRect::from_bounds(100.0, 100.0, 50.0, 50.0);
let intersection = a.intersect(&b);
assert!(!intersection.has_area());
}
#[test]
fn test_clip_rect_infinite() {
let infinite = ClipRect::infinite();
assert!(infinite.is_infinite());
let finite = ClipRect::from_bounds(0.0, 0.0, 100.0, 100.0);
assert!(!finite.is_infinite());
let intersection = infinite.intersect(&finite);
assert_eq!(intersection.min, finite.min);
assert_eq!(intersection.max, finite.max);
}
#[test]
fn test_clip_rect_to_physical() {
let clip = ClipRect::from_bounds(10.5, 20.5, 100.0, 50.0);
let physical = clip.to_physical(2.0);
assert_eq!(physical.x, 21);
assert_eq!(physical.y, 41);
assert_eq!(physical.width, 200);
assert_eq!(physical.height, 100);
}
#[test]
fn test_clip_rect_to_physical_no_overshoot() {
let clip = ClipRect::from_bounds(0.0, 0.0, 550.25, 550.25);
let physical = clip.to_physical(2.0);
assert_eq!(physical.x, 0);
assert_eq!(physical.y, 0);
assert_eq!(physical.width, 1100);
assert_eq!(physical.height, 1100);
}
#[test]
fn test_physical_clip_rect_clamp() {
let physical = PhysicalClipRect::new(100, 100, 500, 500);
let clamped = physical.clamp_to_viewport(400, 300);
assert_eq!(clamped.x, 100);
assert_eq!(clamped.y, 100);
assert_eq!(clamped.width, 300); assert_eq!(clamped.height, 200); }
#[test]
fn test_should_clip() {
assert!(!should_clip(Overflow::Visible, Overflow::Visible));
assert!(should_clip(Overflow::Hidden, Overflow::Visible));
assert!(should_clip(Overflow::Visible, Overflow::Hidden));
assert!(should_clip(Overflow::Hidden, Overflow::Hidden));
assert!(should_clip(Overflow::Scroll, Overflow::Visible));
assert!(should_clip(Overflow::Auto, Overflow::Visible));
}
#[test]
fn test_compute_clip_rect() {
let layout = LayoutRect {
x: 100.0,
y: 100.0,
width: 200.0,
height: 150.0,
};
let clip = compute_clip_rect(&layout, Overflow::Visible, Overflow::Visible, None);
assert!(clip.is_infinite());
let clip = compute_clip_rect(&layout, Overflow::Hidden, Overflow::Hidden, None);
assert!(!clip.is_infinite());
assert_eq!(clip.min, Vec2::new(100.0, 100.0));
assert_eq!(clip.max, Vec2::new(300.0, 250.0));
let parent_clip = ClipRect::from_bounds(50.0, 50.0, 300.0, 250.0);
let clip = compute_clip_rect(
&layout,
Overflow::Hidden,
Overflow::Hidden,
Some(parent_clip),
);
assert_eq!(clip.min, Vec2::new(100.0, 100.0));
assert_eq!(clip.max, Vec2::new(300.0, 250.0));
}
}