use crate::layout::TextSpan;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SpaceInsertion {
pub insert: bool,
}
impl SpaceInsertion {
#[inline]
pub const fn yes() -> Self {
Self { insert: true }
}
#[inline]
pub const fn no() -> Self {
Self { insert: false }
}
}
#[derive(Debug, Clone, Copy)]
pub struct SpacingConfig {
pub word_margin: f32,
}
impl Default for SpacingConfig {
fn default() -> Self {
Self { word_margin: 0.1 }
}
}
impl SpacingConfig {
pub fn tight() -> Self {
Self { word_margin: 0.05 }
}
pub fn loose() -> Self {
Self { word_margin: 0.15 }
}
}
pub fn should_insert_space(
prev: &TextSpan,
next: &TextSpan,
config: &SpacingConfig,
) -> SpaceInsertion {
if has_boundary_whitespace(&prev.text, &next.text) {
return SpaceInsertion::no();
}
let prev_right = prev.bbox.right();
let next_left = next.bbox.left();
let gap = next_left - prev_right;
let char_size = prev.bbox.width.max(prev.bbox.height);
let margin = config.word_margin * char_size;
if gap > margin {
SpaceInsertion::yes()
} else {
SpaceInsertion::no()
}
}
#[inline]
fn has_boundary_whitespace(prev: &str, next: &str) -> bool {
prev.chars().last().is_some_and(|c| c.is_whitespace())
|| next.chars().next().is_some_and(|c| c.is_whitespace())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::Rect;
use crate::layout::{Color, FontWeight};
fn make_span(text: &str, x: f32, width: f32) -> TextSpan {
TextSpan {
artifact_type: None,
text: text.to_string(),
bbox: Rect::new(x, 0.0, width, 12.0),
font_name: "Arial".to_string(),
font_size: 12.0,
font_weight: FontWeight::Normal,
is_italic: false,
is_monospace: false,
color: Color::new(0.0, 0.0, 0.0),
mcid: None,
sequence: 0,
split_boundary_before: false,
offset_semantic: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
}
}
#[test]
fn test_clear_word_gap() {
let prev = make_span("Hello", 0.0, 30.0);
let next = make_span("World", 40.0, 30.0); let config = SpacingConfig::default();
assert_eq!(should_insert_space(&prev, &next, &config), SpaceInsertion::yes());
}
#[test]
fn test_tight_kerning() {
let prev = make_span("Hel", 0.0, 20.0);
let next = make_span("lo", 21.0, 10.0); let config = SpacingConfig::default();
assert_eq!(should_insert_space(&prev, &next, &config), SpaceInsertion::no());
}
#[test]
fn test_existing_boundary_space() {
let prev = make_span("Hello ", 0.0, 30.0); let next = make_span("World", 35.0, 30.0);
let config = SpacingConfig::default();
assert_eq!(should_insert_space(&prev, &next, &config), SpaceInsertion::no());
}
#[test]
fn test_word_margin_variations() {
let tight = SpacingConfig { word_margin: 0.05 };
let loose = SpacingConfig { word_margin: 0.15 };
let prev = make_span("Hello", 0.0, 30.0);
let next = make_span("World", 33.0, 30.0);
assert_eq!(should_insert_space(&prev, &next, &tight), SpaceInsertion::yes());
assert_eq!(should_insert_space(&prev, &next, &loose), SpaceInsertion::no());
}
#[test]
fn test_exactly_at_margin() {
let prev = make_span("Hello", 0.0, 20.0);
let config = SpacingConfig::default();
let next = make_span("World", 22.0, 20.0);
assert_eq!(should_insert_space(&prev, &next, &config), SpaceInsertion::no());
let next = make_span("World", 22.1, 20.0);
assert_eq!(should_insert_space(&prev, &next, &config), SpaceInsertion::yes());
}
#[test]
fn test_leading_space_in_next() {
let prev = make_span("Hello", 0.0, 30.0);
let next = make_span(" World", 40.0, 30.0); let config = SpacingConfig::default();
assert_eq!(should_insert_space(&prev, &next, &config), SpaceInsertion::no());
}
}