Skip to main content

concinnity_core/render/
display_mode.rs

1//! The backend-agnostic display-mode list behind the "Resolution" settings row.
2//! A backend enumerates the modes (width x height at refresh rate) the display
3//! it renders to supports; this module holds the shared shaping: the row/list
4//! label format, the dedup + sort that turns a raw enumeration into the menu
5//! list, the persisted-choice -> list-index recovery, and the static fallback a
6//! backend without enumeration (or an embedded view with no window) uses so the
7//! row still drives the windowed resize path.
8//!
9//! How a chosen mode is applied stays per window mode: windowed resizes the
10//! window's content area to the resolution; fullscreen switches the display to
11//! the mode itself (resolution + refresh rate); borderless always covers the
12//! display's current mode, so the row is inert there.
13
14use alloc::format;
15use alloc::string::String;
16use alloc::vec::Vec;
17
18/// One display mode the hardware supports: pixel dimensions plus refresh rate.
19/// `refresh_hz` of 0 means unknown (some built-in panels report none); the label
20/// then omits the rate and a fullscreen apply keeps the display's current rate.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
22pub struct DisplayMode {
23    /// Width in pixels.
24    pub width: u32,
25    /// Height in pixels.
26    pub height: u32,
27    /// Refresh rate in Hz; 0 when the display reports none.
28    pub refresh_hz: u32,
29}
30
31impl DisplayMode {
32    /// The option text shown in the Resolution row and its dropdown list, e.g.
33    /// "2560 x 1440 (165Hz)"; a mode with an unknown rate reads "2560 x 1440".
34    pub fn label(&self) -> String {
35        if self.refresh_hz == 0 {
36            format!("{} x {}", self.width, self.height)
37        } else {
38            format!("{} x {} ({}Hz)", self.width, self.height, self.refresh_hz)
39        }
40    }
41}
42
43/// The menu list for a raw enumeration: duplicates collapsed, ordered by width,
44/// then height, then refresh rate ascending (each resolution's rate variants
45/// group together).
46pub fn normalize(mut modes: Vec<DisplayMode>) -> Vec<DisplayMode> {
47    modes.sort();
48    modes.dedup();
49    modes
50}
51
52/// The list index for a (possibly persisted) choice. An exact match wins; a
53/// choice whose resolution is listed but whose rate is not (the display
54/// changed) snaps to that resolution's nearest rate; otherwise the nearest
55/// resolution by pixel count, so a stale persisted mode still lands somewhere
56/// sensible. Returns 0 for an empty list (callers guard, but stay total).
57pub fn index_of(modes: &[DisplayMode], choice: DisplayMode) -> usize {
58    if let Some(i) = modes.iter().position(|m| *m == choice) {
59        return i;
60    }
61    let same_res = modes
62        .iter()
63        .enumerate()
64        .filter(|(_, m)| m.width == choice.width && m.height == choice.height)
65        .min_by_key(|(_, m)| m.refresh_hz.abs_diff(choice.refresh_hz))
66        .map(|(i, _)| i);
67    if let Some(i) = same_res {
68        return i;
69    }
70    let choice_px = u64::from(choice.width) * u64::from(choice.height);
71    modes
72        .iter()
73        .enumerate()
74        .min_by_key(|(_, m)| {
75            let px = u64::from(m.width) * u64::from(m.height);
76            (
77                px.abs_diff(choice_px),
78                m.refresh_hz.abs_diff(choice.refresh_hz),
79            )
80        })
81        .map(|(i, _)| i)
82        .unwrap_or(0)
83}
84
85/// The index in `modes` of the native mode to apply for `want`: an exact
86/// (resolution, rate) match wins; a `want` with an unknown rate (0) or a rate
87/// the display no longer offers takes the matching resolution's highest rate;
88/// `None` when no mode has that resolution (e.g. a stale persisted choice from
89/// another monitor), so the caller leaves the display alone. Used by the
90/// DirectX + Vulkan apply paths; Metal does the same matching natively over
91/// CGDisplayModes (`find_native_mode`), so this is dead on a Metal-only build.
92pub fn best_native_index(modes: &[DisplayMode], want: DisplayMode) -> Option<usize> {
93    let mut best: Option<(u32, usize)> = None;
94    for (i, m) in modes.iter().enumerate() {
95        if m.width != want.width || m.height != want.height {
96            continue;
97        }
98        if want.refresh_hz != 0 && m.refresh_hz == want.refresh_hz {
99            return Some(i);
100        }
101        if best.as_ref().is_none_or(|(hz, _)| m.refresh_hz > *hz) {
102            best = Some((m.refresh_hz, i));
103        }
104    }
105    best.map(|(_, i)| i)
106}
107
108/// The static list used when the backend cannot enumerate the display (DirectX /
109/// Vulkan today, or an embedded view with no window). Common resolutions with no
110/// rate, so the row keeps driving the windowed resize path.
111pub fn fallback_modes() -> Vec<DisplayMode> {
112    [(1280, 720), (1600, 900), (1920, 1080), (2560, 1440)]
113        .into_iter()
114        .map(|(width, height)| DisplayMode {
115            width,
116            height,
117            refresh_hz: 0,
118        })
119        .collect()
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    use alloc::vec;
127    fn mode(width: u32, height: u32, refresh_hz: u32) -> DisplayMode {
128        DisplayMode {
129            width,
130            height,
131            refresh_hz,
132        }
133    }
134
135    #[test]
136    fn label_includes_rate_only_when_known() {
137        assert_eq!(mode(2560, 1440, 165).label(), "2560 x 1440 (165Hz)");
138        assert_eq!(mode(1920, 1080, 60).label(), "1920 x 1080 (60Hz)");
139        assert_eq!(mode(1280, 720, 0).label(), "1280 x 720");
140    }
141
142    #[test]
143    fn normalize_dedups_and_groups_rates_per_resolution() {
144        let raw = vec![
145            mode(2560, 1440, 60),
146            mode(1024, 768, 120),
147            mode(1024, 768, 60),
148            mode(2560, 1440, 165),
149            mode(1024, 768, 60), // duplicate
150            mode(1280, 720, 75),
151        ];
152        let list = normalize(raw);
153        assert_eq!(
154            list,
155            vec![
156                mode(1024, 768, 60),
157                mode(1024, 768, 120),
158                mode(1280, 720, 75),
159                mode(2560, 1440, 60),
160                mode(2560, 1440, 165),
161            ]
162        );
163    }
164
165    #[test]
166    fn index_of_prefers_exact_then_rate_then_resolution() {
167        let list = vec![
168            mode(1280, 720, 60),
169            mode(1280, 720, 120),
170            mode(1920, 1080, 60),
171            mode(2560, 1440, 165),
172        ];
173        // Exact match.
174        assert_eq!(index_of(&list, mode(1920, 1080, 60)), 2);
175        // Listed resolution, unlisted rate: nearest rate for that resolution.
176        assert_eq!(index_of(&list, mode(1280, 720, 144)), 1);
177        // A fallback-preset choice (rate 0) lands on the resolution's lowest rate.
178        assert_eq!(index_of(&list, mode(1280, 720, 0)), 0);
179        // Unlisted resolution: nearest by pixel count (1600x900 sits closer to
180        // 1280x720 than to 1920x1080; 2048x1152 closer to 1920x1080).
181        assert_eq!(index_of(&list, mode(1600, 900, 60)), 0);
182        assert_eq!(index_of(&list, mode(2048, 1152, 60)), 2);
183        // Empty list stays total.
184        assert_eq!(index_of(&[], mode(1920, 1080, 60)), 0);
185    }
186
187    #[test]
188    fn best_native_index_snaps_rate_and_rejects_unknown_resolution() {
189        let list = vec![
190            mode(1280, 720, 60),
191            mode(1280, 720, 120),
192            mode(1920, 1080, 60),
193        ];
194        // Exact (resolution, rate) match wins.
195        assert_eq!(best_native_index(&list, mode(1280, 720, 120)), Some(1));
196        // Unknown wanted rate (0, a fallback preset) takes the resolution's
197        // highest rate.
198        assert_eq!(best_native_index(&list, mode(1280, 720, 0)), Some(1));
199        // A rate the display no longer offers also snaps to the highest.
200        assert_eq!(best_native_index(&list, mode(1920, 1080, 144)), Some(2));
201        // An unlisted resolution applies nothing (the display is left alone).
202        assert_eq!(best_native_index(&list, mode(2560, 1440, 60)), None);
203    }
204
205    #[test]
206    fn fallback_modes_are_sorted_rate_free_presets() {
207        let list = fallback_modes();
208        assert_eq!(normalize(list.clone()), list);
209        assert!(list.len() > 2, "must expand as a dropdown row");
210        assert!(list.iter().all(|m| m.refresh_hz == 0));
211        assert!(list.iter().any(|m| (m.width, m.height) == (1920, 1080)));
212    }
213}