raster_font 0.1.1

A format for authoring and using image-backed fonts
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
//! Builder pattern for constructing a [`RasterFont`] in multiple stages.
//!
//! [`FontAtlasBuilder`] uses Rust's type system to enforce that required build steps are
//! completed in order before the font can be finalized.
//!
//! # Typical build sequence
//!
//! ```rust,no_run
//! # use std::error::Error;
//! # use raster_font::{
//! #     backend::prelude::*,
//! #     builder::{GlyphSheet, FontAtlasBuilder, errors::FontBuilderError},
//! #     meta::FontLayout
//! # };
//! #
//! fn build_font<Backend, Builder, Image>(
//!     name: Option<String>,
//!     layout: FontLayout,
//!     sheet: GlyphSheet<Image>,
//!     backend_builder: Builder,
//! ) -> Result<RasterFont<B>, FontBuilderError<Builder::Error>>
//!     where
//!         Backend: Backend,
//!         Builder: BackendBuilder<Backend = Backend, Error: Error, Sheet = Image>
//! {
//!     FontAtlasBuilder::from(layout.unique()) // reserve capacity for the number of unique glyphs and sequences in the layout.
//!         .with_image(sheet)                  // attach the glyph sprite sheet
//!         .populate_layout(&layout)           // compute glyph regions from the layout
//!         .custom_glyphs(layout.custom())?    // (optional) register hand-specified glyphs
//!         .with_name(name)                    // (optional) name the font
//!         .build(backend_builder)             // finalize and upload to the backend
//! }
//! ```
//!
//! Steps that have not yet been completed are simply absent from the type's API -- the compiler
//! rejects any attempt to call `build` before `populate_layout`, for example.
use crate::{
    backend::{Backend, BackendBuilder, RasterFont},
    core::{
        AtlasIndex, IGlyphOffset, IGlyphRegion, Sequence, Token, UGlyphRegion, UGlyphSize, Unique,
    },
    meta::{CustomGlyph, FontLayout, FontTrack, GlyphOverride, PackingMode},
};

pub mod prelude {
    pub use super::{FontAtlasBuilder, GlyphSheet, errors::FontBuilderError};
}

#[cfg(feature = "bevy")]
use bevy_platform::collections::HashMap;
#[cfg(not(feature = "bevy"))]
use std::collections::HashMap;

use std::{error::Error, marker::PhantomData};

/// Glyph rendering properties expressed in *unsigned* pixel coordinates.
#[derive(Clone, Debug)]
pub struct UTokenProps {
    /// The axis-aligned bounding rectangle of the glyph within the texture atlas.
    pub region: UGlyphRegion,
    /// A signed pixel offset applied to the glyph's draw position at render time.
    pub offset: IGlyphOffset,
}

/// Glyph rendering properties in *signed* pixel coordinates, before clamping to the atlas image.
///
/// Used internally during layout population. Coordinates may extend beyond `[0, image_size)`
/// before being clamped to a valid unsigned rectangle for the atlas. Converted to [`UTokenProps`]
/// once layout is complete.
#[derive(Clone, Debug)]
pub struct ITokenProps {
    pub region: IGlyphRegion,
    pub offset: IGlyphOffset,
}

impl FontTrack {
    /// Returns the default [`ITokenProps`] for any glyph in this track, ignoring per-token overrides.
    ///
    /// Use this when you need the track's baseline region and offset without consulting the
    /// per-token override table. If you are computing props for a specific token during layout
    /// packing, use [`props`](Self::props) instead.
    pub const fn as_token_props(&self) -> ITokenProps {
        ITokenProps {
            region: self.glyph_region,
            offset: self.offset,
        }
    }

