cranpose-render-common 0.9.21

Scene, renderer and hit-test contracts for Cranpose backends
Documentation
use cranpose_ui::text::{RangeStyle, SpanStyle};

use super::*;

const TABULAR_ZERO: u16 = 19;
const SLASHED_ZERO: u16 = 2251;
const SMALL_CAP_A: u16 = 1868;

fn font() -> SoftwareTextFont {
    default_software_text_font().expect("bundled default font")
}

fn featured(settings: Option<&str>) -> TextStyle {
    TextStyle::from_span_style(SpanStyle {
        font_feature_settings: settings.map(Into::into),
        ..Default::default()
    })
}

fn shaped_glyph_ids(font: &SoftwareTextFont, style: &TextStyle, text: &str) -> Vec<u16> {
    let shaped = font.shaped_for(style);
    text.chars().map(|ch| shaped.font.glyph_id(ch).0).collect()
}

fn atlas_glyph_ids(
    fonts: &SoftwareTextFontSet,
    text: &AnnotatedString,
    style: &TextStyle,
) -> Vec<u32> {
    let mut cache = SoftwareGlyphRasterCache::with_capacity_at_least_one(64);
    let mut run = Vec::new();
    collect_solid_text_atlas_run(
        text,
        Rect {
            x: 0.0,
            y: 0.0,
            width: 400.0,
            height: 40.0,
        },
        style,
        Color::WHITE,
        18.0,
        1.0,
        fonts,
        &mut cache,
        &mut run,
    )
    .expect("atlas run");
    run.iter()
        .map(|glyph| glyph.placement().key.glyph_id)
        .collect()
}

fn layout_widths(font: &SoftwareTextFont, text: &str, style: &TextStyle) -> Vec<f32> {
    layout_text_with_font(text, style, font)
        .glyph_layouts()
        .iter()
        .map(|glyph| glyph.width)
        .collect()
}

#[test]
fn pnum_makes_digits_proportional_and_narrows_the_measured_width() {
    let font = font();
    let plain = featured(None);
    let pnum = featured(Some("\"pnum\""));
    let size = resolve_font_size(&plain);
    let tabular = measure_text_with_font("1111", &plain, size, &font).width;
    let proportional = measure_text_with_font("1111", &pnum, size, &font).width;
    assert!(
        proportional < tabular * 0.85,
        "proportional ones {proportional} should be narrower than tabular {tabular}"
    );
    let advances: Vec<f32> = layout_widths(&font, "1", &plain)
        .into_iter()
        .chain(layout_widths(&font, "1", &pnum))
        .collect();
    assert!(
        (advances[1] / advances[0] - 441.0 / 572.0).abs() < 1e-3,
        "{advances:?}"
    );

    let measurer = SoftwareTextMeasurer::new(font, 64);
    let text = AnnotatedString::from("1111");
    for (style, width) in [(&plain, tabular), (&pnum, proportional), (&plain, tabular)] {
        assert_eq!(measurer.measure(&text, style).width, width);
        let line = measurer
            .measure_line_width(&text, 0..text.text.len(), style)
            .expect("line width");
        assert!((line - width).abs() < 0.01, "{line} vs {width}");
    }
}

#[test]
fn zero_substitutes_the_glyph_of_zero_where_it_is_measured_and_drawn() {
    let font = font();
    let fonts = SoftwareTextFontSet::from_font(font.clone());
    let plain = featured(None);
    let zero = featured(Some("\"zero\" on"));
    assert_eq!(shaped_glyph_ids(&font, &plain, "10"), [20, TABULAR_ZERO]);
    assert_eq!(shaped_glyph_ids(&font, &zero, "10"), [20, SLASHED_ZERO]);
    let text = AnnotatedString::from("10");
    assert_eq!(
        atlas_glyph_ids(&fonts, &text, &plain),
        [20, u32::from(TABULAR_ZERO)]
    );
    assert_eq!(
        atlas_glyph_ids(&fonts, &text, &zero),
        [20, u32::from(SLASHED_ZERO)]
    );
    assert_eq!(
        measure_text_with_font("0", &zero, 18.0, &font).width,
        measure_text_with_font("0", &plain, 18.0, &font).width
    );
}

#[test]
fn zero_draws_different_pixels_from_the_plain_zero() {
    let font = font();
    let rect = Rect {
        x: 0.0,
        y: 0.0,
        width: 48.0,
        height: 32.0,
    };
    let draw = |style: &TextStyle| {
        rasterize_text_to_image("0", rect, style, Color::WHITE, 24.0, 1.0, &font).expect("image")
    };
    assert_ne!(
        draw(&featured(Some("zero"))).pixels(),
        draw(&featured(None)).pixels()
    );
    assert_eq!(
        draw(&featured(Some("zero 0"))).pixels(),
        draw(&featured(None)).pixels()
    );
}

