Skip to main content

guise/video/
fit.rs

1//! How a frame's rectangle maps into the view's box.
2//!
3//! Pure so the letterbox and crop arithmetic can be tested without a window.
4
5/// How a frame fills a `VideoView`.
6#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
7pub enum VideoFit {
8  /// Keep the aspect ratio and show the whole frame, letterboxing the rest.
9  #[default]
10  Contain,
11  /// Keep the aspect ratio and fill the box, cropping what overflows.
12  Cover,
13  /// Scale each axis independently to fill the box.
14  Stretch,
15}
16
17/// Where a `src`-sized frame lands in a `dst`-sized box, as `(x, y, w, h)` with
18/// the origin at the box's top-left. Degenerate sizes give an empty rect.
19pub fn fit_rect(src: (f32, f32), dst: (f32, f32), fit: VideoFit) -> (f32, f32, f32, f32) {
20  let (sw, sh) = src;
21  let (dw, dh) = dst;
22  if sw <= 0.0 || sh <= 0.0 || dw <= 0.0 || dh <= 0.0 {
23    return (0.0, 0.0, 0.0, 0.0);
24  }
25  let (w, h) = match fit {
26    VideoFit::Stretch => (dw, dh),
27    VideoFit::Contain => {
28      let scale = (dw / sw).min(dh / sh);
29      (sw * scale, sh * scale)
30    }
31    VideoFit::Cover => {
32      let scale = (dw / sw).max(dh / sh);
33      (sw * scale, sh * scale)
34    }
35  };
36  ((dw - w) * 0.5, (dh - h) * 0.5, w, h)
37}
38
39#[cfg(test)]
40mod tests {
41  use super::*;
42
43  #[test]
44  fn contain_letterboxes_a_wide_frame_in_a_square() {
45    let (x, y, w, h) = fit_rect((200.0, 100.0), (100.0, 100.0), VideoFit::Contain);
46    assert_eq!((x, y, w, h), (0.0, 25.0, 100.0, 50.0));
47  }
48
49  #[test]
50  fn cover_crops_a_wide_frame_in_a_square() {
51    let (x, y, w, h) = fit_rect((200.0, 100.0), (100.0, 100.0), VideoFit::Cover);
52    assert_eq!((x, y, w, h), (-50.0, 0.0, 200.0, 100.0));
53  }
54
55  #[test]
56  fn stretch_ignores_the_aspect_ratio() {
57    let r = fit_rect((200.0, 100.0), (100.0, 300.0), VideoFit::Stretch);
58    assert_eq!(r, (0.0, 0.0, 100.0, 300.0));
59  }
60
61  #[test]
62  fn a_matching_box_is_left_alone() {
63    let r = fit_rect((640.0, 360.0), (640.0, 360.0), VideoFit::Contain);
64    assert_eq!(r, (0.0, 0.0, 640.0, 360.0));
65  }
66
67  #[test]
68  fn degenerate_sizes_paint_nothing() {
69    let empty = (0.0, 0.0, 0.0, 0.0);
70    assert_eq!(
71      fit_rect((0.0, 10.0), (10.0, 10.0), VideoFit::Contain),
72      empty
73    );
74    assert_eq!(fit_rect((10.0, 10.0), (0.0, 10.0), VideoFit::Cover), empty);
75  }
76}