mod autosize;
mod bidi;
mod classify;
mod comb;
pub(crate) mod edit_ap;
pub mod hit;
mod place;
mod split;
use kurbo::Rect;
use std::ops::Range;
pub use bidi::Direction;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Alignment {
#[default]
Left,
Center,
Right,
}
impl Alignment {
#[must_use]
pub fn from_quadding(q: i64) -> Alignment {
match q {
1 => Alignment::Center,
2 => Alignment::Right,
_ => Alignment::Left,
}
}
}
pub(crate) const FONT_SCALE: f32 = 0.001;
pub(crate) const FONT_SIZE_STEPS: [u8; 25] = [
4, 6, 8, 9, 10, 12, 14, 18, 20, 25, 30, 35, 40, 45, 50, 55, 60, 70, 80, 90, 100, 110, 120, 130,
144,
];
#[derive(Clone, Copy)]
pub struct Metrics<'a> {
pub width: &'a dyn Fn(u32) -> i32,
pub ascent: i32,
pub descent: i32,
}
impl std::fmt::Debug for Metrics<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Metrics")
.field("ascent", &self.ascent)
.field("descent", &self.descent)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
pub plate: Rect,
pub alignment: Alignment,
pub font_size: f32,
pub multi_line: bool,
pub auto_return: bool,
pub sub_word: Option<char>,
pub limit_char: usize,
pub char_array: usize,
pub direction: Direction,
}
impl Default for Config {
fn default() -> Self {
Config {
plate: Rect::ZERO,
alignment: Alignment::Left,
font_size: 0.0,
multi_line: false,
auto_return: false,
sub_word: None,
limit_char: 0,
char_array: 0,
direction: Direction::Auto,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Word {
pub ch: u32,
pub x: f32,
pub y: f32,
pub tail: f32,
pub is_rtl: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Line {
pub words: Option<Range<u32>>,
pub x: f32,
pub y: f32,
pub width: f32,
pub ascent: f32,
pub descent: f32,
}
impl Line {
#[must_use]
pub fn word_range(&self, available: usize) -> Range<usize> {
let Some(words) = self.words.clone() else {
return 0..0;
};
let begin = usize::try_from(words.start).unwrap_or(usize::MAX);
let end = usize::try_from(words.end).unwrap_or(usize::MAX);
if begin >= available {
return 0..0;
}
begin..end.min(available)
}
#[must_use]
pub fn last_word(&self) -> Option<u32> {
let words = self.words.clone()?;
words.end.checked_sub(1).filter(|last| *last >= words.start)
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Section {
pub words: Vec<Word>,
pub lines: Vec<Line>,
pub rect: Rect,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Layout {
pub sections: Vec<Section>,
pub content_rect: Rect,
pub font_size: f32,
}
impl Layout {
#[must_use]
pub fn to_pdf(plate: Rect, x: f32, y: f32) -> (f32, f32) {
(crate::geom::left(plate) + x, crate::geom::top(plate) - y)
}
#[must_use]
pub fn content_rect_pdf(&self, plate: Rect) -> Rect {
let (left, top) = Layout::to_pdf(
plate,
crate::geom::left(self.content_rect),
crate::geom::bottom(self.content_rect),
);
let (right, bottom) = Layout::to_pdf(
plate,
crate::geom::right(self.content_rect),
crate::geom::top(self.content_rect),
);
crate::geom::rect(left, bottom, right, top)
}
pub fn words(&self) -> impl Iterator<Item = (usize, usize, &Word)> {
self.sections.iter().enumerate().flat_map(|(s, section)| {
section.lines.iter().enumerate().flat_map(move |(l, line)| {
let range = line.word_range(section.words.len());
section
.words
.get(range)
.unwrap_or_default()
.iter()
.map(move |word| (s, l, word))
})
})
}
}
#[must_use]
pub(crate) fn split_sections(text: &str, config: &Config) -> Vec<Vec<u32>> {
let mut sections: Vec<Vec<u32>> = vec![Vec::new()];
let mut count = 0usize;
let chars: Vec<char> = text.chars().collect();
let mut index = 0;
while index < chars.len() {
if config.limit_char > 0 && count >= config.limit_char {
break;
}
if config.char_array > 0 && count >= config.char_array {
break;
}
let ch = chars.get(index).copied().unwrap_or('\0');
match ch {
'\r' | '\n' => {
let partner = if ch == '\r' { '\n' } else { '\r' };
if chars.get(index + 1) == Some(&partner) {
index += 1;
}
if config.multi_line {
sections.push(Vec::new());
}
}
_ => {
let ch = if ch == '\t' { ' ' } else { ch };
if let Some(last) = sections.last_mut() {
last.push(ch as u32);
}
}
}
count += 1;
index += 1;
}
sections
}
#[must_use]
pub fn layout(text: &str, config: &Config, metrics: &Metrics<'_>) -> Layout {
let mut config = config.clone();
let mut sections: Vec<Section> = split_sections(text, &config)
.into_iter()
.map(|words| Section {
words: words
.into_iter()
.map(|ch| Word {
ch,
x: 0.0,
y: 0.0,
tail: 0.0,
is_rtl: false,
})
.collect(),
lines: Vec::new(),
rect: Rect::ZERO,
})
.collect();
if config.font_size == 0.0 {
config.font_size = autosize::auto_font_size(§ions, &config, metrics);
}
let content_rect = rearrange(&mut sections, &config, metrics);
Layout {
sections,
content_rect,
font_size: config.font_size,
}
}
fn rearrange(sections: &mut [Section], config: &Config, metrics: &Metrics<'_>) -> Rect {
let mut y = 0.0;
let mut union: Option<Rect> = None;
for section in sections.iter_mut() {
let height = if config.char_array > 0 {
comb::rearrange_char_array(section, config, metrics)
} else {
section.lines.clear();
let measured = split::split_lines(section, config, metrics, true);
place::output_lines(section, config, metrics, measured)
};
section.rect = crate::geom::rect(
crate::geom::left(height),
crate::geom::bottom(height) + y,
crate::geom::right(height),
crate::geom::top(height) + y,
);
let x = crate::geom::left(height);
for word in &mut section.words {
word.x += x;
word.y += y;
}
for line in &mut section.lines {
line.x += x;
line.y += y;
}
y += crate::geom::height(height);
union = Some(match union {
None => section.rect,
Some(previous) => crate::geom::union(previous, section.rect),
});
}
union.unwrap_or(Rect::ZERO)
}
#[must_use]
pub(crate) fn word_width(
word: &Word,
config: &Config,
metrics: &Metrics<'_>,
font_size: f32,
) -> f32 {
let shown = config.sub_word.map_or(word.ch, |sub| sub as u32);
#[allow(clippy::cast_precision_loss)]
let width = (metrics.width)(shown) as f32;
width * font_size * FONT_SCALE + word.tail
}
#[must_use]
pub(crate) fn font_ascent(metrics: &Metrics<'_>, font_size: f32) -> f32 {
#[allow(clippy::cast_precision_loss)]
let ascent = metrics.ascent as f32;
ascent * font_size * FONT_SCALE
}
#[must_use]
pub(crate) fn font_descent(metrics: &Metrics<'_>, font_size: f32) -> f32 {
#[allow(clippy::cast_precision_loss)]
let descent = metrics.descent as f32;
descent * font_size * FONT_SCALE
}
#[cfg(test)]
pub(crate) mod stub {
use super::Metrics;
pub(crate) fn width(_: u32) -> i32 {
10
}
pub(crate) fn metrics() -> Metrics<'static> {
Metrics {
width: &width,
ascent: 10,
descent: -2,
}
}
}
#[cfg(test)]
mod tests {
use super::{Alignment, Config, split_sections};
#[test]
fn a_quadding_outside_the_defined_range_is_left_aligned() {
assert_eq!(Alignment::from_quadding(0), Alignment::Left);
assert_eq!(Alignment::from_quadding(1), Alignment::Center);
assert_eq!(Alignment::from_quadding(2), Alignment::Right);
assert_eq!(Alignment::from_quadding(3), Alignment::Left);
assert_eq!(Alignment::from_quadding(-1), Alignment::Left);
}
fn multi(text: &str) -> Vec<Vec<u32>> {
split_sections(
text,
&Config {
multi_line: true,
..Config::default()
},
)
}
#[test]
fn a_paired_line_break_counts_once_in_either_order() {
assert_eq!(multi("a\r\nb").len(), 2);
assert_eq!(multi("a\n\rb").len(), 2);
assert_eq!(multi("a\n\nb").len(), 3);
}
#[test]
fn a_break_in_a_single_line_field_produces_nothing_at_all() {
let single = split_sections("ab\ncd", &Config::default());
assert_eq!(single.len(), 1);
assert_eq!(single.first().map(Vec::len), Some(4));
}
#[test]
fn a_tab_becomes_a_space() {
let got = split_sections("a\tb", &Config::default());
assert_eq!(got.first().map(Vec::as_slice), Some(&[97, 32, 98][..]));
}
#[test]
fn the_character_limit_counts_line_breaks_too() {
let config = Config {
multi_line: true,
limit_char: 5,
..Config::default()
};
let got = split_sections("ab\ncd", &config);
assert_eq!(got.len(), 2);
assert_eq!(got.first().map(Vec::len), Some(2));
assert_eq!(got.get(1).map(Vec::len), Some(2));
let clipped = split_sections("ab\ncde", &config);
assert_eq!(clipped.get(1).map(Vec::len), Some(2));
}
#[test]
fn a_comb_cell_count_caps_the_characters_as_a_limit_does() {
let config = Config {
char_array: 3,
..Config::default()
};
assert_eq!(
split_sections("abcdef", &config).first().map(Vec::len),
Some(3)
);
}
}