    /// Returns the [`ITokenProps`] for a glyph in this track, applying any per-token overrides.
    ///
    /// If `overrides` is `Some`, each field (offset and region) is individually replaced only
    /// if the override provides a value for it; fields absent from the override fall back to the
    /// track defaults. If `overrides` is `None`, this is equivalent to [`as_token_props`](Self::as_token_props).
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// # use raster_font::{core::*, meta::*};
    /// # let track: FontTrack = todo!();
    /// # let my_offset: IGlyphOffset = todo!();
    /// #
    /// // Override only the offset, keep the track's default region.
    /// let props = track.props(Some(&GlyphOverride { offset: Some(my_offset), region: None }));
    /// ```
    pub const fn props<'f>(&'f self, overrides: Option<&'f GlyphOverride>) -> ITokenProps {
        if let Some(GlyphOverride { offset, region }) = overrides {
            let offset = match offset.as_ref() {
                Some(&offset) => offset,
                None => self.offset,
            };
            let region = match region.as_ref() {
                Some(&region) => region,
                None => self.glyph_region,
            };
            ITokenProps { offset, region }
        } else {
            self.as_token_props()
        }
    }
}

/// Marker type indicating that a build phase has **not** yet been completed.
///
/// Used as a default type parameter on [`FontAtlasBuilder`] to prevent calling phase-gated
/// methods before their prerequisites have been satisfied. See also [`Populated`].
pub struct Unpopulated;
/// Marker type indicating that a build phase **has** been completed.
///
/// Once a phase transitions to `Populated`, the methods gated on that phase become available
/// on [`FontAtlasBuilder`]. See also [`Unpopulated`].
pub struct Populated;

mod _marker {
    use super::*;

    pub trait Marker {}
    impl Marker for Unpopulated {}
    impl Marker for Populated {}
    pub trait ImageMarker {}
    impl ImageMarker for PhantomData<Unpopulated> {}
    impl<T> ImageMarker for GlyphSheet<T> {}
}
use _marker::{ImageMarker, Marker};

/// A staged builder for constructing a [`RasterFont`].
///
///| Parameter         | Default                       | Meaning                                                |
///| :---------------: | :---------------------------- | :----------------------------------------------------- |
///| `LayoutPopulated` | `Unpopulated`                 | Whether glyph regions have been computed from a layout |
///| `CustomPopulated` | `Unpopulated`                 | Whether custom/hand-specified glyphs have been added   |
///| `Image`           | `PhantomData<Unpopulated>`    | The attached sprite sheet, or absent if not yet set    |
///| `Named`           | `Unpopulated`                 | Whether the font has been named                        |
///
/// Methods that require a specific phase to have been completed are only present on the
/// corresponding specialisation, so missing a step results in a compile error rather than a
/// runtime panic or silently producing an invalid font.
///
/// Construct a builder with `default` or [`with_capacity`](Self::with_capacity),
/// then follow the build sequence described in the [module docs](crate::builder).
#[derive(Clone, Debug)]
pub struct FontAtlasBuilder<
    LayoutPopulated: Marker = Unpopulated,
    CustomPopulated: Marker = Unpopulated,
    Sheet: ImageMarker = PhantomData<Unpopulated>,
    Named: Marker = Unpopulated,
> {
    name: Option<String>,
    image: Sheet,
    scratch: Scratch,
    _state: PhantomData<(LayoutPopulated, CustomPopulated, Named)>,
}

/// Internal accumulator for glyph data during the build.
///
/// Stores the flat list of [`UTokenProps`] (indexed by [`AtlasIndex`]) and the map from
/// [`Sequence`] to [`AtlasIndex`] that is handed off to [`LigatureTree`] and [`RasterFont`].
///
/// [`LigatureTree`]: crate::tree::LigatureTree
#[derive(Clone, Default, Debug)]
pub(crate) struct Scratch {
    glyphs: Vec<UTokenProps>,
    sequence_map: HashMap<Sequence, AtlasIndex>,
}

/// A raw image paired with its pixel dimensions, used as the glyph sprite sheet.
///
/// The `image` field holds backend-specific pixel data (e.g. `Vec<u8>`, a file path, or a
/// Bevy `Handle<Image>`). The `size` field is required by the builder to compute tile positions
/// within the sheet during layout packing.
#[derive(Clone, Debug)]
pub struct GlyphSheet<Image> {
    /// The raw image data or handle for this sprite sheet.
    pub image: Image,
    /// The dimensions of the image in pixels.
    pub size: UGlyphSize,
}

