use crate::Rhythm;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RhythmLineMetrics {
ascent: f32,
descent: f32,
line_rhythms: u32,
grid: Rhythm,
}
impl RhythmLineMetrics {
#[inline]
pub fn new(ascent: f32, descent: f32, line_rhythms: u32, grid: Rhythm) -> Self {
assert!(
ascent.is_finite() && ascent >= 0.0,
"ascent must be finite and non-negative"
);
assert!(
descent.is_finite() && descent >= 0.0,
"descent must be finite and non-negative"
);
assert!(line_rhythms > 0, "line_rhythms must be greater than zero");
Self {
ascent,
descent,
line_rhythms,
grid,
}
}
pub fn at_least(ascent: f32, descent: f32, line_rhythms: u32, grid: Rhythm) -> Self {
let line = Self::new(ascent, descent, line_rhythms, grid);
Self {
line_rhythms: line_rhythms.max(line.min_line_rhythms()),
..line
}
}
pub fn covering(metrics: &[RhythmLineMetrics], grid: Rhythm) -> Self {
assert!(
!metrics.is_empty(),
"a covering line box must cover at least one line"
);
let mut ascent = 0.0;
let mut descent = 0.0;
let mut line_rhythms = 1;
for line in metrics {
assert_eq!(
line.grid, grid,
"every covered line must be built on the covering grid"
);
ascent = f32::max(ascent, line.ascent);
descent = f32::max(descent, line.descent);
line_rhythms = line_rhythms.max(line.line_rhythms);
}
Self::at_least(ascent, descent, line_rhythms, grid)
}
#[inline]
pub const fn ascent(&self) -> f32 {
self.ascent
}
#[inline]
pub const fn descent(&self) -> f32 {
self.descent
}
#[inline]
pub const fn line_rhythms(&self) -> u32 {
self.line_rhythms
}
#[inline]
pub const fn grid(&self) -> Rhythm {
self.grid
}
#[inline]
pub fn line_height(&self) -> f32 {
self.grid.size() * self.line_rhythms as f32
}
#[inline]
pub fn half_leading(&self) -> f32 {
(self.line_height() - self.ascent - self.descent) / 2.0
}
#[inline]
pub fn baseline_above(&self) -> f32 {
self.half_leading() + self.ascent
}
#[inline]
pub fn baseline_below(&self) -> f32 {
self.half_leading() + self.descent
}
#[inline]
pub fn paint_origin_for(&self, target_baseline: f32) -> f32 {
target_baseline - self.baseline_above()
}
#[inline]
pub fn min_line_rhythms(&self) -> u32 {
let rows = self.minimum_line_rows();
assert!(
rows <= f64::from(u32::MAX),
"minimum line rhythm count exceeds u32"
);
rows as u32
}
#[inline]
pub fn overflows_line_box(&self) -> bool {
self.minimum_line_rows() > f64::from(self.line_rhythms)
}
#[inline]
fn minimum_line_rows(&self) -> f64 {
let envelope = f64::from(self.ascent) + f64::from(self.descent);
let grid_size = f64::from(self.grid.size());
let rows = envelope / grid_size;
let nearest = rows.round();
let nearest_height = grid_size * nearest;
let tolerance = envelope * f64::from(f32::EPSILON) * 8.0;
let snapped = if (envelope - nearest_height).abs() <= tolerance {
nearest
} else {
rows.ceil()
};
snapped.max(1.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RhythmBlockMetrics {
line: RhythmLineMetrics,
top: i32,
bottom: i32,
ink_ascent: Option<f32>,
}
impl RhythmBlockMetrics {
pub const fn new(line: RhythmLineMetrics, top: i32, bottom: i32) -> Self {
Self {
line,
top,
bottom,
ink_ascent: None,
}
}
pub fn ink_anchored(line: RhythmLineMetrics, ink_ascent: f32, top: i32, bottom: i32) -> Self {
assert!(
ink_ascent.is_finite() && ink_ascent > 0.0,
"ink anchor ascent must be finite and greater than zero"
);
Self {
line,
top,
bottom,
ink_ascent: Some(ink_ascent),
}
}
#[inline]
pub const fn line(&self) -> RhythmLineMetrics {
self.line
}
#[inline]
pub const fn top_rhythms(&self) -> i32 {
self.top
}
#[inline]
pub const fn bottom_rhythms(&self) -> i32 {
self.bottom
}
fn grid(&self) -> Rhythm {
self.line.grid()
}
#[inline]
pub fn opening(&self) -> f32 {
let above = self.line.baseline_above();
match self.ink_ascent {
None => self.grid().height(self.top) - above,
Some(ink_ascent) => self.grid().height(self.top) - (above - ink_ascent),
}
}
#[inline]
pub fn closing(&self) -> f32 {
match self.ink_ascent {
None => self.grid().height(self.bottom) - self.line.baseline_below(),
Some(ink_ascent) => {
self.grid().height(self.bottom) + (self.line.baseline_above() - ink_ascent)
}
}
}
#[inline]
pub fn first_baseline(&self) -> f32 {
self.baseline_at_row(i64::from(self.top))
}
#[inline]
pub fn height(&self, lines: u32) -> f32 {
self.first_height(lines) + self.closing()
}
#[inline]
pub fn first_height(&self, lines: u32) -> f32 {
self.opening() + self.middle_height(lines)
}
#[inline]
pub fn middle_height(&self, lines: u32) -> f32 {
assert!(lines > 0, "a fragment must contain at least one line");
self.line.line_height() * lines as f32
}
#[inline]
pub fn last_height(&self, lines: u32) -> f32 {
self.middle_height(lines) + self.closing()
}
#[inline]
pub fn rows(&self, lines: u32) -> i32 {
checked_rows(
i128::from(self.top) + self.spanned_rows(lines) + i128::from(self.bottom)
- self.trailing_rows(),
)
}
#[inline]
pub fn first_rows(&self, lines: u32) -> i32 {
checked_rows(i128::from(self.top) + self.spanned_rows(lines))
}
#[inline]
pub fn middle_rows(&self, lines: u32) -> i32 {
checked_rows(self.spanned_rows(lines))
}
#[inline]
pub fn baseline_at_row(&self, row: i64) -> f32 {
self.grid().size() * row as f32 + self.ink_ascent.unwrap_or(0.0)
}
#[inline]
pub fn last_rows(&self, lines: u32) -> i32 {
checked_rows(self.spanned_rows(lines) + i128::from(self.bottom) - self.trailing_rows())
}
#[inline]
fn trailing_rows(&self) -> i128 {
if self.ink_ascent.is_some() {
0
} else {
i128::from(self.line.line_rhythms())
}
}
#[inline]
fn spanned_rows(&self, lines: u32) -> i128 {
assert!(lines > 0, "a fragment must contain at least one line");
i128::from(lines) * i128::from(self.line.line_rhythms())
}
}
#[inline]
fn checked_rows(rows: i128) -> i32 {
i32::try_from(rows).expect("block row count exceeds i32")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FontRhythm;
const GRID: Rhythm = Rhythm::new(8.0);
fn georgia_line() -> RhythmLineMetrics {
RhythmLineMetrics::new(16.0 * 1878.0 / 2048.0, 16.0 * 449.0 / 2048.0, 3, GRID)
}
fn georgia_font() -> FontRhythm {
FontRhythm::from_platform_metrics(
16.0,
3,
16.0 * 1878.0 / 2048.0,
-16.0 * 449.0 / 2048.0,
16.0 * 1419.0 / 2048.0,
16.0 * 986.0 / 2048.0,
)
}
#[test]
fn paint_origin_solves_the_renderer_baseline_formula() {
let line = georgia_line();
let target = GRID.height(5);
let origin = line.paint_origin_for(target);
let painted =
origin + (line.line_height() - line.ascent() - line.descent()) / 2.0 + line.ascent();
assert!((painted - target).abs() < 1e-4);
}
#[test]
fn min_line_rhythms_covers_the_metric_envelope() {
let line = georgia_line();
assert_eq!(line.min_line_rhythms(), 3);
assert!(!line.overflows_line_box());
let tall = RhythmLineMetrics::new(20.0, 6.0, 3, GRID);
assert_eq!(tall.min_line_rhythms(), 4);
assert!(tall.overflows_line_box());
assert!(tall.half_leading() < 0.0);
let grown = RhythmLineMetrics::new(20.0, 6.0, 4, GRID);
assert!(!grown.overflows_line_box());
let exact = RhythmLineMetrics::new(20.0, 4.0, 3, GRID);
assert_eq!(exact.min_line_rhythms(), 3);
assert!(!exact.overflows_line_box());
let noisy = RhythmLineMetrics::new(20.000_001, 4.0, 3, GRID);
assert_eq!(noisy.min_line_rhythms(), 3);
}
#[test]
fn at_least_grows_only_an_overflowing_line_box() {
let grown = RhythmLineMetrics::at_least(20.0, 6.0, 3, GRID);
assert_eq!(grown.line_rhythms(), 4);
assert!(!grown.overflows_line_box());
let roomy = RhythmLineMetrics::at_least(20.0, 6.0, 6, GRID);
assert_eq!(roomy.line_rhythms(), 6);
assert_eq!(
RhythmLineMetrics::at_least(georgia_line().ascent(), georgia_line().descent(), 3, GRID,),
georgia_line()
);
}
#[test]
fn line_height_preserves_the_full_u32_count() {
let count = i32::MAX as u32 + 1;
let line = RhythmLineMetrics::new(1.0, 1.0, count, GRID);
assert_eq!(line.line_height(), GRID.size() * count as f32);
assert!(line.line_height().is_sign_positive());
}
#[test]
fn overflow_is_reported_when_no_u32_line_count_can_fit_the_metric_envelope() {
let tiny_grid = Rhythm::new(f32::MIN_POSITIVE);
let line = RhythmLineMetrics::new(1.0, 0.0, u32::MAX, tiny_grid);
assert!(line.overflows_line_box());
}
#[test]
#[should_panic(expected = "minimum line rhythm count exceeds u32")]
fn minimum_line_count_rejects_a_value_outside_the_return_type() {
let tiny_grid = Rhythm::new(f32::MIN_POSITIVE);
let line = RhythmLineMetrics::new(1.0, 0.0, 1, tiny_grid);
let _ = line.min_line_rhythms();
}
#[test]
fn empty_shaped_lines_are_representable() {
let empty = RhythmLineMetrics::new(0.0, 0.0, 3, GRID);
assert_eq!(empty.min_line_rhythms(), 1);
assert!(!empty.overflows_line_box());
assert!((empty.baseline_above() - GRID.height(3) / 2.0).abs() < 1e-6);
}
#[test]
#[should_panic(expected = "ascent must be finite and non-negative")]
fn line_metrics_reject_a_negative_ascent() {
let _ = RhythmLineMetrics::new(-1.0, 3.0, 3, GRID);
}
#[test]
#[should_panic(expected = "line_rhythms must be greater than zero")]
fn line_metrics_reject_zero_rhythms() {
let _ = RhythmLineMetrics::new(15.0, 4.0, 0, GRID);
}
#[test]
fn baseline_anchors_match_the_style_level_spacing() {
let line = georgia_line();
let block = RhythmBlockMetrics::new(line, 3, 1);
let font = georgia_font();
assert!((block.first_baseline() - GRID.height(3)).abs() < 1e-6);
let first_origin = line.paint_origin_for(block.first_baseline());
assert!((first_origin - font.baseline_top(GRID, 3)).abs() < 1e-6);
let closing = GRID.height(block.rows(1)) - (first_origin + line.line_height());
assert!((closing - font.baseline_bottom(GRID, 1)).abs() < 1e-6);
}
#[test]
fn cap_anchors_match_the_style_level_spacing() {
let font = georgia_font();
let cap_height = font.cap_height().unwrap();
let block = RhythmBlockMetrics::ink_anchored(georgia_line(), cap_height, 3, 0);
assert!((block.opening() - font.cap_top(GRID, 3).unwrap()).abs() < 1e-6);
assert!((block.closing() - font.cap_bottom(GRID, 0).unwrap()).abs() < 1e-6);
assert!((block.first_baseline() - (GRID.height(3) + cap_height)).abs() < 1e-6);
}
#[test]
fn rows_is_exact_integer_arithmetic() {
let block = RhythmBlockMetrics::new(georgia_line(), 3, 1);
for lines in [1, 2, 5, 40] {
assert_eq!(block.rows(lines), 3 + 1 + (lines as i32 - 1) * 3);
assert!((block.height(lines) - GRID.height(block.rows(lines))).abs() < 1e-3);
}
let ink_block = RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, 3, 0);
for lines in [1, 2, 5] {
assert_eq!(ink_block.rows(lines), 3 + lines as i32 * 3);
assert!((ink_block.height(lines) - GRID.height(ink_block.rows(lines))).abs() < 1e-3);
}
}
#[test]
fn fragment_heights_sum_to_the_whole_block() {
for block in [
RhythmBlockMetrics::new(georgia_line(), 3, 1),
RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, 3, 0),
] {
let split = block.first_height(2) + block.middle_height(4) + block.last_height(3);
assert!((split - block.height(9)).abs() < 1e-3);
}
}
#[test]
#[should_panic(expected = "block row count exceeds i32")]
fn rows_reject_a_count_outside_the_return_type() {
let line = RhythmLineMetrics::new(1.0, 1.0, u32::MAX, GRID);
let _ = RhythmBlockMetrics::new(line, 0, 0).rows(2);
}
#[test]
fn fragment_rows_partition_the_block_exactly() {
for (block, ink_anchored) in [
(RhythmBlockMetrics::new(georgia_line(), 3, 1), false),
(
RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, 3, 0),
true,
),
(RhythmBlockMetrics::new(georgia_line(), -2, 1), false),
(
RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, -2, -1),
true,
),
] {
let line_rhythms = block.line().line_rhythms() as i32;
assert_eq!(
block.first_rows(2) + block.middle_rows(4) + block.last_rows(3),
block.rows(9)
);
assert_eq!(block.first_rows(1) + block.last_rows(1), block.rows(2));
assert_eq!(block.first_rows(5), block.top_rhythms() + 5 * line_rhythms);
assert_eq!(block.middle_rows(4), 4 * line_rhythms);
let trailing = if ink_anchored { 0 } else { line_rhythms };
assert_eq!(
block.last_rows(3),
3 * line_rhythms + block.bottom_rhythms() - trailing
);
}
}
#[test]
fn fragment_row_cursor_reconstructs_every_continuation_origin() {
for block in [
RhythmBlockMetrics::new(georgia_line(), 3, 1),
RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, 3, 0),
RhythmBlockMetrics::new(georgia_line(), -2, 1),
RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, -2, -1),
] {
let line = block.line();
let mut rows = i64::from(block.first_rows(2));
let mut expected =
line.paint_origin_for(block.first_baseline()) + 2.0 * line.line_height();
for lines in [4, 1, 3] {
let origin = line.paint_origin_for(block.baseline_at_row(rows));
assert!((origin - expected).abs() < 1e-3);
rows = rows
.checked_add(i64::from(block.middle_rows(lines)))
.expect("test row cursor overflowed");
expected += line.line_height() * lines as f32;
}
let last_origin = line.paint_origin_for(block.baseline_at_row(rows));
assert!((last_origin - expected).abs() < 1e-3);
rows = rows
.checked_add(i64::from(block.last_rows(2)))
.expect("test row cursor overflowed");
assert_eq!(rows, i64::from(block.rows(2 + 4 + 1 + 3 + 2)));
}
}
#[test]
fn baseline_at_row_accepts_a_wide_cursor_without_i32_saturation() {
let block = RhythmBlockMetrics::new(georgia_line(), 3, 1);
let wide_row = i64::from(i32::MAX) * 4;
assert_eq!(
block.baseline_at_row(wide_row),
GRID.size() * wide_row as f32
);
}
#[test]
fn virtualizer_rebases_a_wide_multi_block_cursor_before_float_conversion() {
let preceding = RhythmBlockMetrics::new(georgia_line(), 3, 1);
let visible = RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, 3, 0);
let mut document_cursor = i64::from(i32::MAX) * 4;
document_cursor = document_cursor
.checked_add(i64::from(preceding.rows(40)))
.expect("test document cursor overflowed");
let visible_top = document_cursor;
document_cursor = document_cursor
.checked_add(i64::from(visible.rows(9)))
.expect("test document cursor overflowed");
assert!(document_cursor > i64::from(i32::MAX));
let viewport_origin = visible_top
.checked_sub(2)
.expect("test viewport origin overflowed");
let first_row = visible_top
.checked_add(i64::from(visible.top_rhythms()))
.and_then(|row| row.checked_sub(viewport_origin))
.expect("test first baseline cursor overflowed");
let continuation_row = visible_top
.checked_add(i64::from(visible.first_rows(2)))
.and_then(|row| row.checked_sub(viewport_origin))
.expect("test continuation cursor overflowed");
let first_baseline = visible.baseline_at_row(first_row);
let continuation_baseline = visible.baseline_at_row(continuation_row);
assert!((first_baseline - (GRID.height(2) + visible.first_baseline())).abs() < 1e-6);
assert!(
(continuation_baseline - first_baseline - 2.0 * visible.line().line_height()).abs()
< 1e-4
);
}
#[test]
fn baseline_at_row_preserves_ink_phase() {
let ink_block = RhythmBlockMetrics::ink_anchored(georgia_line(), 11.09, 3, 0);
let row = ink_block.first_rows(2);
let expected = ink_block.first_baseline() + 2.0 * ink_block.line().line_height();
assert!((ink_block.baseline_at_row(i64::from(row)) - expected).abs() < 1e-5);
assert!((ink_block.baseline_at_row(0) - 11.09).abs() < 1e-6);
}
#[test]
fn covering_fits_every_mixture_of_the_set_it_covers() {
let body = georgia_line();
let mono = RhythmLineMetrics::new(15.0, 6.0, 3, GRID);
let display = RhythmLineMetrics::new(28.8, 4.0, 3, GRID);
let covering = RhythmLineMetrics::covering(&[body, mono, display], GRID);
assert_eq!(covering.ascent(), 28.8);
assert_eq!(covering.descent(), 6.0);
assert_eq!(covering.line_rhythms(), 5); assert!(!covering.overflows_line_box());
for ascent in [body.ascent(), mono.ascent(), display.ascent()] {
for descent in [body.descent(), mono.descent(), display.descent()] {
let line = RhythmLineMetrics::new(ascent, descent, covering.line_rhythms(), GRID);
assert!(!line.overflows_line_box(), "{ascent} / {descent} overflows");
}
}
}
#[test]
fn covering_keeps_the_tallest_requested_line_height() {
let short = RhythmLineMetrics::new(14.67, 3.51, 3, GRID);
let tall = RhythmLineMetrics::new(10.0, 3.0, 5, GRID);
assert_eq!(
RhythmLineMetrics::covering(&[short, tall], GRID).line_rhythms(),
5
);
assert_eq!(RhythmLineMetrics::covering(&[short], GRID), short);
}
#[test]
#[should_panic(expected = "must cover at least one line")]
fn covering_rejects_an_empty_set() {
let _ = RhythmLineMetrics::covering(&[], GRID);
}
#[test]
#[should_panic(expected = "must be built on the covering grid")]
fn covering_rejects_a_line_from_another_grid() {
let other = RhythmLineMetrics::new(14.67, 3.51, 3, Rhythm::new(10.0));
let _ = RhythmLineMetrics::covering(&[georgia_line(), other], GRID);
}
#[test]
fn negative_anchor_counts_stay_meaningful_as_margins() {
let block = RhythmBlockMetrics::new(georgia_line(), 0, -1);
assert!(block.line().paint_origin_for(block.first_baseline()) < 0.0);
assert_eq!(block.rows(2), 0 - 1 + 3);
}
#[test]
#[should_panic(expected = "at least one line")]
fn fragments_reject_zero_lines() {
let _ = RhythmBlockMetrics::new(georgia_line(), 3, 1).rows(0);
}
#[test]
#[should_panic(expected = "ink anchor ascent must be finite and greater than zero")]
fn ink_anchored_blocks_reject_an_unusable_ascent() {
let _ = RhythmBlockMetrics::ink_anchored(georgia_line(), 0.0, 3, 0);
}
}