use crate::text::{BoundaryContext, CharacterInfo};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LigatureDecision {
Keep,
Split,
}
pub struct LigatureDecisionMaker;
impl LigatureDecisionMaker {
pub fn decide(
char_info: &CharacterInfo,
context: &BoundaryContext,
next_char: Option<&CharacterInfo>,
) -> LigatureDecision {
let next = match next_char {
Some(c) => c,
None => return LigatureDecision::Keep,
};
if let Some(tj_offset) = next.tj_offset {
if tj_offset < -100 {
return LigatureDecision::Split;
}
}
let ligature_end = char_info.x_position + char_info.width;
let gap = next.x_position - ligature_end;
let threshold = context.font_size * 0.5;
if gap > threshold {
return LigatureDecision::Split;
}
LigatureDecision::Keep
}
}
pub fn get_ligature_components(ligature: char) -> Option<&'static str> {
match ligature {
'ff' => Some("ff"), 'fi' => Some("fi"), 'fl' => Some("fl"), 'ffi' => Some("ffi"), 'ffl' => Some("ffl"), 'ſt' => Some("st"), 'st' => Some("st"), _ => None,
}
}
pub fn expand_ligature_to_chars(ligature: char, original_width: f32) -> Vec<(char, f32)> {
let components_str = match get_ligature_components(ligature) {
Some(s) => s,
None => return Vec::new(), };
let num_components = components_str.chars().count();
if num_components == 0 {
return Vec::new();
}
let width_per_component = original_width / num_components as f32;
components_str
.chars()
.map(|c| (c, width_per_component))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_ligature_components_all_ligatures() {
assert_eq!(get_ligature_components('ff'), Some("ff"));
assert_eq!(get_ligature_components('fi'), Some("fi"));
assert_eq!(get_ligature_components('fl'), Some("fl"));
assert_eq!(get_ligature_components('ffi'), Some("ffi"));
assert_eq!(get_ligature_components('ffl'), Some("ffl"));
}
#[test]
fn test_get_ligature_components_non_ligatures() {
assert_eq!(get_ligature_components('a'), None);
assert_eq!(get_ligature_components('A'), None);
assert_eq!(get_ligature_components('1'), None);
assert_eq!(get_ligature_components(' '), None);
}
#[test]
fn test_expand_ligature_fi() {
let result = expand_ligature_to_chars('fi', 500.0);
assert_eq!(result.len(), 2);
assert_eq!(result[0], ('f', 250.0));
assert_eq!(result[1], ('i', 250.0));
}
#[test]
fn test_expand_ligature_ffl() {
let result = expand_ligature_to_chars('ffl', 600.0);
assert_eq!(result.len(), 3);
assert_eq!(result[0], ('f', 200.0));
assert_eq!(result[1], ('f', 200.0));
assert_eq!(result[2], ('l', 200.0));
}
#[test]
fn test_expand_non_ligature() {
let result = expand_ligature_to_chars('a', 400.0);
assert!(result.is_empty());
}
#[test]
fn test_ligature_decision_no_next_char() {
let char_info = CharacterInfo {
code: 0xFB01,
glyph_id: Some(1),
width: 500.0,
x_position: 0.0,
tj_offset: None,
font_size: 12.0,
is_ligature: true,
original_ligature: None,
protected_from_split: false,
};
let context = BoundaryContext::new(12.0);
let decision = LigatureDecisionMaker::decide(&char_info, &context, None);
assert_eq!(decision, LigatureDecision::Keep);
}
#[test]
fn test_ligature_decision_large_tj_offset() {
let ligature = CharacterInfo {
code: 0xFB01,
glyph_id: Some(1),
width: 500.0,
x_position: 0.0,
tj_offset: None,
font_size: 12.0,
is_ligature: true,
original_ligature: None,
protected_from_split: false,
};
let next = CharacterInfo {
code: 0x63,
glyph_id: Some(2),
width: 400.0,
x_position: 500.0,
tj_offset: Some(-150),
font_size: 12.0,
is_ligature: false,
original_ligature: None,
protected_from_split: false,
};
let context = BoundaryContext::new(12.0);
let decision = LigatureDecisionMaker::decide(&ligature, &context, Some(&next));
assert_eq!(decision, LigatureDecision::Split);
}
#[test]
fn test_ligature_decision_large_gap() {
let ligature = CharacterInfo {
code: 0xFB01,
glyph_id: Some(1),
width: 500.0,
x_position: 0.0,
tj_offset: None,
font_size: 12.0,
is_ligature: true,
original_ligature: None,
protected_from_split: false,
};
let next = CharacterInfo {
code: 0x61,
glyph_id: Some(2),
width: 400.0,
x_position: 510.0, tj_offset: None,
font_size: 12.0,
is_ligature: false,
original_ligature: None,
protected_from_split: false,
};
let context = BoundaryContext::new(12.0);
let decision = LigatureDecisionMaker::decide(&ligature, &context, Some(&next));
assert_eq!(decision, LigatureDecision::Split);
}
#[test]
fn test_ligature_decision_keep() {
let ligature = CharacterInfo {
code: 0xFB01,
glyph_id: Some(1),
width: 500.0,
x_position: 0.0,
tj_offset: None,
font_size: 12.0,
is_ligature: true,
original_ligature: None,
protected_from_split: false,
};
let next = CharacterInfo {
code: 0x6E,
glyph_id: Some(2),
width: 400.0,
x_position: 500.0, tj_offset: None,
font_size: 12.0,
is_ligature: false,
original_ligature: None,
protected_from_split: false,
};
let context = BoundaryContext::new(12.0);
let decision = LigatureDecisionMaker::decide(&ligature, &context, Some(&next));
assert_eq!(decision, LigatureDecision::Keep);
}
}