impl Default for FontAtlasBuilder {
    fn default() -> Self {
        Self {
            name: None,
            image: PhantomData,
            scratch: Scratch::default(),
            _state: PhantomData,
        }
    }
}

impl<A: Marker, B: Marker, C: ImageMarker> FontAtlasBuilder<A, B, C, Unpopulated> {
    /// Assign a human-readable name to the font being built. This is optional but can be useful
    /// for debugging and error messages.
    #[inline]
    pub fn with_name(self, name: Option<String>) -> FontAtlasBuilder<A, B, C, Populated> {
        FontAtlasBuilder {
            name,
            image: self.image,
            scratch: self.scratch,
            _state: PhantomData,
        }
    }
}

impl<A: Marker, B: Marker, C: ImageMarker, Named: Marker> FontAtlasBuilder<A, B, C, Named> {
    #[doc(hidden)]
    #[inline]
    fn rebrand<X: Marker, Y: Marker>(self) -> FontAtlasBuilder<X, Y, C, Named> {
        FontAtlasBuilder {
            name: self.name,
            image: self.image,
            scratch: self.scratch,
            _state: PhantomData,
        }
    }

    /// Register a glyph in the builder, assigning it the next available [`AtlasIndex`].
    ///
    /// All sequences produced by iterating `token` are mapped to the new index, overwriting
    /// any existing entries for those sequences. This means later calls for the same sequence
    /// silently win — callers should ensure each sequence appears in at most one token.
    ///
    /// Called internally by [`uniform_layout`](Self::uniform_layout),
    /// [`dynamic_layout`](Self::dynamic_layout), and [`custom_glyphs`](Self::custom_glyphs).
    pub fn add_token(&mut self, token: &Token, props: UTokenProps) {
        let index = AtlasIndex(self.scratch.glyphs.len());
        self.scratch.glyphs.push(props);
        self.scratch
            .sequence_map
            .extend(token.iter().cloned().zip(std::iter::repeat(index)));
    }

    /// Look up the [`UTokenProps`] for a sequence already registered in the builder.
    ///
    /// Returns `None` if the sequence has not been registered yet. Primarily used by
    /// [`custom_glyphs`](FontAtlasBuilder::custom_glyphs) to resolve relative glyph references.
    pub fn get_glyph_by_sequence(&self, token: &Sequence) -> Option<&UTokenProps> {
        // let glyph = self.lookup.get(token)?;
        let index = self.scratch.sequence_map.get(token)?;
        self.scratch.glyphs.get(index.0)
    }
}

impl FontAtlasBuilder {
    /// Create a builder with pre-allocated capacity.
    ///
    /// Use this when the number of unique glyphs and sequences is known ahead of time to avoid
    /// reallocations during layout population.
    ///
    /// - `num_unique_glyphs` — expected number of distinct glyph entries (i.e. unique [`AtlasIndex`]es).
    /// - `num_unique_sequences` — expected total number of sequence strings mapped to glyphs.
    ///
    /// See also [`FontLayout::unique`] and [`Self::from(unique)`](Unique) for convenient
    /// alternatives to this method.
    #[inline]
    pub fn with_capacity(num_unique_glyphs: usize, num_unique_sequences: usize) -> Self {
        Self {
            name: None,
            scratch: Scratch {
                glyphs: Vec::with_capacity(num_unique_glyphs),
                sequence_map: HashMap::with_capacity(num_unique_sequences),
            },
            _state: PhantomData,
            image: PhantomData,
        }
    }
}

impl<Named: Marker> FontAtlasBuilder<Unpopulated, Unpopulated, PhantomData<Unpopulated>, Named> {
    /// Attach a glyph sprite sheet to this builder, advancing the `Image` state parameter.
    ///
    /// Must be called before [`populate_layout`](FontAtlasBuilder::populate_layout). The `size`
    /// field of the [`GlyphSheet`] is used to compute tile positions during layout packing.
    #[inline]
    pub fn with_image<Image>(
        self,
        image: GlyphSheet<Image>,
    ) -> FontAtlasBuilder<Unpopulated, Unpopulated, GlyphSheet<Image>, Named>
    where
        GlyphSheet<Image>: ImageMarker,
    {
        FontAtlasBuilder {
            name: self.name,
            image,
            scratch: self.scratch,
            _state: PhantomData,
        }
    }
}

