1#![allow(dead_code)]
2#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
6pub enum ViewportOrigin {
7 #[default]
9 TopLeft,
10 BottomLeft,
12}
13
14impl ViewportOrigin {
15 #[must_use]
17 pub fn is_top_left(&self) -> bool {
18 matches!(self, Self::TopLeft)
19 }
20}
21
22#[derive(Debug, Clone, PartialEq)]
24pub struct Viewport {
25 pub x: f32,
27 pub y: f32,
29 pub width: f32,
31 pub height: f32,
33 pub min_depth: f32,
35 pub max_depth: f32,
37 pub origin: ViewportOrigin,
39}
40
41impl Viewport {
42 #[must_use]
44 pub fn new(width: f32, height: f32) -> Self {
45 Self {
46 x: 0.0,
47 y: 0.0,
48 width,
49 height,
50 min_depth: 0.0,
51 max_depth: 1.0,
52 origin: ViewportOrigin::TopLeft,
53 }
54 }
55
56 #[must_use]
58 pub fn with_offset(
59 x: f32,
60 y: f32,
61 width: f32,
62 height: f32,
63 min_depth: f32,
64 max_depth: f32,
65 ) -> Self {
66 Self {
67 x,
68 y,
69 width,
70 height,
71 min_depth,
72 max_depth,
73 origin: ViewportOrigin::TopLeft,
74 }
75 }
76
77 #[allow(clippy::cast_precision_loss)]
79 #[must_use]
80 pub fn aspect_ratio(&self) -> f32 {
81 if self.height == 0.0 {
82 return f32::INFINITY;
83 }
84 self.width / self.height
85 }
86
87 #[must_use]
89 pub fn is_portrait(&self) -> bool {
90 self.height > self.width
91 }
92
93 #[must_use]
95 pub fn is_landscape(&self) -> bool {
96 self.width > self.height
97 }
98
99 #[must_use]
101 pub fn has_valid_depth_range(&self) -> bool {
102 self.min_depth >= 0.0 && self.max_depth <= 1.0 && self.min_depth < self.max_depth
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
108pub struct ViewportScissor {
109 pub left: i32,
111 pub top: i32,
113 pub width: u32,
115 pub height: u32,
117}
118
119impl ViewportScissor {
120 #[must_use]
122 pub fn new(left: i32, top: i32, width: u32, height: u32) -> Self {
123 Self {
124 left,
125 top,
126 width,
127 height,
128 }
129 }
130
131 #[must_use]
133 pub fn clips(&self, px: i32, py: i32) -> bool {
134 px >= self.left
135 && py >= self.top
136 && px < self.left + self.width as i32
137 && py < self.top + self.height as i32
138 }
139
140 #[must_use]
142 pub fn area(&self) -> u64 {
143 u64::from(self.width) * u64::from(self.height)
144 }
145
146 #[must_use]
148 pub fn is_non_empty(&self) -> bool {
149 self.width > 0 && self.height > 0
150 }
151}
152
153#[derive(Debug, Default)]
155pub struct ViewportStack {
156 stack: Vec<Viewport>,
157}
158
159impl ViewportStack {
160 #[must_use]
162 pub fn new() -> Self {
163 Self::default()
164 }
165
166 pub fn push(&mut self, viewport: Viewport) {
168 self.stack.push(viewport);
169 }
170
171 pub fn pop(&mut self) -> Option<Viewport> {
173 self.stack.pop()
174 }
175
176 #[must_use]
178 pub fn current(&self) -> Option<&Viewport> {
179 self.stack.last()
180 }
181
182 #[must_use]
184 pub fn depth(&self) -> usize {
185 self.stack.len()
186 }
187
188 #[must_use]
190 pub fn is_empty(&self) -> bool {
191 self.stack.is_empty()
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_viewport_origin_is_top_left() {
201 assert!(ViewportOrigin::TopLeft.is_top_left());
202 assert!(!ViewportOrigin::BottomLeft.is_top_left());
203 }
204
205 #[test]
206 fn test_viewport_aspect_ratio() {
207 let vp = Viewport::new(1920.0, 1080.0);
208 let ar = vp.aspect_ratio();
209 assert!((ar - 16.0 / 9.0).abs() < 1e-4);
210 }
211
212 #[test]
213 fn test_viewport_aspect_ratio_zero_height() {
214 let vp = Viewport::new(100.0, 0.0);
215 assert_eq!(vp.aspect_ratio(), f32::INFINITY);
216 }
217
218 #[test]
219 fn test_viewport_is_portrait() {
220 let vp = Viewport::new(720.0, 1280.0);
221 assert!(vp.is_portrait());
222 assert!(!vp.is_landscape());
223 }
224
225 #[test]
226 fn test_viewport_is_landscape() {
227 let vp = Viewport::new(1920.0, 1080.0);
228 assert!(vp.is_landscape());
229 assert!(!vp.is_portrait());
230 }
231
232 #[test]
233 fn test_viewport_valid_depth_range() {
234 let vp = Viewport::new(100.0, 100.0);
235 assert!(vp.has_valid_depth_range());
236 }
237
238 #[test]
239 fn test_viewport_invalid_depth_range() {
240 let mut vp = Viewport::new(100.0, 100.0);
241 vp.min_depth = 0.9;
242 vp.max_depth = 0.5;
243 assert!(!vp.has_valid_depth_range());
244 }
245
246 #[test]
247 fn test_scissor_clips_inside() {
248 let s = ViewportScissor::new(10, 10, 100, 100);
249 assert!(s.clips(50, 50));
250 }
251
252 #[test]
253 fn test_scissor_clips_outside() {
254 let s = ViewportScissor::new(10, 10, 100, 100);
255 assert!(!s.clips(5, 50));
256 assert!(!s.clips(50, 5));
257 assert!(!s.clips(110, 50));
258 }
259
260 #[test]
261 fn test_scissor_area() {
262 let s = ViewportScissor::new(0, 0, 1920, 1080);
263 assert_eq!(s.area(), 1920 * 1080);
264 }
265
266 #[test]
267 fn test_scissor_is_non_empty() {
268 let s = ViewportScissor::new(0, 0, 10, 10);
269 assert!(s.is_non_empty());
270 let empty = ViewportScissor::new(0, 0, 0, 0);
271 assert!(!empty.is_non_empty());
272 }
273
274 #[test]
275 fn test_viewport_stack_push_pop() {
276 let mut stack = ViewportStack::new();
277 assert!(stack.is_empty());
278 stack.push(Viewport::new(800.0, 600.0));
279 stack.push(Viewport::new(400.0, 300.0));
280 assert_eq!(stack.depth(), 2);
281 let top = stack.pop().expect("stack pop should return a value");
282 assert!((top.width - 400.0).abs() < 1e-6);
283 assert_eq!(stack.depth(), 1);
284 }
285
286 #[test]
287 fn test_viewport_stack_current() {
288 let mut stack = ViewportStack::new();
289 assert!(stack.current().is_none());
290 stack.push(Viewport::new(1280.0, 720.0));
291 let cur = stack.current().expect("current should return a value");
292 assert!((cur.width - 1280.0).abs() < 1e-6);
293 }
294
295 #[test]
296 fn test_viewport_stack_pop_empty() {
297 let mut stack = ViewportStack::new();
298 assert!(stack.pop().is_none());
299 }
300}