codecraft 0.1.1

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, an immediate-mode UI, audio and gamepad haptics
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
//! Text, drawn in Monaspace.
//!
//! Five faces ship, and they are the point: Neon is the neo-grotesque you get
//! by default, Argon a humanist, Xenon a slab serif, Radon a handwriting, and
//! Krypton a mechanical. They are drawn from the same skeleton and share an
//! **advance**, so switching between them changes how the UI reads without
//! moving a single button. See [`Family::next`], which is what a key bound to
//! it would call.
//!
//! A glyph is rasterised once into the shared atlas and drawn as one quad, and
//! the atlas holds a white mask that the quad's colour tints. That is the same
//! deal an icon gets, which is why they share a texture -- see
//! [`super::atlas`].
//!
//! ```no_run
//! # use codecraft::ui::font::{Family, Font};
//! let mut font = Font::default();
//! assert_eq!(font.family(), Family::Neon);
//! font.set_family(Family::Radon);
//! ```
use ab_glyph::{Font as _, FontRef, PxScale, ScaleFont as _};

use super::atlas::{Atlas, Uv};
use super::color::{Color, linear_rgba};
use super::renderer2d::QuadInstance;

/// Which of the five Monaspace faces the UI is drawn in.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Family {
    /// Neo-grotesque. The plain one, and the default.
    #[default]
    Neon,
    /// Humanist.
    Argon,
    /// Slab serif.
    Xenon,
    /// Handwriting.
    Radon,
    /// Mechanical.
    Krypton,
}

impl Family {
    /// All five, in the order the type lists them.
    pub const ALL: [Family; 5] = [
        Family::Neon,
        Family::Argon,
        Family::Xenon,
        Family::Radon,
        Family::Krypton,
    ];

    pub fn label(self) -> &'static str {
        match self {
            Family::Neon => "Neon",
            Family::Argon => "Argon",
            Family::Xenon => "Xenon",
            Family::Radon => "Radon",
            Family::Krypton => "Krypton",
        }
    }

    /// The next one round, so a key can cycle through them.
    pub fn next(self) -> Self {
        let at = Self::ALL.iter().position(|&f| f == self).unwrap_or(0);
        Self::ALL[(at + 1) % Self::ALL.len()]
    }

    /// The face itself, compiled in.
    ///
    /// All five ship rather than one, because being able to change the face is
    /// the reason this family was chosen. Together they are under two
    /// megabytes, which is less than the icons that are *not* compiled in.
    fn bytes(self) -> &'static [u8] {
        macro_rules! face {
            ($file:literal) => {
                include_bytes!(concat!(
                    env!("CARGO_MANIFEST_DIR"),
                    "/assets/fonts/monaspace/",
                    $file
                ))
            };
        }
        match self {
            Family::Neon => face!("MonaspaceNeon-Regular.otf"),
            Family::Argon => face!("MonaspaceArgon-Regular.otf"),
            Family::Xenon => face!("MonaspaceXenon-Regular.otf"),
            Family::Radon => face!("MonaspaceRadon-Regular.otf"),
            Family::Krypton => face!("MonaspaceKrypton-Regular.otf"),
        }
    }
}

/// How tall a capital is, in the units a caller measures text in.
///
/// The bitmap font this replaced was five wide and seven tall with the capitals
/// filling every row, so `pixel_size` meant a seventh of a capital and every
/// layout in the codebase is written in those terms. Keeping the meaning is
/// what lets the font change without a single panel moving: see
/// [`text_height`].
const CAP_UNITS: f32 = 7.0;

/// How far apart two characters sit, as a fraction of the em, and the cap
/// height the advance was chosen against.
///
/// Written down rather than read off the face, because the advance is the same
/// for all five and everything that *measures* text -- a button sizing itself
/// to its label, a panel sizing itself to its widest row -- is plain layout
/// code with no font to hand. `the_measurements_match_the_faces` is what keeps
/// these honest.
const ADVANCE_PER_EM: f32 = 0.541_666_6;
const NOMINAL_CAP_PER_EM: f32 = 0.65625;

/// How far apart two characters sit, in the units a caller measures text in.
const ADVANCE_UNITS: f32 = ADVANCE_PER_EM / NOMINAL_CAP_PER_EM * CAP_UNITS;

/// One face, and where its glyphs sit in a cell.
///
/// How much room text takes is not here -- that is the same for all five and
/// lives in the constants above, where layout code can reach it.
pub struct Face {
    family: Family,
    font: FontRef<'static>,
    /// Baseline from the top of the em box, over em.
    ascent: f32,
    /// Cap height over em, from the height of an `H`.
    ///
    /// Read off the face rather than shared, because the five do *not* quite
    /// agree here -- Radon draws its capitals a few per cent taller, the way a
    /// hand would. Scaling each face by its own is what puts every one of them
    /// on the same line at the same size, so switching face changes the shapes
    /// and nothing else.
    cap: f32,
}