/// Constructs a [`FontAtlasBuilder`] pre-sized to fit the glyph counts described by a [`Unique`]
/// layout.
impl From<Unique<'_>> for FontAtlasBuilder {
    #[inline]
    fn from(unique: Unique) -> Self {
        Self::with_capacity(unique.num_regions, unique.sequences.len())
    }
}

/// Compute the unsigned pixel region of a glyph inside a texture atlas.
///
/// Translates the per-track `region` (relative to the tile's top-left corner) by `tile_start`
/// (the top-left corner of the tile within the full atlas image) and converts the result to
/// an unsigned rectangle.
#[inline]
#[must_use]
fn extract_region(tile_start: IGlyphOffset, region: IGlyphRegion) -> UGlyphRegion {
    let region = IGlyphRegion {
        min: tile_start + region.min,
        max: tile_start + region.max,
    };
    region.as_urect()
}

impl<Image, Named: Marker> FontAtlasBuilder<Unpopulated, Unpopulated, GlyphSheet<Image>, Named>
where
    GlyphSheet<Image>: ImageMarker,
{
    /// Populate glyph regions and the sequence map from a [`FontLayout`], dispatching the
    /// appropriate packing strategy based on the layout's [`PackingMode`].
    ///
    /// This is the primary entry point for layout population. Advances `LayoutPopulated` to
    /// [`Populated`], enabling the subsequent [`custom_glyphs`](FontAtlasBuilder::custom_glyphs)
    /// and [`build`](FontAtlasBuilder::build) steps.
    pub fn populate_layout(
        self,
        layout: &FontLayout,
    ) -> FontAtlasBuilder<Populated, Unpopulated, GlyphSheet<Image>, Named> {
        match layout.packing_mode() {
            PackingMode::Uniform { track } => self.uniform_layout(layout, track),
            PackingMode::Dynamic { tracks } => self.dynamic_layout(layout, tracks),
        }
    }

    /// Populate glyph regions for a **uniform** (fixed-tile) packing mode.
    ///
    /// In uniform mode, all glyphs share a single [`FontTrack`] and are laid out left-to-right
    /// (wrapping to the next row) on a fixed grid whose tile size is `track.grid_tile_size`. The
    /// tile index for each glyph in `layout.ord_layout()` directly determines its position in the
    /// atlas image.
    ///
    /// Per-token overrides from the layout are respected: if a token has an override entry, its
    /// offset and/or region replace the track defaults for that token only.
    pub fn uniform_layout(
        mut self,
        layout: &FontLayout,
        track: &FontTrack,
    ) -> FontAtlasBuilder<Populated, Unpopulated, GlyphSheet<Image>, Named> {
        for (tile_index, token) in layout.ord_layout().iter().enumerate() {
            // Use preferences for this token
            let ITokenProps { offset, region } =
                track.props(token.first().and_then(|t| layout.get_override(t)));

            // Extract the glyph region from the texture.
            let props = UTokenProps {
                offset,
                region: {
                    let tile_start_x = (tile_index as u32) * track.grid_tile_size.x;
                    let tile_start = UGlyphSize::new(
                        (tile_start_x) % self.image.size.x,
                        (tile_start_x) / self.image.size.x * track.grid_tile_size.y,
                    )
                    .as_ivec2();

                    extract_region(tile_start, region)
                },
            };

            // Add the glyph to the atlas and lookup table.
            self.add_token(token, props);
        }

        self.rebrand()
    }

    /// Populate glyph regions for a **dynamic** (variable-tile) packing mode.
    ///
    /// In dynamic mode, different groups of glyphs may use different [`FontTrack`]s with
    /// distinct tile sizes. Tracks are keyed by a sentinel *head token* in `tracks`; the builder
    /// switches to a new track each time it encounters a head token in the ordered layout, and
    /// advances a cursor that wraps to the next row whenever the current row is full.
    ///
    /// Tokens that appear before any head token are silently skipped (no track is active yet).
    ///
    /// Per-token overrides from the layout are respected for each token, as in uniform mode.
    /// [`uniform_layout`](Self::uniform_layout).
    pub fn dynamic_layout(
        mut self,
        layout: &FontLayout,
        tracks: &HashMap<Token, FontTrack>,
    ) -> FontAtlasBuilder<Populated, Unpopulated, GlyphSheet<Image>, Named> {
        // todo: Could lift this into the builder and allow multiple dynamic layouts in one font.
        struct DynamicPackingCursor {
            pos: IGlyphOffset,
            tallest_in_row: i32,
            max_x: i32,
        }

        impl DynamicPackingCursor {
            const fn new(image_size: UGlyphSize) -> Self {
                Self {
                    pos: IGlyphOffset::ZERO,
                    tallest_in_row: 0,
                    max_x: image_size.x as i32,
                }
            }

            fn advance(&mut self, track: &FontTrack) {
                let tile = track.grid_tile_size.as_ivec2();

                if self.pos.x + tile.x > self.max_x {
                    self.pos.x = 0;
                    self.pos.y += self.tallest_in_row;
                    self.tallest_in_row = 0;
                }

                self.tallest_in_row = self.tallest_in_row.max(tile.y);
                self.pos.x += tile.x;
            }
        }

        let mut current_track: Option<&FontTrack> = None;
        let mut cursor = DynamicPackingCursor::new(self.image.size);
        // todo: end todo

        for token in layout.ord_layout().iter() {
            // switch tracks when we encounter the head token for a track
            if let Some(track) = tracks.get(token) {
                current_track = Some(track);
            }
            // if we're not in a track, we can't place any glyphs, so skip until we find a track
            let track = match current_track {
                Some(track) => track,
                None => continue,
            };

            // Use preferences for this token
            let ITokenProps { offset, region } =
                track.props(token.first().and_then(|t| layout.get_override(t)));

            // Extract the glyph region from the texture.
            let props = UTokenProps {
                offset,
                region: {
                    let tile_start = cursor.pos;
                    cursor.advance(track);
                    extract_region(tile_start, region)
                },
            };

            // Add the glyph to the atlas and lookup table.
            self.add_token(token, props);
        }

        self.rebrand()
    }
}

