Skip to main content

fret_core/
viewport.rs

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    /// Returns the scale from window-local logical pixels ("screen px") to render-target pixels.
62    ///
63    /// This is derived from the mapped draw rect (logical pixels) and the backing render target
64    /// size `self.target_px_size` (physical pixels).
65    ///
66    /// For `ViewportFit::Contain`/`Cover` this is uniform; for `ViewportFit::Stretch` the mapping
67    /// is non-uniform, so this returns the smaller axis scale as a conservative approximation for
68    /// isotropic thresholds (hit radii, click distances).
69    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}