use cranpose_text::{
FontFamily, FontFamilyResolver, LayoutInput, ParagraphLengths, ParagraphStyle, TextAlign,
TextLayout, TextLayoutOptions, TextLayouter, TextOverflow,
};
use crate::fonts::{ROBOTO, span, typeface};
const SHORT: &str = "Short text";
const LONG: &str = "A longer text that wraps onto more than one line at this width";
fn layout(
resolver: &FontFamilyResolver,
text: &str,
(min_width, max_width): (f32, f32),
grid: f32,
options: TextLayoutOptions,
) -> TextLayout {
let family = FontFamily::Default;
let spans = [span(0..text.len(), &family, 400)];
TextLayouter::default().layout(
&LayoutInput {
text,
spans: &spans,
paragraph: &ParagraphStyle::default(),
lengths: ParagraphLengths {
grid,
..ParagraphLengths::default()
},
min_width,
max_width,
options,
},
resolver,
)
}
fn natural(laid: &TextLayout) -> f32 {
laid.line_right(0) - laid.line_left(0)
}
#[test]
fn text_that_fits_takes_its_own_width_rounded_up_to_whole_device_pixels() {
let resolver = FontFamilyResolver::new([typeface(ROBOTO)]);
for grid in [1.0, 2.0, 2.75] {
let laid = layout(
&resolver,
SHORT,
(0.0, 500.0),
grid,
TextLayoutOptions::default(),
);
let expected = (natural(&laid) * grid).ceil() / grid;
assert!(
(laid.width() - expected).abs() < 1e-4,
"{grid}: {}",
laid.width()
);
assert!(!laid.did_overflow_width());
}
}
#[test]
fn wrapped_text_takes_its_widest_line_and_a_min_width_widens_short_text() {
let resolver = FontFamilyResolver::new([typeface(ROBOTO)]);
let wrapped = layout(
&resolver,
LONG,
(0.0, 150.0),
1.0,
TextLayoutOptions::default(),
);
let widened = layout(
&resolver,
SHORT,
(300.0, 500.0),
1.0,
TextLayoutOptions::default(),
);
let fixed = layout(
&resolver,
SHORT,
(200.0, 200.0),
1.0,
TextLayoutOptions::default(),
);
assert!(wrapped.line_count() > 1);
let widest = (0..wrapped.line_count())
.map(|line| wrapped.line_right(line) - wrapped.line_left(line))
.fold(0.0, f32::max);
assert!(widest < 150.0);
assert_eq!(wrapped.width(), widest.ceil());
assert_eq!(widened.width(), 300.0);
assert_eq!(fixed.width(), 200.0);
}
#[test]
fn wrapped_text_keeps_its_width_at_the_max_widths_that_keep_its_lines() {
let resolver = FontFamilyResolver::new([typeface(ROBOTO)]);
let options = TextLayoutOptions::default();
for (text, laid_at) in [(LONG, 150.0), ("three", 4.0)] {
let mut laid = layout(&resolver, text, (0.0, laid_at), 1.0, options);
let (width, lines) = (laid.width(), laid.line_count());
let (low, high) = laid.max_widths_kept(0.0);
assert!(low <= laid_at && laid_at <= high, "{text}: {low}..{high}");
for max_width in [low, (low + high) / 2.0, high] {
assert!(laid.holds(0.0, max_width), "{text} at {max_width}");
laid.align(0.0, max_width, TextAlign::Start);
let fresh = layout(&resolver, text, (0.0, max_width), 1.0, options);
assert_eq!(
(laid.width(), fresh.width(), fresh.line_count()),
(width, width, lines),
"{text} at {max_width}"
);
}
let past = layout(&resolver, text, (0.0, high + 1.0), 1.0, options);
assert_ne!(
(past.line_count(), past.width().to_bits()),
(lines, width.to_bits()),
"{text}"
);
}
}
#[test]
fn unwrapped_clipped_text_keeps_its_own_width_past_the_max_and_overflows_it() {
let resolver = FontFamilyResolver::new([typeface(ROBOTO)]);
let options = TextLayoutOptions {
soft_wrap: false,
overflow: TextOverflow::Clip,
..TextLayoutOptions::default()
};
let laid = layout(&resolver, LONG, (0.0, 150.0), 1.0, options);
assert_eq!(laid.line_count(), 1);
assert_eq!(laid.width(), natural(&laid).ceil());
assert!(laid.did_overflow_width());
}
#[test]
fn the_lines_reach_spans_the_visible_text_of_its_lines_wherever_they_lie() {
let resolver = FontFamilyResolver::new([typeface(ROBOTO)]);
let spanned = |laid: &TextLayout| {
(0..laid.line_count()).fold((f32::INFINITY, f32::NEG_INFINITY), |(left, right), line| {
(
left.min(laid.line_left(line)),
right.max(laid.line_right(line)),
)
})
};
for (text, width) in [(SHORT, 300.0), (LONG, 150.0)] {
let mut laid = layout(
&resolver,
text,
(width, width),
1.0,
TextLayoutOptions::default(),
);
let at_indents = laid.lines_reach();
assert_eq!(at_indents, spanned(&laid), "{text}");
for align in [
TextAlign::Center,
TextAlign::End,
TextAlign::Justify,
TextAlign::Start,
] {
laid.align(width, width, align);
assert_eq!(laid.lines_reach(), spanned(&laid), "{text} {align:?}");
}
assert_eq!(laid.lines_reach(), at_indents, "{text}");
}
let mut short = layout(
&resolver,
SHORT,
(300.0, 300.0),
1.0,
TextLayoutOptions::default(),
);
let at_indents = short.lines_reach();
short.align(300.0, 300.0, TextAlign::Center);
assert_ne!(short.lines_reach(), at_indents, "centering moves the line");
}