impl Face {
    pub fn new(family: Family) -> Self {
        // The five files ship with the crate, so a failure here is a broken
        // build rather than anything a running game could do about it.
        let font = FontRef::try_from_slice(family.bytes()).expect("a Monaspace face");

        // Measured at an arbitrary scale and divided back out: what is wanted
        // is the shape of the face, not a size.
        let ascent = font.as_scaled(PxScale::from(PROBE)).ascent() / PROBE;
        let cap = font
            .outline_glyph(font.glyph_id('H').with_scale(PROBE))
            .map(|outlined| outlined.px_bounds().height() / PROBE)
            // A face with no `H` is not one of ours, but the nominal beats a
            // divide by zero.
            .unwrap_or(NOMINAL_CAP_PER_EM);

        Self {
            family,
            font,
            ascent,
            cap,
        }
    }

    pub fn family(&self) -> Family {
        self.family
    }

    /// The em size that puts this face's capitals at `CAP_UNITS * pixel_size`.
    fn em(&self, pixel_size: f32) -> f32 {
        CAP_UNITS * pixel_size / self.cap
    }

    /// One glyph as a square of RGBA8, `size` on a side, white and masked by
    /// its coverage.
    ///
    /// `None` for anything with no ink -- a space, or a character the face has
    /// no glyph for.
    ///
    /// The glyph is placed in the cell by its baseline rather than by its own
    /// bounding box, so an `o` sits where an `O` does and a `g` hangs below
    /// both. A cell packed tight around each outline would line none of them
    /// up.
    pub fn rasterize(&self, ch: char, size: u32) -> Option<Vec<u8>> {
        // Short of the cell, so the parts of a face that overshoot the em box
        // -- a tall bracket, the tail of a `Q` -- have somewhere to go.
        let scale = size as f32 * CELL_FILL;
        let inset = (size as f32 - scale) * 0.5;
        let baseline = inset + self.ascent * scale;

        let outlined = self
            .font
            .outline_glyph(self.font.glyph_id(ch).with_scale(scale))?;
        let bounds = outlined.px_bounds();

        let mut pixels = vec![0u8; (size * size * 4) as usize];
        outlined.draw(|x, y, coverage| {
            // `draw` counts from the glyph's own top-left; `px_bounds` says
            // where that is against the baseline.
            let px = bounds.min.x + x as f32;
            let py = baseline + bounds.min.y + y as f32;
            if px < 0.0 || py < 0.0 || px >= size as f32 || py >= size as f32 {
                return;
            }
            let at = ((py as u32 * size + px as u32) * 4) as usize;
            // White, with the coverage in alpha: the colour is the quad's.
            pixels[at] = 255;
            pixels[at + 1] = 255;
            pixels[at + 2] = 255;
            pixels[at + 3] = (coverage.clamp(0.0, 1.0) * 255.0) as u8;
        });
        Some(pixels)
    }
}

/// How much of a glyph cell the em box takes up. The rest is margin for the
/// glyphs that reach past it.
const CELL_FILL: f32 = 0.82;

/// The scale ratios are measured at, and divided back out of.
const PROBE: f32 = 64.0;

impl Default for Face {
    fn default() -> Self {
        Self::new(Family::default())
    }
}

/// The face the UI is drawn in.
///
/// A resource, so anything that draws text can ask what it is being drawn in,
/// and so changing it is one write.
#[derive(bevy_ecs::prelude::Resource, Default)]
pub struct Font {
    face: Face,
    /// Set when the family changed, and spent by the system that has the atlas
    /// to clear. Changing the face is not something a caller can do to the
    /// texture directly, so it leaves a note instead.
    changed: bool,
}

impl Font {
    pub fn family(&self) -> Family {
        self.face.family()
    }

    pub fn face(&self) -> &Face {
        &self.face
    }

    /// Draws the UI in another face from the next frame.
    ///
    /// Nothing reflows: the five are metric-compatible, so every button is
    /// already the right width for the new one.
    pub fn set_family(&mut self, family: Family) {
        if self.face.family() == family {
            return;
        }
        self.face = Face::new(family);
        self.changed = true;
        log::info!("drawing the UI in Monaspace {}", family.label());
    }

    /// The next face round.
    pub fn cycle(&mut self) {
        self.set_family(self.family().next());
    }

