use rustyfi_backend::{Context, FontKey, FontMetrics, Length, PureHorzBox, Script};
use rustyfi_lang::eval::Interp;
use rustyfi_lang::primitives;
use rustyfi_lang::quoted::IText;
use rustyfi_lang::value::{BaseEnv, Env, Value};
struct Stub;
impl FontMetrics for Stub {
fn advance(&self, _f: FontKey, _c: char, size: Length) -> Option<Length> {
Some(size * 0.5)
}
fn ascender(&self, _f: FontKey, size: Length) -> Length {
size * 0.8
}
fn descender(&self, _f: FontKey, size: Length) -> Length {
size * 0.2
}
fn resolve_font_abbrev(&self, abbrev: &str) -> Option<FontKey> {
match abbrev {
"mykana" => Some(FontKey(7)),
"mylatin" => Some(FontKey(9)),
_ => None,
}
}
}
fn set_font(
interp: &mut Interp,
env: &BaseEnv,
script: &str,
abbrev: &str,
ratio: f64,
rising: f64,
ctx: Context,
) -> Context {
let prim = env.lookup("set-font").expect("set-font is registered");
let script_v = Value::Ctor(script.to_string(), None);
let font_v = Value::Tuple(vec![
Value::Str(abbrev.to_string()),
Value::Float(ratio),
Value::Float(rising),
]);
let ctx_v = Value::Context(Box::new(ctx));
let v1 = interp.apply(prim, script_v).unwrap();
let v2 = interp.apply(v1, font_v).unwrap();
match interp.apply(v2, ctx_v).unwrap() {
Value::Context(c) => *c,
other => panic!("set-font did not return a context: {other:?}"),
}
}
fn get_font(
interp: &mut Interp,
env: &BaseEnv,
script: &str,
ctx: Context,
) -> (String, f64, f64) {
let prim = env.lookup("get-font").expect("get-font is registered");
let script_v = Value::Ctor(script.to_string(), None);
let v1 = interp.apply(prim, script_v).unwrap();
match interp.apply(v1, Value::Context(Box::new(ctx))).unwrap() {
Value::Tuple(vs) => match &vs[..] {
[Value::Str(a), Value::Float(r), Value::Float(ri)] => (a.clone(), *r, *ri),
other => panic!("get-font returned an unexpected triple: {other:?}"),
},
other => panic!("get-font did not return a tuple: {other:?}"),
}
}
#[test]
fn get_font_reads_back_what_set_font_wrote() {
let stub = Stub;
let mut interp = Interp::new(&stub);
let env = primitives::base_env();
let ctx = Context::initial(Length::pt(400.0));
let after = set_font(&mut interp, &env, "Kana", "mykana", 0.88, 0.25, ctx);
let (abbrev, ratio, rising) = get_font(&mut interp, &env, "Kana", after.clone());
assert_eq!(ratio, 0.88, "the size ratio must round-trip exactly");
assert_eq!(rising, 0.25, "so must the rising ratio");
assert_eq!(abbrev, "", "no reverse map on this provider");
let (_, latin_ratio, latin_rising) = get_font(&mut interp, &env, "Latin", after);
assert_eq!((latin_ratio, latin_rising), (1.0, 0.0));
}
#[test]
fn get_font_normalizes_other_script_through_the_dominant_narrow_script() {
let stub = Stub;
let mut interp = Interp::new(&stub);
let env = primitives::base_env();
let ctx = Context::initial(Length::pt(400.0));
let mut ctx = set_font(&mut interp, &env, "Kana", "mykana", 0.88, 0.0, ctx);
let (_, before, _) = get_font(&mut interp, &env, "OtherScript", ctx.clone());
assert_eq!(before, 1.0, "unset OtherScript reads its own (initial) slot");
ctx.dominant_narrow_script = Script::Kana;
let (_, after, _) = get_font(&mut interp, &env, "OtherScript", ctx);
assert_eq!(
after, 0.88,
"with a dominant narrow script, OtherScript resolves to ITS slot"
);
}
#[test]
fn set_font_kana_changes_only_scheme_kana() {
let stub = Stub;
let mut interp = Interp::new(&stub);
let env = primitives::base_env();
let ctx = Context::initial(Length::pt(400.0));
let before_font = ctx.font;
let before_latin = ctx.font_scheme[Script::Latin as usize];
let after = set_font(&mut interp, &env, "Kana", "mykana", 0.88, 0.0, ctx);
assert_eq!(
after.font, before_font,
"set-font Kana must not move ctx.font"
);
assert_eq!(
after.font_scheme[Script::Latin as usize],
before_latin,
"set-font Kana must not touch the Latin slot"
);
let kana = after.font_scheme[Script::Kana as usize];
assert_eq!(kana.font, FontKey(7));
assert_eq!(kana.ratio, 0.88);
assert_eq!(kana.rising, 0.0);
}
#[test]
fn set_font_latin_also_moves_ctx_font() {
let stub = Stub;
let mut interp = Interp::new(&stub);
let env = primitives::base_env();
let ctx = Context::initial(Length::pt(400.0));
let after = set_font(&mut interp, &env, "Latin", "mylatin", 1.0, 0.0, ctx);
assert_eq!(after.font, FontKey(9), "set-font Latin must move ctx.font");
assert_eq!(after.font_scheme[Script::Latin as usize].font, FontKey(9));
}
#[test]
fn set_font_unknown_abbrev_falls_back_to_heuristic() {
let stub = Stub;
let mut interp = Interp::new(&stub);
let env = primitives::base_env();
let ctx = Context::initial(Length::pt(400.0));
let after = set_font(&mut interp, &env, "OtherScript", "myboldish", 1.0, 0.0, ctx);
assert_eq!(
after.font_scheme[Script::OtherScript as usize].font,
FontKey(1)
);
}
#[test]
fn mixed_script_paragraph_produces_two_font_keys_and_scaled_size() {
let stub = Stub;
let mut interp = Interp::new(&stub);
let mut ctx = Context::initial(Length::pt(400.0));
ctx.font_scheme[Script::Kana as usize] = rustyfi_backend::ScriptFont {
font: FontKey(7),
ratio: 0.88,
rising: 0.0,
};
let elems = vec![IText::Text("hi あ".to_string())];
let boxes = primitives::read_inline(&mut interp, &ctx, &elems, &Env::root())
.expect("read_inline should succeed");
let mut saw_latin = false;
let mut saw_kana = false;
for hb in &boxes {
let rustyfi_backend::HorzBox::Pure(PureHorzBox::InnerString { info, .. }) = hb else {
continue;
};
if info.font == FontKey(0) {
saw_latin = true;
assert_eq!(
info.size, ctx.font_size,
"Latin run keeps ctx.font_size (ratio 1.0)"
);
} else if info.font == FontKey(7) {
saw_kana = true;
assert_eq!(
info.size,
ctx.font_size * 0.88,
"Kana run is scaled by font_scheme[Kana].ratio"
);
}
}
assert!(
saw_latin,
"expected a Latin-script InnerString (FontKey(0))"
);
assert!(saw_kana, "expected a Kana-script InnerString (FontKey(7))");
}
#[test]
fn manual_rising_is_added_to_inner_string_rising() {
let stub = Stub;
let mut interp = Interp::new(&stub);
let elems = vec![IText::Text("hi".to_string())];
let base_ctx = Context::initial(Length::pt(400.0));
let base_boxes = primitives::read_inline(&mut interp, &base_ctx, &elems, &Env::root())
.expect("read_inline should succeed");
let base_rising = base_boxes
.iter()
.find_map(|hb| match hb {
rustyfi_backend::HorzBox::Pure(PureHorzBox::InnerString { info, .. }) => {
Some(info.rising)
}
_ => None,
})
.expect("expected an InnerString");
assert_eq!(
base_rising,
Length::ZERO,
"default manual_rising leaves rising at ZERO"
);
let mut risen_ctx = Context::initial(Length::pt(400.0));
risen_ctx.manual_rising = Length::pt(3.0);
let risen_boxes = primitives::read_inline(&mut interp, &risen_ctx, &elems, &Env::root())
.expect("read_inline should succeed");
let risen = risen_boxes
.iter()
.find_map(|hb| match hb {
rustyfi_backend::HorzBox::Pure(PureHorzBox::InnerString { info, .. }) => {
Some(info.rising)
}
_ => None,
})
.expect("expected an InnerString");
assert_eq!(
risen,
Length::pt(3.0),
"set-manual-rising must reach HorzStringInfo.rising"
);
}
#[test]
fn other_script_resolves_through_the_dominant_narrow_script() {
let stub = Stub;
let mut interp = Interp::new(&stub);
let mut ctx = Context::initial(Length::pt(400.0));
ctx.font_scheme[Script::Kana as usize] = rustyfi_backend::ScriptFont {
font: FontKey(7),
ratio: 0.88,
rising: 0.0,
};
ctx.font_scheme[Script::OtherScript as usize] = rustyfi_backend::ScriptFont {
font: FontKey(3),
ratio: 1.0,
rising: 0.0,
};
let elems = vec![IText::Text("\u{25A1}".to_string())];
let boxes = primitives::read_inline(&mut interp, &ctx, &elems, &Env::root()).unwrap();
let (font, size) = boxes
.iter()
.find_map(|hb| match hb {
rustyfi_backend::HorzBox::Pure(PureHorzBox::InnerString { info, .. }) => {
Some((info.font, info.size))
}
_ => None,
})
.expect("expected an InnerString");
assert_eq!(
font,
FontKey(3),
"default must still read the OtherScript slot"
);
assert_eq!(size, ctx.font_size);
ctx.dominant_narrow_script = Script::Kana;
let boxes = primitives::read_inline(&mut interp, &ctx, &elems, &Env::root()).unwrap();
let (font, size) = boxes
.iter()
.find_map(|hb| match hb {
rustyfi_backend::HorzBox::Pure(PureHorzBox::InnerString { info, .. }) => {
Some((info.font, info.size))
}
_ => None,
})
.expect("expected an InnerString");
assert_eq!(
font,
FontKey(7),
"OtherScript must follow dominant_narrow_script"
);
assert_eq!(
size,
ctx.font_size * 0.88,
"and pick up that script's ratio too"
);
}