1use std::collections::BTreeMap;
5
6use super::proto;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct DisplaySize {
11 pub width: u32,
12 pub height: u32,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct Hardware {
18 pub display: DisplaySize,
20 pub keyboard: bool,
24}
25
26impl Hardware {
27 pub fn from_entries<'a>(
29 entries: impl IntoIterator<Item = (&'a str, &'a str)>,
30 ) -> Result<Self, String> {
31 let entries: BTreeMap<&str, &str> = entries.into_iter().collect();
32 let pixels = |key: &str| -> Result<u32, String> {
33 let value = entries
34 .get(key)
35 .ok_or_else(|| format!("the emulator reports no {key}"))?;
36 match value.trim().parse() {
37 Ok(n) if n > 0 => Ok(n),
38 _ => Err(format!("{key} is {value:?}, not a positive pixel count")),
39 }
40 };
41 Ok(Self {
42 display: DisplaySize {
43 width: pixels("hw.lcd.width")?,
44 height: pixels("hw.lcd.height")?,
45 },
46 keyboard: entries
47 .get("hw.keyboard")
48 .is_some_and(|v| v.trim().eq_ignore_ascii_case("yes")),
49 })
50 }
51
52 #[allow(deprecated)]
57 pub fn from_status(status: &proto::EmulatorStatus) -> Result<Self, String> {
58 let hardware = status
59 .hardware_config
60 .iter()
61 .flat_map(|list| &list.entry)
62 .map(|e| (e.key.as_str(), e.value.as_str()));
63 let platform = status
64 .platform_config
65 .iter()
66 .filter(|(_, v)| !v.is_empty())
67 .map(|(k, v)| (k.as_str(), v.as_str()));
68 Self::from_entries(hardware.chain(platform))
70 }
71}
72
73pub fn scaled_size(display: DisplaySize, working_width: u32) -> (u32, u32) {
80 if working_width == 0 || working_width >= display.width {
81 return (0, 0);
82 }
83 let height =
84 (u64::from(working_width) * u64::from(display.height)).div_ceil(u64::from(display.width));
85 (working_width, u32::try_from(height).unwrap_or(u32::MAX))
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 const PIXEL_8: DisplaySize = DisplaySize {
93 width: 1080,
94 height: 2400,
95 };
96
97 #[test]
98 fn scaling_sets_both_width_and_height() {
99 assert_eq!(scaled_size(PIXEL_8, 540), (540, 1200));
100 assert_eq!(scaled_size(PIXEL_8, 270), (270, 600));
101 }
102
103 #[test]
104 fn the_height_keeps_the_aspect_ratio_rounded_up() {
105 assert_eq!(scaled_size(PIXEL_8, 500), (500, 1112));
106 let tall = DisplaySize {
107 width: 1080,
108 height: 2340,
109 };
110 assert_eq!(scaled_size(tall, 540), (540, 1170));
111 }
112
113 #[test]
114 fn no_scaling_requests_the_display_size_as_zeros() {
115 assert_eq!(scaled_size(PIXEL_8, 0), (0, 0));
116 assert_eq!(scaled_size(PIXEL_8, 1080), (0, 0));
117 assert_eq!(scaled_size(PIXEL_8, 4000), (0, 0));
118 }
119
120 #[test]
121 fn the_display_size_comes_from_the_lcd_keys() {
122 let hw = Hardware::from_entries([
123 ("hw.lcd.density", "420"),
124 ("hw.lcd.width", "1080"),
125 ("hw.lcd.height", "2400"),
126 ("hw.keyboard", "no"),
127 ])
128 .unwrap();
129 assert_eq!(hw.display, PIXEL_8);
130 assert!(!hw.keyboard);
131 }
132
133 #[test]
134 fn a_missing_or_zero_display_size_is_an_error() {
135 let missing = Hardware::from_entries([("hw.lcd.width", "1080")]).unwrap_err();
136 assert!(missing.contains("hw.lcd.height"), "{missing}");
137 let zero =
138 Hardware::from_entries([("hw.lcd.width", "0"), ("hw.lcd.height", "2400")]).unwrap_err();
139 assert!(zero.contains("hw.lcd.width"), "{zero}");
140 let garbage = Hardware::from_entries([("hw.lcd.width", "wide"), ("hw.lcd.height", "2400")])
141 .unwrap_err();
142 assert!(garbage.contains("wide"), "{garbage}");
143 }
144
145 #[test]
146 fn keys_go_through_the_controller_only_with_a_declared_keyboard() {
147 let with = |value: &'static str| {
148 Hardware::from_entries([
149 ("hw.lcd.width", "1080"),
150 ("hw.lcd.height", "2400"),
151 ("hw.keyboard", value),
152 ])
153 .unwrap()
154 .keyboard
155 };
156 assert!(with("yes"));
157 assert!(with("YES"));
158 assert!(!with("no"));
159 assert!(!with(""));
160 let absent =
161 Hardware::from_entries([("hw.lcd.width", "1080"), ("hw.lcd.height", "2400")]).unwrap();
162 assert!(!absent.keyboard);
163 }
164
165 #[test]
166 #[allow(deprecated)]
167 fn the_status_falls_back_to_the_deprecated_hardware_config() {
168 let entry = |key: &str, value: &str| proto::Entry {
169 key: key.into(),
170 value: value.into(),
171 };
172 let mut status = proto::EmulatorStatus {
173 hardware_config: Some(proto::EntryList {
174 entry: vec![
175 entry("hw.lcd.width", "1080"),
176 entry("hw.lcd.height", "2400"),
177 entry("hw.keyboard", "no"),
178 ],
179 }),
180 ..Default::default()
181 };
182 let hw = Hardware::from_status(&status).unwrap();
183 assert_eq!(hw.display, PIXEL_8);
184 assert!(!hw.keyboard);
185
186 status
188 .platform_config
189 .insert("hw.keyboard".into(), "yes".into());
190 assert!(Hardware::from_status(&status).unwrap().keyboard);
191
192 let empty = proto::EmulatorStatus::default();
193 assert!(Hardware::from_status(&empty).is_err());
194 }
195}