    /// Whether the face has changed since this was last asked, which is the
    /// atlas's cue to forget what it rasterised from the old one.
    pub fn take_changed(&mut self) -> bool {
        std::mem::take(&mut self.changed)
    }
}

/// Appends one quad per glyph of `text`, with the top-left of the first
/// capital at `(origin_x, origin_y)`.
///
/// A capital is [`CAP_UNITS`] `pixel_size`s tall, so `origin_y` and
/// [`text_height`] bound the capitals rather than the whole face -- a `g`
/// hangs below the box, as it does on paper.
pub fn push_text(
    quads: &mut Vec<QuadInstance>,
    atlas: &mut Atlas,
    face: &Face,
    text: &str,
    origin_x: f32,
    origin_y: f32,
    pixel_size: f32,
    color: Color,
) {
    let em = face.em(pixel_size);
    let advance = ADVANCE_UNITS * pixel_size;
    // The cell on screen, and where its top sits: the raster put the baseline
    // a fixed way down the cell, and the baseline belongs at the foot of the
    // capitals.
    let cell = em / CELL_FILL;
    let inset = (cell - em) * 0.5;
    let top = origin_y + CAP_UNITS * pixel_size - (inset + face.ascent * em);

    let color = linear_rgba(color);
    for (i, ch) in text.chars().enumerate() {
        // A space and an unknown character both draw nothing and both still
        // move the cursor, which is what keeps a column of text a column.
        let Some(Uv { min, max }) = atlas.glyph(face, ch) else {
            continue;
        };
        quads.push(QuadInstance {
            pos: [origin_x + i as f32 * advance - inset, top],
            size: [cell, cell],
            color,
            uv: [min[0], min[1], max[0], max[1]],
        });
    }
}

/// How wide `text` is on screen.
///
/// Monospaced, so this is a multiplication rather than a walk over the string,
/// and it takes no face because all five give the same answer.
pub fn text_width(text: &str, pixel_size: f32) -> f32 {
    text.chars().count() as f32 * ADVANCE_UNITS * pixel_size
}