impl<Sheet: ImageMarker, Named: Marker> FontAtlasBuilder<Populated, Unpopulated, Sheet, Named> {
    /// Register hand-specified [`CustomGlyph`]s that are not part of the main layout.
    ///
    /// Custom glyphs come in two flavours:
    ///
    /// - [`CustomGlyph::Absolute`] — the region and offset are given directly in atlas coordinates.
    /// - [`CustomGlyph::Relative`] — the region is expressed relative to a *reference sequence*
    ///   that must already exist in the builder. This is useful for sub-glyphs that share a
    ///   sprite sheet tile with a base glyph (e.g. combining a [diacritic]).
    ///
    /// # Errors
    ///
    /// Returns [`UnknownSequence`] if a relative custom glyph references a
    /// sequence that has not been registered by the preceding layout population step.
    ///
    /// # Notes
    ///
    /// This method is only available after `LayoutPopulated = Populated` because relative
    /// references must be resolvable against already-registered glyphs.
    ///
    /// [diacritic]: https://en.wikipedia.org/wiki/Diacritic
    pub fn custom_glyphs<'a>(
        mut self,
        iter: impl IntoIterator<Item = (&'a Token, &'a CustomGlyph)>,
    ) -> Result<FontAtlasBuilder<Populated, Populated, Sheet, Named>, UnknownSequence> {
        for (token, sub_glyph) in iter {
            #[allow(clippy::match_ref_pats)]
            let props = match sub_glyph {
                &CustomGlyph::Absolute { offset, region } => UTokenProps { offset, region },
                &CustomGlyph::Relative {
                    offset,
                    ref reference,
                    region,
                } => {
                    let Some(reference_props) = self.get_glyph_by_sequence(reference) else {
                        return Err(UnknownSequence(reference.clone()));
                    };

                    UTokenProps {
                        offset,
                        region: extract_region(reference_props.region.min.as_ivec2(), region),
                    }
                }
            };

            self.add_token(token, props);
        }

        Ok(self.rebrand())
    }
}

