use super::FontRegistry;
#[derive(Debug, Clone)]
pub struct FontRun {
pub start: usize,
pub end: usize,
pub family: String,
}
pub(crate) fn resolve_family(
ch: char,
families: &str,
weight: u32,
italic: bool,
previous: Option<&str>,
registry: &FontRegistry,
) -> String {
if let Some(family) = previous {
let shaping_control = matches!(ch, '\u{200c}' | '\u{200d}' | '\u{fe00}'..='\u{fe0f}' | '\u{e0100}'..='\u{e01ef}');
let combining_mark = unicode_bidi::bidi_class(ch) == unicode_bidi::BidiClass::NSM;
if shaping_control
|| (combining_mark && registry.resolve(family, weight, italic).has_char(ch))
{
return family.to_string();
}
}
registry.resolve_for_char(families, ch, weight, italic).1
}
pub fn segment_by_font(
chars: &[char],
families: &str,
weight: u32,
italic: bool,
registry: &FontRegistry,
) -> Vec<FontRun> {
if chars.is_empty() {
return vec![];
}
if !families.contains(',') {
let family = families.trim().trim_matches('"').trim_matches('\'');
let font = registry.resolve(family, weight, italic);
let all_covered = chars.iter().all(|&ch| ch.is_control() || font.has_char(ch));
if all_covered {
return vec![FontRun {
start: 0,
end: chars.len(),
family: family.to_string(),
}];
}
}
let mut runs = Vec::new();
let first_family = resolve_family(chars[0], families, weight, italic, None, registry);
let mut current_family = first_family;
let mut run_start = 0;
for (i, &ch) in chars.iter().enumerate().skip(1) {
let family = resolve_family(
ch,
families,
weight,
italic,
Some(¤t_family),
registry,
);
if family != current_family {
runs.push(FontRun {
start: run_start,
end: i,
family: current_family,
});
current_family = family;
run_start = i;
}
}
runs.push(FontRun {
start: run_start,
end: chars.len(),
family: current_family,
});
runs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_font_fast_path() {
let registry = FontRegistry::new();
let chars: Vec<char> = "Hello world".chars().collect();
let runs = segment_by_font(&chars, "Helvetica", 400, false, ®istry);
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].family, "Helvetica");
assert_eq!(runs[0].start, 0);
assert_eq!(runs[0].end, 11);
}
#[test]
fn test_empty_input() {
let registry = FontRegistry::new();
let chars: Vec<char> = vec![];
let runs = segment_by_font(&chars, "Helvetica, Times", 400, false, ®istry);
assert!(runs.is_empty());
}
#[test]
fn test_single_font_builtin_fallback() {
let registry = FontRegistry::new();
let chars: Vec<char> = "\u{041F}\u{0440}\u{0438}\u{0432}\u{0435}\u{0442}"
.chars()
.collect();
let runs = segment_by_font(&chars, "Helvetica", 400, false, ®istry);
assert!(!runs.is_empty(), "Should produce at least one run");
assert_eq!(runs[0].family, "Noto Sans", "Cyrillic should use Noto Sans");
}
#[test]
fn test_single_font_mixed_latin_cyrillic() {
let registry = FontRegistry::new();
let chars: Vec<char> = "Hi \u{041F}".chars().collect();
let runs = segment_by_font(&chars, "Helvetica", 400, false, ®istry);
assert!(
runs.len() >= 2,
"Should have at least 2 runs (Latin + Cyrillic), got {}",
runs.len()
);
}
#[test]
fn test_all_chars_same_font() {
let registry = FontRegistry::new();
let chars: Vec<char> = "ABC".chars().collect();
let runs = segment_by_font(&chars, "Helvetica, Times", 400, false, ®istry);
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].family, "Helvetica");
}
}