1use crate::fonts::FontMetricsTable;
18
19#[derive(Debug, Clone, Copy, PartialEq, Default)]
23pub struct Strut {
24 pub above: f32,
26 pub below: f32,
28}
29
30impl Strut {
31 pub fn from_metrics(metrics: FontMetricsTable, size: f32, line_height: f32) -> Strut {
33 let upem = metrics.units_per_em as f32;
34 let ascent = metrics.ascender as f32 / upem * size;
35 let descent = -metrics.descender as f32 / upem * size;
36 let gap = metrics.line_gap as f32 / upem * size;
37 let half_leading = (line_height * size - (ascent + descent + gap)) / 2.0;
38 Strut {
39 above: ascent + half_leading + gap / 2.0,
40 below: descent + half_leading + gap / 2.0,
41 }
42 }
43
44 pub fn content(metrics: FontMetricsTable, size: f32) -> Strut {
50 let upem = metrics.units_per_em as f32;
51 Strut {
52 above: metrics.ascender as f32 / upem * size,
53 below: -metrics.descender as f32 / upem * size,
54 }
55 }
56
57 pub fn height(self) -> f32 {
59 self.above + self.below
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq)]
65pub struct LineBox {
66 pub height: f32,
68 pub baseline: f32,
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::fonts::{FontRegistry, bundled_registry};
77 use crate::lines::{LineLayout, ParagraphStyle, ShapedRun};
78
79 fn layout() -> LineLayout<'static> {
80 static REGISTRY: std::sync::OnceLock<FontRegistry> = std::sync::OnceLock::new();
81 let registry = REGISTRY.get_or_init(|| bundled_registry().expect("bundled font parses"));
82 LineLayout::new(registry)
83 }
84
85 fn body() -> ParagraphStyle {
88 static REGISTRY: std::sync::OnceLock<FontRegistry> = std::sync::OnceLock::new();
89 let registry = REGISTRY.get_or_init(|| bundled_registry().expect("bundled font parses"));
90 crate::style::defaults(&crate::content::Book::default(), registry)
91 .root()
92 .paragraph()
93 }
94
95 fn run(size: f32) -> ShapedRun {
96 ShapedRun {
97 font_id: 0,
98 size,
99 text: String::new(),
100 source: String::new(),
101 source_map: Vec::new(),
102 text_start: 0,
103 origin: None,
104 pseudo_element: None,
105 inline: None,
106 lead: 0.0,
107 trail: 0.0,
108 features: crate::fonts::Features::NONE,
109 color: crate::style::Color::BLACK,
110 decoration: crate::style::TextDecoration::NONE,
111 glyphs: Vec::new(),
112 advance: 0,
113 }
114 }
115
116 fn assert_close(got: f32, want: f32, what: &str) {
117 assert!((got - want).abs() < 1e-3, "{what}: got {got}, want {want}");
118 }
119
120 #[test]
125 fn strut_splits_the_leading_in_half() {
126 let strut = layout().strut(&body());
127 assert_close(strut.above, 11.5995, "above");
128 assert_close(strut.below, 3.8005, "below");
129 assert_close(strut.height(), 15.4, "height");
130 }
131
132 #[test]
136 fn box_totals_line_height_at_any_knob() {
137 let metrics = bundled_registry().unwrap().metrics(0).unwrap();
138 for size in [6.0, 11.0, 24.0] {
139 for line_height in [0.9, 1.305, 1.4, 2.0] {
140 let strut = Strut::from_metrics(metrics, size, line_height);
141 assert_close(
142 strut.height(),
143 line_height * size,
144 &format!("{size}pt at {line_height}"),
145 );
146 }
147 }
148 }
149
150 #[test]
153 fn strut_is_the_minimum_independent_of_content() {
154 let strut = layout().strut(&body());
155 let line_box = layout().line_box(&[run(6.0)], &body());
156 assert_close(line_box.baseline, strut.above, "baseline");
157 assert_close(line_box.height, strut.height(), "height");
158 }
159
160 #[test]
164 fn mixed_sizes_share_one_baseline() {
165 let line_box = layout().line_box(&[run(12.0), run(24.0)], &body());
166 assert_close(line_box.baseline, 25.308, "baseline");
167 assert_close(line_box.height, 33.6, "height");
168 let alone = layout().line_box(&[run(24.0)], &body());
169 assert_eq!(alone, line_box, "the 12pt run moved the line");
170 }
171
172 #[test]
174 fn empty_line_is_the_strut() {
175 let strut = layout().strut(&body());
176 let line_box = layout().line_box(&[], &body());
177 assert_close(line_box.baseline, strut.above, "baseline");
178 assert_close(line_box.height, strut.height(), "height");
179 }
180}