1use crate::error::CaptureError;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct PxRect {
6 pub x: u32,
7 pub y: u32,
8 pub w: u32,
9 pub h: u32,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum Region {
16 Full,
17 Percent { x: f32, y: f32, w: f32, h: f32 },
18 Px { x: u32, y: u32, w: u32, h: u32 },
19}
20
21impl Region {
22 pub fn percent(x: f32, y: f32, w: f32, h: f32) -> Self {
23 Region::Percent { x, y, w, h }
24 }
25
26 pub fn px(x: u32, y: u32, w: u32, h: u32) -> Self {
27 Region::Px { x, y, w, h }
28 }
29
30 pub fn resolve(&self, frame_w: u32, frame_h: u32) -> Result<PxRect, CaptureError> {
31 if frame_w == 0 || frame_h == 0 {
32 return Err(CaptureError::InvalidRegion("zero-sized frame".into()));
33 }
34 let rect = match *self {
35 Region::Full => PxRect {
36 x: 0,
37 y: 0,
38 w: frame_w,
39 h: frame_h,
40 },
41 Region::Px { x, y, w, h } => PxRect { x, y, w, h },
42 Region::Percent { x, y, w, h } => {
43 let valid = (0.0..=1.0).contains(&x)
44 && (0.0..=1.0).contains(&y)
45 && w > 0.0
46 && h > 0.0
47 && x + w <= 1.0 + f32::EPSILON
50 && y + h <= 1.0 + f32::EPSILON;
51 if !valid {
52 return Err(CaptureError::InvalidRegion(format!(
53 "percent region out of range: x={x} y={y} w={w} h={h}"
54 )));
55 }
56 let px = ((x * frame_w as f32).round() as u32).min(frame_w - 1);
59 let py = ((y * frame_h as f32).round() as u32).min(frame_h - 1);
60 let pw = ((w * frame_w as f32).round() as u32).clamp(1, frame_w - px);
61 let ph = ((h * frame_h as f32).round() as u32).clamp(1, frame_h - py);
62 PxRect {
63 x: px,
64 y: py,
65 w: pw,
66 h: ph,
67 }
68 }
69 };
70 let in_bounds = rect.w > 0
73 && rect.h > 0
74 && rect.x.checked_add(rect.w).is_some_and(|r| r <= frame_w)
75 && rect.y.checked_add(rect.h).is_some_and(|b| b <= frame_h);
76 if !in_bounds {
77 return Err(CaptureError::InvalidRegion(format!(
78 "{rect:?} out of bounds for {frame_w}x{frame_h}"
79 )));
80 }
81 Ok(rect)
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn full_resolves_to_whole_frame() {
91 assert_eq!(
92 Region::Full.resolve(1920, 1080).unwrap(),
93 PxRect {
94 x: 0,
95 y: 0,
96 w: 1920,
97 h: 1080
98 }
99 );
100 }
101
102 #[test]
103 fn percent_resolves_and_rounds() {
104 let r = Region::percent(0.05, 0.03, 0.25, 0.06)
105 .resolve(1920, 1080)
106 .unwrap();
107 assert_eq!(
108 r,
109 PxRect {
110 x: 96,
111 y: 32,
112 w: 480,
113 h: 65
114 }
115 );
116 }
117
118 #[test]
119 fn percent_clamps_rounding_overflow() {
120 let r = Region::percent(0.5, 0.0, 0.5, 1.0)
122 .resolve(101, 10)
123 .unwrap();
124 assert_eq!(r.x, 51);
125 assert_eq!(r.x + r.w, 101);
126 }
127
128 #[test]
129 fn percent_out_of_range_errors() {
130 assert!(Region::percent(0.8, 0.0, 0.4, 0.5)
131 .resolve(100, 100)
132 .is_err()); assert!(Region::percent(0.0, 0.0, 0.0, 0.5)
134 .resolve(100, 100)
135 .is_err()); assert!(Region::percent(-0.1, 0.0, 0.5, 0.5)
137 .resolve(100, 100)
138 .is_err());
139 }
140
141 #[test]
142 fn px_out_of_bounds_errors() {
143 assert!(Region::px(90, 0, 20, 10).resolve(100, 100).is_err());
144 assert_eq!(
145 Region::px(10, 20, 30, 40).resolve(100, 100).unwrap(),
146 PxRect {
147 x: 10,
148 y: 20,
149 w: 30,
150 h: 40
151 }
152 );
153 }
154
155 #[test]
156 fn percent_at_frame_edge_stays_in_bounds() {
157 let r = Region::percent(0.999, 0.0, 0.001, 1.0)
159 .resolve(100, 10)
160 .unwrap();
161 assert_eq!(
162 r,
163 PxRect {
164 x: 99,
165 y: 0,
166 w: 1,
167 h: 10
168 }
169 );
170 let r = Region::percent(0.0, 0.0, 1.0, 1.0).resolve(1, 1).unwrap();
172 assert_eq!(
173 r,
174 PxRect {
175 x: 0,
176 y: 0,
177 w: 1,
178 h: 1
179 }
180 );
181 }
182
183 #[test]
184 fn percent_rejects_nan() {
185 assert!(Region::percent(f32::NAN, 0.0, 0.5, 0.5)
186 .resolve(100, 100)
187 .is_err());
188 assert!(Region::percent(0.0, 0.0, f32::NAN, 0.5)
189 .resolve(100, 100)
190 .is_err());
191 }
192}