1use crate::prelude::*;
17
18use alloc::collections::BTreeMap;
19use core::fmt;
20
21use serde::Deserialize;
22use serde::de::value::MapAccessDeserializer;
23use serde::de::{Deserializer, MapAccess, Visitor};
24
25use super::{Lang, LangCode, text_width};
26use crate::font::CellWidths;
27
28#[derive(Debug, Clone, Default, PartialEq, Eq)]
30#[non_exhaustive]
31pub struct Place {
32 pub max_cells: Option<usize>,
34 pub note: Option<String>,
37}
38
39pub type Places = BTreeMap<String, Place>;
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub(super) struct UiValue {
45 pub(super) text: String,
46 pub(super) place: Place,
47}
48
49#[derive(Deserialize)]
51#[serde(deny_unknown_fields)]
52struct Placed {
53 text: String,
54 #[serde(default)]
55 max_cells: Option<usize>,
56 #[serde(default)]
57 note: Option<String>,
58}
59
60impl<'de> Deserialize<'de> for UiValue {
61 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
62 struct ValueVisitor;
63
64 impl<'de> Visitor<'de> for ValueVisitor {
65 type Value = UiValue;
66
67 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 f.write_str("a text, or (text: …, max_cells: …, note: …)")
69 }
70
71 fn visit_str<E: serde::de::Error>(self, text: &str) -> Result<UiValue, E> {
72 Ok(UiValue {
73 text: text.to_owned(),
74 place: Place::default(),
75 })
76 }
77
78 fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<UiValue, A::Error> {
79 let placed = Placed::deserialize(MapAccessDeserializer::new(map))?;
80 Ok(UiValue {
81 text: placed.text,
82 place: Place {
83 max_cells: placed.max_cells,
84 note: placed.note,
85 },
86 })
87 }
88 }
89
90 deserializer.deserialize_any(ValueVisitor)
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96#[non_exhaustive]
97pub struct Overflow {
98 pub key: String,
100 pub max_cells: usize,
102 pub width: usize,
104 pub text: String,
106}
107
108impl Lang {
109 #[must_use]
111 pub fn with_places(mut self, places: Places) -> Self {
112 self.places = places;
113 self
114 }
115
116 pub fn place(&self, key: &str) -> Option<&Place> {
118 self.places.get(key)
119 }
120
121 pub fn max_cells(&self, key: &str) -> Option<usize> {
123 self.place(key).and_then(|p| p.max_cells)
124 }
125
126 pub fn note(&self, key: &str) -> Option<&str> {
128 self.place(key).and_then(|p| p.note.as_deref())
129 }
130
131 pub fn overflow(&self, code: &LangCode, widths: &CellWidths) -> Option<Vec<Overflow>> {
137 let pack = self.packs.get(code)?;
138 let widths = &pack.info.widths(widths);
139 let too_wide = |(key, entry): (&String, &super::Entry)| {
140 let max_cells = self.max_cells(key)?;
141 let width = text_width(&entry.text, widths);
142 (width > max_cells).then(|| Overflow {
143 key: key.clone(),
144 max_cells,
145 width,
146 text: entry.text.clone(),
147 })
148 };
149 Some(pack.ui.iter().filter_map(too_wide).collect())
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::super::{
156 LangFile, PackRules, english_and_places_from_source, english_from_source, from_files,
157 pack_from_sources,
158 };
159 use super::*;
160
161 const SOURCE_UI: &str = r#"{
162 "title.start": (text: "Start", max_cells: 8, note: "The title menu's first item."),
163 "title.quit": (text: "Quit", max_cells: 6),
164 "title.help": (text: "Press {Confirm}", note: "{Confirm} is a key's name."),
165 "title.name": "Demo",
166 }"#;
167
168 const RULES: PackRules<'static> = PackRules {
169 data_problem: |_, _| None,
170 tokens: &crate::dialogue::NoTokens,
171 limits: crate::dialogue::tests::LIMITS,
172 widths: &CellWidths::NARROW,
173 variants: &[],
174 };
175
176 fn errors(source: &str) -> Vec<String> {
177 english_and_places_from_source("en/ui.ron", source)
178 .unwrap_err()
179 .iter()
180 .map(ToString::to_string)
181 .collect()
182 }
183
184 #[test]
185 fn a_key_with_max_cells_and_a_note_loads() {
186 let (english, places) = english_and_places_from_source("en/ui.ron", SOURCE_UI).unwrap();
187 assert_eq!(english["title.start"], "Start");
188 assert_eq!(english["title.name"], "Demo");
189 assert_eq!(places.len(), 3, "{places:?}");
190 let lang = Lang::new(english, BTreeMap::new()).with_places(places);
191 assert_eq!(
192 lang.note("title.start"),
193 Some("The title menu's first item.")
194 );
195 assert_eq!(lang.max_cells("title.start"), Some(8));
196 assert_eq!(lang.note("title.quit"), None);
197 assert_eq!(lang.max_cells("title.quit"), Some(6));
198 assert_eq!(lang.max_cells("title.help"), None);
199 assert_eq!(lang.note("title.name"), None);
200 assert_eq!(lang.place("title.name"), None);
201 assert_eq!(lang.place("no.such"), None);
202 assert_eq!(
204 english_from_source("en/ui.ron", SOURCE_UI).unwrap().len(),
205 4
206 );
207 }
208
209 #[test]
210 fn a_place_must_say_something_sensible() {
211 assert_eq!(
212 errors(r#"{"a": (text: "A", max_cells: 0), "b": (text: "B", note: " ")}"#),
213 [
214 "en/ui.ron: \"a\": max_cells must be at least 1",
215 "en/ui.ron: \"b\": a note must not be empty",
216 ]
217 );
218 let unknown = errors(r#"{"a": (text: "A", width: 3)}"#);
219 assert!(unknown[0].contains("width"), "{unknown:?}");
220 let no_text = errors(r#"{"a": (max_cells: 3)}"#);
221 assert!(no_text[0].contains("text"), "{no_text:?}");
222 let wrong = errors(r#"{"a": 3}"#);
223 assert!(wrong[0].contains("a text, or (text:"), "{wrong:?}");
224 }
225
226 #[test]
227 fn a_pack_sets_neither() {
228 let (english, _) = english_and_places_from_source("en/ui.ron", SOURCE_UI).unwrap();
229 let info = r#"(name: "X", made_by: Machine)"#;
230 let ui = r#"[(key: "title.quit", source: "Quit", text: "Q", max_cells: 9)]"#;
231 let errors = pack_from_sources(("xx/lang.ron", info), ("xx/ui.ron", ui), &english, &RULES)
232 .unwrap_err();
233 assert!(errors[0].to_string().contains("max_cells"), "{errors:?}");
234 }
235
236 #[test]
237 fn overflow_lists_the_entries_wider_than_their_place() {
238 let ui = r#"[
239 (key: "title.start", source: "Start", text: "はじめから"),
240 (key: "title.quit", source: "Quit", text: "Beenden!"),
241 (key: "title.help", source: "Press {Confirm}", text: "Drücke {Confirm} lange"),
242 (key: "title.name", source: "Demo", text: "Démonstration"),
243 ]"#;
244 let files = [
245 ("en/ui.ron", SOURCE_UI),
246 ("xx/lang.ron", r#"(name: "X", made_by: Machine)"#),
247 ("xx/ui.ron", ui),
248 ]
249 .map(|(path, source)| LangFile {
250 path,
251 source: Some(source),
252 });
253 let lang = from_files(&files, None, str::to_owned, &RULES).unwrap();
254 let xx = LangCode::new("xx").unwrap();
255 let overflow = |widths: &CellWidths| lang.overflow(&xx, widths).unwrap();
256 let narrow = overflow(&CellWidths::NARROW);
258 assert_eq!(
259 narrow,
260 [Overflow {
261 key: "title.quit".to_owned(),
262 max_cells: 6,
263 width: 8,
264 text: "Beenden!".to_owned(),
265 }]
266 );
267 let wide = overflow(&CellWidths::wide("はじめから".chars()));
269 let keys: Vec<(&str, usize)> = wide.iter().map(|o| (o.key.as_str(), o.width)).collect();
270 assert_eq!(keys, [("title.quit", 8), ("title.start", 10)]);
271 let exact = overflow(&CellWidths::wide("はじめ".chars()));
273 let keys: Vec<&str> = exact.iter().map(|o| o.key.as_str()).collect();
274 assert_eq!(keys, ["title.quit"]);
275 assert_eq!(
276 lang.overflow(&LangCode::default(), &CellWidths::NARROW),
277 None
278 );
279 }
280}