Skip to main content

shuyi_assets/
font.rs

1//! A bitmap font atlas: a grid of fixed-size glyph cells in one image, and
2//! a glyph map (a RON file) from `char` to cell index. The game names the
3//! two files and the glyphs it needs. As a format (ADR-0007), the glyph
4//! map is a sidecar read with the PNG beside it ([`FontAtlasRon`]). Once
5//! the atlas and the game's text both load, [`undrawn`] finds every
6//! character of the text the atlas can't draw.
7//!
8//! An atlas may hold a second, wide font beside the narrow one: a wide
9//! glyph (a Japanese or Chinese character) takes two cells of the atlas,
10//! side by side, and two cells of the screen. [`CellWidths`] says which
11//! characters are wide, for everything that measures text.
12//!
13//! **Wide sets.** Japanese and Chinese share code points but not shapes
14//! (直, 骨, 角), so an atlas may hold several wide fonts, each a **set**:
15//! the unnamed one ([`FontAtlasDef::wide`]) and named ones
16//! ([`FontAtlasDef::wide_sets`]). A language pack names the set it draws
17//! from ([`LangInfo::wide_font`](crate::lang::LangInfo::wide_font)), and a
18//! wide glyph is looked up in that set first, then in the others
19//! ([`FontAtlasDef::wide_cell`]), so a pack's name for itself shows in its
20//! own shapes whatever language is on. The punctuation of
21//! [`WIDE_PUNCTUATION`] is narrow, but wide in a language whose pack says
22//! so ([`CellWidths::in_lang`]).
23use crate::prelude::*;
24
25use alloc::collections::{BTreeMap, BTreeSet};
26use alloc::rc::Rc;
27
28use serde::{Deserialize, Serialize};
29
30use crate::error::ContentError;
31use crate::format::{AssetKind, AssetPath, DecodeCx, Format, TEXT_MAX_BYTES, text};
32use crate::image::png_size;
33use crate::lang::{Lang, LangCode, TextAt, TextPart};
34use crate::ron::parse_ron;
35
36impl AssetKind for FontAtlasDef {
37    const NAME: &'static str = "font atlas";
38}
39
40/// A font atlas's glyph map (`font-atlas`, `.ron`), checked as
41/// [`FontAtlasDef::from_source`] says against the PNG beside it with its
42/// name (`fonts/atlas.ron` and `fonts/atlas.png`) and the glyphs the game
43/// requires.
44#[derive(Debug, Clone, Default, PartialEq, Eq)]
45pub struct FontAtlasRon {
46    required: String,
47}
48
49impl FontAtlasRon {
50    /// The glyph map of an atlas that must hold every glyph of `required`.
51    pub fn requiring(required: impl Into<String>) -> Self {
52        Self {
53            required: required.into(),
54        }
55    }
56}
57
58impl Format for FontAtlasRon {
59    type Asset = FontAtlasDef;
60
61    fn id(&self) -> &'static str {
62        "font-atlas"
63    }
64
65    fn extensions(&self) -> &'static [&'static str] {
66        &["ron"]
67    }
68
69    fn max_bytes(&self) -> usize {
70        TEXT_MAX_BYTES
71    }
72
73    fn decode(
74        &self,
75        file: &AssetPath,
76        bytes: &[u8],
77        cx: &DecodeCx<'_>,
78    ) -> Result<FontAtlasDef, Vec<ContentError>> {
79        let source = text(file, bytes)?;
80        let png = file.with_extension("png");
81        FontAtlasDef::from_source(
82            file.shown(),
83            source,
84            &cx.shown(&png),
85            cx.bytes(&png),
86            &self.required,
87        )
88    }
89}
90
91/// The glyph used when a cell's glyph is not in the atlas.
92pub const FALLBACK_GLYPH: char = '?';
93
94/// The punctuation narrow in English and wide in Chinese, with the same
95/// code points: `“ ” ‘ ’ … —`. An atlas may have each both narrow and in a
96/// wide set; a language's pack says which it is
97/// ([`LangInfo::wide_punctuation`](crate::lang::LangInfo::wide_punctuation)).
98pub const WIDE_PUNCTUATION: [char; 6] = [
99    '\u{201C}', '\u{201D}', '\u{2018}', '\u{2019}', '\u{2026}', '\u{2014}',
100];
101
102/// Whether `c` is one of [`WIDE_PUNCTUATION`].
103pub fn is_wide_punctuation(c: char) -> bool {
104    WIDE_PUNCTUATION.contains(&c)
105}
106
107/// A set of wide glyphs: each character → the cell index of its left half.
108pub type WideSet = BTreeMap<char, u32>;
109
110/// The validated font atlas layout.
111#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
112pub struct FontAtlasDef {
113    /// Cell width in pixels.
114    pub cell_w: u32,
115    /// Cell height in pixels.
116    pub cell_h: u32,
117    /// Cells per atlas row.
118    pub columns: u32,
119    /// Every glyph in the atlas → its cell index (row-major).
120    pub glyphs: BTreeMap<char, u32>,
121    /// The unnamed wide set: every wide glyph, two cells wide, → the cell
122    /// index of its left half; its right half is the next cell, in the
123    /// same row. A character is narrow or wide, never both, but for
124    /// [`WIDE_PUNCTUATION`]. Empty in an atlas without wide glyphs, and
125    /// left out of its file.
126    #[serde(default)]
127    pub wide: WideSet,
128    /// The named wide sets, by name (the atlas's own word, which a pack's
129    /// `wide_font` names), each laid out as [`wide`](Self::wide). A
130    /// character may be in several sets, each with its own shape. Empty in
131    /// an atlas of one wide set or none, and left out of its file.
132    #[serde(default)]
133    pub wide_sets: BTreeMap<String, WideSet>,
134}
135
136/// A pixel rectangle inside the atlas image.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub struct AtlasRect {
139    /// Left edge in pixels.
140    pub x: u32,
141    /// Top edge in pixels.
142    pub y: u32,
143    /// Width in pixels.
144    pub w: u32,
145    /// Height in pixels.
146    pub h: u32,
147}
148
149impl FontAtlasDef {
150    /// The pixel rectangle of cell `index`. `columns` must be non-zero (a
151    /// validated atlas guarantees it).
152    pub fn cell_rect(&self, index: u32) -> AtlasRect {
153        AtlasRect {
154            x: index % self.columns * self.cell_w,
155            y: index / self.columns * self.cell_h,
156            w: self.cell_w,
157            h: self.cell_h,
158        }
159    }
160
161    /// The pixel rectangle of narrow glyph `c`, or `None` if the atlas
162    /// lacks it or has it wide ([`wide_rect`](Self::wide_rect)).
163    pub fn glyph_rect(&self, c: char) -> Option<AtlasRect> {
164        self.glyphs.get(&c).map(|&i| self.cell_rect(i))
165    }
166
167    /// The pixel rectangle of wide glyph `c`, both its cells, or `None` if
168    /// no wide set has `c`: [`wide_rect_in`](Self::wide_rect_in) with no
169    /// set named.
170    pub fn wide_rect(&self, c: char) -> Option<AtlasRect> {
171        self.wide_rect_in(None, c)
172    }
173
174    /// The pixel rectangle of wide glyph `c`, both its cells, looked up as
175    /// [`wide_cell`](Self::wide_cell) says, or `None` if no wide set has
176    /// `c`.
177    pub fn wide_rect_in(&self, set: Option<&str>, c: char) -> Option<AtlasRect> {
178        self.wide_cell(set, c).map(|i| {
179            let rect = self.cell_rect(i);
180            AtlasRect {
181                w: rect.w * 2,
182                ..rect
183            }
184        })
185    }
186
187    /// The cell index of wide glyph `c`'s left half: from the named set
188    /// `set` (the language's, [`CellWidths::wide_font`]) first, then the
189    /// unnamed set, then the other named sets in name order. `None` if no
190    /// set has `c`. Every renderer looks wide glyphs up with it.
191    pub fn wide_cell(&self, set: Option<&str>, c: char) -> Option<u32> {
192        let named = set.and_then(|name| self.wide_sets.get(name));
193        named
194            .and_then(|glyphs| glyphs.get(&c))
195            .or_else(|| self.wide.get(&c))
196            .or_else(|| self.wide_sets.values().find_map(|glyphs| glyphs.get(&c)))
197            .copied()
198    }
199
200    /// The wide set `name` names, the unnamed one for `None`, if the atlas
201    /// has it (the unnamed one always).
202    pub fn wide_set(&self, name: Option<&str>) -> Option<&WideSet> {
203        match name {
204            None => Some(&self.wide),
205            Some(name) => self.wide_sets.get(name),
206        }
207    }
208
209    /// Every wide set: the unnamed one (`None`), then the named ones in
210    /// name order.
211    pub fn all_wide_sets(&self) -> impl Iterator<Item = (Option<&str>, &WideSet)> + '_ {
212        let named = self
213            .wide_sets
214            .iter()
215            .map(|(name, glyphs)| (Some(name.as_str()), glyphs));
216        core::iter::once((None, &self.wide)).chain(named)
217    }
218
219    /// The characters two cells wide where no language is named
220    /// ([`CellWidths::of`]): every set's, but punctuation of
221    /// [`WIDE_PUNCTUATION`] the atlas also has narrow.
222    pub fn wide_chars(&self) -> BTreeSet<char> {
223        self.all_wide_sets()
224            .flat_map(|(_, glyphs)| glyphs.keys().copied())
225            .filter(|&c| !(is_wide_punctuation(c) && self.glyphs.contains_key(&c)))
226            .collect()
227    }
228
229    /// Whether any wide set has `c`.
230    pub fn has_wide(&self, c: char) -> bool {
231        self.all_wide_sets()
232            .any(|(_, glyphs)| glyphs.contains_key(&c))
233    }
234
235    /// Whether the atlas draws `c`, narrow or wide in any set: the one
236    /// answer to "can this character be drawn?" where no language is named
237    /// (a name box's letters). [`undrawn`] checks a language's text against
238    /// its own set.
239    pub fn draws(&self, c: char) -> bool {
240        self.glyphs.contains_key(&c) || self.has_wide(c)
241    }
242
243    /// Every character [`draws`](Self::draws) accepts, in code-point order,
244    /// each once.
245    pub fn drawn(&self) -> impl Iterator<Item = char> + '_ {
246        let wide = self.all_wide_sets().flat_map(|(_, glyphs)| glyphs.keys());
247        let all: BTreeSet<char> = self.glyphs.keys().chain(wide).copied().collect();
248        all.into_iter()
249    }
250
251    /// One past the last cell index any glyph takes: how many cells of the
252    /// atlas its glyphs reach.
253    pub fn cell_end(&self) -> u32 {
254        self.cells()
255            .map(|(i, _)| i.saturating_add(1))
256            .max()
257            .unwrap_or(0)
258    }
259
260    /// Parses glyph map `source` (errors attributed to `file`) and validates
261    /// it: it must hold every glyph of `required`, and fit the atlas image
262    /// `png` (`None` if missing; errors attributed to `png_file`).
263    pub fn from_source(
264        file: &str,
265        source: &str,
266        png_file: &str,
267        png: Option<&[u8]>,
268        required: &str,
269    ) -> Result<Self, Vec<ContentError>> {
270        let def: Self = parse_ron(file, source).map_err(|e| vec![e])?;
271        let mut errors: Vec<ContentError> = def
272            .problems(required)
273            .into_iter()
274            .map(|m| ContentError::new(file, m))
275            .collect();
276        match png.map(png_size) {
277            None => errors.push(ContentError::not_found(png_file)),
278            Some(None) => errors.push(ContentError::new(png_file, "not a valid PNG file")),
279            Some(Some((w, h))) => errors.extend(
280                def.image_problems(w, h)
281                    .into_iter()
282                    .map(|m| ContentError::new(png_file, m)),
283            ),
284        }
285        if errors.is_empty() {
286            Ok(def)
287        } else {
288            Err(errors)
289        }
290    }
291
292    /// Every cell a glyph takes: `(cell index, glyph)`, a narrow glyph's
293    /// one and both of a wide glyph's, in every set.
294    fn cells(&self) -> impl Iterator<Item = (u32, char)> + '_ {
295        let narrow = self.glyphs.iter().map(|(&c, &i)| (i, c));
296        let wide = self
297            .all_wide_sets()
298            .flat_map(|(_, glyphs)| glyphs)
299            .flat_map(|(&c, &i)| [(i, c), (i.saturating_add(1), c)]);
300        narrow.chain(wide)
301    }
302
303    /// Problems with the glyph map on its own, in plain words: a size of 0,
304    /// a glyph of `required` it lacks, two glyphs in one cell, a character
305    /// both narrow and wide (but for [`WIDE_PUNCTUATION`]), a wide glyph
306    /// split across a row's end, or a named set with an empty name.
307    pub fn problems(&self, required: &str) -> Vec<String> {
308        let mut out = Vec::new();
309        for (name, v) in [
310            ("cell_w", self.cell_w),
311            ("cell_h", self.cell_h),
312            ("columns", self.columns),
313        ] {
314            if v == 0 {
315                out.push(format!("{name} must be greater than 0"));
316            }
317        }
318        let missing: Vec<String> = required
319            .chars()
320            .filter(|&c| !self.draws(c))
321            .map(describe)
322            .collect();
323        if !missing.is_empty() {
324            out.push(format!("missing required glyphs: {}", missing.join(", ")));
325        }
326        if self.wide_sets.keys().any(|name| name.trim().is_empty()) {
327            out.push("a wide set's name must not be empty".to_owned());
328        }
329        for (name, glyphs) in self.all_wide_sets() {
330            let set = in_set(name);
331            let both = glyphs
332                .keys()
333                .filter(|&&c| self.glyphs.contains_key(&c) && !is_wide_punctuation(c));
334            for &c in both {
335                out.push(format!("{} is both narrow and wide{set}", describe(c)));
336            }
337            if self.columns == 0 {
338                continue;
339            }
340            for (&c, &i) in glyphs {
341                if i % self.columns == self.columns - 1 {
342                    out.push(format!(
343                        "wide {}{set} has cell index {i}, the last of its row: its right half \
344                         would start the next row",
345                        describe(c)
346                    ));
347                }
348            }
349        }
350        let mut owner: BTreeMap<u32, char> = BTreeMap::new();
351        for (i, c) in self.cells() {
352            if let Some(&first) = owner.get(&i) {
353                if first != c {
354                    out.push(format!(
355                        "{} and {} share cell index {i}",
356                        describe(first),
357                        describe(c)
358                    ));
359                }
360            } else {
361                owner.insert(i, c);
362            }
363        }
364        out
365    }
366
367    /// Problems fitting the glyph map onto a `width × height` pixel image.
368    pub fn image_problems(&self, width: u32, height: u32) -> Vec<String> {
369        if self.cell_w == 0 || self.cell_h == 0 || self.columns == 0 {
370            return Vec::new(); // Already reported by `problems`.
371        }
372        let mut out = Vec::new();
373        let expected_w = u64::from(self.columns) * u64::from(self.cell_w);
374        if u64::from(width) != expected_w {
375            out.push(format!(
376                "image is {width} px wide, expected {expected_w} (columns × cell_w)"
377            ));
378        }
379        if !height.is_multiple_of(self.cell_h) {
380            out.push(format!(
381                "image height {height} px is not a multiple of cell_h {}",
382                self.cell_h
383            ));
384        }
385        let cells = u64::from(self.columns) * u64::from(height / self.cell_h);
386        if let Some((i, c)) = self.cells().find(|&(i, _)| u64::from(i) >= cells) {
387            out.push(format!(
388                "{} has cell index {i}, but the image only holds {cells} cells",
389                describe(c)
390            ));
391        }
392        out
393    }
394}
395
396/// How many cells of the screen each character takes, in one language:
397/// two for a character with a wide glyph in any set of the atlas
398/// ([`FontAtlasDef::wide`], [`FontAtlasDef::wide_sets`]), and for
399/// [`WIDE_PUNCTUATION`] in a language whose pack has it wide; one for any
400/// other. [`text_width`](crate::lang::text_width) measures text with it,
401/// and a glyph buffer prints with it. It also names the language's wide
402/// set, which a renderer looks wide glyphs up in first
403/// ([`wide_font`](Self::wide_font)). [`CellWidths::NARROW`] has nothing
404/// wide. Cheap to clone: the set is shared.
405#[derive(Debug, Clone, Default, PartialEq, Eq)]
406pub struct CellWidths {
407    /// The wide characters; `None` when there are none.
408    wide: Option<Rc<BTreeSet<char>>>,
409    /// Whether [`WIDE_PUNCTUATION`] is wide.
410    punctuation: bool,
411    /// The language's wide set, if it names one.
412    set: Option<Rc<str>>,
413}
414
415impl CellWidths {
416    /// Nothing is wide: one cell for every character.
417    pub const NARROW: Self = Self {
418        wide: None,
419        punctuation: false,
420        set: None,
421    };
422
423    /// The widths of `atlas`'s glyphs where no language is named: a
424    /// character with a wide glyph in any set takes two cells, but one of
425    /// [`WIDE_PUNCTUATION`] the atlas also has narrow takes one
426    /// ([`in_lang`](Self::in_lang) makes it wide).
427    pub fn of(atlas: &FontAtlasDef) -> Self {
428        Self::wide(atlas.wide_chars())
429    }
430
431    /// Widths where the characters of `wide` take two cells.
432    pub fn wide(wide: impl IntoIterator<Item = char>) -> Self {
433        let wide: BTreeSet<char> = wide.into_iter().collect();
434        Self {
435            wide: (!wide.is_empty()).then(|| Rc::new(wide)),
436            ..Self::NARROW
437        }
438    }
439
440    /// These widths in a language that draws from the wide set `wide_font`
441    /// (`None`: no set of its own) and has [`WIDE_PUNCTUATION`] wide or
442    /// not: what its pack's `lang.ron` says
443    /// ([`LangInfo::widths`](crate::lang::LangInfo::widths)).
444    #[must_use]
445    pub fn in_lang(self, wide_font: Option<&str>, wide_punctuation: bool) -> Self {
446        Self {
447            punctuation: wide_punctuation,
448            set: wide_font.map(Rc::from),
449            ..self
450        }
451    }
452
453    /// The wide set the language draws from first, if it names one
454    /// ([`FontAtlasDef::wide_cell`]).
455    pub fn wide_font(&self) -> Option<&str> {
456        self.set.as_deref()
457    }
458
459    /// Whether [`WIDE_PUNCTUATION`] takes two cells in this language.
460    pub fn wide_punctuation(&self) -> bool {
461        self.punctuation
462    }
463
464    /// Whether `c` takes two cells.
465    pub fn is_wide(&self, c: char) -> bool {
466        self.wide.as_ref().is_some_and(|wide| wide.contains(&c))
467            || (self.punctuation && is_wide_punctuation(c))
468    }
469
470    /// How many cells `c` takes: 1 or 2.
471    pub fn of_char(&self, c: char) -> usize {
472        if self.is_wide(c) { 2 } else { 1 }
473    }
474}
475
476/// ` in wide set "ja"` for a named set, nothing for the unnamed one: what a
477/// message about a wide glyph adds to say which set.
478fn in_set(name: Option<&str>) -> String {
479    name.map(|name| format!(" in wide set {name:?}"))
480        .unwrap_or_default()
481}
482
483/// `'x' (U+0078)`, readable even for invisible or confusable glyphs.
484pub fn describe(c: char) -> String {
485    format!("'{}' (U+{:04X})", c.escape_debug(), u32::from(c))
486}
487
488/// Whether `c` is a combining mark (U+0300 to U+036F). A cell draws one
489/// beside its letter, not over it, so text needs the accented letter as one
490/// character instead (`é`, U+00E9), and an atlas never keeps one.
491pub fn is_combining_mark(c: char) -> bool {
492    ('\u{300}'..='\u{36f}').contains(&c)
493}
494
495/// The ASCII spelling of typographic punctuation (`’` is `'`, `…` is
496/// `...`), if `c` is some.
497pub fn ascii_spelling(c: char) -> Option<&'static str> {
498    match c {
499        '\u{2018}' | '\u{2019}' => Some("'"),
500        '\u{201C}' | '\u{201D}' => Some("\""),
501        '\u{2026}' => Some("..."),
502        '\u{2014}' => Some("--"),
503        '\u{2013}' => Some("-"),
504        _ => None,
505    }
506}
507
508/// A character the atlas can't draw, in one language: where it is first
509/// used, and every key that uses it.
510struct Undrawn<'a> {
511    c: char,
512    first: TextAt<'a>,
513    keys: BTreeSet<(TextPart, &'a str)>,
514    /// The wide set the language draws `c` from, when it is wide there and
515    /// the language names one.
516    set: Option<&'a str>,
517}
518
519impl Undrawn<'_> {
520    fn error(&self) -> ContentError {
521        let more = self.keys.len() - 1;
522        let keys = match more {
523            0 => self.first.key.to_owned(),
524            1 => format!("{} and 1 more key", self.first.key),
525            _ => format!("{} and {more} more keys", self.first.key),
526        };
527        let c = describe(self.c);
528        let message = if is_combining_mark(self.c) {
529            format!(
530                "{c} is a combining mark, which a cell draws beside its letter, not over it \
531                 ({keys}); write the accented letter as one character, as 'é' (U+00E9) is"
532            )
533        } else if let Some(set) = self.set {
534            format!(
535                "{c} isn't in the font atlas's wide set {set:?}, which the language draws from \
536                 ({keys}); remake the atlas with font-atlas --wide {set}=<font.bdf> from a font \
537                 that has it"
538            )
539        } else {
540            let instead = ascii_spelling(self.c)
541                .map(|ascii| format!(", or write {ascii} instead"))
542                .unwrap_or_default();
543            format!(
544                "{c} isn't in the font atlas ({keys}); remake the atlas with font-atlas from a \
545                 font that has it{instead}"
546            )
547        };
548        ContentError::new(self.first.path(), message)
549    }
550}
551
552/// The wide set a language draws from, as [`undrawn`] checks it.
553#[derive(Clone, Copy)]
554enum SetOf<'a> {
555    /// It names none: any set.
556    Any,
557    /// It names this one, which the atlas has.
558    Named(&'a str, &'a WideSet),
559    /// It names one the atlas lacks (said once): its wide characters
560    /// aren't checked.
561    Missing,
562}
563
564/// How one language's text is checked against the atlas: its width table,
565/// and the wide set it draws from.
566struct LangFont<'a> {
567    widths: CellWidths,
568    set: SetOf<'a>,
569}
570
571impl<'a> LangFont<'a> {
572    /// Whether the language draws `c`: from its own wide set if it names
573    /// one and `c` is wide there, any set if wide and it names none, else
574    /// narrow.
575    fn draws(&self, atlas: &FontAtlasDef, c: char) -> bool {
576        if !self.widths.is_wide(c) {
577            return atlas.glyphs.contains_key(&c);
578        }
579        match self.set {
580            SetOf::Any => atlas.has_wide(c),
581            SetOf::Named(_, glyphs) => glyphs.contains_key(&c),
582            SetOf::Missing => true,
583        }
584    }
585
586    /// The set named in the error for `c`: the language's, when `c` is wide.
587    fn set_for(&self, c: char) -> Option<&'a str> {
588        match self.set {
589            SetOf::Named(name, _) if self.widths.is_wide(c) => Some(name),
590            _ => None,
591        }
592    }
593}
594
595/// Every character of `lang`'s text ([`Lang::texts`]) that `atlas` can't
596/// draw in its language: a check across assets, after both load. A
597/// language's wide characters (as its widths have them, [`Lang::widths`]:
598/// a pack's or the source's) must be in the wide set it names, or in any
599/// set if it names none; the rest narrow. A pack that names a set the atlas
600/// lacks is one error, on its `lang.ron` (the source, on its folder), and
601/// its wide characters aren't checked. One error per language and
602/// character, in the order of their first use, on the file of that use
603/// ([`TextAt::path`]), naming its key and how many more keys of that
604/// language use it. Control characters (a `\n`) are never errors; a
605/// combining mark ([`is_combining_mark`]) always is, glyph or not. The
606/// errors have no line: a [`Lang`] keeps keys, which find the line.
607pub fn undrawn(atlas: &FontAtlasDef, lang: &Lang) -> Vec<ContentError> {
608    let base = CellWidths::of(atlas);
609    let mut errors = Vec::new();
610    let mut fonts: BTreeMap<Option<&LangCode>, LangFont<'_>> = BTreeMap::new();
611    let mut found: Vec<Undrawn<'_>> = Vec::new();
612    let mut index: BTreeMap<(Option<&LangCode>, char), usize> = BTreeMap::new();
613    for (at, text) in lang.texts() {
614        let font = fonts.entry(at.lang).or_insert_with(|| {
615            let source = &lang.source_lang().code;
616            let widths = lang.widths(at.lang.unwrap_or(source), &base);
617            let set = match widths.wide_font() {
618                None => SetOf::Any,
619                Some(name) => {
620                    if let Some((name, glyphs)) = atlas.wide_sets.get_key_value(name) {
621                        SetOf::Named(name, glyphs)
622                    } else {
623                        // A pack names it in its lang.ron; the source, in
624                        // the game's code (`SourceLang::with_wide_font`).
625                        let (path, whose) = match at.lang {
626                            Some(code) => (format!("{code}/{}", crate::lang::INFO_FILE), ""),
627                            None => (source.to_string(), "the source language's "),
628                        };
629                        errors.push(ContentError::new(
630                            path,
631                            format!(
632                                "{whose}wide_font names {name:?}, a wide set the font atlas \
633                                 doesn't have; remake the atlas with font-atlas --wide \
634                                 {name}=<font.bdf>"
635                            ),
636                        ));
637                        SetOf::Missing
638                    }
639                }
640            };
641            LangFont { widths, set }
642        });
643        for c in text.chars() {
644            if c.is_control() || (font.draws(atlas, c) && !is_combining_mark(c)) {
645                continue;
646            }
647            let set = font.set_for(c);
648            let i = *index.entry((at.lang, c)).or_insert_with(|| {
649                found.push(Undrawn {
650                    c,
651                    first: at,
652                    keys: BTreeSet::new(),
653                    set,
654                });
655                found.len() - 1
656            });
657            found[i].keys.insert((at.part, at.key));
658        }
659    }
660    errors.extend(found.iter().map(Undrawn::error));
661    errors
662}
663
664#[cfg(test)]
665mod tests {
666    use super::*;
667    use crate::image::png_header;
668
669    /// The glyphs these tests require: a few, an invisible one among them.
670    const REQUIRED: &str = " !'?ABCabc\u{ad}♣─";
671
672    /// A valid atlas holding exactly the required glyphs, 16 per row.
673    fn required_only() -> FontAtlasDef {
674        FontAtlasDef {
675            cell_w: 8,
676            cell_h: 16,
677            columns: 16,
678            glyphs: REQUIRED.chars().zip(0..).collect(),
679            ..FontAtlasDef::default()
680        }
681    }
682
683    fn rows_for(def: &FontAtlasDef) -> u32 {
684        let n = u32::try_from(def.glyphs.len()).unwrap_or(u32::MAX);
685        n.div_ceil(def.columns)
686    }
687
688    fn from_source(source: &str, png: Option<&[u8]>) -> Result<FontAtlasDef, Vec<ContentError>> {
689        FontAtlasDef::from_source("a.ron", source, "base/fonts/atlas.png", png, REQUIRED)
690    }
691
692    #[test]
693    fn describe_names_the_glyph_and_its_code_point() {
694        assert_eq!(describe('a'), "'a' (U+0061)");
695        assert_eq!(describe('\''), "'\\'' (U+0027)");
696        assert_eq!(describe('\u{ad}'), "'\\u{ad}' (U+00AD)");
697        assert_eq!(describe('♣'), "'♣' (U+2663)");
698        assert!(REQUIRED.contains(FALLBACK_GLYPH));
699    }
700
701    #[test]
702    fn cell_and_glyph_rects() {
703        let def = FontAtlasDef {
704            cell_w: 8,
705            cell_h: 16,
706            columns: 32,
707            glyphs: [('a', 0), ('b', 31), ('c', 32), ('d', 70)].into(),
708            wide: [('あ', 33)].into(),
709            wide_sets: BTreeMap::new(),
710        };
711        let r = |x, y| Some(AtlasRect { x, y, w: 8, h: 16 });
712        assert_eq!(def.glyph_rect('a'), r(0, 0));
713        assert_eq!(def.glyph_rect('b'), r(248, 0));
714        assert_eq!(def.glyph_rect('c'), r(0, 16));
715        assert_eq!(def.glyph_rect('d'), r(48, 32));
716        assert_eq!(def.glyph_rect('z'), None);
717        // A wide glyph is both its cells, and no narrow glyph.
718        let wide = Some(AtlasRect {
719            x: 8,
720            y: 16,
721            w: 16,
722            h: 16,
723        });
724        assert_eq!(def.wide_rect('あ'), wide);
725        assert_eq!(def.glyph_rect('あ'), None);
726        assert_eq!(def.wide_rect('a'), None);
727    }
728
729    /// [`required_only`] with wide glyphs `あ` and `い` after it, in cells
730    /// 16 and 17, and 18 and 19.
731    fn with_wide() -> FontAtlasDef {
732        FontAtlasDef {
733            wide: [('あ', 16), ('い', 18)].into(),
734            ..required_only()
735        }
736    }
737
738    #[test]
739    fn a_wide_glyph_is_drawn_and_listed_among_the_narrow_ones() {
740        let def = with_wide();
741        assert_eq!(def.problems(REQUIRED), Vec::<String>::new());
742        assert_eq!(def.image_problems(128, 32), Vec::<String>::new());
743        assert!(def.draws('あ') && def.draws('A') && !def.draws('う'));
744        let drawn: Vec<char> = def.drawn().collect();
745        let mut sorted = drawn.clone();
746        sorted.sort_unstable();
747        assert_eq!(drawn, sorted, "in code-point order");
748        assert_eq!(drawn.len(), REQUIRED.chars().count() + 2);
749        // A required glyph may be wide.
750        assert_eq!(def.problems("あ"), Vec::<String>::new());
751        // Narrow and wide in turn are listed in turn, either first.
752        let mixed = FontAtlasDef {
753            glyphs: [('b', 0), ('d', 1)].into(),
754            wide: [('a', 2), ('c', 4), ('e', 6)].into(),
755            ..FontAtlasDef::default()
756        };
757        assert_eq!(mixed.drawn().collect::<String>(), "abcde");
758    }
759
760    #[test]
761    fn wide_glyph_problems() {
762        // Both narrow and wide.
763        let mut def = with_wide();
764        def.glyphs.insert('あ', 30);
765        assert_eq!(
766            def.problems(REQUIRED),
767            ["'あ' (U+3042) is both narrow and wide"]
768        );
769        // Its right half in a narrow glyph's cell.
770        let mut def = with_wide();
771        def.wide.insert('う', 12);
772        assert_eq!(
773            def.problems(REQUIRED),
774            ["'─' (U+2500) and 'う' (U+3046) share cell index 12"]
775        );
776        // Split across a row's end: cell 31 is the last of the second row,
777        // and the last the image holds, so its right half is past it too.
778        let mut def = with_wide();
779        def.wide.insert('う', 31);
780        assert_eq!(
781            def.problems(REQUIRED),
782            [
783                "wide 'う' (U+3046) has cell index 31, the last of its row: its right half \
784                 would start the next row"
785            ]
786        );
787        assert_eq!(
788            def.image_problems(128, 32),
789            ["'う' (U+3046) has cell index 32, but the image only holds 32 cells"]
790        );
791    }
792
793    /// [`required_only`] with wide sets `ja` (直 and あ, in cells 16 and
794    /// 18) and `zh-Hans` (直 and 简, in cells 20 and 22), and `“` and `…`
795    /// in `zh-Hans` too (24 and 26) beside their narrow glyphs (28, 29).
796    fn with_sets() -> FontAtlasDef {
797        let mut def = required_only();
798        def.glyphs.extend([('“', 28), ('…', 29)]);
799        def.wide_sets = [
800            ("ja".to_owned(), [('直', 16), ('あ', 18)].into()),
801            (
802                "zh-Hans".to_owned(),
803                [('直', 20), ('简', 22), ('“', 24), ('…', 26)].into(),
804            ),
805        ]
806        .into();
807        def
808    }
809
810    #[test]
811    fn a_wide_glyph_is_looked_up_in_the_languages_set_first_then_any_other() {
812        let def = with_sets();
813        assert_eq!(def.problems(REQUIRED), Vec::<String>::new());
814        assert_eq!(def.image_problems(128, 32), Vec::<String>::new());
815        // 直 is in both sets, each with its own shape.
816        assert_eq!(def.wide_cell(Some("ja"), '直'), Some(16));
817        assert_eq!(def.wide_cell(Some("zh-Hans"), '直'), Some(20));
818        // A character the language's set lacks comes from another set.
819        assert_eq!(def.wide_cell(Some("ja"), '简'), Some(22));
820        assert_eq!(def.wide_cell(Some("zh-Hans"), 'あ'), Some(18));
821        // No set named, or one the atlas lacks: the unnamed set, then the
822        // named ones in name order.
823        assert_eq!(def.wide_cell(None, '直'), Some(16));
824        assert_eq!(def.wide_cell(Some("ko"), '直'), Some(16));
825        let mut unnamed = with_sets();
826        unnamed.wide.insert('直', 30);
827        assert_eq!(unnamed.wide_cell(None, '直'), Some(30));
828        assert_eq!(unnamed.wide_cell(Some("zh-Hans"), '直'), Some(20));
829        assert_eq!(def.wide_cell(Some("ja"), 'A'), None);
830        let rect = def.wide_rect_in(Some("zh-Hans"), '直');
831        assert_eq!(rect.map(|r| (r.x, r.y, r.w)), Some((32, 16, 16)));
832        assert_eq!(def.wide_rect('直'), def.wide_rect_in(Some("ja"), '直'));
833        // Every set counts as drawn, and is listed once.
834        assert!(def.draws('简') && def.draws('あ') && !def.draws('骨'));
835        assert_eq!(def.drawn().filter(|&c| c == '直').count(), 1);
836        assert_eq!(def.wide_set(Some("ja")).map(BTreeMap::len), Some(2));
837        assert_eq!(def.wide_set(None), Some(&BTreeMap::new()));
838        assert_eq!(def.wide_set(Some("ko")), None);
839        assert_eq!(def.cell_end(), 30);
840    }
841
842    #[test]
843    fn wide_set_problems() {
844        // Six punctuation marks may be narrow and wide; no other character.
845        let mut def = with_sets();
846        def.glyphs.insert('简', 30);
847        assert_eq!(
848            def.problems(REQUIRED),
849            ["'简' (U+7B80) is both narrow and wide in wide set \"zh-Hans\""]
850        );
851        // A set's glyph past the image, or across a row's end.
852        let mut def = with_sets();
853        def.wide_sets.get_mut("ja").unwrap().insert('骨', 31);
854        assert_eq!(
855            def.problems(REQUIRED),
856            [
857                "wide '骨' (U+9AA8) in wide set \"ja\" has cell index 31, the last of its row: \
858                 its right half would start the next row"
859            ]
860        );
861        assert_eq!(
862            def.image_problems(128, 32),
863            ["'骨' (U+9AA8) has cell index 32, but the image only holds 32 cells"]
864        );
865        // Two sets' glyphs in one cell, and an empty name.
866        let mut def = with_sets();
867        def.wide_sets.get_mut("ja").unwrap().insert('骨', 20);
868        def.wide_sets.insert(" ".to_owned(), BTreeMap::new());
869        assert_eq!(
870            def.problems(REQUIRED),
871            [
872                "a wide set's name must not be empty",
873                "'骨' (U+9AA8) and '直' (U+76F4) share cell index 20",
874                "'骨' (U+9AA8) and '直' (U+76F4) share cell index 21",
875            ]
876        );
877        // An atlas file without sets reads as none.
878        let src = "(cell_w: 8, cell_h: 16, columns: 16, glyphs: {}, wide: {'あ': 0})";
879        let def: FontAtlasDef = ron::from_str(src).unwrap();
880        assert!(def.wide_sets.is_empty());
881        let src =
882            "(cell_w: 8, cell_h: 16, columns: 16, glyphs: {}, wide_sets: {\"ja\": {'あ': 0}})";
883        let def: FontAtlasDef = ron::from_str(src).unwrap();
884        assert_eq!(def.wide_cell(Some("ja"), 'あ'), Some(0));
885    }
886
887    #[test]
888    fn widths_by_language_make_six_punctuation_marks_wide() {
889        let base = CellWidths::of(&with_sets());
890        // Every set's characters are wide; punctuation the atlas also has
891        // narrow is narrow where no language says otherwise.
892        assert!(base.is_wide('直') && base.is_wide('简') && base.is_wide('あ'));
893        assert!(!base.is_wide('“') && !base.is_wide('…'));
894        assert_eq!(base.wide_font(), None);
895        let chinese = base.clone().in_lang(Some("zh-Hans"), true);
896        assert_eq!(chinese.wide_font(), Some("zh-Hans"));
897        assert!(chinese.wide_punctuation());
898        for c in WIDE_PUNCTUATION {
899            assert!(chinese.is_wide(c), "{c}");
900            assert!(!base.is_wide(c), "{c}");
901        }
902        let width = |text: &str, widths: &CellWidths| crate::lang::text_width(text, widths);
903        assert_eq!(width("“直”", &chinese), 6);
904        assert_eq!(width("……", &chinese), 4);
905        assert_eq!(width("“ok”", &base), 4);
906        assert_eq!(width("“ok”", &base.clone().in_lang(Some("ja"), false)), 4);
907        // Punctuation only wide in the atlas (2301's) stays wide.
908        let mut only_wide = with_sets();
909        only_wide.glyphs.remove(&'…');
910        assert!(CellWidths::of(&only_wide).is_wide('…'));
911        assert!(!CellWidths::NARROW.wide_punctuation());
912    }
913
914    #[test]
915    fn widths_count_a_wide_glyph_twice() {
916        let widths = CellWidths::of(&with_wide());
917        assert!(widths.is_wide('あ') && !widths.is_wide('A'));
918        assert_eq!((widths.of_char('い'), widths.of_char('A')), (2, 1));
919        assert_eq!(CellWidths::of(&required_only()), CellWidths::NARROW);
920        assert_eq!(CellWidths::wide([]), CellWidths::default());
921        assert!(!CellWidths::NARROW.is_wide('あ'));
922    }
923
924    #[test]
925    fn valid_atlas_has_no_problems() {
926        let def = required_only();
927        assert_eq!(def.problems(REQUIRED), Vec::<String>::new());
928        let rows = rows_for(&def);
929        assert_eq!(def.image_problems(128, rows * 16), Vec::<String>::new());
930    }
931
932    #[test]
933    fn zero_sizes_are_problems() {
934        let mut def = required_only();
935        def.cell_w = 0;
936        def.columns = 0;
937        assert_eq!(
938            def.problems(REQUIRED),
939            [
940                "cell_w must be greater than 0",
941                "columns must be greater than 0"
942            ]
943        );
944        let got = def.image_problems(0, 0);
945        assert!(got.is_empty(), "{got:?}");
946        let mut def = required_only();
947        def.cell_h = 0;
948        assert_eq!(def.problems(REQUIRED), ["cell_h must be greater than 0"]);
949        let got = def.image_problems(0, 0);
950        assert!(got.is_empty(), "{got:?}");
951        // No columns, and a wide glyph: said, not divided by.
952        let mut def = with_wide();
953        def.columns = 0;
954        assert_eq!(def.problems(REQUIRED), ["columns must be greater than 0"]);
955    }
956
957    #[test]
958    fn missing_required_glyphs_listed_together() {
959        let mut def = required_only();
960        def.glyphs.remove(&'♣');
961        def.glyphs.remove(&'\'');
962        assert_eq!(
963            def.problems(REQUIRED),
964            ["missing required glyphs: '\\'' (U+0027), '♣' (U+2663)"]
965        );
966        // Nothing required, nothing missing.
967        assert_eq!(def.problems(""), Vec::<String>::new());
968    }
969
970    #[test]
971    fn shared_index_is_problem() {
972        let mut def = required_only();
973        def.glyphs.insert('\u{2603}', 0);
974        assert_eq!(
975            def.problems(REQUIRED),
976            ["' ' (U+0020) and '☃' (U+2603) share cell index 0"]
977        );
978    }
979
980    #[test]
981    fn image_size_problems() {
982        let def = required_only();
983        let rows = rows_for(&def);
984        assert_eq!(
985            def.image_problems(120, rows * 16),
986            ["image is 120 px wide, expected 128 (columns × cell_w)"]
987        );
988        assert_eq!(
989            def.image_problems(128, rows * 16 + 3),
990            [format!(
991                "image height {} px is not a multiple of cell_h 16",
992                rows * 16 + 3
993            )]
994        );
995        // Two columns of three rows hold six cells: the last is index 5.
996        let narrow = FontAtlasDef {
997            cell_w: 8,
998            cell_h: 16,
999            columns: 2,
1000            glyphs: [('a', 5)].into(),
1001            ..FontAtlasDef::default()
1002        };
1003        assert_eq!(narrow.image_problems(16, 48), Vec::<String>::new());
1004        assert_eq!(narrow.image_problems(16, 32).len(), 1);
1005        // A glyph in the cell just past what the image holds.
1006        let mut def = required_only();
1007        def.glyphs.insert('z', rows * 16);
1008        let problems = def.image_problems(128, rows * 16);
1009        assert_eq!(
1010            problems,
1011            [format!(
1012                "'z' (U+007A) has cell index {}, but the image only holds {} cells",
1013                rows * 16,
1014                rows * 16
1015            )]
1016        );
1017    }
1018
1019    #[test]
1020    fn from_source_reports_file_and_image_errors() {
1021        let src = "(cell_w: 8, cell_h: 16, columns: 0, glyphs: {'a': 0})";
1022        let errs = from_source(src, None).err().unwrap_or_default();
1023        let text: Vec<String> = errs.iter().map(ToString::to_string).collect();
1024        assert_eq!(text.len(), 3, "{text:?}");
1025        assert_eq!(text[0], "a.ron: columns must be greater than 0");
1026        assert!(text[1].starts_with("a.ron: missing required glyphs: ' ' (U+0020)"));
1027        assert_eq!(
1028            text[2],
1029            "base/fonts/atlas.png: file not found in asset bundle"
1030        );
1031        let errs = from_source(src, Some(b"nope")).err().unwrap_or_default();
1032        assert_eq!(
1033            errs.last().map(ToString::to_string).as_deref(),
1034            Some("base/fonts/atlas.png: not a valid PNG file")
1035        );
1036    }
1037
1038    #[test]
1039    fn draws_exactly_the_characters_drawn_lists() {
1040        let def = required_only();
1041        let drawn: Vec<char> = def.drawn().collect();
1042        let mut sorted = drawn.clone();
1043        sorted.sort_unstable();
1044        assert_eq!(drawn, sorted, "in code-point order");
1045        assert_eq!(drawn.len(), REQUIRED.chars().count());
1046        assert!(drawn.iter().all(|&c| def.draws(c)));
1047        assert!(!def.draws('Ж') && !def.draws('z'));
1048        assert_eq!(FontAtlasDef::default().drawn().count(), 0);
1049    }
1050
1051    mod undrawn {
1052        use alloc::collections::BTreeMap;
1053
1054        use super::super::*;
1055        use crate::lang::{PackRules, pack_from_sources};
1056
1057        const RULES: PackRules<'static> = PackRules {
1058            data_problem: |_, _| None,
1059            tokens: &crate::dialogue::NoTokens,
1060            limits: crate::dialogue::tests::LIMITS,
1061            widths: &crate::font::CellWidths::NARROW,
1062            variants: &[],
1063        };
1064
1065        /// An atlas of ASCII's printable characters, and `extra`.
1066        fn ascii_and(extra: &str) -> FontAtlasDef {
1067            FontAtlasDef {
1068                cell_w: 8,
1069                cell_h: 16,
1070                columns: 16,
1071                glyphs: (' '..='~').chain(extra.chars()).zip(0..).collect(),
1072                ..FontAtlasDef::default()
1073            }
1074        }
1075
1076        fn owned(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
1077            pairs
1078                .iter()
1079                .map(|&(k, v)| (k.to_owned(), v.to_owned()))
1080                .collect()
1081        }
1082
1083        /// English screen text `english`, and a pack per `(code, entries)`
1084        /// translating those keys to those texts.
1085        fn lang(english: &[(&str, &str)], packs: &[(&str, &[(&str, &str)])]) -> Lang {
1086            let english = owned(english);
1087            let packs = packs
1088                .iter()
1089                .map(|&(code, entries)| {
1090                    let ui: Vec<String> = entries
1091                        .iter()
1092                        .map(|(key, text)| {
1093                            let source = &english[*key];
1094                            format!("(key: {key:?}, source: {source:?}, text: {text:?})")
1095                        })
1096                        .collect();
1097                    let ui = format!("[{}]", ui.join(", "));
1098                    let info = r#"(name: "Test", made_by: Machine)"#;
1099                    let pack =
1100                        pack_from_sources(("lang.ron", info), ("ui.ron", &ui), &english, &RULES);
1101                    (LangCode::new(code).unwrap(), pack.unwrap())
1102                })
1103                .collect();
1104            Lang::new(english, packs)
1105        }
1106
1107        fn errors(atlas: &FontAtlasDef, lang: &Lang) -> Vec<String> {
1108            undrawn(atlas, lang)
1109                .iter()
1110                .map(ToString::to_string)
1111                .collect()
1112        }
1113
1114        #[test]
1115        fn a_character_the_atlas_lacks_names_its_file_its_key_and_what_to_do() {
1116            let lang = lang(
1117                &[("menu.title", "Start")],
1118                &[("ru", &[("menu.title", "Жa")])],
1119            );
1120            let found = undrawn(&ascii_and(""), &lang);
1121            assert_eq!(found.len(), 1, "{found:?}");
1122            assert_eq!(found[0].file, "ru/ui.ron");
1123            assert_eq!(found[0].line, None);
1124            assert_eq!(
1125                found[0].message,
1126                "'Ж' (U+0416) isn't in the font atlas (menu.title); remake the atlas with \
1127                 font-atlas from a font that has it"
1128            );
1129            assert_eq!(errors(&ascii_and("Ж"), &lang), Vec::<String>::new());
1130        }
1131
1132        #[test]
1133        fn a_wide_glyph_counts_as_drawn() {
1134            let lang = lang(
1135                &[("menu.title", "Start")],
1136                &[("ja", &[("menu.title", "あA")])],
1137            );
1138            let found = errors(&ascii_and(""), &lang);
1139            assert_eq!(found.len(), 1, "{found:?}");
1140            assert!(found[0].starts_with("ja/ui.ron: 'あ' (U+3042) isn't in the font atlas"));
1141            let mut atlas = ascii_and("");
1142            atlas.wide.insert('あ', 96);
1143            assert_eq!(errors(&atlas, &lang), Vec::<String>::new());
1144        }
1145
1146        /// English's `menu.title`, and a pack per `(code, lang.ron's extra
1147        /// fields, its menu.title)`.
1148        fn lang_with_info(packs: &[(&str, &str, &str)]) -> Lang {
1149            let english = owned(&[("menu.title", "Start")]);
1150            let packs = packs
1151                .iter()
1152                .map(|&(code, extra, text)| {
1153                    let ui = format!(r#"[(key: "menu.title", source: "Start", text: {text:?})]"#);
1154                    let info = format!(r#"(name: "Test", made_by: Machine{extra})"#);
1155                    let pack =
1156                        pack_from_sources(("lang.ron", &info), ("ui.ron", &ui), &english, &RULES);
1157                    (LangCode::new(code).unwrap(), pack.unwrap())
1158                })
1159                .collect();
1160            Lang::new(english, packs)
1161        }
1162
1163        /// An ASCII atlas with wide sets `ja` (直, あ) and `zh-Hans` (直, 简,
1164        /// and `“ ”` beside their narrow glyphs).
1165        fn two_sets() -> FontAtlasDef {
1166            let mut atlas = ascii_and("“”");
1167            atlas.wide_sets = [
1168                ("ja".to_owned(), [('直', 100), ('あ', 102)].into()),
1169                (
1170                    "zh-Hans".to_owned(),
1171                    [('直', 104), ('简', 106), ('“', 108), ('”', 110)].into(),
1172                ),
1173            ]
1174            .into();
1175            atlas
1176        }
1177
1178        #[test]
1179        fn a_packs_wide_characters_must_be_in_the_set_it_names() {
1180            let ja = r#", wide_font: Some("ja")"#;
1181            let zh = r#", wide_font: Some("zh-Hans"), wide_punctuation: true"#;
1182            // Each pack's characters are in its own set: nothing to say.
1183            let fine = lang_with_info(&[("ja", ja, "直あ"), ("zh-Hans", zh, "“直简”")]);
1184            assert_eq!(errors(&two_sets(), &fine), Vec::<String>::new());
1185            // 简 is in the atlas, but not in the set Japanese draws from.
1186            let lacks = lang_with_info(&[("ja", ja, "直简")]);
1187            assert_eq!(
1188                errors(&two_sets(), &lacks),
1189                [
1190                    "ja/ui.ron: '简' (U+7B80) isn't in the font atlas's wide set \"ja\", which \
1191                     the language draws from (menu.title); remake the atlas with font-atlas \
1192                     --wide ja=<font.bdf> from a font that has it"
1193                ]
1194            );
1195            // A narrow character it lacks is the atlas's, not its set's.
1196            let narrow_missing = lang_with_info(&[("ja", ja, "直Ж")]);
1197            assert_eq!(
1198                errors(&two_sets(), &narrow_missing),
1199                [
1200                    "ja/ui.ron: 'Ж' (U+0416) isn't in the font atlas (menu.title); remake the \
1201                     atlas with font-atlas from a font that has it"
1202                ]
1203            );
1204            // Its wide punctuation must be in its set too; narrow, `“` is.
1205            let mut no_quotes = two_sets();
1206            no_quotes.wide_sets.get_mut("zh-Hans").unwrap().remove(&'“');
1207            let found = errors(&no_quotes, &fine);
1208            assert_eq!(found.len(), 1, "{found:?}");
1209            assert!(
1210                found[0]
1211                    .starts_with("zh-Hans/ui.ron: '“' (U+201C) isn't in the font atlas's wide set")
1212            );
1213            let narrow = lang_with_info(&[("ja", ja, "“直”")]);
1214            assert_eq!(errors(&no_quotes, &narrow), Vec::<String>::new());
1215            // A pack naming no set may draw from any.
1216            let any = lang_with_info(&[("ko", "", "直简あ")]);
1217            assert_eq!(errors(&two_sets(), &any), Vec::<String>::new());
1218            // A set the atlas lacks is said once, on the pack's lang.ron.
1219            let ko = r#", wide_font: Some("ko")"#;
1220            let missing = lang_with_info(&[("ko", ko, "直简Ж")]);
1221            let found = errors(&two_sets(), &missing);
1222            assert_eq!(
1223                found[0],
1224                "ko/lang.ron: wide_font names \"ko\", a wide set the font atlas doesn't have; \
1225                 remake the atlas with font-atlas --wide ko=<font.bdf>"
1226            );
1227            assert_eq!(found.len(), 2, "{found:?}");
1228            assert!(found[1].starts_with("ko/ui.ron: 'Ж' (U+0416) isn't in the font atlas ("));
1229        }
1230
1231        #[test]
1232        fn the_sources_wide_characters_must_be_in_the_set_it_names() {
1233            use crate::lang::SourceLang;
1234            let source = |set: &str| {
1235                let code = LangCode::new("zh-Hans").unwrap();
1236                SourceLang::new(code, "中文")
1237                    .with_wide_font(set)
1238                    .with_wide_punctuation(true)
1239            };
1240            let written = |text: &str, set: &str| {
1241                lang(&[("menu.title", text)], &[]).with_source_lang(source(set))
1242            };
1243            assert_eq!(
1244                errors(&two_sets(), &written("“直简”", "zh-Hans")),
1245                Vec::<String>::new()
1246            );
1247            // 简 is wide in the atlas, but not in the set it names.
1248            assert_eq!(
1249                errors(&two_sets(), &written("直简", "ja")),
1250                [
1251                    "zh-Hans: '简' (U+7B80) isn't in the font atlas's wide set \"ja\", which the \
1252                     language draws from (menu.title); remake the atlas with font-atlas --wide \
1253                     ja=<font.bdf> from a font that has it"
1254                ]
1255            );
1256            // Its punctuation is wide, so it must be in its set too.
1257            let mut no_quotes = two_sets();
1258            no_quotes.wide_sets.get_mut("zh-Hans").unwrap().remove(&'“');
1259            let found = errors(&no_quotes, &written("“直”", "zh-Hans"));
1260            assert_eq!(found.len(), 1, "{found:?}");
1261            assert!(
1262                found[0].starts_with("zh-Hans: '“' (U+201C) isn't in the font atlas's wide set")
1263            );
1264            // A set the atlas lacks is said once, on the source's folder.
1265            assert_eq!(
1266                errors(&two_sets(), &written("直", "ko")),
1267                [
1268                    "zh-Hans: the source language's wide_font names \"ko\", a wide set the font \
1269                     atlas doesn't have; remake the atlas with font-atlas --wide ko=<font.bdf>"
1270                ]
1271            );
1272        }
1273
1274        #[test]
1275        fn one_error_a_language_and_character_counting_its_other_keys() {
1276            let english = [("a.one", "One"), ("a.three", "Three"), ("a.two", "Two")];
1277            let ru: &[(&str, &str)] = &[("a.one", "Ж"), ("a.two", "ЖЖ"), ("a.three", "Жи")];
1278            let uk: &[(&str, &str)] = &[("a.two", "Ж")];
1279            let both = lang(&english, &[("ru", ru), ("uk", uk)]);
1280            let lacks = "isn't in the font atlas";
1281            let fix = "remake the atlas with font-atlas from a font that has it";
1282            assert_eq!(
1283                errors(&ascii_and(""), &both),
1284                [
1285                    format!("ru/ui.ron: 'Ж' (U+0416) {lacks} (a.one and 2 more keys); {fix}"),
1286                    format!("ru/ui.ron: 'и' (U+0438) {lacks} (a.three); {fix}"),
1287                    format!("uk/ui.ron: 'Ж' (U+0416) {lacks} (a.two); {fix}"),
1288                ]
1289            );
1290            let two = lang(&english, &[("ru", &[("a.one", "Ж"), ("a.two", "Ж")])]);
1291            assert!(errors(&ascii_and(""), &two)[0].contains("(a.one and 1 more key)"));
1292        }
1293
1294        #[test]
1295        fn the_english_text_is_checked_too() {
1296            let lang = lang(&[("menu.title", "Café")], &[]);
1297            let found = undrawn(&ascii_and(""), &lang);
1298            assert_eq!(found.len(), 1);
1299            assert_eq!(found[0].file, "en");
1300            assert!(
1301                found[0]
1302                    .message
1303                    .starts_with("'é' (U+00E9) isn't in the font atlas")
1304            );
1305        }
1306
1307        #[test]
1308        fn punctuation_names_its_ascii_spelling_and_a_newline_is_never_an_error() {
1309            let lang = lang(&[("a.one", "It’s\nhere…")], &[]);
1310            let found = errors(&ascii_and(""), &lang);
1311            assert_eq!(found.len(), 2, "{found:?}");
1312            assert!(found[0].starts_with("en: '’' (U+2019)"), "{found:?}");
1313            assert!(found[0].ends_with(", or write ' instead"), "{found:?}");
1314            assert!(found[1].ends_with(", or write ... instead"), "{found:?}");
1315            for c in ['\u{2018}', '\u{201C}', '\u{201D}', '\u{2014}', '\u{2013}'] {
1316                assert!(ascii_spelling(c).is_some(), "{c:?}");
1317            }
1318            assert_eq!(ascii_spelling('a'), None);
1319        }
1320
1321        #[test]
1322        fn a_combining_mark_is_an_error_even_with_its_glyph() {
1323            let lang = lang(&[("a.one", "Cafe\u{301}")], &[]);
1324            let found = errors(&ascii_and("\u{301}"), &lang);
1325            assert_eq!(
1326                found,
1327                [
1328                    "en: '\\u{301}' (U+0301) is a combining mark, which a cell draws beside its \
1329                  letter, not over it (a.one); write the accented letter as one character, as \
1330                  'é' (U+00E9) is"
1331                ]
1332            );
1333            assert!(is_combining_mark('\u{300}') && is_combining_mark('\u{36f}'));
1334            assert!(!is_combining_mark('\u{2ff}') && !is_combining_mark('\u{370}'));
1335        }
1336
1337        proptest::proptest! {
1338            /// `undrawn` is empty exactly when every character of the text
1339            /// but a control character is drawn and none is a combining
1340            /// mark.
1341            #[test]
1342            fn undrawn_is_empty_exactly_when_the_atlas_draws_the_text(
1343                texts in proptest::collection::vec("\\PC{0,6}|[a\\n\u{301}é]{0,4}", 1..4),
1344                extra in "[\u{301}éЖ]{0,3}",
1345            ) {
1346                let english: Vec<(String, String)> = texts
1347                    .iter()
1348                    .enumerate()
1349                    .map(|(i, text)| (format!("k.{i}"), text.clone()))
1350                    .collect();
1351                let lang = Lang::new(english.into_iter().collect(), BTreeMap::new());
1352                let atlas = ascii_and(&extra);
1353                let clean = texts.iter().flat_map(|t| t.chars()).all(|c| {
1354                    c.is_control() || (atlas.draws(c) && !is_combining_mark(c))
1355                });
1356                proptest::prop_assert_eq!(undrawn(&atlas, &lang).is_empty(), clean);
1357            }
1358        }
1359    }
1360
1361    #[test]
1362    fn from_source_ok_and_syntax_error() {
1363        let def = required_only();
1364        let src = ron::to_string(&def).unwrap_or_default();
1365        let png = png_header(128, rows_for(&def) * 16);
1366        assert_eq!(from_source(&src, Some(&png)), Ok(def));
1367        let png = png_header(64, rows_for(&required_only()) * 16);
1368        let errs = from_source(&src, Some(&png)).err().unwrap_or_default();
1369        assert_eq!(errs.len(), 1);
1370        assert_eq!(errs[0].file, "base/fonts/atlas.png");
1371        let errs = from_source("(cell_w: ", None).err().unwrap_or_default();
1372        assert_eq!(errs.len(), 1);
1373        assert!(errs[0].line.is_some());
1374    }
1375}