Skip to main content

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}