mirage-engine 0.1.0

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! What a session draws its UI text in, and what a font a game cannot draw
//! with does to startup.

use std::sync::Arc;

use super::*;
use crate::egui;

/// The example's font, read from the working directory a test runs in.
const SOURCE: &str = "examples/assets/pixel-operator.ttf";

/// The name that source holds its font under.
const OPERATOR: &str = "pixel-operator";

/// A text long enough for two fonts to draw it as different pixels.
const LABEL: &str = "score: 1200";

/// The side of the target that text is read off, wide enough for the whole
/// line.
const LINE: u32 = 64;

/// One line of UI text, in whatever fonts the UI draws with.
struct Hud;

impl Game for Hud {
    type Meshes = CubeSet;
    type Sounds = NoSounds;
    type InputActions = NoInputActions;
    type Skyboxes = NoSkyboxes;
    type SurfaceStyles = ();
    type PostEffects = ();

    fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}

    fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
        ctx.ui(|ui| {
            ui.label(LABEL);
        });
    }
}

/// What the first frame of a session draws, after `install` has run over
/// its startup, or `None` where the machine has no usable graphics adapter.
fn drawn(
    config: Config,
    install: impl FnOnce(&mut Startup<'_>) -> Result<(), Error>,
) -> Option<Vec<u8>> {
    let mut session = started(
        config,
        UVec2::splat(LINE),
        |ctx: &mut InitContext<'_, Hud>| {
            install(ctx.startup())?;
            Ok(Hud)
        },
    )?;

    session.step();
    Some(session.pixels().expect("the target reads back"))
}

/// Whether anything was drawn over the color the target's corner holds.
fn holds_a_label(pixels: &[u8]) -> bool {
    let mut corner_first = pixels.chunks_exact(4);
    let corner = corner_first.next().expect("a target has pixels");

    corner_first.any(|pixel| pixel != corner)
}

/// The `egui::FontDefinitions` a game's own font goes in front of egui's
/// own in.
fn in_front(font: egui::FontData) -> egui::FontDefinitions {
    let mut fonts = egui::FontDefinitions::default();
    fonts.font_data.insert(OPERATOR.to_owned(), Arc::new(font));
    fonts
        .families
        .entry(egui::FontFamily::Proportional)
        .or_default()
        .insert(0, OPERATOR.to_owned());

    fonts
}

/// What one session that failed to start reports, or `None` where the
/// machine has no usable graphics adapter.
fn refused(
    config: Config,
    install: impl FnOnce(&mut Startup<'_>) -> Result<(), Error>,
) -> Option<String> {
    let started = Session::new(
        config,
        UVec2::splat(LINE),
        |ctx: &mut InitContext<'_, Hud>| {
            install(ctx.startup())?;
            Ok(Hud)
        },
    );

    match started {
        Ok(_) => panic!("the fonts were no fonts to draw with"),
        Err(error) if error.to_string().starts_with(NO_ADAPTER) => None,
        Err(error) => Some(error.to_string()),
    }
}

#[test]
fn a_label_is_drawn_in_the_font_the_game_put_first() {
    let Some(as_egui_draws) = drawn(raw("headless fonts default"), |_startup| Ok(())) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let Some(as_the_game_draws) =
        drawn(raw("headless fonts own").with_assets([SOURCE]), |startup| {
            let font = startup.font(OPERATOR)?;
            startup.set_fonts(in_front(font))
        })
    else {
        return;
    };

    assert!(holds_a_label(&as_egui_draws), "egui drew the label");
    assert!(holds_a_label(&as_the_game_draws), "and so did the game");
    assert_ne!(
        as_the_game_draws, as_egui_draws,
        "the same label in two fonts is two readings"
    );
}

#[test]
fn a_font_a_game_reads_and_never_sets_changes_no_pixel() {
    let Some(as_egui_draws) = drawn(raw("headless fonts default"), |_startup| Ok(())) else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };
    let Some(read_alone) = drawn(
        raw("headless fonts read").with_assets([SOURCE]),
        |startup| startup.font(OPERATOR).map(drop),
    ) else {
        return;
    };

    assert_eq!(
        read_alone, as_egui_draws,
        "setting the fonts is what draws in them"
    );
}

#[test]
fn a_family_naming_a_font_the_data_does_not_hold_stops_startup() {
    let refused = refused(raw("headless fonts absent"), |startup| {
        let mut fonts = egui::FontDefinitions::default();
        fonts
            .families
            .entry(egui::FontFamily::Proportional)
            .or_default()
            .insert(0, "nothing".to_owned());
        startup.set_fonts(fonts)
    });
    let Some(error) = refused else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };

    assert_eq!(
        error,
        "the font family `Proportional` lists `nothing`, which `font_data` does not hold"
    );
}

#[test]
fn font_data_that_does_not_decode_stops_startup() {
    let refused = refused(raw("headless fonts junk"), |startup| {
        let mut fonts = egui::FontDefinitions::default();
        fonts.font_data.insert(
            "junk".to_owned(),
            Arc::new(egui::FontData::from_static(b"not a font at all")),
        );
        startup.set_fonts(fonts)
    });
    let Some(error) = refused else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };

    assert!(
        error.starts_with("the font `junk` did not decode"),
        "got {error}"
    );
}

#[test]
fn a_font_name_no_source_holds_stops_startup() {
    let refused = refused(raw("headless fonts missing"), |startup| {
        startup.font("nothing").map(drop)
    });
    let Some(error) = refused else {
        eprintln!("skipped: this machine has no usable graphics adapter");
        return;
    };

    assert_eq!(error, "no asset is named `nothing`");
}