Skip to main content

shuyi_ui/frame/
console.rs

1//! The console's size, which the game picks ([`ConsoleSpec`]), and how it
2//! fits the window (game ADR-0012).
3
4#[cfg(not(feature = "std"))]
5use crate::float::Float;
6
7/// A console's size: `cols × rows` cells of `cell_w × cell_h` logical
8/// pixels. The frame ([`GlyphBuffer`](super::GlyphBuffer)) keeps the spec
9/// it was made with; the game picks one when it starts (game ADR-0070 §6).
10#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
11pub struct ConsoleSpec {
12    /// Width in cells.
13    pub cols: u16,
14    /// Height in cells.
15    pub rows: u16,
16    /// Cell width in logical pixels.
17    pub cell_w: u16,
18    /// Cell height in logical pixels.
19    pub cell_h: u16,
20}
21
22impl ConsoleSpec {
23    /// `cols × rows` cells of 8 × 16 pixels, the cell of a classic text
24    /// console (and of [`GlyphBuffer::new`](super::GlyphBuffer::new)).
25    pub const fn text(cols: u16, rows: u16) -> Self {
26        Self {
27            cols,
28            rows,
29            cell_w: 8,
30            cell_h: 16,
31        }
32    }
33
34    /// The same cells, `cols × rows` of them.
35    #[must_use]
36    pub const fn sized(self, cols: u16, rows: u16) -> Self {
37        Self { cols, rows, ..self }
38    }
39
40    /// A cell's width and height in logical pixels.
41    pub fn cell_px(self) -> (i32, i32) {
42        (i32::from(self.cell_w), i32::from(self.cell_h))
43    }
44
45    /// The whole console's width and height in logical pixels.
46    pub fn px(self) -> (i32, i32) {
47        (
48            i32::from(self.cols) * i32::from(self.cell_w),
49            i32::from(self.rows) * i32::from(self.cell_h),
50        )
51    }
52}
53
54/// Where the console goes in a window: integer `scale` and the top-left
55/// pixel offset that centres it. Everything around it is letterbox.
56#[derive(Copy, Clone, PartialEq, Debug)]
57pub struct Layout {
58    /// Pixels per logical pixel; never 0.
59    pub scale: u32,
60    /// Left edge of the console in window pixels (negative if it overflows).
61    pub offset_x: f32,
62    /// Top edge of the console in window pixels (negative if it overflows).
63    pub offset_y: f32,
64}
65
66/// Fits a `spec` console into a `screen_w × screen_h` window: the largest
67/// integer scale that fits (at least 1, so a too-small window crops the
68/// edges evenly), centred on whole pixels so glyphs stay crisp.
69pub fn layout(spec: ConsoleSpec, screen_w: f32, screen_h: f32) -> Layout {
70    let console_w = f32::from(spec.cols) * f32::from(spec.cell_w);
71    let console_h = f32::from(spec.rows) * f32::from(spec.cell_h);
72    let finite = |v: f32| if v.is_finite() { v } else { 0.0 };
73    let (screen_w, screen_h) = (finite(screen_w), finite(screen_h));
74    let fit = (screen_w / console_w).min(screen_h / console_h).floor();
75    // `fit` is in 0..=f32::MAX here; `as` saturates anyway.
76    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
77    let scale = (fit.max(1.0) as u32).max(1);
78    #[allow(clippy::cast_precision_loss)] // scale ≤ 2^24 for any real window
79    let s = scale as f32;
80    Layout {
81        scale,
82        offset_x: ((screen_w - console_w * s) / 2.0).floor(),
83        offset_y: ((screen_h - console_h * s) / 2.0).floor(),
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    /// A console of 100 × 32 text cells: 800 × 512 pixels.
92    const WIDE: ConsoleSpec = ConsoleSpec::text(100, 32);
93
94    fn l(scale: u32, offset_x: f32, offset_y: f32) -> Layout {
95        Layout {
96            scale,
97            offset_x,
98            offset_y,
99        }
100    }
101
102    #[test]
103    fn exact_fit() {
104        assert_eq!(layout(WIDE, 800.0, 512.0), l(1, 0.0, 0.0));
105        assert_eq!(layout(WIDE, 1600.0, 1024.0), l(2, 0.0, 0.0));
106        assert_eq!(layout(WIDE, 2400.0, 1536.0), l(3, 0.0, 0.0));
107    }
108
109    #[test]
110    fn oversize_window_letterboxes() {
111        // 1080p: 2x (1600x1024), bars of 160 px left/right and 28 top/bottom.
112        assert_eq!(layout(WIDE, 1920.0, 1080.0), l(2, 160.0, 28.0));
113        // Height limits: 3000 wide would allow 3x, 1100 tall only 2x.
114        assert_eq!(layout(WIDE, 3000.0, 1100.0), l(2, 700.0, 38.0));
115        // Width limits.
116        assert_eq!(layout(WIDE, 1700.0, 5000.0), l(2, 50.0, 1988.0));
117        // Odd leftover pixels round down to a whole pixel.
118        assert_eq!(layout(WIDE, 801.0, 513.0), l(1, 0.0, 0.0));
119        assert_eq!(layout(WIDE, 803.0, 515.0), l(1, 1.0, 1.0));
120    }
121
122    #[test]
123    fn undersize_window_keeps_scale_1_and_centres() {
124        assert_eq!(layout(WIDE, 600.0, 412.0), l(1, -100.0, -50.0));
125        assert_eq!(layout(WIDE, 799.0, 1024.0), l(1, -1.0, 256.0));
126        assert_eq!(layout(WIDE, 0.0, 0.0), l(1, -400.0, -256.0));
127        assert_eq!(layout(WIDE, -5.0, -5.0).scale, 1);
128    }
129
130    #[test]
131    fn a_console_of_any_size_fits_its_own_size() {
132        let small = ConsoleSpec::text(50, 15);
133        assert_eq!(small.px(), (400, 240));
134        assert_eq!(WIDE.px(), (800, 512));
135        assert_eq!(small.cell_px(), (8, 16));
136        assert_eq!(
137            small.sized(3, 2),
138            ConsoleSpec {
139                cols: 3,
140                rows: 2,
141                ..small
142            }
143        );
144        assert_eq!(layout(small, 400.0, 240.0), l(1, 0.0, 0.0));
145        // A 1080p window takes it at 4x: 1600 × 960, bars of 160 and 60.
146        assert_eq!(layout(small, 1920.0, 1080.0), l(4, 160.0, 60.0));
147        // Cells of another size: 40 × 20 cells of 8 × 8 are 320 × 160.
148        let square = ConsoleSpec {
149            cell_h: 8,
150            ..ConsoleSpec::text(40, 20)
151        };
152        assert_eq!(square.px(), (320, 160));
153        assert_eq!(square.cell_px(), (8, 8));
154        assert_eq!(layout(square, 1920.0, 1080.0), l(6, 0.0, 60.0));
155    }
156
157    #[test]
158    fn non_finite_sizes_act_as_zero() {
159        assert_eq!(layout(WIDE, f32::NAN, f32::INFINITY), l(1, -400.0, -256.0));
160        assert_eq!(layout(WIDE, f32::NEG_INFINITY, 512.0), l(1, -400.0, 0.0));
161    }
162}