1use crate::text::style::{
26 LineHeightAlignment, LineHeightMode, LineHeightStyle, LineHeightTrim, TextStyle,
27};
28
29#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct LineBox {
33 pub height: f32,
35 pub baseline: f32,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq)]
45pub struct FontExtent {
46 pub ascent: f32,
47 pub descent: f32,
48 pub line_gap: f32,
50}
51
52impl FontExtent {
53 pub fn new(ascent: f32, descent: f32, line_gap: f32) -> Self {
54 Self {
55 ascent,
56 descent,
57 line_gap,
58 }
59 }
60
61 pub fn natural(self) -> f32 {
63 self.ascent + self.descent
64 }
65}
66
67pub fn line_box(style: &TextStyle, extent: FontExtent, asked: f32, grid: f32) -> LineBox {
77 match style.paragraph_style.line_height_style {
78 None => unstyled_line_box(extent, asked),
79 Some(line_height_style) => {
80 let padding = font_padding(style, extent);
81 let grid = if grid.is_finite() && grid > 0.0 {
82 grid
83 } else {
84 1.0
85 };
86 aosp_line_box(line_height_style, extent, asked, padding, grid)
87 }
88 }
89}
90
91fn unstyled_line_box(extent: FontExtent, asked: f32) -> LineBox {
97 let natural = extent.natural().ceil();
98 LineBox {
99 height: asked,
100 baseline: extent.ascent + (asked - natural) * 0.5,
101 }
102}
103
104fn font_padding(style: &TextStyle, extent: FontExtent) -> f32 {
112 let asked = style
113 .paragraph_style
114 .platform_style
115 .and_then(|platform| platform.include_font_padding)
116 .unwrap_or(false);
117 if asked && extent.line_gap.is_finite() && extent.line_gap > 0.0 {
118 extent.line_gap
119 } else {
120 0.0
121 }
122}
123
124fn aosp_line_box(
125 style: LineHeightStyle,
126 extent: FontExtent,
127 asked: f32,
128 padding: f32,
129 grid: f32,
130) -> LineBox {
131 let up = |value: f32| (value * grid).ceil() / grid;
132 let down = |value: f32| (value * grid).floor() / grid;
133 let ascent = up(extent.ascent.max(0.0));
144 let descent = up(extent.descent.max(0.0));
145 let above_padding = down(padding * 0.5);
148 let below_padding = padding - above_padding;
149 let natural = up(ascent + descent + padding);
150 let asked = if asked.is_finite() {
151 up(asked)
152 } else {
153 natural
154 };
155
156 let height = match style.mode {
157 LineHeightMode::Fixed => asked.max(1.0),
159 LineHeightMode::Minimum => asked.max(natural).max(1.0),
162 LineHeightMode::Tight => natural.max(1.0),
164 };
165
166 let leading = height - (ascent + descent + padding);
170 let (mut above, mut below) = match style.alignment {
171 LineHeightAlignment::Top => (0.0, leading),
173 LineHeightAlignment::Bottom => (leading, 0.0),
175 LineHeightAlignment::Center => {
179 let below = up(leading * 0.5);
180 (leading - below, below)
181 }
182 LineHeightAlignment::Proportional => {
184 let total = ascent + descent;
185 if total > 0.0 {
186 let above = leading * (ascent / total);
187 (above, leading - above)
188 } else {
189 (leading * 0.5, leading * 0.5)
190 }
191 }
192 };
193 above += above_padding;
194 below += below_padding;
195
196 let (trim_above, trim_below) = match style.trim {
203 LineHeightTrim::None => (false, false),
204 LineHeightTrim::FirstLineTop => (true, false),
205 LineHeightTrim::LastLineBottom => (false, true),
206 LineHeightTrim::Both => (true, true),
207 };
208 let mut height = height;
209 if trim_above {
210 height -= above;
211 above = 0.0;
212 }
213 if trim_below {
214 height -= below;
215 }
216
217 LineBox {
218 height: height.max(1.0),
219 baseline: above + ascent,
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::text::style::{ParagraphStyle, PlatformParagraphStyle};
227 use crate::text::TextUnit;
228
229 fn roboto_16sp() -> FontExtent {
233 FontExtent::new(32.0 * 1900.0 / 2048.0, 32.0 * 500.0 / 2048.0, 0.0)
234 }
235
236 fn styled(line_height_px: f32, line_height_style: Option<LineHeightStyle>) -> TextStyle {
237 TextStyle {
238 paragraph_style: ParagraphStyle {
239 line_height: TextUnit::Sp(line_height_px),
240 line_height_style,
241 ..ParagraphStyle::default()
242 },
243 ..TextStyle::default()
244 }
245 }
246
247 fn wear() -> LineHeightStyle {
248 LineHeightStyle {
249 alignment: LineHeightAlignment::Center,
250 trim: LineHeightTrim::None,
251 mode: LineHeightMode::Minimum,
252 }
253 }
254
255 #[test]
256 fn a_style_that_asks_for_nothing_gets_exactly_what_it_got_before() {
257 let extent = roboto_16sp();
258 let plain = line_box(&styled(36.0, None), extent, 36.0, 1.0);
259 assert_eq!(plain.height, 36.0);
260 let natural = extent.natural().ceil();
261 assert_eq!(plain.baseline, extent.ascent + (36.0 - natural) * 0.5);
262 }
263
264 #[test]
265 fn title_medium_overflows_its_own_line_height_and_the_font_wins() {
266 let box_ = line_box(&styled(36.0, Some(wear())), roboto_16sp(), 36.0, 1.0);
268 assert_eq!(box_.height, 38.0);
269 }
270
271 #[test]
272 fn a_line_height_the_font_fits_inside_is_honoured_as_asked() {
273 let extent = FontExtent::new(30.0 * 1900.0 / 2048.0, 30.0 * 500.0 / 2048.0, 0.0);
276 let box_ = line_box(&styled(36.0, Some(wear())), extent, 36.0, 1.0);
277 assert_eq!(box_.height, 36.0);
278 assert_eq!(box_.baseline, 28.0);
281 }
282
283 #[test]
284 fn the_odd_unit_of_leading_goes_below_the_baseline_not_above() {
285 let extent = FontExtent::new(20.0, 10.0, 0.0);
287 let box_ = line_box(&styled(33.0, Some(wear())), extent, 33.0, 1.0);
288 assert_eq!(box_.height, 33.0);
289 assert_eq!(box_.baseline, 21.0);
290 assert_ne!(box_.baseline, 20.0 + 1.5);
293 }
294
295 #[test]
296 fn a_line_height_is_a_whole_number_of_pixels() {
297 let extent = FontExtent::new(20.0, 10.0, 0.0);
298 let box_ = line_box(&styled(33.4, Some(wear())), extent, 33.4, 1.0);
299 assert_eq!(box_.height, 34.0);
300 }
301
302 #[test]
303 fn top_alignment_puts_the_glyphs_at_the_top_and_bottom_at_the_bottom() {
304 let extent = FontExtent::new(20.0, 10.0, 0.0);
305 let top = line_box(
306 &styled(
307 40.0,
308 Some(LineHeightStyle {
309 alignment: LineHeightAlignment::Top,
310 ..wear()
311 }),
312 ),
313 extent,
314 40.0,
315 1.0,
316 );
317 assert_eq!(top.baseline, 20.0);
318 let bottom = line_box(
319 &styled(
320 40.0,
321 Some(LineHeightStyle {
322 alignment: LineHeightAlignment::Bottom,
323 ..wear()
324 }),
325 ),
326 extent,
327 40.0,
328 1.0,
329 );
330 assert_eq!(bottom.baseline, 30.0);
331 assert_eq!(bottom.height - bottom.baseline, extent.descent);
332 }
333
334 #[test]
335 fn proportional_alignment_splits_the_leading_the_way_the_font_is_split() {
336 let extent = FontExtent::new(20.0, 10.0, 0.0);
337 let style = LineHeightStyle {
338 alignment: LineHeightAlignment::Proportional,
339 ..wear()
340 };
341 let box_ = line_box(&styled(60.0, Some(style)), extent, 60.0, 1.0);
342 assert_eq!(box_.baseline, 40.0);
344 }
345
346 #[test]
347 fn a_fixed_line_height_lets_the_font_overflow_and_tight_ignores_the_ask() {
348 let extent = roboto_16sp();
349 let fixed = line_box(
350 &styled(
351 36.0,
352 Some(LineHeightStyle {
353 mode: LineHeightMode::Fixed,
354 ..wear()
355 }),
356 ),
357 extent,
358 36.0,
359 1.0,
360 );
361 assert_eq!(
362 fixed.height, 36.0,
363 "the ask wins even though the font needs 38"
364 );
365 let tight = line_box(
366 &styled(
367 80.0,
368 Some(LineHeightStyle {
369 mode: LineHeightMode::Tight,
370 ..wear()
371 }),
372 ),
373 extent,
374 80.0,
375 1.0,
376 );
377 assert_eq!(tight.height, 38.0);
379 assert_eq!(tight.baseline, 30.0);
380 }
381
382 #[test]
383 fn trimming_removes_the_leading_on_the_edge_it_names() {
384 let extent = FontExtent::new(20.0, 10.0, 0.0);
385 let both = line_box(
386 &styled(
387 40.0,
388 Some(LineHeightStyle {
389 trim: LineHeightTrim::Both,
390 ..wear()
391 }),
392 ),
393 extent,
394 40.0,
395 1.0,
396 );
397 assert_eq!(both.height, 30.0);
398 assert_eq!(both.baseline, 20.0);
399
400 let top_only = line_box(
401 &styled(
402 40.0,
403 Some(LineHeightStyle {
404 trim: LineHeightTrim::FirstLineTop,
405 ..wear()
406 }),
407 ),
408 extent,
409 40.0,
410 1.0,
411 );
412 assert_eq!(top_only.height, 35.0);
414 assert_eq!(top_only.baseline, 20.0);
415 }
416
417 #[test]
418 fn font_padding_is_only_spent_when_a_style_asks_for_it() {
419 let extent = FontExtent::new(20.0, 10.0, 4.0);
420 let without = line_box(&styled(30.0, Some(wear())), extent, 30.0, 1.0);
421 assert_eq!(without.height, 30.0);
422 assert_eq!(without.baseline, 20.0);
423
424 let padded = TextStyle {
425 paragraph_style: ParagraphStyle {
426 line_height: TextUnit::Sp(30.0),
427 line_height_style: Some(wear()),
428 platform_style: Some(PlatformParagraphStyle {
429 include_font_padding: Some(true),
430 shaping: None,
431 }),
432 ..ParagraphStyle::default()
433 },
434 ..TextStyle::default()
435 };
436 let with = line_box(&padded, extent, 30.0, 1.0);
437 assert_eq!(with.height, 34.0, "the line gap widens the font's demand");
438 assert_eq!(with.baseline, 22.0, "and half of it sits above the ascent");
439 }
440
441 #[test]
442 fn a_nonsense_line_height_falls_back_to_the_font_rather_than_producing_nan() {
443 let extent = FontExtent::new(20.0, 10.0, 0.0);
444 let box_ = line_box(&styled(30.0, Some(wear())), extent, f32::NAN, 1.0);
445 assert_eq!(box_.height, 30.0);
446 assert!(box_.baseline.is_finite());
447 }
448}