1use repose_core::prelude::{Color, Modifier, View};
16use repose_core::Dp;
17use repose_ui::{Box as RBox, Spacer, ViewExt as _};
18
19pub const DESIGN: [f32; 2] = [960.0, 540.0];
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum ScaleError {
25 NotFinite,
28 NotPositive,
31}
32
33impl std::fmt::Display for ScaleError {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 ScaleError::NotFinite => f.write_str("viewport or design side is not finite"),
37 ScaleError::NotPositive => f.write_str("viewport or design side is not positive"),
38 }
39 }
40}
41
42impl std::error::Error for ScaleError {}
43
44#[derive(Clone, Copy, Debug, PartialEq)]
46pub struct Scale {
47 fit: f32,
48 cover: f32,
49}
50
51impl Scale {
52 pub fn new(viewport: [f32; 2], design: [f32; 2]) -> Result<Self, ScaleError> {
55 for side in viewport.into_iter().chain(design) {
56 if !side.is_finite() {
57 return Err(ScaleError::NotFinite);
58 }
59 if side <= 0.0 {
60 return Err(ScaleError::NotPositive);
61 }
62 }
63 let fit = (viewport[0] / design[0]).min(viewport[1] / design[1]);
64 let cover = (viewport[0] / design[0]).max(viewport[1] / design[1]);
65 Ok(Self { fit, cover })
66 }
67
68 pub const fn unit() -> Self {
71 Self {
72 fit: 1.0,
73 cover: 1.0,
74 }
75 }
76
77 pub fn fit_or_unit(viewport: [f32; 2], design: [f32; 2]) -> Self {
81 Self::new(viewport, design).unwrap_or_else(|_| Self::unit())
82 }
83
84 pub const fn fit(&self) -> f32 {
86 self.fit
87 }
88
89 pub const fn cover(&self) -> f32 {
91 self.cover
92 }
93
94 pub fn fit_units(&self, units: f32) -> f32 {
96 units * self.fit
97 }
98
99 pub fn cover_units(&self, units: f32) -> f32 {
101 units * self.cover
102 }
103}
104
105impl Default for Scale {
106 fn default() -> Self {
107 Self::unit()
108 }
109}
110
111#[derive(Clone, Copy, Debug, PartialEq)]
113pub struct AnchorEdges {
114 pub left: f32,
115 pub top: f32,
116 pub right: f32,
117 pub bottom: f32,
118}
119
120pub fn anchor_edges(
126 viewport: [f32; 2],
127 scale: f32,
128 anchor: [f32; 4],
129 offset: [f32; 4],
130) -> AnchorEdges {
131 let left = anchor[0] * viewport[0] + offset[0] * scale;
132 let top = anchor[1] * viewport[1] + offset[1] * scale;
133 let right = anchor[2] * viewport[0] + offset[2] * scale;
134 let bottom = anchor[3] * viewport[1] + offset[3] * scale;
135 AnchorEdges {
136 left,
137 top,
138 right: (viewport[0] - right).max(0.0),
139 bottom: (viewport[1] - bottom).max(0.0),
140 }
141}
142
143pub fn anchor_rect(viewport: [f32; 2], scale: f32, anchor: [f32; 4], offset: [f32; 4]) -> Modifier {
148 let edges = anchor_edges(viewport, scale, anchor, offset);
149 Modifier::new()
150 .absolute()
151 .offset_left(Dp(edges.left))
152 .offset_top(Dp(edges.top))
153 .offset_right(Dp(edges.right))
154 .offset_bottom(Dp(edges.bottom))
155}
156
157pub fn disc(viewport: [f32; 2], scale: f32, at: [f32; 2], radius: f32, color: Color) -> View {
159 RBox(
160 anchor_rect(
161 viewport,
162 scale,
163 [0.0; 4],
164 [
165 at[0] - radius,
166 at[1] - radius,
167 at[0] + radius,
168 at[1] + radius,
169 ],
170 )
171 .background(color)
172 .clip_rounded(Dp(radius * scale))
173 .hit_passthrough(),
174 )
175 .child(Spacer())
176}
177
178pub fn ring(
180 viewport: [f32; 2],
181 scale: f32,
182 at: [f32; 2],
183 radius: f32,
184 width: f32,
185 color: Color,
186) -> View {
187 RBox(
188 anchor_rect(
189 viewport,
190 scale,
191 [0.0; 4],
192 [
193 at[0] - radius,
194 at[1] - radius,
195 at[0] + radius,
196 at[1] + radius,
197 ],
198 )
199 .border(Dp(width * scale), color, Dp(radius * scale))
200 .background(Color::TRANSPARENT)
201 .hit_passthrough(),
202 )
203 .child(Spacer())
204}
205
206pub fn design_units(pixels: repose_core::Vec2, scale: f32) -> glam::Vec2 {
212 let density = repose_core::locals::effective_density_scale().max(1e-6);
213 let factor = (density * scale.max(1e-6)).max(1e-6);
214 glam::Vec2::new(pixels.x, pixels.y) / factor
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn fit_is_the_smaller_ratio_and_cover_the_larger() {
223 let scale = Scale::new([1280.0, 720.0], DESIGN).expect("16:9 viewport scales");
224 assert!((scale.fit() - 1280.0 / 960.0).abs() < 1e-6);
225 assert!((scale.cover() - 1280.0 / 960.0).abs() < 1e-6);
226
227 let tall = Scale::new([600.0, 900.0], DESIGN).expect("4:3-ish viewport scales");
228 assert!((tall.fit() - 600.0 / 960.0).abs() < 1e-6);
229 assert!((tall.cover() - 900.0 / 540.0).abs() < 1e-6);
230 assert!(tall.cover() > tall.fit());
231 }
232
233 #[test]
234 fn unit_scales_are_neutral() {
235 let unit = Scale::unit();
236 assert_eq!(unit.fit_units(46.0), 46.0);
237 assert_eq!(unit.cover_units(46.0), 46.0);
238 assert_eq!(Scale::fit_or_unit([0.0, 0.0], DESIGN), Scale::unit());
239 assert_eq!(
240 Scale::fit_or_unit([960.0, 540.0], DESIGN),
241 Scale::new([960.0, 540.0], DESIGN).expect("scales")
242 );
243 }
244
245 #[test]
246 fn a_zero_sized_viewport_is_rejected_until_it_is_measured() {
247 assert_eq!(Scale::new([0.0, 720.0], DESIGN), Err(ScaleError::NotPositive));
248 assert_eq!(Scale::new([1280.0, -1.0], DESIGN), Err(ScaleError::NotPositive));
249 assert_eq!(Scale::new([1280.0, 720.0], [960.0, 0.0]), Err(ScaleError::NotPositive));
250 }
251
252 #[test]
253 fn a_non_finite_viewport_is_rejected() {
254 assert_eq!(Scale::new([f32::NAN, 720.0], DESIGN), Err(ScaleError::NotFinite));
255 assert_eq!(
256 Scale::new([1280.0, 720.0], [f32::INFINITY, 540.0]),
257 Err(ScaleError::NotFinite)
258 );
259 }
260
261 #[test]
262 fn anchor_edges_follow_the_viewport_fractions_and_offsets() {
263 let edges = anchor_edges([1000.0, 500.0], 2.0, [0.5, 0.5, 0.5, 0.5], [-100.0, -50.0, 100.0, 50.0]);
264 assert_eq!(edges.left, 500.0 - 200.0);
265 assert_eq!(edges.top, 250.0 - 100.0);
266 assert_eq!(edges.right, 1000.0 - 700.0);
267 assert_eq!(edges.bottom, 500.0 - 350.0);
268 }
269
270 #[test]
271 fn right_and_bottom_edges_clamp_into_the_viewport_left_does_not() {
272 let edges = anchor_edges([1000.0, 500.0], 1.0, [0.0, 0.0, 2.0, 2.0], [-40.0, 0.0, 0.0, 0.0]);
273 assert_eq!(edges.left, -40.0, "an off-screen left edge stays off-screen");
274 assert_eq!(edges.right, 0.0);
275 assert_eq!(edges.bottom, 0.0);
276 }
277
278 #[test]
279 fn a_full_bleed_anchor_covers_the_viewport() {
280 let edges = anchor_edges([1000.0, 500.0], 3.0, [0.0, 0.0, 1.0, 1.0], [0.0; 4]);
281 assert_eq!((edges.left, edges.top), (0.0, 0.0));
282 assert_eq!((edges.right, edges.bottom), (0.0, 0.0));
283 }
284}