cranpose_ui/text/line_box.rs
1//! Where a line of text sits inside the height it was given.
2//!
3//! [`LineHeightStyle`] has been a declared-but-unread field on
4//! [`ParagraphStyle`](crate::text::ParagraphStyle) since it was added: nothing
5//! outside `merge` and the hash keys ever looked at it, and the rasterizer's
6//! line box was a fixed rule — the box is exactly the requested line height,
7//! and the leading is split evenly above and below. That rule is not what
8//! Android does, and the difference is visible.
9//!
10//! AOSP's `StaticLayout` differs in four ways that each move a glyph row:
11//!
12//! - the font's ascent and descent are **whole pixels**, rounded the way
13//! `Paint.getFontMetricsInt()` rounds them, and the line is built from that
14//! pair rather than from the float metrics;
15//! - the line advance is a **whole pixel**, `ceil`ed, not a float;
16//! - a requested line height **shorter than the font's own ascent + descent
17//! does not shrink the line** — the font wins, which is why a 16sp/18sp
18//! style lays out in 38px rather than 36px at density 2;
19//! - the leading is split with the **odd pixel below** the baseline, not above.
20//!
21//! [`line_box`] implements that, and it implements it **only when the caller
22//! asked for it**. A style whose `line_height_style` is `None` gets exactly the
23//! arithmetic it got before, bit for bit. That is deliberate: the rule changes
24//! where every glyph lands, and it is not a change to make silently on behalf
25//! of text that never asked. The Wear widgets ask for it through
26//! [`WearTextStyle`](crate::widgets::wear::WearTextStyle), and a
27//! [`DrawScope`](cranpose_ui_graphics::DrawScope) run asks for it through
28//! [`TextStyle::with_line_height_style`](cranpose_ui_graphics::TextStyle::with_line_height_style)
29//! — which is what lets a canvas and a `Text` on one screen agree.
30
31use crate::text::style::{
32 LineHeightAlignment, LineHeightMode, LineHeightStyle, LineHeightTrim, TextStyle,
33};
34
35/// A resolved line box: how tall the line is and where its baseline sits inside
36/// it, both measured down from the top of the box.
37#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct LineBox {
39 /// Baseline-to-baseline advance, and the height of a single-line block.
40 pub height: f32,
41 /// Distance from the top of the box down to the baseline.
42 pub baseline: f32,
43}
44
45/// The font's own vertical extent, in the same unit as the line height.
46///
47/// `ascent` and `descent` are both **positive distances** from the baseline,
48/// which is the sign convention AOSP states its rule in and the opposite of the
49/// one `ab_glyph` reports `descent` in.
50#[derive(Clone, Copy, Debug, PartialEq)]
51pub struct FontExtent {
52 pub ascent: f32,
53 pub descent: f32,
54 /// `hhea.lineGap`. Only read when a style asks for font padding.
55 pub line_gap: f32,
56}
57
58impl FontExtent {
59 pub fn new(ascent: f32, descent: f32, line_gap: f32) -> Self {
60 Self {
61 ascent,
62 descent,
63 line_gap,
64 }
65 }
66
67 /// Ascent plus descent — the height the font needs with no leading at all.
68 pub fn natural(self) -> f32 {
69 self.ascent + self.descent
70 }
71}
72
73/// The line box a style asks for, given the font's extent and the line height
74/// already resolved from the style's own units.
75///
76/// `asked` is the line height in the same unit as the extent. `grid` is how
77/// many device pixels there are to one of those units, and it is what every
78/// rounding in the AOSP rule is done against — pass `1.0` when the values are
79/// already device pixels, or the density when they are layout points. Getting
80/// it wrong does not shift a baseline by a fraction; it quantises the whole
81/// line box to the wrong step.
82pub fn line_box(style: &TextStyle, extent: FontExtent, asked: f32, grid: f32) -> LineBox {
83 let grid = if grid.is_finite() && grid > 0.0 {
84 grid
85 } else {
86 1.0
87 };
88 match style.paragraph_style.line_height_style {
89 None => unstyled_line_box(extent, asked, grid),
90 Some(line_height_style) => {
91 let padding = font_padding(style, extent);
92 aosp_line_box(line_height_style, extent, asked, padding, grid)
93 }
94 }
95}
96
97/// The rule for a style that names no line-height policy: the box is the
98/// requested height and the leading is split evenly.
99///
100/// The ceil is on the **device grid**, not on the caller's own unit, and that
101/// is the whole of the fix here. A measurer works in layout points and passes
102/// the density; the rasterizer works in device pixels and passes `1.0`. Ceiling
103/// in the caller's unit made those two disagree: one 12sp/16sp style came out
104/// `ceil(14.0625 dp) = 15 dp = 30 px` on the measuring side and
105/// `ceil(28.125 px) = 29 px` on the drawing side, and the glyph landed on a
106/// half pixel that then rounded down. Faces whose two ceils happened to agree
107/// were exact, which is why this hid for so long and why it showed on Credits
108/// and the title but never on Settings.
109///
110/// There is no ground truth for what an unstyled box *should* be — Compose
111/// always names a policy, so there is no platform behaviour to copy — and this
112/// change does not invent one. It moves the measurer onto the number the
113/// rasterizer already produces, because the rasterizer can only work in whole
114/// device pixels and is therefore the side that cannot be wrong about them.
115/// At `grid = 1.0` the arithmetic is unchanged bit for bit.
116fn unstyled_line_box(extent: FontExtent, asked: f32, grid: f32) -> LineBox {
117 let natural = (extent.natural() * grid).ceil() / grid;
118 LineBox {
119 height: asked,
120 baseline: extent.ascent + (asked - natural) * 0.5,
121 }
122}
123
124/// `includeFontPadding`'s share of the leading.
125///
126/// Android's font padding is the gap between the `hhea` metrics and the tighter
127/// typographic ones. `ab_glyph` reports one pair of metrics and the line gap
128/// separately, so the closest honest reading is the line gap: `Some(true)`
129/// spends it, `Some(false)` and `None` do not. Wear's `DefaultTextStyle` sets
130/// it to `false`, which is the case this module exists to serve.
131fn font_padding(style: &TextStyle, extent: FontExtent) -> f32 {
132 let asked = style
133 .paragraph_style
134 .platform_style
135 .and_then(|platform| platform.include_font_padding)
136 .unwrap_or(false);
137 if asked && extent.line_gap.is_finite() && extent.line_gap > 0.0 {
138 extent.line_gap
139 } else {
140 0.0
141 }
142}
143
144fn aosp_line_box(
145 style: LineHeightStyle,
146 extent: FontExtent,
147 asked: f32,
148 padding: f32,
149 grid: f32,
150) -> LineBox {
151 let up = |value: f32| (value * grid).ceil() / grid;
152 let down = |value: f32| (value * grid).floor() / grid;
153 // Android hands its layout `Paint.FontMetricsInt`, whose ascent and descent
154 // are whole pixels. Doing the leading split on unrounded metrics leaves a
155 // fractional remainder that the rounding below then spends in the wrong
156 // direction, and the baseline comes out under the ascent.
157 //
158 // The rounding is Skia's `SkScalarRoundToInt`, `floor(x + 0.5)`, applied to
159 // the SIGNED metric — so it is round-half-up on the descent and round-half-
160 // down on the ascent, which is a positive distance here and therefore the
161 // negative one there. It is NOT rounding away from the baseline. Measured
162 // on a Wear OS 5 emulator (API 34, density 2) by asking the platform:
163 // `Paint.getFontMetricsInt()` for Roboto at eight text sizes, against
164 // `getFontMetrics()`'s floats.
165 //
166 // size px float ascent / descent FontMetricsInt ascent / descent
167 // 24.0 -22.265625 / 5.859375 -22 / 6
168 // 26.0 -24.121094 / 6.3476562 -24 / 6
169 // 30.0 -27.832031 / 7.3242188 -28 / 7
170 // 32.0 -29.6875 / 7.8125 -30 / 8
171 // 37.2 -34.51172 / 9.082031 -35 / 9
172 // 38.72 -35.921875 / 9.453125 -36 / 9
173 //
174 // Five of those six fractions are under a half on at least one metric, and
175 // a `ceil` gets every one of them wrong. `StaticLayout`'s own line height
176 // with `includeFontPadding=false` was `(-ascent) + descent` of the rounded
177 // pair in all eight sizes, so this is the pair the layout is built from.
178 //
179 // The size that made it visible is 19sp at density 2, a 38px font: the
180 // rounded pair is 35 and 9 for a 44px line, and `ceil` makes it 36 and 10
181 // for a 46px one. The shipping Compose build lays that line out in 44.
182 let round = |value: f32| ((value * grid) + 0.5).floor() / grid;
183 let ascent = -round(-extent.ascent.max(0.0));
184 let descent = round(extent.descent.max(0.0));
185 // Font padding widens the font's own demand, so it survives `Tight` and it
186 // is what a shorter requested line height has to beat.
187 let above_padding = down(padding * 0.5);
188 let below_padding = padding - above_padding;
189 let natural = up(ascent + descent + padding);
190 let asked = if asked.is_finite() {
191 up(asked)
192 } else {
193 natural
194 };
195
196 let height = match style.mode {
197 // The requested height, whatever the font wants.
198 LineHeightMode::Fixed => asked.max(1.0),
199 // The font's demand is a floor. This is what Android does and what the
200 // Wear text styles measure as.
201 LineHeightMode::Minimum => asked.max(natural).max(1.0),
202 // The font's demand, whatever was requested.
203 LineHeightMode::Tight => natural.max(1.0),
204 };
205
206 // Leading is whatever the box has over the font's own extent. It can be
207 // negative under `Fixed`, and then the glyphs simply overflow their box —
208 // which is also what Android does.
209 let leading = height - (ascent + descent + padding);
210 let (mut above, mut below) = match style.alignment {
211 // All of it below: the text sits at the top of its box.
212 LineHeightAlignment::Top => (0.0, leading),
213 // All of it above.
214 LineHeightAlignment::Bottom => (leading, 0.0),
215 // Split evenly, with the odd whole unit going BELOW the baseline.
216 // Splitting the other way is a one-pixel error on every line whose
217 // leading is odd, which at density 2 is every other line height.
218 LineHeightAlignment::Center => {
219 let below = up(leading * 0.5);
220 (leading - below, below)
221 }
222 // Split in the font's own ascent-to-descent ratio.
223 LineHeightAlignment::Proportional => {
224 let total = ascent + descent;
225 if total > 0.0 {
226 let above = leading * (ascent / total);
227 (above, leading - above)
228 } else {
229 (leading * 0.5, leading * 0.5)
230 }
231 }
232 };
233 above += above_padding;
234 below += below_padding;
235
236 // Trimming removes the leading the alignment just handed out, on whichever
237 // edge of the block it lands. A Wear row is one line, so both edges belong
238 // to the same box; a multi-line paragraph needs per-line boxes, which
239 // `TextMetrics` does not carry (its height is a flat `lines * advance`),
240 // so trimming there is not yet expressible and this stays a single-line
241 // rule.
242 let (trim_above, trim_below) = match style.trim {
243 LineHeightTrim::None => (false, false),
244 LineHeightTrim::FirstLineTop => (true, false),
245 LineHeightTrim::LastLineBottom => (false, true),
246 LineHeightTrim::Both => (true, true),
247 };
248 let mut height = height;
249 if trim_above {
250 height -= above;
251 above = 0.0;
252 }
253 if trim_below {
254 height -= below;
255 }
256
257 LineBox {
258 height: height.max(1.0),
259 baseline: above + ascent,
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use crate::text::style::{ParagraphStyle, PlatformParagraphStyle};
267 use crate::text::TextUnit;
268
269 /// Roboto at 16sp on a density-2 watch: 32px of glyph, `hhea` ascender
270 /// 1900/2048 and descender 500/2048, so 29.69px above the baseline and
271 /// 7.81px below — 37.5px of natural extent against a 36px line height.
272 fn roboto_16sp() -> FontExtent {
273 FontExtent::new(32.0 * 1900.0 / 2048.0, 32.0 * 500.0 / 2048.0, 0.0)
274 }
275
276 fn styled(line_height_px: f32, line_height_style: Option<LineHeightStyle>) -> TextStyle {
277 TextStyle {
278 paragraph_style: ParagraphStyle {
279 line_height: TextUnit::Sp(line_height_px),
280 line_height_style,
281 ..ParagraphStyle::default()
282 },
283 ..TextStyle::default()
284 }
285 }
286
287 fn wear() -> LineHeightStyle {
288 LineHeightStyle {
289 alignment: LineHeightAlignment::Center,
290 trim: LineHeightTrim::None,
291 mode: LineHeightMode::Minimum,
292 }
293 }
294
295 #[test]
296 fn a_style_that_asks_for_nothing_gets_exactly_what_it_got_before() {
297 let extent = roboto_16sp();
298 let plain = line_box(&styled(36.0, None), extent, 36.0, 1.0);
299 assert_eq!(plain.height, 36.0);
300 let natural = extent.natural().ceil();
301 assert_eq!(plain.baseline, extent.ascent + (36.0 - natural) * 0.5);
302 }
303
304 #[test]
305 fn an_unstyled_box_is_the_same_box_measured_in_points_or_in_pixels() {
306 // The measurer works in layout points and passes the density; the
307 // rasterizer works in device pixels and passes 1.0. A style naming no
308 // line-height policy has to come out the same physical box either way,
309 // or a run measures one height and draws another. It did not: at
310 // density 2 the 12sp/16sp case below was 15dp = 30px measured and 29px
311 // drawn, and the glyph landed on a half pixel.
312 //
313 // The style here is deliberately the unstyled one -- `None` -- because
314 // the AOSP branch has always taken the grid and was never adrift.
315 let density = 2.0;
316 for glyph_px in [24.0f32, 26.0, 28.125, 30.0, 32.0, 37.2, 38.72] {
317 let ascent_px = glyph_px * 1900.0 / 2048.0;
318 let descent_px = glyph_px * 500.0 / 2048.0;
319 let asked_px = (glyph_px * 4.0 / 3.0).round();
320
321 let in_pixels = line_box(
322 &styled(asked_px, None),
323 FontExtent::new(ascent_px, descent_px, 0.0),
324 asked_px,
325 1.0,
326 );
327 let in_points = line_box(
328 &styled(asked_px / density, None),
329 FontExtent::new(ascent_px / density, descent_px / density, 0.0),
330 asked_px / density,
331 density,
332 );
333
334 assert!(
335 (in_points.height * density - in_pixels.height).abs() < 1e-4,
336 "{glyph_px}px: height {} in points against {} in pixels",
337 in_points.height * density,
338 in_pixels.height
339 );
340 assert!(
341 (in_points.baseline * density - in_pixels.baseline).abs() < 1e-4,
342 "{glyph_px}px: baseline {} in points against {} in pixels",
343 in_points.baseline * density,
344 in_pixels.baseline
345 );
346 }
347 }
348
349 #[test]
350 fn title_medium_overflows_its_own_line_height_and_the_font_wins() {
351 // The measured case: 16sp/18sp titleMedium lays out in 38px, not 36px.
352 let box_ = line_box(&styled(36.0, Some(wear())), roboto_16sp(), 36.0, 1.0);
353 assert_eq!(box_.height, 38.0);
354 }
355
356 #[test]
357 fn a_line_height_the_font_fits_inside_is_honoured_as_asked() {
358 // 15sp labelMedium: 30px glyph, 35.16px natural, 36px asked. The ask
359 // wins, and this is the case that is NOT visible on these screens.
360 let extent = FontExtent::new(30.0 * 1900.0 / 2048.0, 30.0 * 500.0 / 2048.0, 0.0);
361 let box_ = line_box(&styled(36.0, Some(wear())), extent, 36.0, 1.0);
362 assert_eq!(box_.height, 36.0);
363 // Whole-pixel metrics are 28 above and 7 below, so one pixel of leading
364 // is left and it goes below the baseline.
365 assert_eq!(box_.baseline, 28.0);
366 }
367
368 #[test]
369 fn the_font_metrics_are_rounded_the_way_the_platform_rounds_them() {
370 // `Paint.getFontMetricsInt()` is `SkScalarRoundToInt` per metric, which
371 // is `floor(x + 0.5)` on the SIGNED value. Rounding away from the
372 // baseline instead makes a 19sp line at density 2 two pixels too tall,
373 // which moved the whole title screen. Every pair here was read off a
374 // Wear OS 5 emulator at density 2; the extent is the font's own float
375 // metrics at that size, and the assertion is the pair the platform
376 // reported.
377 for (size_px, ascent_px, descent_px) in [
378 (24.0_f32, 22.0_f32, 6.0_f32),
379 (26.0, 24.0, 6.0),
380 (30.0, 28.0, 7.0),
381 (32.0, 30.0, 8.0),
382 (37.2, 35.0, 9.0),
383 (38.72, 36.0, 9.0),
384 (43.76, 41.0, 11.0),
385 // Not in the probe's list, but the size the defect showed up at:
386 // 19sp at density 2, which Compose lays out in 44px and a `ceil`
387 // would lay out in 46.
388 (38.0, 35.0, 9.0),
389 ] {
390 let extent = FontExtent::new(size_px * 1900.0 / 2048.0, size_px * 500.0 / 2048.0, 0.0);
391 // `Tight` reports the font's own demand, which is exactly the
392 // rounded pair the platform's layout is built from.
393 let tight = line_box(
394 &styled(
395 0.0,
396 Some(LineHeightStyle {
397 mode: LineHeightMode::Tight,
398 ..wear()
399 }),
400 ),
401 extent,
402 0.0,
403 1.0,
404 );
405 assert_eq!(
406 (tight.baseline, tight.height - tight.baseline),
407 (ascent_px, descent_px),
408 "{size_px}px",
409 );
410 }
411 }
412
413 #[test]
414 fn the_wear_type_scale_lays_out_in_the_boxes_the_platform_gives_it() {
415 // The composed widgets and the drawn canvas both resolve through here,
416 // so these are the numbers a Wear screen is built out of. Density 2,
417 // device pixels, at the two text-size settings that matter: 1.0, where
418 // an sp is a plain doubling, and the setting Wear calls large, where
419 // Android 14's table gives 13sp -> 32.72px, 15sp -> 37.2px,
420 // 16sp -> 38.72px and 18sp -> 41.76px.
421 for (name, size_px, line_height_px, height, baseline) in [
422 ("titleMedium 1.0", 32.0_f32, 36.0_f32, 38.0_f32, 30.0_f32),
423 ("labelMedium 1.0", 30.0, 36.0, 36.0, 28.0),
424 ("labelSmall 1.0", 26.0, 32.0, 32.0, 25.0),
425 ("titleMedium 1.24", 38.72, 41.76, 45.0, 36.0),
426 ("labelMedium 1.24", 37.2, 41.76, 44.0, 35.0),
427 ("labelSmall 1.24", 32.72, 38.72, 39.0, 30.0),
428 ] {
429 let extent = FontExtent::new(size_px * 1900.0 / 2048.0, size_px * 500.0 / 2048.0, 0.0);
430 let resolved = line_box(
431 &styled(line_height_px, Some(wear())),
432 extent,
433 line_height_px,
434 1.0,
435 );
436 assert_eq!(
437 (resolved.height, resolved.baseline),
438 (height, baseline),
439 "{name}",
440 );
441 }
442 }
443
444 #[test]
445 fn the_odd_unit_of_leading_goes_below_the_baseline_not_above() {
446 // 3 units of leading over a whole-numbered font: 1 above, 2 below.
447 let extent = FontExtent::new(20.0, 10.0, 0.0);
448 let box_ = line_box(&styled(33.0, Some(wear())), extent, 33.0, 1.0);
449 assert_eq!(box_.height, 33.0);
450 assert_eq!(box_.baseline, 21.0);
451 // Splitting the other way would put the baseline at 21.5 and every
452 // glyph row half a pixel out.
453 assert_ne!(box_.baseline, 20.0 + 1.5);
454 }
455
456 #[test]
457 fn a_line_height_is_a_whole_number_of_pixels() {
458 let extent = FontExtent::new(20.0, 10.0, 0.0);
459 let box_ = line_box(&styled(33.4, Some(wear())), extent, 33.4, 1.0);
460 assert_eq!(box_.height, 34.0);
461 }
462
463 #[test]
464 fn top_alignment_puts_the_glyphs_at_the_top_and_bottom_at_the_bottom() {
465 let extent = FontExtent::new(20.0, 10.0, 0.0);
466 let top = line_box(
467 &styled(
468 40.0,
469 Some(LineHeightStyle {
470 alignment: LineHeightAlignment::Top,
471 ..wear()
472 }),
473 ),
474 extent,
475 40.0,
476 1.0,
477 );
478 assert_eq!(top.baseline, 20.0);
479 let bottom = line_box(
480 &styled(
481 40.0,
482 Some(LineHeightStyle {
483 alignment: LineHeightAlignment::Bottom,
484 ..wear()
485 }),
486 ),
487 extent,
488 40.0,
489 1.0,
490 );
491 assert_eq!(bottom.baseline, 30.0);
492 assert_eq!(bottom.height - bottom.baseline, extent.descent);
493 }
494
495 #[test]
496 fn proportional_alignment_splits_the_leading_the_way_the_font_is_split() {
497 let extent = FontExtent::new(20.0, 10.0, 0.0);
498 let style = LineHeightStyle {
499 alignment: LineHeightAlignment::Proportional,
500 ..wear()
501 };
502 let box_ = line_box(&styled(60.0, Some(style)), extent, 60.0, 1.0);
503 // 30 of leading split 2:1, so 20 above.
504 assert_eq!(box_.baseline, 40.0);
505 }
506
507 #[test]
508 fn a_fixed_line_height_lets_the_font_overflow_and_tight_ignores_the_ask() {
509 let extent = roboto_16sp();
510 let fixed = line_box(
511 &styled(
512 36.0,
513 Some(LineHeightStyle {
514 mode: LineHeightMode::Fixed,
515 ..wear()
516 }),
517 ),
518 extent,
519 36.0,
520 1.0,
521 );
522 assert_eq!(
523 fixed.height, 36.0,
524 "the ask wins even though the font needs 38"
525 );
526 let tight = line_box(
527 &styled(
528 80.0,
529 Some(LineHeightStyle {
530 mode: LineHeightMode::Tight,
531 ..wear()
532 }),
533 ),
534 extent,
535 80.0,
536 1.0,
537 );
538 // Whole-pixel metrics: 30 above the baseline and 8 below.
539 assert_eq!(tight.height, 38.0);
540 assert_eq!(tight.baseline, 30.0);
541 }
542
543 #[test]
544 fn trimming_removes_the_leading_on_the_edge_it_names() {
545 let extent = FontExtent::new(20.0, 10.0, 0.0);
546 let both = line_box(
547 &styled(
548 40.0,
549 Some(LineHeightStyle {
550 trim: LineHeightTrim::Both,
551 ..wear()
552 }),
553 ),
554 extent,
555 40.0,
556 1.0,
557 );
558 assert_eq!(both.height, 30.0);
559 assert_eq!(both.baseline, 20.0);
560
561 let top_only = line_box(
562 &styled(
563 40.0,
564 Some(LineHeightStyle {
565 trim: LineHeightTrim::FirstLineTop,
566 ..wear()
567 }),
568 ),
569 extent,
570 40.0,
571 1.0,
572 );
573 // 10 of leading, 5 above and 5 below; only the top is removed.
574 assert_eq!(top_only.height, 35.0);
575 assert_eq!(top_only.baseline, 20.0);
576 }
577
578 #[test]
579 fn font_padding_is_only_spent_when_a_style_asks_for_it() {
580 let extent = FontExtent::new(20.0, 10.0, 4.0);
581 let without = line_box(&styled(30.0, Some(wear())), extent, 30.0, 1.0);
582 assert_eq!(without.height, 30.0);
583 assert_eq!(without.baseline, 20.0);
584
585 let padded = TextStyle {
586 paragraph_style: ParagraphStyle {
587 line_height: TextUnit::Sp(30.0),
588 line_height_style: Some(wear()),
589 platform_style: Some(PlatformParagraphStyle {
590 include_font_padding: Some(true),
591 shaping: None,
592 }),
593 ..ParagraphStyle::default()
594 },
595 ..TextStyle::default()
596 };
597 let with = line_box(&padded, extent, 30.0, 1.0);
598 assert_eq!(with.height, 34.0, "the line gap widens the font's demand");
599 assert_eq!(with.baseline, 22.0, "and half of it sits above the ascent");
600 }
601
602 #[test]
603 fn a_nonsense_line_height_falls_back_to_the_font_rather_than_producing_nan() {
604 let extent = FontExtent::new(20.0, 10.0, 0.0);
605 let box_ = line_box(&styled(30.0, Some(wear())), extent, f32::NAN, 1.0);
606 assert_eq!(box_.height, 30.0);
607 assert!(box_.baseline.is_finite());
608 }
609}