#[test]
fn smcp_substitutes_lowercase_letters_only() {
    let font = font();
    let plain = featured(None);
    let smcp = featured(Some("'smcp'"));
    let lower: String = ('a'..='z').collect();
    let upper: String = ('A'..='Z').collect();
    let small_caps = shaped_glyph_ids(&font, &smcp, &lower);
    for (ch, (small, plain)) in lower.chars().zip(
        small_caps
            .iter()
            .zip(shaped_glyph_ids(&font, &plain, &lower)),
    ) {
        assert_ne!(*small, plain, "{ch}");
    }
    assert_eq!(small_caps[0], SMALL_CAP_A);
    assert_eq!(
        shaped_glyph_ids(&font, &smcp, &upper),
        shaped_glyph_ids(&font, &plain, &upper)
    );
    let fonts = SoftwareTextFontSet::from_font(font);
    let small_caps_run = atlas_glyph_ids(&fonts, &AnnotatedString::from("abc"), &smcp);
    assert_eq!(small_caps_run[0], u32::from(SMALL_CAP_A));
}

#[test]
fn no_settings_or_disabled_features_give_todays_glyphs_and_advances() {
    let font = font();
    let fonts = SoftwareTextFontSet::from_font(font.clone());
    let plain = featured(None);
    let text = "Tabular 0123456789 abc AV To";
    let annotated = AnnotatedString::from(text);
    let plain_width = measure_text_with_font(text, &plain, 18.0, &font).width;
    let plain_glyphs = atlas_glyph_ids(&fonts, &annotated, &plain);
    let plain_layout = layout_widths(&font, text, &plain);
    for settings in [
        "\"pnum\" 0",
        "pnum off, zero 0",
        "liga",
        "frac",
        "",
        "bogus",
    ] {
        let style = featured(Some(settings));
        assert!(
            matches!(font.shaped_for(&style), Cow::Borrowed(_)),
            "{settings} should shape nothing"
        );
        assert_eq!(
            measure_text_with_font(text, &style, 18.0, &font).width,
            plain_width
        );
        assert_eq!(atlas_glyph_ids(&fonts, &annotated, &style), plain_glyphs);
        assert_eq!(layout_widths(&font, text, &style), plain_layout);
    }
    assert!(matches!(font.shaped_for(&plain), Cow::Borrowed(_)));
}

#[test]
fn a_shaped_face_shares_raster_caches_but_not_character_glyphs() {
    let font = font();
    let zero = font.shaped_for(&featured(Some("zero")));
    assert_eq!(zero.content_hash(), font.content_hash());
    assert_ne!(zero.glyph_map_hash, font.glyph_map_hash);
    assert!(matches!(
        zero.shaped_for(&featured(Some("\"zero\" 1"))),
        Cow::Borrowed(_)
    ));
    let unshaped = zero.shaped_for(&featured(None));
    assert_eq!(unshaped.glyph_map_hash, font.glyph_map_hash);
    assert_eq!(
        shaped_glyph_ids(&unshaped, &featured(None), "0"),
        [TABULAR_ZERO]
    );
}

#[test]
fn a_measurer_keeps_feature_and_plain_widths_apart_for_non_ascii_text() {
    let measurer = SoftwareTextMeasurer::new(font(), 64);
    let text = AnnotatedString::from("ééé");
    let plain = featured(None);
    let smcp = featured(Some("smcp"));
    let line = |style: &TextStyle| {
        measurer
            .measure_line_width(&text, 0..text.text.len(), style)
            .expect("line width")
    };
    let plain_width = line(&plain);
    let small_caps_width = line(&smcp);
    assert!(
        small_caps_width < plain_width * 0.9,
        "{small_caps_width} vs {plain_width}"
    );
    assert_eq!(line(&plain), plain_width);
    assert_eq!(line(&smcp), small_caps_width);
}

#[test]
fn a_span_shapes_only_its_own_range() {
    let fonts = SoftwareTextFontSet::from_font(font());
    let text = AnnotatedString {
        text: "0000".to_owned(),
        span_styles: vec![RangeStyle {
            item: SpanStyle {
                font_feature_settings: Some("zero".into()),
                ..Default::default()
            },
            range: 0..2,
        }],
        ..Default::default()
    };
    let slashed = u32::from(SLASHED_ZERO);
    let plain = u32::from(TABULAR_ZERO);
    assert_eq!(
        atlas_glyph_ids(&fonts, &text, &featured(None)),
        [slashed, slashed, plain, plain]
    );
}