use crate::area::TraitSet;
use fop_types::{FontRegistry, Length};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextAlign {
Left,
Right,
Center,
Justify,
}
impl fmt::Display for TextAlign {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TextAlign::Left => write!(f, "left"),
TextAlign::Right => write!(f, "right"),
TextAlign::Center => write!(f, "center"),
TextAlign::Justify => write!(f, "justify"),
}
}
}
pub struct InlineLayoutContext {
pub available_width: Length,
pub current_x: Length,
pub line_height: Length,
pub inline_areas: Vec<InlineArea>,
pub text_align: TextAlign,
pub letter_spacing: Length,
pub word_spacing: Length,
}
#[derive(Debug, Clone)]
pub struct InlineArea {
pub width: Length,
pub height: Length,
pub content: Option<String>,
pub traits: TraitSet,
}
impl InlineLayoutContext {
pub fn new(available_width: Length, line_height: Length) -> Self {
Self {
available_width,
current_x: Length::ZERO,
line_height,
inline_areas: Vec::new(),
text_align: TextAlign::Left,
letter_spacing: Length::ZERO,
word_spacing: Length::ZERO,
}
}
pub fn with_text_align(mut self, align: TextAlign) -> Self {
self.text_align = align;
self
}
pub fn with_letter_spacing(mut self, spacing: Length) -> Self {
self.letter_spacing = spacing;
self
}
pub fn with_word_spacing(mut self, spacing: Length) -> Self {
self.word_spacing = spacing;
self
}
pub fn fits(&self, width: Length) -> bool {
self.current_x + width <= self.available_width
}
pub fn add(&mut self, area: InlineArea) -> bool {
if !self.fits(area.width) {
return false; }
self.current_x += area.width;
if area.height > self.line_height {
self.line_height = area.height;
}
self.inline_areas.push(area);
true
}
pub fn remaining_width(&self) -> Length {
self.available_width - self.current_x
}
pub fn is_empty(&self) -> bool {
self.inline_areas.is_empty()
}
pub fn used_width(&self) -> Length {
self.current_x
}
pub fn calculate_alignment_offset(&self) -> Length {
let unused_width = self.available_width - self.current_x;
match self.text_align {
TextAlign::Left => Length::ZERO,
TextAlign::Right => unused_width,
TextAlign::Center => unused_width / 2,
TextAlign::Justify => Length::ZERO, }
}
pub fn apply_alignment(&mut self) {
let offset = self.calculate_alignment_offset();
if offset > Length::ZERO {
}
}
}
pub struct LineBreaker {
available_width: Length,
font_registry: FontRegistry,
letter_spacing: Length,
word_spacing: Length,
}
impl LineBreaker {
pub fn new(available_width: Length) -> Self {
Self {
available_width,
font_registry: FontRegistry::new(),
letter_spacing: Length::ZERO,
word_spacing: Length::ZERO,
}
}
pub fn with_letter_spacing(mut self, spacing: Length) -> Self {
self.letter_spacing = spacing;
self
}
pub fn with_word_spacing(mut self, spacing: Length) -> Self {
self.word_spacing = spacing;
self
}
pub fn break_into_words(&self, text: &str) -> Vec<String> {
text.split_whitespace().map(|s| s.to_string()).collect()
}
pub fn measure_text(&self, text: &str, font_size: Length) -> Length {
self.measure_text_with_font(text, font_size, "Helvetica")
}
pub fn measure_text_with_font(&self, text: &str, font_size: Length, font_name: &str) -> Length {
let font_metrics = self.font_registry.get_or_default(font_name);
let base_width = font_metrics.measure_text(text, font_size);
let char_count = text.chars().count();
let letter_spacing_total = if char_count > 0 {
self.letter_spacing * (char_count.saturating_sub(1) as i32)
} else {
Length::ZERO
};
let space_count = text.chars().filter(|&c| c == ' ').count();
let word_spacing_total = self.word_spacing * (space_count as i32);
base_width + letter_spacing_total + word_spacing_total
}
pub fn break_lines(&self, text: &str, font_size: Length) -> Vec<String> {
let words = self.break_into_words(text);
let mut lines = Vec::new();
let mut current_line = String::new();
let _space_width = self.measure_text(" ", font_size);
for word in words {
let _word_width = self.measure_text(&word, font_size);
let line_with_word = if current_line.is_empty() {
word.clone()
} else {
format!("{} {}", current_line, word)
};
let total_width = self.measure_text(&line_with_word, font_size);
if total_width <= self.available_width {
current_line = line_with_word;
} else {
if !current_line.is_empty() {
lines.push(current_line);
}
current_line = word;
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
lines
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_align_display_left() {
assert_eq!(format!("{}", TextAlign::Left), "left");
}
#[test]
fn test_text_align_display_right() {
assert_eq!(format!("{}", TextAlign::Right), "right");
}
#[test]
fn test_text_align_display_center() {
assert_eq!(format!("{}", TextAlign::Center), "center");
}
#[test]
fn test_text_align_display_justify() {
assert_eq!(format!("{}", TextAlign::Justify), "justify");
}
#[test]
fn test_inline_context_initial_state() {
let ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0));
assert!(ctx.is_empty());
assert_eq!(ctx.available_width, Length::from_pt(100.0));
assert_eq!(ctx.current_x, Length::ZERO);
assert_eq!(ctx.line_height, Length::from_pt(12.0));
assert_eq!(ctx.text_align, TextAlign::Left);
assert_eq!(ctx.letter_spacing, Length::ZERO);
assert_eq!(ctx.word_spacing, Length::ZERO);
}
#[test]
fn test_inline_context_with_text_align() {
let ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0))
.with_text_align(TextAlign::Center);
assert_eq!(ctx.text_align, TextAlign::Center);
}
#[test]
fn test_inline_context_with_letter_spacing() {
let ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0))
.with_letter_spacing(Length::from_pt(1.0));
assert_eq!(ctx.letter_spacing, Length::from_pt(1.0));
}
#[test]
fn test_inline_context_with_word_spacing() {
let ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0))
.with_word_spacing(Length::from_pt(2.0));
assert_eq!(ctx.word_spacing, Length::from_pt(2.0));
}
#[test]
fn test_inline_area_text_creation() {
let area = InlineArea {
width: Length::from_pt(30.0),
height: Length::from_pt(12.0),
content: Some("hello".to_string()),
traits: TraitSet::default(),
};
assert_eq!(area.width, Length::from_pt(30.0));
assert_eq!(area.height, Length::from_pt(12.0));
assert_eq!(area.content.as_deref(), Some("hello"));
}
#[test]
fn test_inline_area_space_creation() {
let area = InlineArea {
width: Length::from_pt(5.0),
height: Length::from_pt(12.0),
content: None,
traits: TraitSet::default(),
};
assert_eq!(area.width, Length::from_pt(5.0));
assert!(area.content.is_none());
}
#[test]
fn test_inline_area_glue_zero_width() {
let area = InlineArea {
width: Length::ZERO,
height: Length::from_pt(12.0),
content: None,
traits: TraitSet::default(),
};
assert_eq!(area.width, Length::ZERO);
}
#[test]
fn test_inline_context_add_single_area() {
let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0));
let area = InlineArea {
width: Length::from_pt(30.0),
height: Length::from_pt(12.0),
content: Some("test".to_string()),
traits: TraitSet::default(),
};
let result = ctx.add(area);
assert!(result);
assert!(!ctx.is_empty());
assert_eq!(ctx.used_width(), Length::from_pt(30.0));
assert_eq!(ctx.remaining_width(), Length::from_pt(70.0));
}
#[test]
fn test_inline_context_add_multiple_areas() {
let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0));
for _ in 0..3 {
let area = InlineArea {
width: Length::from_pt(20.0),
height: Length::from_pt(12.0),
content: Some("x".to_string()),
traits: TraitSet::default(),
};
assert!(ctx.add(area));
}
assert_eq!(ctx.inline_areas.len(), 3);
assert_eq!(ctx.used_width(), Length::from_pt(60.0));
}
#[test]
fn test_inline_area_width_overflow_not_added() {
let mut ctx = InlineLayoutContext::new(Length::from_pt(50.0), Length::from_pt(12.0));
let area = InlineArea {
width: Length::from_pt(60.0),
height: Length::from_pt(12.0),
content: Some("toolong".to_string()),
traits: TraitSet::default(),
};
let result = ctx.add(area);
assert!(!result);
assert!(ctx.is_empty());
assert_eq!(ctx.used_width(), Length::ZERO);
}
#[test]
fn test_inline_context_fits_exact_width() {
let mut ctx = InlineLayoutContext::new(Length::from_pt(50.0), Length::from_pt(12.0));
let area = InlineArea {
width: Length::from_pt(50.0),
height: Length::from_pt(12.0),
content: Some("exact".to_string()),
traits: TraitSet::default(),
};
assert!(ctx.add(area));
assert_eq!(ctx.used_width(), Length::from_pt(50.0));
assert_eq!(ctx.remaining_width(), Length::ZERO);
}
#[test]
fn test_inline_context_overflow_detection() {
let mut ctx = InlineLayoutContext::new(Length::from_pt(50.0), Length::from_pt(12.0));
let area1 = InlineArea {
width: Length::from_pt(30.0),
height: Length::from_pt(12.0),
content: Some("ok".to_string()),
traits: TraitSet::default(),
};
assert!(ctx.add(area1));
let area2 = InlineArea {
width: Length::from_pt(25.0),
height: Length::from_pt(12.0),
content: Some("overflow".to_string()),
traits: TraitSet::default(),
};
assert!(!ctx.add(area2));
assert_eq!(ctx.inline_areas.len(), 1);
}
#[test]
fn test_line_height_updated_by_taller_area() {
let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0));
let area = InlineArea {
width: Length::from_pt(20.0),
height: Length::from_pt(20.0), content: None,
traits: TraitSet::default(),
};
ctx.add(area);
assert_eq!(ctx.line_height, Length::from_pt(20.0));
}
#[test]
fn test_line_height_not_reduced_by_shorter_area() {
let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(14.0));
let area = InlineArea {
width: Length::from_pt(20.0),
height: Length::from_pt(10.0), content: None,
traits: TraitSet::default(),
};
ctx.add(area);
assert_eq!(ctx.line_height, Length::from_pt(14.0));
}
#[test]
fn test_alignment_offset_left_is_zero() {
let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0))
.with_text_align(TextAlign::Left);
ctx.add(InlineArea {
width: Length::from_pt(60.0),
height: Length::from_pt(12.0),
content: None,
traits: TraitSet::default(),
});
assert_eq!(ctx.calculate_alignment_offset(), Length::ZERO);
}
#[test]
fn test_alignment_offset_right_is_unused_width() {
let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0))
.with_text_align(TextAlign::Right);
ctx.add(InlineArea {
width: Length::from_pt(60.0),
height: Length::from_pt(12.0),
content: None,
traits: TraitSet::default(),
});
assert_eq!(ctx.calculate_alignment_offset(), Length::from_pt(40.0));
}
#[test]
fn test_alignment_offset_center_is_half_unused() {
let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0))
.with_text_align(TextAlign::Center);
ctx.add(InlineArea {
width: Length::from_pt(60.0),
height: Length::from_pt(12.0),
content: None,
traits: TraitSet::default(),
});
assert_eq!(ctx.calculate_alignment_offset(), Length::from_pt(20.0));
}
#[test]
fn test_alignment_offset_justify_is_zero() {
let mut ctx = InlineLayoutContext::new(Length::from_pt(100.0), Length::from_pt(12.0))
.with_text_align(TextAlign::Justify);
ctx.add(InlineArea {
width: Length::from_pt(60.0),
height: Length::from_pt(12.0),
content: None,
traits: TraitSet::default(),
});
assert_eq!(ctx.calculate_alignment_offset(), Length::ZERO);
}
#[test]
fn test_line_breaker_new() {
let breaker = LineBreaker::new(Length::from_pt(100.0));
assert_eq!(breaker.available_width, Length::from_pt(100.0));
}
#[test]
fn test_break_into_words_basic() {
let breaker = LineBreaker::new(Length::from_pt(100.0));
let words = breaker.break_into_words("Hello world test");
assert_eq!(words.len(), 3);
assert_eq!(words[0], "Hello");
assert_eq!(words[1], "world");
assert_eq!(words[2], "test");
}
#[test]
fn test_break_into_words_empty_string() {
let breaker = LineBreaker::new(Length::from_pt(100.0));
let words = breaker.break_into_words("");
assert!(words.is_empty());
}
#[test]
fn test_break_into_words_single_word() {
let breaker = LineBreaker::new(Length::from_pt(100.0));
let words = breaker.break_into_words("Hello");
assert_eq!(words.len(), 1);
assert_eq!(words[0], "Hello");
}
#[test]
fn test_break_into_words_extra_whitespace() {
let breaker = LineBreaker::new(Length::from_pt(100.0));
let words = breaker.break_into_words(" one two ");
assert_eq!(words.len(), 2);
assert_eq!(words[0], "one");
assert_eq!(words[1], "two");
}
#[test]
fn test_measure_text_positive_width() {
let breaker = LineBreaker::new(Length::from_pt(100.0));
let width = breaker.measure_text("test", Length::from_pt(12.0));
assert!(width > Length::ZERO);
}
#[test]
fn test_measure_text_longer_text_wider() {
let breaker = LineBreaker::new(Length::from_pt(100.0));
let w1 = breaker.measure_text("hi", Length::from_pt(12.0));
let w2 = breaker.measure_text("hello world", Length::from_pt(12.0));
assert!(w2 > w1);
}
#[test]
fn test_measure_text_larger_font_wider() {
let breaker = LineBreaker::new(Length::from_pt(100.0));
let w_small = breaker.measure_text("test", Length::from_pt(10.0));
let w_large = breaker.measure_text("test", Length::from_pt(20.0));
assert!(w_large > w_small);
}
#[test]
fn test_measure_text_with_letter_spacing() {
let breaker_plain = LineBreaker::new(Length::from_pt(200.0));
let breaker_spaced =
LineBreaker::new(Length::from_pt(200.0)).with_letter_spacing(Length::from_pt(1.0));
let text = "hello";
let font_size = Length::from_pt(12.0);
let w_plain = breaker_plain.measure_text(text, font_size);
let w_spaced = breaker_spaced.measure_text(text, font_size);
assert!(w_spaced > w_plain);
}
#[test]
fn test_measure_text_with_word_spacing() {
let breaker_plain = LineBreaker::new(Length::from_pt(200.0));
let breaker_spaced =
LineBreaker::new(Length::from_pt(200.0)).with_word_spacing(Length::from_pt(3.0));
let text = "hello world";
let font_size = Length::from_pt(12.0);
let w_plain = breaker_plain.measure_text(text, font_size);
let w_spaced = breaker_spaced.measure_text(text, font_size);
assert!(w_spaced > w_plain);
}
#[test]
fn test_break_lines_short_text_one_line() {
let breaker = LineBreaker::new(Length::from_pt(300.0));
let lines = breaker.break_lines("Hello world", Length::from_pt(12.0));
assert_eq!(lines.len(), 1);
assert_eq!(lines[0], "Hello world");
}
#[test]
fn test_break_lines_long_text_multiple_lines() {
let breaker = LineBreaker::new(Length::from_pt(100.0));
let long_text = "This is a very long piece of text that definitely needs breaking";
let lines = breaker.break_lines(long_text, Length::from_pt(12.0));
assert!(lines.len() > 1);
}
#[test]
fn test_break_lines_single_word_fits_one_line() {
let breaker = LineBreaker::new(Length::from_pt(200.0));
let lines = breaker.break_lines("Hello", Length::from_pt(12.0));
assert_eq!(lines.len(), 1);
assert_eq!(lines[0], "Hello");
}
#[test]
fn test_break_lines_empty_string_produces_no_lines() {
let breaker = LineBreaker::new(Length::from_pt(100.0));
let lines = breaker.break_lines("", Length::from_pt(12.0));
assert!(lines.is_empty());
}
#[test]
fn test_break_lines_all_words_preserved() {
let breaker = LineBreaker::new(Length::from_pt(80.0));
let text = "alpha beta gamma delta";
let lines = breaker.break_lines(text, Length::from_pt(12.0));
let all_words: Vec<String> = lines
.iter()
.flat_map(|l| l.split_whitespace().map(|s| s.to_string()))
.collect();
let expected_words: Vec<String> = text.split_whitespace().map(|s| s.to_string()).collect();
assert_eq!(all_words, expected_words);
}
#[test]
fn test_break_lines_very_narrow_width_one_word_per_line() {
let breaker = LineBreaker::new(Length::from_pt(1.0));
let lines = breaker.break_lines("one two three", Length::from_pt(12.0));
assert!(!lines.is_empty());
}
}