jevsnes.git / packages / panels / src / layout.rs

Where everything in the window goes, as one function of the window's size.

The picture's cell is the picture: 4:3, as a CRT showed every SNES mode, and not a point wider or taller. What is left of the width is split evenly, so the two side panels are the same width at every window size because they are one number. Directly under the picture: the replay scrubber (a fixed strip, its own width), then the pad.

9use egui::{Rect, pos2, vec2};

Width over height of the picture.

12const PICTURE: f32 = 4.0 / 3.0;

The narrowest a side panel gets before the picture gives way instead.

15const NARROWEST_SIDE: f32 = 220.0;
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct Layout {
19    pub left: Rect,
20    pub picture: Rect,

The replay timeline: scrubber, play/pause/skip, speed - directly under the picture, spanning its width, above the pad.

23    pub scrubber: Rect,
24    pub pad: Rect,
25    pub right: Rect,
26}
28impl Layout {

Lay out window, keeping at least scrubber_height + pad_height 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}
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}