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}