1use crate::geometry::{Point, Px, Rect, Size};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum ViewportFit {
5 Stretch,
6 Contain,
7 Cover,
8}
9
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct ViewportMapping {
12 pub content_rect: Rect,
13 pub target_px_size: (u32, u32),
14 pub fit: ViewportFit,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct ViewportMapped {
19 pub draw_rect: Rect,
20}
21
22impl ViewportMapping {
23 pub fn map(self) -> ViewportMapped {
24 let (tw, th) = self.target_px_size;
25 let tw = tw.max(1) as f32;
26 let th = th.max(1) as f32;
27
28 let cw = self.content_rect.size.width.0.max(0.0);
29 let ch = self.content_rect.size.height.0.max(0.0);
30 if cw <= 0.0 || ch <= 0.0 {
31 return ViewportMapped {
32 draw_rect: Rect::new(self.content_rect.origin, Size::new(Px(0.0), Px(0.0))),
33 };
34 }
35
36 match self.fit {
37 ViewportFit::Stretch => ViewportMapped {
38 draw_rect: self.content_rect,
39 },
40 ViewportFit::Contain | ViewportFit::Cover => {
41 let sx = cw / tw;
42 let sy = ch / th;
43 let s = match self.fit {
44 ViewportFit::Contain => sx.min(sy),
45 ViewportFit::Cover => sx.max(sy),
46 ViewportFit::Stretch => unreachable!(),
47 };
48
49 let dw = tw * s;
50 let dh = th * s;
51 let x = self.content_rect.origin.x.0 + (cw - dw) * 0.5;
52 let y = self.content_rect.origin.y.0 + (ch - dh) * 0.5;
53
54 ViewportMapped {
55 draw_rect: Rect::new(Point::new(Px(x), Px(y)), Size::new(Px(dw), Px(dh))),
56 }
57 }
58 }
59 }
60
61 pub fn target_px_per_screen_px(self) -> Option<f32> {
70 let (tw, th) = self.target_px_size;
71 let tw = tw.max(1) as f32;
72 let th = th.max(1) as f32;
73
74 let rect = self.map().draw_rect;
75 let dw = rect.size.width.0.max(0.0);
76 let dh = rect.size.height.0.max(0.0);
77 if dw <= 0.0 || dh <= 0.0 || !dw.is_finite() || !dh.is_finite() {
78 return None;
79 }
80
81 let sx = tw / dw;
82 let sy = th / dh;
83 let s = sx.min(sy);
84 (s.is_finite() && s > 0.0).then_some(s)
85 }
86
87 pub fn window_point_to_uv(self, p: Point) -> Option<(f32, f32)> {
88 let mapped = self.map();
89 if !mapped.draw_rect.contains(p) {
90 return None;
91 }
92
93 let x = (p.x.0 - mapped.draw_rect.origin.x.0) / mapped.draw_rect.size.width.0.max(1.0);
94 let y = (p.y.0 - mapped.draw_rect.origin.y.0) / mapped.draw_rect.size.height.0.max(1.0);
95 Some((x.clamp(0.0, 1.0), y.clamp(0.0, 1.0)))
96 }
97
98 pub fn window_point_to_uv_clamped(self, p: Point) -> (f32, f32) {
99 let mapped = self.map();
100 let x = (p.x.0 - mapped.draw_rect.origin.x.0) / mapped.draw_rect.size.width.0.max(1.0);
101 let y = (p.y.0 - mapped.draw_rect.origin.y.0) / mapped.draw_rect.size.height.0.max(1.0);
102 (x.clamp(0.0, 1.0), y.clamp(0.0, 1.0))
103 }
104
105 pub fn window_point_to_target_px(self, p: Point) -> Option<(u32, u32)> {
106 let (u, v) = self.window_point_to_uv(p)?;
107 let (tw, th) = self.target_px_size;
108 let x = (u * tw as f32)
109 .floor()
110 .clamp(0.0, (tw.saturating_sub(1)) as f32) as u32;
111 let y = (v * th as f32)
112 .floor()
113 .clamp(0.0, (th.saturating_sub(1)) as f32) as u32;
114 Some((x, y))
115 }
116
117 pub fn window_point_to_target_px_clamped(self, p: Point) -> (u32, u32) {
118 let (u, v) = self.window_point_to_uv_clamped(p);
119 let (tw, th) = self.target_px_size;
120 let x = (u * tw as f32)
121 .floor()
122 .clamp(0.0, (tw.saturating_sub(1)) as f32) as u32;
123 let y = (v * th as f32)
124 .floor()
125 .clamp(0.0, (th.saturating_sub(1)) as f32) as u32;
126 (x, y)
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn target_px_per_screen_px_matches_draw_rect_mapping() {
136 let mapping = ViewportMapping {
137 content_rect: Rect::new(
138 Point::new(Px(0.0), Px(0.0)),
139 Size::new(Px(200.0), Px(100.0)),
140 ),
141 target_px_size: (1000, 500),
142 fit: ViewportFit::Contain,
143 };
144 let scale = mapping.target_px_per_screen_px().unwrap();
145 assert!((scale - 5.0).abs() <= 1.0e-6);
146 }
147}