codecraft 0.2.0

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, a yakui-drawn UI, audio and gamepad haptics; its binary maps any folder, and the symbols of its Rust files, as a 3D wall of boxes
Documentation
//! Text is drawn in Monaspace: five faces (Neon, Argon, Xenon, Radon, Krypton)
//! that share an advance, so switching faces moves nothing.
use std::cell::Cell;
use std::rc::Rc;

/// Which of the five Monaspace faces the UI is drawn in.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Family {
    #[default]
    Neon,
    Argon,
    Xenon,
    Radon,
    Krypton,
}

impl Family {
    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 name the face is registered under with yakui.
    pub fn name(self) -> &'static str {
        match self {
            Family::Neon => "monaspace-neon",
            Family::Argon => "monaspace-argon",
            Family::Xenon => "monaspace-xenon",
            Family::Radon => "monaspace-radon",
            Family::Krypton => "monaspace-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()]
    }

    // All five are compiled in (under 2 MB) because switching faces is why this family was chosen.
    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"),
        }
    }

    /// Parses the face; the files ship with the crate, so failure is a broken build.
    pub(crate) fn load(self) -> fontdue::Font {
        fontdue::Font::from_bytes(self.bytes(), fontdue::FontSettings::default())
            .expect("a Monaspace face")
    }
}

/// The face the UI is drawn in.
#[derive(bevy_ecs::prelude::Resource, Default)]
pub struct Font {
    family: Family,
}

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

    /// Draws the UI in another face from the next frame.
    pub fn set_family(&mut self, family: Family) {
        if self.family == family {
            return;
        }
        self.family = family;
        log::info!("drawing the UI in Monaspace {}", family.label());
    }

    pub fn cycle(&mut self) {
        self.set_family(self.family().next());
    }
}

/// This frame's face, kept in the yakui DOM so a widget function with no world to hand can still ask.
#[derive(Clone, Default)]
pub(crate) struct Face(Rc<Cell<Family>>);

impl Face {
    pub fn set(&self, family: Family) {
        self.0.set(family);
    }

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

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

    #[test]
    fn all_five_faces_load_and_draw_a_capital() {
        for family in Family::ALL {
            let font = family.load();
            let (metrics, raster) = font.rasterize('H', 64.0);
            assert!(
                metrics.width > 0 && metrics.height > 0,
                "{}",
                family.label()
            );
            assert!(
                raster.iter().any(|&p| p > 0),
                "{} drew nothing",
                family.label()
            );
        }
    }

    #[test]
    fn the_faces_share_an_advance() {
        let neon = Family::Neon.load().metrics('M', 100.0).advance_width;
        assert!(
            (neon - crate::ui::widgets::ADVANCE_PER_EM * 100.0).abs() < 0.5,
            "the advance the layout code measures with is the face's: {neon}",
        );
        for family in Family::ALL {
            let font = family.load();
            for ch in ['M', 'i', '.', 'W'] {
                let advance = font.metrics(ch, 100.0).advance_width;
                assert!(
                    (advance - neon).abs() < 0.1,
                    "{} advances {advance} for {ch:?}, Neon {neon}",
                    family.label(),
                );
            }
        }
    }

    #[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 every_face_has_a_name_of_its_own() {
        let mut names: Vec<&str> = Family::ALL.iter().map(|f| f.name()).collect();
        names.sort();
        names.dedup();
        assert_eq!(names.len(), Family::ALL.len());
    }
}