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

            
4
use azul_core::{
5
    dom::NodeId,
6
    geom::LogicalPosition,
7
    ui_solver::GlyphInstance,
8
};
9
use azul_css::props::basic::ColorU;
10
use azul_css::props::style::StyleBackgroundContent;
11

            
12
use 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)]
19
pub 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)]
30
pub 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)]
54
pub 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.
73
5425
pub fn get_glyph_runs_simple(layout: &UnifiedLayout) -> Vec<SimpleGlyphRun> {
74
5425
    let mut runs: Vec<SimpleGlyphRun> = Vec::new();
75
5425
    let mut current_run: Option<SimpleGlyphRun> = None;
76

            
77
61915
    for item in &layout.items {
78
56490
        let (item_ascent, _) = get_item_vertical_metrics_approx(&item.item);
79
56490
        let baseline_y = item.position.y + item_ascent;
80

            
81
56490
        let mut process_glyphs =
82
            |positioned_glyphs: &[ShapedGlyph],
83
             item_origin_x: f32,
84
             writing_mode: crate::text3::cache::WritingMode,
85
56070
             source_node_id: Option<NodeId>| {
86
56070
                let mut pen_x = item_origin_x;
87

            
88
112140
                for glyph in positioned_glyphs {
89
56070
                    let glyph_color = glyph.style.color;
90
56070
                    let glyph_background = glyph.style.background_color;
91
56070
                    let glyph_background_content = glyph.style.background_content.clone();
92
56070
                    let glyph_border = glyph.style.border.clone();
93
56070
                    let font_hash = glyph.font_hash;
94
56070
                    let font_size_px = glyph.style.font_size_px;
95
56070
                    let text_decoration = glyph.style.text_decoration.clone();
96

            
97
56070
                    let absolute_position = LogicalPosition {
98
56070
                        x: pen_x + glyph.offset.x,
99
56070
                        y: baseline_y - glyph.offset.y,
100
56070
                    };
101

            
102
56070
                    let instance =
103
56070
                        glyph.into_glyph_instance_at_simple(writing_mode, absolute_position);
104

            
105
56070
                    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
50890
                        if run.font_hash == font_hash
111
50890
                            && run.color == glyph_color
112
50890
                            && run.background_color == glyph_background
113
50890
                            && run.background_content == glyph_background_content
114
50890
                            && run.border == glyph_border
115
50890
                            && run.font_size_px == font_size_px
116
50890
                            && run.text_decoration == text_decoration
117
50890
                            && run.source_node_id == source_node_id
118
50820
                        {
119
50820
                            run.glyphs.push(instance);
120
50820
                        } else {
121
70
                            runs.push(run.clone());
122
70
                            current_run = Some(SimpleGlyphRun {
123
70
                                glyphs: vec![instance],
124
70
                                color: glyph_color,
125
70
                                background_color: glyph_background,
126
70
                                background_content: glyph_background_content.clone(),
127
70
                                border: glyph_border.clone(),
128
70
                                font_hash,
129
70
                                font_size_px,
130
70
                                text_decoration: text_decoration.clone(),
131
70
                                is_ime_preview: false,
132
70
                                source_node_id,
133
70
                            });
134
70
                        }
135
5180
                    } else {
136
5180
                        current_run = Some(SimpleGlyphRun {
137
5180
                            glyphs: vec![instance],
138
5180
                            color: glyph_color,
139
5180
                            background_color: glyph_background,
140
5180
                            background_content: glyph_background_content.clone(),
141
5180
                            border: glyph_border.clone(),
142
5180
                            font_hash,
143
5180
                            font_size_px,
144
5180
                            text_decoration: text_decoration.clone(),
145
5180
                            is_ime_preview: false,
146
5180
                            source_node_id,
147
5180
                        });
148
5180
                    }
149

            
150
56070
                    pen_x += glyph.advance + glyph.kerning;
151
                }
152
56070
            };
153

            
154
56490
        match &item.item {
155
56070
            ShapedItem::Cluster(cluster) => {
156
56070
                let writing_mode = cluster.style.writing_mode;
157
56070
                process_glyphs(&cluster.glyphs, item.position.x, writing_mode, cluster.source_node_id);
158
56070
            }
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
420
            _ => {}
167
        }
168
    }
169

            
170
5425
    if let Some(run) = current_run {
171
5180
        runs.push(run);
172
5180
    }
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
5425
    if runs.len() > 1 {
180
35
        let mut i = 0;
181
140
        while i < runs.len() {
182
105
            if let Some(node_id) = runs[i].source_node_id {
183
105
                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
105
                }
209
            }
210
105
            i += 1;
211
        }
212
5390
    }
213

            
214
5425
    runs
215
5425
}
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.
220
pub 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)]
343
pub 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)]
375
pub 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
393
pub 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.
560
pub 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)]
628
pub 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

            
635
impl 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
}