tests.rs raw

use guiduck_scene::paint::{Color, FontData};
use guiduck_scene::{Glyph, GlyphRun, Synthesis};
use skrifa::MetadataProvider;

use super::*;

const SIZE: f32 = 48.0;

/// One `l` from the sample face at the origin: a tall upright stem, so a lean
/// shows as its top moving and its foot staying put.
fn stem(synthesis: Synthesis, hint: bool) -> GlyphRun {
    let data = guiduck_samples::sample_font();
    let font = FontRef::from_index(data.as_ref(), 0).expect("the sample face");
    let id = font.charmap().map('l').expect("the face has an l").to_u32();
    GlyphRun {
        font: FontData::new(data, 0),
        size: SIZE,
        brush: Color::BLACK.into(),
        glyphs: vec![Glyph { id, x: 0.0, y: 0.0 }],
        normalized_coords: Vec::new(),
        hint,
        synthesis,
    }
}

fn path_bounds(run: &GlyphRun) -> tiny_skia::Rect {
    let placement = RunPlacement::of(run, Affine::IDENTITY);
    run_to_path(run, &placement, &mut GlyphCache::default())
        .expect("draws")
        .expect("has ink")
        .bounds()
}

const ITALIC: Synthesis = Synthesis {
    skew: Some(14.0),
    embolden: false,
};
const BOLD: Synthesis = Synthesis {
    skew: None,
    embolden: true,
};

#[test]
fn a_synthesized_italic_leans_the_outline_forward() {
    for hint in [false, true] {
        let upright = path_bounds(&stem(Synthesis::default(), hint));
        let leaned = path_bounds(&stem(ITALIC, hint));
        // The foot sits on the baseline and does not move; the top, the
        // stem's height above it, moves right by tan(14°) of that height.
        let height = f64::from(upright.bottom() - upright.top());
        let expected = height * 14f64.to_radians().tan();
        let moved = f64::from(leaned.right() - upright.right());
        assert!(
            (moved - expected).abs() < 1.0,
            "hint {hint}: the top moved {moved}, expected about {expected}"
        );
        assert!(
            (leaned.left() - upright.left()).abs() < 0.5,
            "hint {hint}: the foot stayed put"
        );
        assert_eq!(leaned.top(), upright.top(), "hint {hint}: no taller");
    }
}

#[test]
fn a_synthesized_bold_grows_the_outline_on_every_side() {
    let grow = f64::from(SIZE) * Synthesis::EMBOLDEN_PER_SIDE_EM;
    for hint in [false, true] {
        let regular = path_bounds(&stem(Synthesis::default(), hint));
        let bold = path_bounds(&stem(BOLD, hint));
        for (side, delta) in [
            ("left", regular.left() - bold.left()),
            ("right", bold.right() - regular.right()),
            ("top", regular.top() - bold.top()),
            ("bottom", bold.bottom() - regular.bottom()),
        ] {
            assert!(
                (f64::from(delta) - grow).abs() < 0.05,
                "hint {hint}: {side} grew {delta}, expected {grow}"
            );
        }
    }
}

/// The raster cache keys on the synthesis: the same glyph at the same size,
/// asked for upright and then leaned, is two rasters rather than one reused.
#[test]
fn the_raster_cache_tells_synthesized_glyphs_apart() {
    let mut cache = GlyphCache::default();
    let black = [0, 0, 0, 255];
    let bounds = |cache: &mut GlyphCache, synthesis| {
        cache
            .hinted_run_bounds(&stem(synthesis, true), 1.0, 0.0, 0.0, black)
            .expect("draws")
            .expect("has ink")
    };
    let upright = bounds(&mut cache, Synthesis::default());
    let leaned = bounds(&mut cache, ITALIC);
    let bold = bounds(&mut cache, BOLD);
    assert!(
        leaned.x1 > upright.x1,
        "the leaned raster reaches further right"
    );
    assert!(bold.width() > upright.width(), "the bold raster is wider");
}