/// How tall a line of text is: the capitals, not the whole face.
///
/// Independent of the face on purpose -- it is what every layout in the
/// codebase is measured in, and it must not move when the font does.
pub fn text_height(pixel_size: f32) -> f32 {
    CAP_UNITS * pixel_size
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn all_five_faces_load() {
        for family in Family::ALL {
            assert_eq!(Face::new(family).family(), family);
        }
    }

    /// The whole reason for choosing this family: changing the face must not
    /// move a single button. That holds only while every face measures the
    /// same, and while the constants the layout code uses say what the faces
    /// actually do -- so this checks both at once.
    #[test]
    fn the_measurements_match_the_faces() {
        use ab_glyph::Font as _;
        for family in Family::ALL {
            let font = FontRef::try_from_slice(family.bytes()).expect("a face");
            let scaled = font.as_scaled(PxScale::from(PROBE));

            for ch in ['M', 'i', '.', 'W'] {
                let advance = scaled.h_advance(font.glyph_id(ch)) / PROBE;
                assert!(
                    (advance - ADVANCE_PER_EM).abs() < 1e-3,
                    "{} advances {advance} for {ch:?}, not {ADVANCE_PER_EM}",
                    family.label(),
                );
            }

            // The cap height is *not* shared -- Radon draws taller capitals --
            // so each face is scaled by its own. What has to hold is that they
            // all land on the same line at the same size.
            let cap = Face::new(family).em(3.0) * Face::new(family).cap;
            assert!(
                (cap - text_height(3.0)).abs() < 1e-4,
                "{} puts a capital at {cap}, not {}",
                family.label(),
                text_height(3.0),
            );
        }
    }

    /// What the layouts are written in. A capital is seven pixel-sizes tall
    /// whatever the face, because that is what `pixel_size` has always meant.
    #[test]
    fn a_capital_is_seven_pixel_sizes_tall() {
        assert_eq!(text_height(3.0), 21.0);
        assert_eq!(text_height(1.0), 7.0);
    }

    #[test]
    fn text_gets_wider_with_the_size_and_the_string() {
        assert!(text_width("MENU", 3.0) > text_width("OK", 3.0));
        assert!(text_width("MENU", 6.0) > text_width("MENU", 3.0));
        assert_eq!(text_width("", 3.0), 0.0);
        assert_eq!(text_width("MMMM", 3.0), text_width("il.'", 3.0), "monospaced");
    }

    #[test]
    fn cycling_goes_round_all_five_and_comes_back() {
        let mut font = Font::default();
        assert_eq!(font.family(), Family::Neon);

        let seen: Vec<Family> = (0..Family::ALL.len())
            .map(|_| {
                font.cycle();
                font.family()
            })
            .collect();
        assert_eq!(seen.last(), Some(&Family::Neon), "back where it started");

        let mut sorted = seen.clone();
        sorted.sort_by_key(|f| f.label());
        sorted.dedup();
        assert_eq!(sorted.len(), 5, "and through every one on the way: {seen:?}");
    }

    #[test]
    fn changing_the_family_is_reported_once() {
        let mut font = Font::default();
        assert!(!font.take_changed(), "nothing has changed yet");

        font.set_family(Family::Xenon);
        assert!(font.take_changed(), "the atlas has letters to forget");
        assert!(!font.take_changed(), "and is only told the once");
    }

    #[test]
    fn setting_the_family_it_already_has_changes_nothing() {
        let mut font = Font::default();
        font.set_family(Family::Neon);
        assert!(
            !font.take_changed(),
            "re-rasterising the alphabet for no change would be a waste",
        );
    }

    /// A glyph has to land inside its cell, or it is clipped along an edge.
    #[test]
    fn a_glyph_is_drawn_inside_its_cell() {
        let face = Face::default();
        for ch in ['A', 'g', 'Q', '(', '_', '`'] {
            let raster = face.rasterize(ch, 64).expect("a glyph");
            assert_eq!(raster.len(), 64 * 64 * 4);
            assert!(
                raster.chunks(4).any(|px| px[3] > 0),
                "{ch:?} rasterised blank",
            );
        }
    }

    /// Capitals share a baseline, which is the thing placing by bounding box
    /// would get wrong.
    #[test]
    fn capitals_sit_on_one_baseline() {
        let face = Face::default();
        let foot = |ch: char| {
            let raster = face.rasterize(ch, 64).expect("a glyph");
            (0..64)
                .rev()
                .find(|&y| (0..64).any(|x| raster[((y * 64 + x) * 4 + 3) as usize] > 0))
                .expect("some ink")
        };
        let (h, e, t): (u32, u32, u32) = (foot('H'), foot('E'), foot('T'));
        assert!(
            h.abs_diff(e) <= 1 && h.abs_diff(t) <= 1,
            "H at {h}, E at {e}, T at {t}",
        );
    }

    /// The whole path, which is the thing that would fail silently: a label
    /// that rasterises nothing draws nothing and says nothing about it.
    #[test]
    fn a_label_becomes_one_quad_per_letter() {
        let mut quads = Vec::new();
        let mut atlas = Atlas::new();
        let face = Face::default();
        push_text(
            &mut quads,
            &mut atlas,
            &face,
            "NEW GAME",
            100.0,
            50.0,
            3.0,
            Color::WHITE,
        );

        assert_eq!(quads.len(), 7, "eight characters, and the space has no ink");
        for quad in &quads {
            assert!(
                quad.uv[2] > quad.uv[0] && quad.uv[3] > quad.uv[1],
                "a glyph quad has to sample the atlas, not draw flat: {:?}",
                quad.uv,
            );
            assert!(quad.size[0] > 0.0 && quad.size[1] > 0.0);
        }

        // Left to right, one advance apart, and the space leaves a gap.
        let advance = ADVANCE_UNITS * 3.0;
        assert!((quads[1].pos[0] - quads[0].pos[0] - advance).abs() < 1e-3);
        assert!(
            (quads[3].pos[0] - quads[2].pos[0] - advance * 2.0).abs() < 1e-3,
            "the space still moves the cursor on",
        );
    }

    /// A line of text sits where the caller put it, whichever face is on.
    #[test]
    fn the_faces_all_draw_to_the_same_place() {
        let laid_out = |family: Family| {
            let mut quads = Vec::new();
            let mut atlas = Atlas::new();
            push_text(
                &mut quads,
                &mut atlas,
                &Face::new(family),
                "MENU",
                10.0,
                20.0,
                3.0,
                Color::WHITE,
            );
            quads.iter().map(|q| q.pos[0]).collect::<Vec<_>>()
        };

        let neon = laid_out(Family::Neon);
        for family in Family::ALL {
            let other = laid_out(family);
            assert_eq!(neon.len(), other.len(), "{}", family.label());
            for (a, b) in neon.iter().zip(&other) {
                assert!(
                    (a - b).abs() < 0.5,
                    "{} puts a letter at {b} where Neon puts it at {a}",
                    family.label(),
                );
            }
        }
    }

    #[test]
    fn a_space_has_nothing_to_draw() {
        assert!(Face::default().rasterize(' ', 64).is_none());
    }
}