use crate::error::CaptureError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PxRect {
pub x: u32,
pub y: u32,
pub w: u32,
pub h: u32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Region {
Full,
Percent { x: f32, y: f32, w: f32, h: f32 },
Px { x: u32, y: u32, w: u32, h: u32 },
}
impl Region {
pub fn percent(x: f32, y: f32, w: f32, h: f32) -> Self {
Region::Percent { x, y, w, h }
}
pub fn px(x: u32, y: u32, w: u32, h: u32) -> Self {
Region::Px { x, y, w, h }
}
pub fn resolve(&self, frame_w: u32, frame_h: u32) -> Result<PxRect, CaptureError> {
if frame_w == 0 || frame_h == 0 {
return Err(CaptureError::InvalidRegion("zero-sized frame".into()));
}
let rect = match *self {
Region::Full => PxRect {
x: 0,
y: 0,
w: frame_w,
h: frame_h,
},
Region::Px { x, y, w, h } => PxRect { x, y, w, h },
Region::Percent { x, y, w, h } => {
let valid = (0.0..=1.0).contains(&x)
&& (0.0..=1.0).contains(&y)
&& w > 0.0
&& h > 0.0
&& x + w <= 1.0 + f32::EPSILON
&& y + h <= 1.0 + f32::EPSILON;
if !valid {
return Err(CaptureError::InvalidRegion(format!(
"percent region out of range: x={x} y={y} w={w} h={h}"
)));
}
let px = ((x * frame_w as f32).round() as u32).min(frame_w - 1);
let py = ((y * frame_h as f32).round() as u32).min(frame_h - 1);
let pw = ((w * frame_w as f32).round() as u32).clamp(1, frame_w - px);
let ph = ((h * frame_h as f32).round() as u32).clamp(1, frame_h - py);
PxRect {
x: px,
y: py,
w: pw,
h: ph,
}
}
};
let in_bounds = rect.w > 0
&& rect.h > 0
&& rect.x.checked_add(rect.w).is_some_and(|r| r <= frame_w)
&& rect.y.checked_add(rect.h).is_some_and(|b| b <= frame_h);
if !in_bounds {
return Err(CaptureError::InvalidRegion(format!(
"{rect:?} out of bounds for {frame_w}x{frame_h}"
)));
}
Ok(rect)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn full_resolves_to_whole_frame() {
assert_eq!(
Region::Full.resolve(1920, 1080).unwrap(),
PxRect {
x: 0,
y: 0,
w: 1920,
h: 1080
}
);
}
#[test]
fn percent_resolves_and_rounds() {
let r = Region::percent(0.05, 0.03, 0.25, 0.06)
.resolve(1920, 1080)
.unwrap();
assert_eq!(
r,
PxRect {
x: 96,
y: 32,
w: 480,
h: 65
}
);
}
#[test]
fn percent_clamps_rounding_overflow() {
let r = Region::percent(0.5, 0.0, 0.5, 1.0)
.resolve(101, 10)
.unwrap();
assert_eq!(r.x, 51);
assert_eq!(r.x + r.w, 101);
}
#[test]
fn percent_out_of_range_errors() {
assert!(Region::percent(0.8, 0.0, 0.4, 0.5)
.resolve(100, 100)
.is_err()); assert!(Region::percent(0.0, 0.0, 0.0, 0.5)
.resolve(100, 100)
.is_err()); assert!(Region::percent(-0.1, 0.0, 0.5, 0.5)
.resolve(100, 100)
.is_err());
}
#[test]
fn px_out_of_bounds_errors() {
assert!(Region::px(90, 0, 20, 10).resolve(100, 100).is_err());
assert_eq!(
Region::px(10, 20, 30, 40).resolve(100, 100).unwrap(),
PxRect {
x: 10,
y: 20,
w: 30,
h: 40
}
);
}
#[test]
fn percent_at_frame_edge_stays_in_bounds() {
let r = Region::percent(0.999, 0.0, 0.001, 1.0)
.resolve(100, 10)
.unwrap();
assert_eq!(
r,
PxRect {
x: 99,
y: 0,
w: 1,
h: 10
}
);
let r = Region::percent(0.0, 0.0, 1.0, 1.0).resolve(1, 1).unwrap();
assert_eq!(
r,
PxRect {
x: 0,
y: 0,
w: 1,
h: 1
}
);
}
#[test]
fn percent_rejects_nan() {
assert!(Region::percent(f32::NAN, 0.0, 0.5, 0.5)
.resolve(100, 100)
.is_err());
assert!(Region::percent(0.0, 0.0, f32::NAN, 0.5)
.resolve(100, 100)
.is_err());
}
}