azul_layout/text3/glyphs.rs
1//! A helper module to extract final, absolute glyph positions from a layout.
2//! This is useful for renderers that work with simple lists of glyphs.
3
4use azul_core::{
5 dom::NodeId,
6 geom::LogicalPosition,
7 ui_solver::GlyphInstance,
8};
9use azul_css::props::basic::ColorU;
10use azul_css::props::style::StyleBackgroundContent;
11
12use crate::text3::cache::{
13 get_item_vertical_metrics_approx, InlineBorderInfo, LoadedFonts, ParsedFontTrait, Point,
14 PositionedItem, ShapedGlyph, ShapedItem, UnifiedLayout,
15};
16
17/// Represents a single glyph ready for rendering, with an absolute position on the baseline.
18#[derive(Debug, Copy, Clone, PartialEq)]
19pub struct PositionedGlyph {
20 pub glyph_id: u16,
21 /// The absolute position of the glyph's origin on the baseline.
22 pub position: Point,
23 /// The advance width of the glyph, useful for caret placement.
24 pub advance: f32,
25}
26
27/// A simple glyph run without font reference - used when fonts aren't available.
28/// The font can be looked up later via font_hash if needed.
29#[derive(Debug, Clone)]
30pub struct SimpleGlyphRun {
31 /// The glyphs in this run, with their positions relative to the start of the run.
32 pub glyphs: Vec<GlyphInstance>,
33 /// The color of the text in this glyph run.
34 pub color: ColorU,
35 /// Background color for this run (rendered behind text)
36 pub background_color: Option<ColorU>,
37 /// Full background content layers (for gradients, images, etc.)
38 pub background_content: Vec<StyleBackgroundContent>,
39 /// Border information for inline elements
40 pub border: Option<InlineBorderInfo>,
41 /// A hash of the font, useful for caching purposes.
42 pub font_hash: u64,
43 /// The font size in pixels.
44 pub font_size_px: f32,
45 /// Text decoration (underline, strikethrough, overline)
46 pub text_decoration: crate::text3::cache::TextDecoration,
47 /// Whether this is an IME composition preview (should be rendered with special styling)
48 pub is_ime_preview: bool,
49 /// The source DOM node that generated this text run (for hit-testing)
50 pub source_node_id: Option<NodeId>,
51}
52
53#[derive(Debug, Clone)]
54pub struct GlyphRun<T: ParsedFontTrait> {
55 /// The glyphs in this run, with their positions relative to the start of the run.
56 pub glyphs: Vec<GlyphInstance>,
57 /// The color of the text in this glyph run.
58 pub color: ColorU,
59 /// The font used for this glyph run.
60 pub font: T, // Changed from Arc<T> - T is already cheap to clone (e.g. FontRef)
61 /// A hash of the font, useful for caching purposes.
62 pub font_hash: u64,
63 /// The font size in pixels.
64 pub font_size_px: f32,
65 /// Text decoration (underline, strikethrough, overline)
66 pub text_decoration: crate::text3::cache::TextDecoration,
67 /// Whether this is an IME composition preview (should be rendered with special styling)
68 pub is_ime_preview: bool,
69}
70
71/// Simple version of get_glyph_runs that doesn't require fonts.
72/// Use this when you only need glyph positions and don't need font references.
73pub fn get_glyph_runs_simple(layout: &UnifiedLayout) -> Vec<SimpleGlyphRun> {
74 let mut runs: Vec<SimpleGlyphRun> = Vec::new();
75 let mut current_run: Option<SimpleGlyphRun> = None;
76
77 for item in &layout.items {
78 let (item_ascent, _) = get_item_vertical_metrics_approx(&item.item);
79 let baseline_y = item.position.y + item_ascent;
80
81 let mut process_glyphs =
82 |positioned_glyphs: &[ShapedGlyph],
83 item_origin_x: f32,
84 writing_mode: crate::text3::cache::WritingMode,
85 source_node_id: Option<NodeId>| {
86 let mut pen_x = item_origin_x;
87
88 for glyph in positioned_glyphs {
89 let glyph_color = glyph.style.color;
90 let glyph_background = glyph.style.background_color;
91 let glyph_background_content = glyph.style.background_content.clone();
92 let glyph_border = glyph.style.border.clone();
93 let font_hash = glyph.font_hash;
94 let font_size_px = glyph.style.font_size_px;
95 let text_decoration = glyph.style.text_decoration.clone();
96
97 let absolute_position = LogicalPosition {
98 x: pen_x + glyph.offset.x,
99 y: baseline_y - glyph.offset.y,
100 };
101
102 let instance =
103 glyph.into_glyph_instance_at_simple(writing_mode, absolute_position);
104
105 if let Some(run) = current_run.as_mut() {
106 // changes (font, color, border, size). Per spec, text-decoration
107 // changes do not affect shaping (shaping is done upstream in
108 // default.rs), but we still break rendering runs for correct drawing.
109 // Border/margin/padding changes break both shaping and rendering runs.
110 if run.font_hash == font_hash
111 && run.color == glyph_color
112 && run.background_color == glyph_background
113 && run.background_content == glyph_background_content
114 && run.border == glyph_border
115 && run.font_size_px == font_size_px
116 && run.text_decoration == text_decoration
117 && run.source_node_id == source_node_id
118 {
119 run.glyphs.push(instance);
120 } else {
121 runs.push(run.clone());
122 current_run = Some(SimpleGlyphRun {
123 glyphs: vec![instance],
124 color: glyph_color,
125 background_color: glyph_background,
126 background_content: glyph_background_content.clone(),
127 border: glyph_border.clone(),
128 font_hash,
129 font_size_px,
130 text_decoration: text_decoration.clone(),
131 is_ime_preview: false,
132 source_node_id,
133 });
134 }
135 } else {
136 current_run = Some(SimpleGlyphRun {
137 glyphs: vec![instance],
138 color: glyph_color,
139 background_color: glyph_background,
140 background_content: glyph_background_content.clone(),
141 border: glyph_border.clone(),
142 font_hash,
143 font_size_px,
144 text_decoration: text_decoration.clone(),
145 is_ime_preview: false,
146 source_node_id,
147 });
148 }
149
150 pen_x += glyph.advance + glyph.kerning;
151 }
152 };
153
154 match &item.item {
155 ShapedItem::Cluster(cluster) => {
156 let writing_mode = cluster.style.writing_mode;
157 process_glyphs(&cluster.glyphs, item.position.x, writing_mode, cluster.source_node_id);
158 }
159 ShapedItem::CombinedBlock { glyphs, .. } => {
160 for g in glyphs {
161 let writing_mode = g.style.writing_mode;
162 // CombinedBlock is for tate-chu-yoko, use None for source_node_id
163 process_glyphs(&[g.clone()], item.position.x, writing_mode, None);
164 }
165 }
166 _ => {}
167 }
168 }
169
170 if let Some(run) = current_run {
171 runs.push(run);
172 }
173
174 // +spec:box-model:6c62d3 - suppress margins/borders/padding at inline box split points
175 // CSS 2.2 §9.4.2: When an inline box is split across lines, margins, borders,
176 // and padding have no visible effect at the split points.
177 // Post-process: for runs from the same source_node_id that have borders,
178 // mark intermediate fragments so left_inset()/right_inset() suppress edges.
179 if runs.len() > 1 {
180 let mut i = 0;
181 while i < runs.len() {
182 if let Some(node_id) = runs[i].source_node_id {
183 if runs[i].border.is_some() {
184 let start = i;
185 let mut end = i + 1;
186 while end < runs.len()
187 && runs[end].source_node_id == Some(node_id)
188 && runs[end].border.is_some()
189 {
190 end += 1;
191 }
192 if end - start > 1 {
193 if let Some(ref mut b) = runs[start].border {
194 b.is_last_fragment = false;
195 }
196 for j in (start + 1)..(end - 1) {
197 if let Some(ref mut b) = runs[j].border {
198 b.is_first_fragment = false;
199 b.is_last_fragment = false;
200 }
201 }
202 if let Some(ref mut b) = runs[end - 1].border {
203 b.is_first_fragment = false;
204 }
205 }
206 i = end;
207 continue;
208 }
209 }
210 i += 1;
211 }
212 }
213
214 runs
215}
216
217/// Same as `get_glyph_positions`, but returns a list of `GlyphRun`s
218/// instead of a flat list of glyphs. This groups glyphs by their font and
219/// color, which can be more efficient for rendering.
220pub fn get_glyph_runs<T: ParsedFontTrait>(
221 layout: &UnifiedLayout,
222 fonts: &LoadedFonts<T>,
223) -> Vec<GlyphRun<T>> {
224 // Group glyphs by font and color
225 let mut runs: Vec<GlyphRun<T>> = Vec::new();
226 let mut current_run: Option<GlyphRun<T>> = None;
227
228 for item in &layout.items {
229 let (item_ascent, _) = get_item_vertical_metrics_approx(&item.item);
230 let baseline_y = item.position.y + item_ascent;
231
232 let mut process_glyphs =
233 |positioned_glyphs: &[ShapedGlyph],
234 item_origin_x: f32,
235 writing_mode: crate::text3::cache::WritingMode| {
236 let mut pen_x = item_origin_x;
237
238 for glyph in positioned_glyphs {
239 let glyph_color = glyph.style.color;
240 let font_hash = glyph.font_hash;
241 let font_size_px = glyph.style.font_size_px;
242 let text_decoration = glyph.style.text_decoration.clone();
243
244 // Look up the font from the fonts container
245 let font = match fonts.get_by_hash(font_hash) {
246 Some(f) => f.clone(),
247 None => continue, // Skip glyphs with unknown fonts
248 };
249
250 // Calculate absolute position: baseline position + GPOS offset
251 let absolute_position = LogicalPosition {
252 x: pen_x + glyph.offset.x,
253 y: baseline_y - glyph.offset.y, // Y-down: subtract positive offset
254 };
255
256 let instance =
257 glyph.into_glyph_instance_at(writing_mode, absolute_position, fonts);
258
259 // changes. Text-decoration does not affect shaping (per spec, shaping
260 // must not break when only text-decoration changes), but rendering
261 // runs still split for correct visual output.
262 if let Some(run) = current_run.as_mut() {
263 if run.font_hash == font_hash
264 && run.color == glyph_color
265 && run.font_size_px == font_size_px
266 && run.text_decoration == text_decoration
267 {
268 run.glyphs.push(instance);
269 } else {
270 // Different font, color, size, or decoration: finalize the
271 // current run and start a new one
272 runs.push(run.clone());
273 current_run = Some(GlyphRun {
274 glyphs: vec![instance],
275 color: glyph_color,
276 font: font.clone(),
277 font_hash,
278 font_size_px,
279 text_decoration: text_decoration.clone(),
280 is_ime_preview: false, // TODO: Set from input context
281 });
282 }
283 } else {
284 // Start a new run
285 current_run = Some(GlyphRun {
286 glyphs: vec![instance],
287 color: glyph_color,
288 font: font.clone(),
289 font_hash,
290 font_size_px,
291 text_decoration: text_decoration.clone(),
292 is_ime_preview: false, // TODO: Set from input context
293 });
294 }
295
296 // Advance the pen for the next glyph in the cluster/block.
297 // TODO: writing-mode support (vertical text) here
298 pen_x += glyph.advance + glyph.kerning;
299 }
300 };
301
302 match &item.item {
303 ShapedItem::Cluster(cluster) => {
304 let writing_mode = cluster.style.writing_mode;
305 process_glyphs(&cluster.glyphs, item.position.x, writing_mode);
306 }
307 // This is a rare case for tate-chu-yoko (mixed horizontal+vertical text)
308 ShapedItem::CombinedBlock { glyphs, .. } => {
309 for g in glyphs {
310 let writing_mode = g.style.writing_mode;
311 process_glyphs(&[g.clone()], item.position.x, writing_mode);
312 }
313 }
314 _ => {
315 // Ignore non-text items like objects, breaks, etc.
316 }
317 }
318 }
319
320 if let Some(run) = current_run {
321 runs.push(run);
322 }
323
324 runs
325}
326
327/// A glyph run optimized for PDF rendering.
328///
329/// Groups glyphs by font, color, size, and style, while breaking at line boundaries.
330/// This struct is used by the PDF renderer to efficiently render text with proper
331/// styling, including inline background colors for `<span>` elements.
332///
333/// # Z-Order for Inline Backgrounds
334///
335/// The `background_color` field enables proper z-ordering of inline backgrounds:
336/// - PDF renderers should iterate over all runs and render backgrounds FIRST
337/// - Then iterate again and render all text SECOND
338/// - This ensures backgrounds appear behind text, not on top of it
339///
340/// The display list (`paint_inline_content`) does NOT emit `push_rect()` for inline
341/// backgrounds because that would cause double-rendering and z-order issues.
342#[derive(Debug, Clone)]
343pub struct PdfGlyphRun<T: ParsedFontTrait> {
344 /// The glyphs in this run with their absolute positions
345 pub glyphs: Vec<PdfPositionedGlyph>,
346 /// The color of the text
347 pub color: ColorU,
348 /// Background color for inline elements (e.g., `<span style="background: yellow">`)
349 ///
350 /// This is rendered as a filled rectangle behind the text by the PDF renderer.
351 /// The rectangle spans from ascent to descent and covers the full width of the run.
352 pub background_color: Option<ColorU>,
353 /// The font used for this run
354 pub font: T,
355 /// Font hash for identification
356 pub font_hash: u64,
357 /// Font size in pixels
358 pub font_size_px: f32,
359 /// Text decoration flags
360 pub text_decoration: crate::text3::cache::TextDecoration,
361 /// The line index this run belongs to (for breaking runs at line boundaries)
362 pub line_index: usize,
363 /// Text direction for this run
364 pub direction: crate::text3::cache::BidiDirection,
365 /// Writing mode for this run
366 pub writing_mode: crate::text3::cache::WritingMode,
367 /// The starting position (baseline) of this run - used for SetTextMatrix
368 pub baseline_start: Point,
369 /// Original cluster text for debugging/CID mapping
370 pub cluster_texts: Vec<String>,
371}
372
373/// A glyph with its absolute position and cluster text for PDF rendering
374#[derive(Debug, Clone)]
375pub struct PdfPositionedGlyph {
376 /// Glyph ID
377 pub glyph_id: u16,
378 /// Absolute position on the baseline (Y-down coordinate system)
379 pub position: Point,
380 /// The advance width of this glyph
381 pub advance: f32,
382 /// The Unicode character(s) this glyph represents (for PDF ToUnicode CMap)
383 /// This is extracted from the cluster text using the glyph's cluster_offset
384 pub unicode_codepoint: String,
385}
386
387/// Extract glyph runs optimized for PDF rendering.
388/// This function:
389/// - Groups consecutive glyphs by font, color, size, style, and line
390/// - Breaks runs at line boundaries (different line_index)
391/// - Preserves absolute positioning for each glyph (critical for RTL and complex scripts)
392/// - Includes cluster text for proper CID/Unicode mapping
393pub fn get_glyph_runs_pdf<T: ParsedFontTrait>(
394 layout: &UnifiedLayout,
395 fonts: &LoadedFonts<T>,
396) -> Vec<PdfGlyphRun<T>> {
397 let mut runs: Vec<PdfGlyphRun<T>> = Vec::new();
398 let mut current_run: Option<PdfGlyphRun<T>> = None;
399
400 for positioned_item in &layout.items {
401 // Only process text clusters
402 let cluster = match &positioned_item.item {
403 ShapedItem::Cluster(c) => c,
404 _ => continue, // Skip non-text items
405 };
406
407 if cluster.glyphs.is_empty() {
408 continue;
409 }
410
411 // Calculate the baseline position for this cluster
412 let (item_ascent, _) = get_item_vertical_metrics_approx(&positioned_item.item);
413 let baseline_y = positioned_item.position.y + item_ascent;
414
415 // Process each glyph in the cluster
416 let mut pen_x = positioned_item.position.x;
417
418 // For extracting the correct unicode codepoint per glyph, we need to track
419 // which portion of the cluster text each glyph represents.
420 // The cluster_offset in ShapedGlyph is the byte offset into cluster.text
421 let cluster_text = &cluster.text;
422 let cluster_glyphs_count = cluster.glyphs.len();
423
424 for (glyph_idx, glyph) in cluster.glyphs.iter().enumerate() {
425 let glyph_color = glyph.style.color;
426 let glyph_background = glyph.style.background_color;
427 let font_hash = glyph.font_hash;
428 let font_size_px = glyph.style.font_size_px;
429 let text_decoration = glyph.style.text_decoration.clone();
430 let line_index = positioned_item.line_index;
431 let direction = cluster.direction;
432 let writing_mode = cluster.style.writing_mode;
433
434 // Look up the font from the fonts container
435 let font = match fonts.get_by_hash(font_hash) {
436 Some(f) => f.clone(),
437 None => continue, // Skip glyphs with unknown fonts
438 };
439
440 // Calculate absolute glyph position on baseline
441 let glyph_position = Point {
442 x: pen_x + glyph.offset.x,
443 y: baseline_y - glyph.offset.y, // Y-down: subtract positive GPOS offset
444 };
445
446 // Extract the unicode codepoint for this specific glyph
447 // For simple 1:1 mappings, each glyph gets one character
448 // For complex scripts (ligatures, etc.), we may need to assign
449 // the whole cluster text to the first glyph, or split it appropriately
450 let unicode_codepoint = if cluster_glyphs_count == 1 {
451 // Simple case: one glyph represents the entire cluster
452 cluster_text.clone()
453 } else {
454 // Multiple glyphs in cluster - try to extract the character at cluster_offset
455 // cluster_offset is the byte offset into the cluster text
456 let byte_offset = glyph.cluster_offset as usize;
457 if byte_offset < cluster_text.len() {
458 // Get the character at this byte offset
459 cluster_text[byte_offset..]
460 .chars()
461 .next()
462 .map(|c| c.to_string())
463 .unwrap_or_else(|| cluster_text.clone())
464 } else {
465 // Fallback: if offset is out of range, use the whole cluster for first glyph
466 // or empty for subsequent glyphs (they share the same codepoint)
467 if glyph_idx == 0 {
468 cluster_text.clone()
469 } else {
470 String::new()
471 }
472 }
473 };
474
475 let pdf_glyph = PdfPositionedGlyph {
476 glyph_id: glyph.glyph_id,
477 position: glyph_position,
478 advance: glyph.advance,
479 unicode_codepoint,
480 };
481
482 // Font hash change = font change (shaping must break per spec).
483 // Border/background change = margin/border/padding non-zero (shaping must break).
484 // Text-decoration change = rendering-only break (shaping unaffected per spec).
485 let should_break = if let Some(run) = current_run.as_ref() {
486 run.font_hash != font_hash
487 || run.color != glyph_color
488 || run.background_color != glyph_background
489 || run.font_size_px != font_size_px
490 || run.text_decoration != text_decoration
491 || run.line_index != line_index
492 || run.direction != direction
493 || run.writing_mode != writing_mode
494 } else {
495 false
496 };
497
498 if should_break {
499 // Finalize the current run and start a new one
500 if let Some(run) = current_run.take() {
501 runs.push(run);
502 }
503 }
504
505 if let Some(run) = current_run.as_mut() {
506 // Add to existing run
507 run.glyphs.push(pdf_glyph);
508 run.cluster_texts.push(cluster.text.clone());
509 } else {
510 // Start a new run
511 current_run = Some(PdfGlyphRun {
512 glyphs: vec![pdf_glyph],
513 color: glyph_color,
514 background_color: glyph_background,
515 font: font.clone(),
516 font_hash,
517 font_size_px,
518 text_decoration: text_decoration.clone(),
519 line_index,
520 direction,
521 writing_mode,
522 baseline_start: Point {
523 x: pen_x,
524 y: baseline_y,
525 },
526 cluster_texts: vec![cluster.text.clone()],
527 });
528 }
529
530 // Advance pen position - DON'T add kerning here because it's already
531 // included in the positioned_item.position.x from the layout engine!
532 // We only advance by the base advance to track our position within this cluster
533 pen_x += glyph.advance + glyph.kerning;
534 }
535 }
536
537 // Push the final run if any
538 if let Some(run) = current_run {
539 runs.push(run);
540 }
541
542 runs
543}
544
545/// Transforms the final layout into a simple list of glyphs and their absolute positions.
546///
547/// This function iterates through all positioned items in a layout, filtering for text clusters
548/// and combined text blocks. It calculates the absolute baseline position for each glyph within
549/// these items and returns a flat vector of `PositionedGlyph` structs. This is useful for
550/// rendering or for clients that need a lower-level representation of the text layout.
551///
552/// # Arguments
553///
554/// - `layout` - A reference to the final `UnifiedLayout` produced by the pipeline.
555///
556/// # Returns
557///
558/// A `Vec<PositionedGlyph>` containing all glyphs from the layout with their
559/// absolute baseline positions.
560pub fn get_glyph_positions(layout: &UnifiedLayout) -> Vec<PositionedGlyph> {
561 let mut final_glyphs = Vec::new();
562
563 for item in &layout.items {
564 let (item_ascent, _) = get_item_vertical_metrics_approx(&item.item);
565 let baseline_y = item.position.y + item_ascent;
566
567 let mut process_glyphs = |positioned_glyphs: &[ShapedGlyph], item_origin_x: f32| {
568 let mut pen_x = item_origin_x;
569 for glyph in positioned_glyphs {
570 // The glyph's final position is its origin on the baseline.
571 // GPOS y-offsets shift the glyph up or down relative to the baseline.
572 // In a Y-down coordinate system, a positive GPOS offset (up) means
573 // subtracting from Y.
574 let glyph_pos = Point {
575 x: pen_x + glyph.offset.x,
576 y: baseline_y - glyph.offset.y,
577 };
578
579 final_glyphs.push(PositionedGlyph {
580 glyph_id: glyph.glyph_id,
581 position: glyph_pos,
582 advance: glyph.advance,
583 });
584
585 // Advance the pen for the next glyph in the cluster/block.
586 pen_x += glyph.advance + glyph.kerning;
587 }
588 };
589
590 match &item.item {
591 ShapedItem::Cluster(cluster) => {
592 process_glyphs(&cluster.glyphs, item.position.x);
593 }
594 ShapedItem::CombinedBlock { glyphs, .. } => {
595 // This assumes horizontal layout for the combined block's glyphs.
596 process_glyphs(glyphs, item.position.x);
597 }
598 _ => {
599 // Ignore non-text items like objects, breaks, etc.
600 }
601 }
602 }
603
604 final_glyphs
605}
606
607// ============================================================================
608// +spec:display-property:e124e9 - Line box height sized to include aligned layout bounds of all inline-level boxes
609// LINE BOX METRICS ACCUMULATOR (CSS 2.2 §10.8.1)
610// +spec:height-calculation:18825a - half-leading model for line box height calculation
611// +spec:inline-formatting-context:ce2b15 - line box height from vertical stack of inline-level boxes
612// ============================================================================
613
614/// Accumulates metrics for a single line box during inline layout.
615///
616// +spec:display-property:61a267 - inline-sizing default (normal): content area height = font metrics (ascent+descent), no layout effect
617// +spec:display-property:a15ae9 - line-height determines layout bounds (contribution to line box logical height)
618// +spec:display-property:adc520 - inline-level baseline alignment: each glyph/inline-box aligned to parent baseline, then shifted by vertical-align
619// +spec:display-property:e2e64f - line box block-axis sizing from inline-level contents via line-height
620/// Implements the CSS 2.2 §10.8.1 "half-leading" model:
621/// - Each inline item has a content area (ascent + descent from font metrics)
622/// - CSS `line-height` distributes "half-leading" equally above and below
623/// - The line box height is the maximum extent of all items after leading
624///
625/// Usage: create a new `LineBoxMetrics`, call `add_item()` for each inline
626/// item on the line, then call `line_height()` and `baseline_offset()`.
627#[derive(Debug, Clone)]
628pub struct LineBoxMetrics {
629 /// Maximum distance above the baseline (positive = up).
630 max_above_baseline: f32,
631 /// Maximum distance below the baseline (positive = down).
632 max_below_baseline: f32,
633}
634
635impl LineBoxMetrics {
636 pub fn new() -> Self {
637 Self {
638 max_above_baseline: 0.0,
639 max_below_baseline: 0.0,
640 }
641 }
642
643 /// Add an inline item's metrics to this line box.
644 ///
645 /// - `ascent`: font ascent (positive, distance from baseline to top of text)
646 /// - `descent`: font descent (positive, distance from baseline to bottom of text)
647 /// - `line_height`: the computed CSS `line-height` for this item
648 ///
649 // +spec:font-metrics:05193a - half-leading model: L = line-height - AD, split above/below
650 /// Half-leading = (line_height - (ascent + descent)) / 2, added above and below.
651 // +spec:box-model:533ca2 - line-fit-edge:leading: line box height uses half-leading, not inline box margin/padding/border
652 // +spec:box-model:04846b - line-fit-edge:leading mode only uses line-height for layout bounds (non-leading modes not yet implemented)
653 // +spec:display-property:a15ae9 - line-height determines inline box layout bounds (contribution to line box height)
654 // +spec:font-metrics:5c5f79 - leading value: ascent/descent plus positive half-leading sizes line box
655 // +spec:font-metrics:3d59af - leading value uses half-leading; margin/padding/border ignored for line box sizing
656 // +spec:line-height:b3be30 - half-leading distributed above/below; line box grows to accommodate overflow
657 // +spec:overflow:196059 - half-leading model: L = line-height - AD, half added above A and below D
658 pub fn add_item(&mut self, ascent: f32, descent: f32, line_height: f32) {
659 let content_height = ascent + descent;
660 let half_leading = (line_height - content_height) / 2.0;
661 let above = ascent + half_leading;
662 let below = descent + half_leading;
663 self.max_above_baseline = self.max_above_baseline.max(above);
664 self.max_below_baseline = self.max_below_baseline.max(below);
665 }
666
667 /// The total height of the line box.
668 pub fn line_height(&self) -> f32 {
669 self.max_above_baseline + self.max_below_baseline
670 }
671
672 /// The offset from the top of the line box to the baseline.
673 pub fn baseline_offset(&self) -> f32 {
674 self.max_above_baseline
675 }
676}