jevsnes.git / packages / panels / src / layout.rs
1//! Where everything in the window goes, as one function of the window's size.
2//!
3//! The picture's cell is the picture: 4:3, as a CRT showed every SNES mode, and
4//! not a point wider or taller. What is left of the width is split evenly, so
5//! the two side panels are the same width at every window size because they
6//! are one number. Directly under the picture: the replay scrubber (a fixed
7//! strip, its own width), then the pad.
8
9use egui::{Rect, pos2, vec2};
10
11/// Width over height of the picture.
12const PICTURE: f32 = 4.0 / 3.0;
13
14/// The narrowest a side panel gets before the picture gives way instead.
15const NARROWEST_SIDE: f32 = 220.0;
16
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct Layout {
19    pub left: Rect,
20    pub picture: Rect,
21    /// The replay timeline: scrubber, play/pause/skip, speed - directly
22    /// under the picture, spanning its width, above the pad.
23    pub scrubber: Rect,
24    pub pad: Rect,
25    pub right: Rect,
26}
27
28impl Layout {
29    /// Lay out `window`, keeping at least `scrubber_height + pad_height`
30    /// under the picture.
31    pub fn of(window: Rect, scrubber_height: f32, pad_height: f32) -> Self {
32        let reserved = scrubber_height + pad_height;
33        // As tall as the reserved strip allows, unless that leaves the sides
34        // too narrow, and then as wide as the sides allow. The pad takes
35        // whatever height the picture and scrubber do not.
36        let tallest = (window.height() - reserved).max(0.0);
37        let widest = (window.width() - 2.0 * NARROWEST_SIDE).max(0.0);
38        let fits = (tallest * PICTURE).min(widest);
39        // A whole number of points, and the right edge measured back from the
40        // window's: then both sides are this number exactly, where halving a
41        // remainder leaves them a float's rounding apart.
42        let side = ((window.width() - fits) / 2.0).ceil();
43        let (top, bottom) = (window.top(), window.bottom());
44        let (inner_left, inner_right) = (window.left() + side, window.right() - side);
45        let width = (inner_right - inner_left).max(0.0);
46        let height = width / PICTURE;
47        let scrubber_top = top + height;
48        let pad_top = scrubber_top + scrubber_height;
49        Self {
50            left: Rect::from_min_max(window.min, pos2(inner_left, bottom)),
51            picture: Rect::from_min_size(pos2(inner_left, top), vec2(width, height)),
52            scrubber: Rect::from_min_max(pos2(inner_left, scrubber_top), pos2(inner_right, pad_top)),
53            pad: Rect::from_min_max(pos2(inner_left, pad_top), pos2(inner_right, bottom)),
54            right: Rect::from_min_max(pos2(inner_right, top), window.max),
55        }
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn sides_match_and_the_picture_has_no_slack() {
65        for (w, h) in [(1300.0, 780.0), (2560.0, 1400.0), (1300.8, 780.4), (700.0, 900.0), (500.0, 300.0)] {
66            let window = Rect::from_min_size(pos2(3.0, 5.0), vec2(w, h));
67            let layout = Layout::of(window, 40.0, 190.0);
68            assert_eq!(layout.left.width(), layout.right.width(), "{w}x{h}");
69            let picture = layout.picture;
70            assert!((picture.width() - picture.height() * PICTURE).abs() < 1e-3, "{w}x{h}");
71            assert_eq!(layout.left.right(), picture.left());
72            assert_eq!(picture.right(), layout.right.left());
73            assert_eq!(picture.bottom(), layout.scrubber.top());
74            assert_eq!(layout.scrubber.left(), picture.left());
75            assert_eq!(layout.scrubber.right(), picture.right());
76            assert_eq!(layout.scrubber.bottom(), layout.pad.top());
77            assert_eq!(layout.right.right(), window.right());
78        }
79    }
80}