/// Intermediate font representation passed from the builder to a [`BackendBuilder`].
///
/// Contains data produced by the layout population phase. [`BackendBuilder::build_resources`]
/// consumes this to produce final [`Backend::Resources`].
pub struct RawFont<Sheet> {
    /// The glyph sprite sheet (image + dimensions).
    pub sheet: GlyphSheet<Sheet>,
    /// A flat list of [`UTokenProps`], indexed by [`AtlasIndex`].
    pub glyphs: Vec<UTokenProps>,
    #[allow(rustdoc::private_intra_doc_links)]
    /// See [`Scratch::max_glyph_height`] for how this is computed.
    pub computed_height: u32,
}

impl<C: Marker, Image, Named: Marker> FontAtlasBuilder<Populated, C, GlyphSheet<Image>, Named>
where
    GlyphSheet<Image>: ImageMarker,
{
    /// Finalize the builder and construct a [`RasterFont`].
    ///
    /// Computes the font's line height, invokes the [`BackendBuilder`] to finalize resources, and
    /// compiles the internal [`LigatureTree`]. All three steps must succeed for a font to be
    /// returned.
    ///
    /// # Errors
    ///
    /// Returns a [`FontBuilderError`] wrapping one of:
    ///
    /// - [`EmptyFont`]: no glyphs were registered.
    /// - [`BackendBuilderError`](errors::FontBuilderError::BackendBuilderError): the backend
    ///   returned an error.
    /// - [`LigatureBindingError`](errors::FontBuilderError::LigatureBindingError): the
    ///   Aho-Corasick automaton failed to compile.
    ///
    /// [`LigatureTree`]: crate::tree::LigatureTree
    pub fn build<B: Backend, Ctx: BackendBuilder<Backend = B, Error: Error, Sheet = Image>>(
        self,
        resource_builder: Ctx,
    ) -> Result<RasterFont<B>, FontBuilderError<Ctx::Error>> {
        let computed_height = self
            .scratch
            .max_glyph_height()
            .ok_or(EmptyFont(PhantomData))?;
        let resources = resource_builder
            .build_resources(RawFont {
                glyphs: self.scratch.glyphs,
                sheet: self.image,
                computed_height,
            })
            .map_err(FontBuilderError::BackendBuilderError)?;

        RasterFont::new(
            self.name,
            computed_height,
            resources,
            self.scratch.sequence_map,
        )
        .map_err(FontBuilderError::LigatureBindingError)
    }
}

use errors::{EmptyFont, FontBuilderError, UnknownSequence};

/// Error types that can be produced during font construction.
pub mod errors {
    use crate::core::Sequence;
    use std::{
        error::Error,
        fmt::{Display, Formatter, Result},
        marker::PhantomData,
    };

    /// The error type returned by [`FontAtlasBuilder::build`](super::FontAtlasBuilder::build).
    ///
    /// Each variant corresponds to a distinct failure mode in the build pipeline:
    ///
    /// - [`EmptyFont`](Self::EmptyFont) — `build` was called with zero registered glyphs.
    /// - [`UnknownSequence`] — a [`CustomGlyph::Relative`](crate::meta::CustomGlyph::Relative)
    ///   referenced a sequence that was not found in the layout.
    /// - [`LigatureBindingError`](Self::LigatureBindingError) — the Aho-Corasick automaton
    ///   failed to compile from the registered sequence set.
    /// - [`BackendBuilderError`](Self::BackendBuilderError) — the backend's
    ///   [`build_resources`](crate::backend::BackendBuilder::build_resources) call failed.
    #[derive(Debug)]
    pub enum FontBuilderError<E: Error> {
        /// No glyphs were registered before `build` was called.
        EmptyFont,
        /// A custom glyph's relative reference could not be resolved.
        UnknownSequence(UnknownSequence),
        /// The Aho-Corasick automaton failed to compile.
        LigatureBindingError(crate::tree::BuildError),
        /// Custom error returned by [`BackendBuilder::build_resources`] when constructing
        /// backend-specific resources for a raster font.
        ///
        /// [`BackendBuilder::build_resources`]: crate::backend::BackendBuilder::build_resources
        BackendBuilderError(E),
    }

