use crate::geometry::{Region, Scale};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GlRegion {
origin_bl: [u32; 2],
size: [u32; 2],
scale: Scale,
}
impl GlRegion {
pub fn from_bottom_px(origin_bl: [u32; 2], size: [u32; 2], scale: Scale) -> Self {
Self {
origin_bl,
size,
scale,
}
}
pub fn intersect(&self, other: &GlRegion) -> Option<GlRegion> {
let [ax, ay] = self.origin_bl;
let [aw, ah] = self.size;
let [bx, by] = other.origin_bl;
let [bw, bh] = other.size;
let x0 = ax.max(bx);
let y0 = ay.max(by);
let x1 = ax.saturating_add(aw).min(bx.saturating_add(bw));
let y1 = ay.saturating_add(ah).min(by.saturating_add(bh));
let w = x1.saturating_sub(x0);
let h = y1.saturating_sub(y0);
if w == 0 || h == 0 {
None
} else {
Some(GlRegion {
origin_bl: [x0, y0],
size: [w, h],
scale: self.scale,
})
}
}
pub fn clip_to(&self, extent: [u32; 2]) -> Option<GlRegion> {
self.intersect(&GlRegion::from_bottom_px([0, 0], extent, self.scale))
}
pub fn origin_bl(self) -> [u32; 2] {
self.origin_bl
}
pub fn size(self) -> [u32; 2] {
self.size
}
pub fn into_region(self) -> Region {
Region {
origin: self.origin_bl,
size: self.size,
scale: self.scale,
}
}
}
impl std::fmt::Display for GlRegion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let [x, y] = self.origin_bl;
let [w, h] = self.size;
let scale = self.scale.factor();
write!(f, "origin-bl ({x}, {y}), size {w}×{h}, scale {scale}")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-4
}
fn gl(origin_bl: [u32; 2], size: [u32; 2], scale: f32) -> GlRegion {
GlRegion::from_bottom_px(origin_bl, size, Scale::new(scale))
}
#[test]
fn into_region_is_a_pure_reinterpret() {
let r = gl([10, 20], [100, 60], 2.0).into_region();
assert_eq!(r.origin, [10, 20]);
assert_eq!(r.size, [100, 60]);
assert!(close(r.scale.factor(), 2.0));
}
#[test]
fn intersect_overlapping_yields_the_overlap_box() {
let overlap = gl([10, 10], [40, 40], 1.0)
.intersect(&gl([30, 20], [40, 40], 1.0))
.expect("the boxes overlap");
assert_eq!(overlap.origin_bl, [30, 20]);
assert_eq!(overlap.size, [20, 30]);
}
#[test]
fn intersect_preserves_self_scale() {
let overlap = gl([0, 0], [50, 50], 2.0)
.intersect(&gl([10, 10], [50, 50], 1.0))
.expect("the boxes overlap");
assert!(close(overlap.scale.factor(), 2.0));
}
#[test]
fn intersect_disjoint_is_none() {
assert_eq!(
gl([0, 0], [10, 10], 1.0).intersect(&gl([20, 0], [10, 10], 1.0)),
None
);
}
#[test]
fn intersect_edge_touching_is_none() {
assert_eq!(
gl([0, 0], [10, 10], 1.0).intersect(&gl([10, 0], [10, 10], 1.0)),
None
);
}
#[test]
fn clip_to_leaves_an_in_bounds_region_unchanged() {
let r = gl([10, 10], [20, 20], 1.0);
assert_eq!(r.clip_to([100, 100]), Some(r));
}
#[test]
fn clip_to_clamps_a_partially_offscreen_region() {
let clipped = gl([90, 90], [20, 20], 2.0)
.clip_to([100, 100])
.expect("partial overlap is not a no-op");
assert_eq!(clipped.origin_bl, [90, 90]);
assert_eq!(clipped.size, [10, 10]);
assert!(close(clipped.scale.factor(), 2.0));
}
#[test]
fn clip_to_is_none_when_origin_past_extent() {
assert_eq!(gl([100, 0], [10, 10], 1.0).clip_to([100, 100]), None);
}
#[test]
fn clip_to_is_none_for_zero_area() {
assert_eq!(gl([0, 0], [0, 10], 1.0).clip_to([100, 100]), None);
}
#[test]
fn intersect_saturates_instead_of_overflowing() {
let r = gl([u32::MAX - 1, 0], [10, 10], 1.0);
assert_eq!(r.clip_to([100, 100]), None);
}
#[test]
fn display_marks_the_origin_bottom_left() {
assert_eq!(
gl([4, 8], [100, 60], 2.0).to_string(),
"origin-bl (4, 8), size 100×60, scale 2"
);
}
}