    impl<E: Error> From<EmptyFont> for FontBuilderError<E> {
        #[inline]
        fn from(_: EmptyFont) -> Self {
            FontBuilderError::EmptyFont
        }
    }

    impl<E: Error> From<UnknownSequence> for FontBuilderError<E> {
        #[inline]
        fn from(e: UnknownSequence) -> Self {
            FontBuilderError::UnknownSequence(e)
        }
    }

    impl<E: Error> Error for FontBuilderError<E> {}
    impl<E: Error> Display for FontBuilderError<E> {
        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
            match self {
                Self::EmptyFont => EmptyFont(PhantomData).fmt(f),
                FontBuilderError::UnknownSequence(a) => a.fmt(f),
                FontBuilderError::LigatureBindingError(a) => a.fmt(f),
                FontBuilderError::BackendBuilderError(e) => {
                    write!(
                        f,
                        "Failed to build backend-specific resources for a raster font: {e}"
                    )
                }
            }
        }
    }

    /// Error returned when [`build`](super::FontAtlasBuilder::build) is called without any
    /// registered glyphs.
    #[derive(Debug)]
    pub struct EmptyFont(pub(super) PhantomData<()>);

    impl Error for EmptyFont {}
    impl Display for EmptyFont {
        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
            write!(f, "Will not build empty font (0 glyphs).")
        }
    }

    /// Error returned when a [`CustomGlyph::Relative`](crate::meta::CustomGlyph::Relative)
    /// references a sequence that does not exist in the layout.
    ///
    /// Contains the unresolved [`Sequence`] for diagnostic purposes.
    ///
    /// For simpliciy, the Sequence is cloned when this error is constructed. In practice this
    /// error is only be returned when their is an authoring mistake in the layout, so production
    /// code should never see this error unless the layout is being generated dynamically from
    /// untrusted input.
    #[derive(Debug)]
    pub struct UnknownSequence(pub(super) Sequence);

    impl Error for UnknownSequence {}
    impl Display for UnknownSequence {
        fn fmt(&self, f: &mut Formatter<'_>) -> Result {
            write!(
                f,
                "A custom glyph references a sequence that does not exist in the layout: {}",
                self.0
            )
        }
    }
}

impl Scratch {
    /// Returns an iterator of `(sequence, props)` pairs for all registered glyphs.
    ///
    /// Sequences whose [`AtlasIndex`] no longer points to a valid entry (which should not occur
    /// under normal use) are silently filtered out.
    #[allow(dead_code)]
    #[inline]
    pub fn iter(&self) -> impl Iterator<Item = (&Sequence, &UTokenProps)> {
        self.sequence_map
            .iter()
            .filter_map(|(seq, index)| self.glyphs.get(index.0).map(|props| (seq, props)))
    }

    /// Returns an iterator of [`UTokenProps`] for all sequences in `self`.
    ///
    /// Multiple sequences that map to the same [`AtlasIndex`] will yield the same props more
    /// than once (once per sequence entry).
    #[inline]
    pub fn iter_props(&self) -> impl Iterator<Item = &UTokenProps> {
        self.sequence_map
            .values()
            .filter_map(|index| self.glyphs.get(index.0))
    }

    /// Returns the maximum effective glyph height across all registered glyphs.
    ///
    /// The effective height of a glyph is `region.height() + offset.y` (saturating). This is
    /// used by [`FontAtlasBuilder::build`](FontAtlasBuilder::build) to compute the font's
    /// line height.
    ///
    /// Returns `None` if no glyphs have been registered yet.
    #[inline]
    pub fn max_glyph_height(&self) -> Option<u32> {
        self.iter_props()
            .map(|g| g.region.height().saturating_add_signed(g.offset.y))
            .max()
    }
}