1
//! Core types and layout pipeline for the text/inline formatting context.
2
//!
3
//! This module defines the central data structures (`UnifiedConstraints`,
4
//! `LayoutCache`, `FontManager`, `UnifiedLayout`, etc.) and implements the
5
//! 5-stage inline layout pipeline:
6
//!
7
//! 1. **Logical Analysis** — `InlineContent` → `LogicalItem`
8
//! 2. **BiDi Reordering** — `LogicalItem` → `VisualItem`
9
//! 3. **Shaping** — `VisualItem` → `ShapedItem`
10
//! 4. **Text Orientation** — vertical writing-mode transforms
11
//! 5. **Flow / Positioning** — line breaking + final `PositionedItem` placement
12
//!
13
//! The module also contains cursor movement helpers, caching infrastructure
14
//! (per-item and monolithic), and font management (`FontContext`, `FontManager`,
15
//! `LoadedFonts`).  Integration with the box layout solver lives in
16
//! `solver3/fc.rs`.
17

            
18
use std::{
19
    cmp::Ordering,
20
    collections::{
21
        hash_map::{DefaultHasher, HashMap},
22
        BTreeSet, HashSet,
23
    },
24
    hash::{Hash, Hasher},
25
    mem::discriminant,
26
    num::NonZeroUsize,
27
    sync::{Arc, Mutex},
28
};
29

            
30
pub use azul_core::selection::{ContentIndex, GraphemeClusterId};
31
use azul_core::{
32
    dom::NodeId,
33
    geom::{LogicalPosition, LogicalRect, LogicalSize},
34
    resources::ImageRef,
35
    selection::{CursorAffinity, SelectionRange, TextCursor},
36
    ui_solver::GlyphInstance,
37
};
38
use azul_css::{
39
    corety::LayoutDebugMessage, props::basic::ColorU, props::style::StyleBackgroundContent,
40
};
41
#[cfg(feature = "text_layout_hyphenation")]
42
use hyphenation::{Hyphenator, Language as HyphenationLanguage, Load, Standard};
43
use rust_fontconfig::{FcFontCache, FcPattern, FcWeight, FontId, PatternMatch, UnicodeRange};
44
use smallvec::{smallvec, SmallVec};
45
use unicode_bidi::{BidiInfo, Level, TextSource};
46
use unicode_segmentation::UnicodeSegmentation;
47

            
48
/// Glyph storage for a single shaped cluster. Inline one glyph (the
49
/// common case for Latin text), spill to heap for ligatures / combining
50
/// marks / multi-glyph clusters. The `union` feature of smallvec packs
51
/// the inline buffer and the heap pointer into the same bytes, so sizeof
52
/// stays `sizeof(ShapedGlyph) + 2*usize` regardless of inline/heap state.
53
pub type ShapedGlyphVec = SmallVec<[ShapedGlyph; 1]>;
54

            
55
/// CSS `line-height` value.
56
///
57
/// `Normal` defers resolution to the point where font metrics are available,
58
/// computing `(ascent + |descent| + lineGap) / upem * fontSize`.
59
/// `Px` is an already-resolved pixel value from an explicit CSS declaration
60
/// (e.g. `line-height: 1.5` → `Px(fontSize * 1.5)`).
61
#[derive(Debug, Clone, Copy)]
62
pub enum LineHeight {
63
    /// `line-height: normal` — resolve from font metrics at layout time
64
    Normal,
65
    /// Pre-resolved pixel value (from CSS `line-height: <number|length|percentage>`)
66
    Px(f32),
67
}
68

            
69
impl Default for LineHeight {
70
    fn default() -> Self {
71
        LineHeight::Normal
72
    }
73
}
74

            
75
impl LineHeight {
76
    /// Resolve to a pixel value, using font metrics when `Normal`.
77
    ///
78
    /// `ascent`, `descent` (negative in OpenType convention), `line_gap` are in font units.
79
    /// `font_size_px` and `units_per_em` are used to scale.
80
3563824
    pub fn resolve(&self, font_size_px: f32, ascent: f32, descent: f32, line_gap: f32, units_per_em: u16) -> f32 {
81
3563824
        match self {
82
255860
            LineHeight::Px(px) => *px,
83
            LineHeight::Normal => {
84
3307964
                if units_per_em == 0 {
85
                    return font_size_px * 1.2; // fallback
86
3307964
                }
87
3307964
                let scale = font_size_px / units_per_em as f32;
88
3307964
                (ascent - descent + line_gap) * scale
89
            }
90
        }
91
3563824
    }
92

            
93
    /// Resolve using a `LayoutFontMetrics` struct for convenience.
94
3307964
    pub fn resolve_with_metrics(&self, font_size_px: f32, metrics: &LayoutFontMetrics) -> f32 {
95
3307964
        self.resolve(font_size_px, metrics.ascent, metrics.descent, metrics.line_gap, metrics.units_per_em)
96
3307964
    }
97
}
98

            
99
impl PartialEq for LineHeight {
100
    fn eq(&self, other: &Self) -> bool {
101
        match (self, other) {
102
            (LineHeight::Normal, LineHeight::Normal) => true,
103
            (LineHeight::Px(a), LineHeight::Px(b)) => a.to_bits() == b.to_bits(),
104
            _ => false,
105
        }
106
    }
107
}
108

            
109
impl Eq for LineHeight {}
110

            
111
impl Hash for LineHeight {
112
233547
    fn hash<H: Hasher>(&self, state: &mut H) {
113
233547
        std::mem::discriminant(self).hash(state);
114
233547
        if let LineHeight::Px(v) = self {
115
            v.to_bits().hash(state);
116
233547
        }
117
233547
    }
118
}
119

            
120
// Stub type when hyphenation is disabled
121
#[cfg(not(feature = "text_layout_hyphenation"))]
122
pub struct Standard;
123

            
124
#[cfg(not(feature = "text_layout_hyphenation"))]
125
impl Standard {
126
    /// Stub hyphenate method that returns no breaks
127
    pub fn hyphenate<'a>(&'a self, _word: &'a str) -> StubHyphenationBreaks {
128
        StubHyphenationBreaks { breaks: Vec::new() }
129
    }
130
}
131

            
132
/// Result of hyphenation (stub when feature is disabled)
133
#[cfg(not(feature = "text_layout_hyphenation"))]
134
pub struct StubHyphenationBreaks {
135
    pub breaks: Vec<usize>,
136
}
137

            
138
// Always import Language from script module
139
use crate::text3::script::{script_to_language, Language, Script};
140

            
141
/// Available space for layout, similar to Taffy's AvailableSpace.
142
///
143
/// This type explicitly represents the three possible states for available space:
144
///
145
/// - `Definite(f32)`: A specific pixel width is available
146
/// - `MinContent`: Layout should use minimum content width (shrink-wrap)
147
/// - `MaxContent`: Layout should use maximum content width (no line breaks unless necessary)
148
///
149
/// This is critical for proper handling of intrinsic sizing in Flexbox/Grid
150
/// where the available space may be indefinite during the measure phase.
151
#[derive(Debug, Clone, Copy, PartialEq)]
152
pub enum AvailableSpace {
153
    /// A specific amount of space is available (in pixels).
154
    /// Must be >= 0.  A value of 0.0 means "genuinely zero-width container"
155
    /// (e.g. `width: 0px`), NOT "unresolved".
156
    Definite(f32),
157
    /// The node should be laid out under a min-content constraint
158
    MinContent,
159
    /// The node should be laid out under a max-content constraint.
160
    /// This is the correct default: "lay out to natural width, no constraint".
161
    MaxContent,
162
}
163

            
164
impl Default for AvailableSpace {
165
    /// Default is `MaxContent` — the absence of a width constraint.
166
    /// Never `Definite(0.0)`, which would make every word overflow.
167
    fn default() -> Self {
168
        AvailableSpace::MaxContent
169
    }
170
}
171

            
172
impl AvailableSpace {
173
    /// Returns true if this is a definite (finite, known) amount of space
174
    pub fn is_definite(&self) -> bool {
175
        matches!(self, AvailableSpace::Definite(_))
176
    }
177

            
178
    /// Returns true if this is an indefinite (min-content or max-content) constraint
179
    pub fn is_indefinite(&self) -> bool {
180
        !self.is_definite()
181
    }
182

            
183
    /// Returns the definite value if available, or a fallback for indefinite constraints
184
    pub fn unwrap_or(self, fallback: f32) -> f32 {
185
        match self {
186
            AvailableSpace::Definite(v) => v,
187
            _ => fallback,
188
        }
189
    }
190

            
191
    /// Returns the definite value, or a large value for both min-content and max-content.
192
    /// 
193
    /// For intrinsic sizing, we use a large value to let text lay out fully,
194
    /// then measure the result. The distinction between min/max-content is handled
195
    /// by the line breaking algorithm, not by constraining the available width.
196
7480
    pub fn to_f32_for_layout(self) -> f32 {
197
7480
        match self {
198
            AvailableSpace::Definite(v) => v,
199
3740
            AvailableSpace::MinContent => f32::MAX / 2.0,
200
3740
            AvailableSpace::MaxContent => f32::MAX / 2.0,
201
        }
202
7480
    }
203

            
204
    /// Create from an f32 value, recognizing special sentinel values.
205
    ///
206
    /// This function provides backwards compatibility with code that uses f32 for constraints:
207
    /// - `f32::INFINITY` or `f32::MAX` → `MaxContent` (no line wrapping)
208
    /// - `0.0` → `MinContent` (maximum line wrapping, return longest word width)
209
    /// - Other values → `Definite(value)`
210
    ///
211
    /// Note: Using sentinel values like 0.0 for MinContent is fragile. Prefer using
212
    /// `AvailableSpace::MinContent` directly when possible.
213
    pub fn from_f32(value: f32) -> Self {
214
        if value.is_infinite() || value >= f32::MAX / 2.0 {
215
            // Treat very large values (including f32::MAX) as MaxContent
216
            AvailableSpace::MaxContent
217
        } else if value <= 0.0 {
218
            // Treat zero or negative as MinContent (shrink-wrap)
219
            AvailableSpace::MinContent
220
        } else {
221
            AvailableSpace::Definite(value)
222
        }
223
    }
224
}
225

            
226
impl Hash for AvailableSpace {
227
    fn hash<H: Hasher>(&self, state: &mut H) {
228
        std::mem::discriminant(self).hash(state);
229
        if let AvailableSpace::Definite(v) = self {
230
            (v.round() as usize).hash(state);
231
        }
232
    }
233
}
234

            
235
// Re-export traits for backwards compatibility
236
pub use crate::font_traits::{ParsedFontTrait, ShallowClone};
237

            
238
// --- Core Data Structures for the New Architecture ---
239

            
240
/// Key for caching font chains - based only on CSS properties, not text content
241
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
242
pub struct FontChainKey {
243
    pub font_families: Vec<String>,
244
    pub weight: FcWeight,
245
    pub italic: bool,
246
    pub oblique: bool,
247
}
248

            
249
/// Either a FontChainKey (resolved via fontconfig) or a direct FontRef hash.
250
/// 
251
/// This enum cleanly separates:
252
/// - `Chain`: Fonts resolved through fontconfig with fallback support
253
/// - `Ref`: Direct FontRef that bypasses fontconfig entirely (e.g., embedded icon fonts)
254
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
255
pub enum FontChainKeyOrRef {
256
    /// Regular font chain resolved via fontconfig
257
    Chain(FontChainKey),
258
    /// Direct FontRef identified by pointer address (covers entire Unicode range, no fallbacks)
259
    Ref(usize),
260
}
261

            
262
impl FontChainKeyOrRef {
263
    /// Create from a FontStack enum
264
    pub fn from_font_stack(font_stack: &FontStack) -> Self {
265
        match font_stack {
266
            FontStack::Stack(selectors) => FontChainKeyOrRef::Chain(FontChainKey::from_selectors(selectors)),
267
            FontStack::Ref(font_ref) => FontChainKeyOrRef::Ref(font_ref.parsed as usize),
268
        }
269
    }
270
    
271
    /// Returns true if this is a direct FontRef
272
    pub fn is_ref(&self) -> bool {
273
        matches!(self, FontChainKeyOrRef::Ref(_))
274
    }
275
    
276
    /// Returns the FontRef pointer if this is a Ref variant
277
    pub fn as_ref_ptr(&self) -> Option<usize> {
278
        match self {
279
            FontChainKeyOrRef::Ref(ptr) => Some(*ptr),
280
            _ => None,
281
        }
282
    }
283
    
284
    /// Returns the FontChainKey if this is a Chain variant
285
    pub fn as_chain(&self) -> Option<&FontChainKey> {
286
        match self {
287
            FontChainKeyOrRef::Chain(key) => Some(key),
288
            _ => None,
289
        }
290
    }
291
}
292

            
293
impl FontChainKey {
294
    /// Create a FontChainKey from a slice of font selectors
295
26312
    pub fn from_selectors(font_stack: &[FontSelector]) -> Self {
296
        // (2026-06-10) FIRST-WINS DEDUP: cascaded font stacks can carry duplicate
297
        // families (e.g. [serif, sans-serif, serif, monospace] when the UA fallback
298
        // list is appended to a stack already naming serif). The pre-resolve
299
        // collector dedupes its stacks, so without deduping HERE the shaping-time
300
        // key never matched the stored key (the g121/g122 chain-lookup misses).
301
        // This is THE canonical FontChainKey constructor — every key-build site
302
        // must go through it so lookups match by construction.
303
26312
        let mut font_families: Vec<String> = Vec::new();
304
111320
        for sel in font_stack {
305
112332
            if sel.family.is_empty() || font_families.iter().any(|f| *f == sel.family) {
306
                continue;
307
85008
            }
308
85008
            font_families.push(sel.family.clone());
309
        }
310

            
311
26312
        let font_families = if font_families.is_empty() {
312
            vec!["serif".to_string()]
313
        } else {
314
26312
            font_families
315
        };
316

            
317
26312
        let weight = font_stack
318
26312
            .first()
319
26312
            .map(|s| s.weight)
320
26312
            .unwrap_or(FcWeight::Normal);
321
26312
        let is_italic = font_stack
322
26312
            .first()
323
26312
            .map(|s| s.style == FontStyle::Italic)
324
26312
            .unwrap_or(false);
325
26312
        let is_oblique = font_stack
326
26312
            .first()
327
26312
            .map(|s| s.style == FontStyle::Oblique)
328
26312
            .unwrap_or(false);
329

            
330
26312
        FontChainKey {
331
26312
            font_families,
332
26312
            weight,
333
26312
            italic: is_italic,
334
26312
            oblique: is_oblique,
335
26312
        }
336
26312
    }
337
}
338

            
339
/// A map of pre-loaded fonts, keyed by FontId (from rust-fontconfig)
340
///
341
/// This is passed to the shaper - no font loading happens during shaping
342
/// The fonts are loaded BEFORE layout based on the font chains and text content.
343
///
344
/// Provides both FontId and hash-based lookup for efficient glyph operations.
345
#[derive(Debug, Clone)]
346
pub struct LoadedFonts<T> {
347
    /// Primary storage: FontId -> Font
348
    pub fonts: HashMap<FontId, T>,
349
    /// Reverse index: font_hash -> FontId for fast hash-based lookups
350
    hash_to_id: HashMap<u64, FontId>,
351
}
352

            
353
impl<T: ParsedFontTrait> LoadedFonts<T> {
354
67833
    pub fn new() -> Self {
355
67833
        Self {
356
67833
            fonts: HashMap::new(),
357
67833
            hash_to_id: HashMap::new(),
358
67833
        }
359
67833
    }
360

            
361
    /// Insert a font with its FontId
362
441202
    pub fn insert(&mut self, font_id: FontId, font: T) {
363
441202
        let hash = font.get_hash();
364
441202
        self.hash_to_id.insert(hash, font_id.clone());
365
441202
        self.fonts.insert(font_id, font);
366
441202
    }
367

            
368
    /// Get a font by FontId
369
18559
    pub fn get(&self, font_id: &FontId) -> Option<&T> {
370
18559
        self.fonts.get(font_id)
371
18559
    }
372

            
373
    /// Get a font by its hash
374
    pub fn get_by_hash(&self, hash: u64) -> Option<&T> {
375
        self.hash_to_id.get(&hash).and_then(|id| self.fonts.get(id))
376
    }
377

            
378
    /// Get the FontId for a hash
379
    pub fn get_font_id_by_hash(&self, hash: u64) -> Option<&FontId> {
380
        self.hash_to_id.get(&hash)
381
    }
382

            
383
    /// Check if a FontId is present
384
    pub fn contains_key(&self, font_id: &FontId) -> bool {
385
        self.fonts.contains_key(font_id)
386
    }
387

            
388
    /// Check if a hash is present
389
    pub fn contains_hash(&self, hash: u64) -> bool {
390
        self.hash_to_id.contains_key(&hash)
391
    }
392

            
393
    /// Iterate over all fonts
394
    pub fn iter(&self) -> impl Iterator<Item = (&FontId, &T)> {
395
        self.fonts.iter()
396
    }
397

            
398
    /// Get the number of loaded fonts
399
    pub fn len(&self) -> usize {
400
        self.fonts.len()
401
    }
402

            
403
    /// Check if empty
404
    pub fn is_empty(&self) -> bool {
405
        self.fonts.is_empty()
406
    }
407
}
408

            
409
impl<T: ParsedFontTrait> Default for LoadedFonts<T> {
410
    fn default() -> Self {
411
        Self::new()
412
    }
413
}
414

            
415
impl<T: ParsedFontTrait> FromIterator<(FontId, T)> for LoadedFonts<T> {
416
67833
    fn from_iter<I: IntoIterator<Item = (FontId, T)>>(iter: I) -> Self {
417
67833
        let mut loaded = LoadedFonts::new();
418
509035
        for (id, font) in iter {
419
441202
            loaded.insert(id, font);
420
441202
        }
421
67833
        loaded
422
67833
    }
423
}
424

            
425
/// Enum that wraps either a fontconfig-resolved font (T) or a direct FontRef.
426
///
427
/// This allows the shaping code to handle both fontconfig-resolved fonts
428
/// and embedded fonts (FontRef) uniformly through the ParsedFontTrait interface.
429
#[derive(Debug, Clone)]
430
pub enum FontOrRef<T> {
431
    /// A font loaded via fontconfig
432
    Font(T),
433
    /// A direct FontRef (embedded font, bypasses fontconfig)
434
    Ref(azul_css::props::basic::FontRef),
435
}
436

            
437
impl<T: ParsedFontTrait> ShallowClone for FontOrRef<T> {
438
    fn shallow_clone(&self) -> Self {
439
        match self {
440
            FontOrRef::Font(f) => FontOrRef::Font(f.shallow_clone()),
441
            FontOrRef::Ref(r) => FontOrRef::Ref(r.clone()),
442
        }
443
    }
444
}
445

            
446
impl<T: ParsedFontTrait> ParsedFontTrait for FontOrRef<T> {
447
    fn shape_text(
448
        &self,
449
        text: &str,
450
        script: Script,
451
        language: Language,
452
        direction: BidiDirection,
453
        style: &StyleProperties,
454
    ) -> Result<Vec<Glyph>, LayoutError> {
455
        match self {
456
            FontOrRef::Font(f) => f.shape_text(text, script, language, direction, style),
457
            FontOrRef::Ref(r) => r.shape_text(text, script, language, direction, style),
458
        }
459
    }
460

            
461
    fn get_hash(&self) -> u64 {
462
        match self {
463
            FontOrRef::Font(f) => f.get_hash(),
464
            FontOrRef::Ref(r) => r.get_hash(),
465
        }
466
    }
467

            
468
    fn get_glyph_size(&self, glyph_id: u16, font_size: f32) -> Option<LogicalSize> {
469
        match self {
470
            FontOrRef::Font(f) => f.get_glyph_size(glyph_id, font_size),
471
            FontOrRef::Ref(r) => r.get_glyph_size(glyph_id, font_size),
472
        }
473
    }
474

            
475
    fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
476
        match self {
477
            FontOrRef::Font(f) => f.get_hyphen_glyph_and_advance(font_size),
478
            FontOrRef::Ref(r) => r.get_hyphen_glyph_and_advance(font_size),
479
        }
480
    }
481

            
482
    fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
483
        match self {
484
            FontOrRef::Font(f) => f.get_kashida_glyph_and_advance(font_size),
485
            FontOrRef::Ref(r) => r.get_kashida_glyph_and_advance(font_size),
486
        }
487
    }
488

            
489
    fn has_glyph(&self, codepoint: u32) -> bool {
490
        match self {
491
            FontOrRef::Font(f) => f.has_glyph(codepoint),
492
            FontOrRef::Ref(r) => r.has_glyph(codepoint),
493
        }
494
    }
495

            
496
    fn get_vertical_metrics(&self, glyph_id: u16) -> Option<VerticalMetrics> {
497
        match self {
498
            FontOrRef::Font(f) => f.get_vertical_metrics(glyph_id),
499
            FontOrRef::Ref(r) => r.get_vertical_metrics(glyph_id),
500
        }
501
    }
502

            
503
    fn get_font_metrics(&self) -> LayoutFontMetrics {
504
        match self {
505
            FontOrRef::Font(f) => f.get_font_metrics(),
506
            FontOrRef::Ref(r) => r.get_font_metrics(),
507
        }
508
    }
509

            
510
    fn num_glyphs(&self) -> u16 {
511
        match self {
512
            FontOrRef::Font(f) => f.num_glyphs(),
513
            FontOrRef::Ref(r) => r.num_glyphs(),
514
        }
515
    }
516

            
517
    fn get_space_width(&self) -> Option<usize> {
518
        match self {
519
            FontOrRef::Font(f) => f.get_space_width(),
520
            FontOrRef::Ref(r) => r.get_space_width(),
521
        }
522
    }
523
}
524

            
525
/// Bundles all font-related state that can be shared across layout passes.
526
///
527
/// Separates font concerns from layout/rendering state (`LayoutWindow`).
528
/// Each test/render creates a fresh `LayoutWindow` from a shared `FontContext`,
529
/// avoiding stale layout cache reuse while keeping parsed fonts warm.
530
///
531
/// Usage:
532
/// ```ignore
533
/// let ctx = FontContext::from_fc_cache(fc_cache);
534
/// ctx.pre_resolve_chains(&styled_dom, &platform);
535
/// ctx.load_fonts_for_chains();
536
///
537
/// // Per-test: create fresh LayoutWindow from context
538
/// let mut window = LayoutWindow::from_font_context(&ctx)?;
539
/// window.layout_and_generate_display_list(styled_dom, ...)?;
540
/// ```
541
#[derive(Debug, Clone)]
542
pub struct FontContext {
543
    /// The shared font cache. As of rust-fontconfig 4.1 this type is
544
    /// itself backed by `Arc<RwLock<_>>`, so cloning is cheap and all
545
    /// clones see builder-thread writes immediately — no more `Arc<T>`
546
    /// wrapping is needed and no more stale-snapshot refresh dance.
547
    pub fc_cache: FcFontCache,
548
    pub parsed_fonts: Arc<Mutex<HashMap<FontId, azul_css::props::basic::FontRef>>>,
549
    pub font_chain_cache: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
550
    pub embedded_fonts: HashMap<u64, azul_css::props::basic::FontRef>,
551
    /// Reverse map: font_family_hash → actual StyleFontFamilyVec.
552
    /// Accumulated across DOMs for persistence. Copied to FontManager on LayoutWindow creation.
553
    pub font_hash_to_families: HashMap<u64, azul_css::props::basic::font::StyleFontFamilyVec>,
554
    /// Optional link back to the live `FcFontRegistry`. Present iff the
555
    /// caller wants the scout-on-demand path
556
    /// ([`rust_fontconfig::registry::FcFontRegistry::request_and_resolve_with_scripts`]),
557
    /// which priority-bumps the builder for not-yet-parsed families
558
    /// rather than falling back to the empty-snapshot response.
559
    pub registry: Option<Arc<rust_fontconfig::registry::FcFontRegistry>>,
560
}
561

            
562
impl FontContext {
563
    /// Create from an `FcFontCache`. Parsed fonts, font chains, and
564
    /// embedded fonts start empty.
565
    ///
566
    /// The resulting `FontContext` has `registry = None`, so font
567
    /// chain resolution only sees what's already in the cache. For
568
    /// the scout-on-demand path, use [`FontContext::from_registry`]
569
    /// instead, which keeps a handle to the registry so that chain
570
    /// resolution can lazy-parse families the DOM needs.
571
    pub fn from_fc_cache(fc_cache: FcFontCache) -> Self {
572
        Self {
573
            fc_cache,
574
            parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
575
            font_chain_cache: HashMap::new(),
576
            embedded_fonts: HashMap::new(),
577
            font_hash_to_families: HashMap::new(),
578
            registry: None,
579
        }
580
    }
581

            
582
    /// Create from a live `FcFontRegistry`. The `fc_cache` field gets
583
    /// a *shared* handle to the registry's cache (cheap `Arc::clone`
584
    /// on the v4.1 shared-state cache) — writes by builder threads
585
    /// show up immediately in every reader. Chain resolution goes
586
    /// through
587
    /// [`rust_fontconfig::registry::FcFontRegistry::request_and_resolve_with_scripts`]
588
    /// which priority-bumps the builder for unparsed families and
589
    /// waits for them. This is the "scout-on-demand" path: a
590
    /// headless renderer can skip the eager common-stack parse and
591
    /// pay only the per-family cost on first use, dropping peak RSS
592
    /// by the common-stack metadata size (~15 MiB on macOS).
593
    pub fn from_registry(
594
        registry: Arc<rust_fontconfig::registry::FcFontRegistry>,
595
    ) -> Self {
596
        let fc_cache = registry.shared_cache();
597
        Self {
598
            fc_cache,
599
            parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
600
            font_chain_cache: HashMap::new(),
601
            embedded_fonts: HashMap::new(),
602
            font_hash_to_families: HashMap::new(),
603
            registry: Some(registry),
604
        }
605
    }
606

            
607
    /// Pre-resolve font chains for a StyledDom's CSS font stacks.
608
    /// Call this before layout so text rendering doesn't skip glyphs.
609
    ///
610
    /// Unicode-fallback fonts are limited to the scripts actually
611
    /// present in the document's text content — for an ASCII-only
612
    /// page, this skips the ~300 MiB Arial-Unicode / CJK / Arabic
613
    /// pull-in entirely. See
614
    /// [`crate::solver3::getters::scripts_present_in_styled_dom`].
615
    pub fn pre_resolve_chains_for_dom(
616
        &mut self,
617
        styled_dom: &azul_core::styled_dom::StyledDom,
618
        platform: &azul_css::system::Platform,
619
    ) {
620
        use crate::solver3::getters::{
621
            collect_font_stacks_from_styled_dom, collect_used_codepoints,
622
            prune_chain_to_used_chars, resolve_font_chains, scripts_present_in_styled_dom,
623
        };
624
        let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
625
        let scripts = scripts_present_in_styled_dom(styled_dom);
626
        let mut chains = resolve_font_chains(&collected, &self.fc_cache, Some(&scripts));
627
        // Coverage-based prune (matches `collect_and_resolve_font_chains_with_registration`).
628
        let used_chars = collect_used_codepoints(styled_dom);
629
        for chain in chains.chains.values_mut() {
630
            prune_chain_to_used_chars(chain, &used_chars);
631
        }
632
        // WEB-LIFT last resort (after prune, so it survives — prune drops the registered
633
        // fallback because its cmap isn't parsed yet): if a chain ended up with no fonts,
634
        // append the first registered font so load_missing_for_chains finds it and text
635
        // shapes instead of measuring 0. (Done in azul-layout, NOT rust-fontconfig, so the
636
        // lift-fragile with_memory_fonts isn't re-codegen'd into a trapping shape.)
637
        for chain in chains.chains.values_mut() {
638
            let total = chain.css_fallbacks.iter().map(|g| g.fonts.len()).sum::<usize>()
639
                + chain.unicode_fallbacks.len();
640
            if total == 0 {
641
                if let Some((pattern, id)) = self.fc_cache.list().first() {
642
                    chain.unicode_fallbacks.push(rust_fontconfig::FontMatch {
643
                        id: *id,
644
                        unicode_ranges: pattern.unicode_ranges.clone(),
645
                        fallbacks: Vec::new(),
646
                    });
647
                }
648
            }
649
        }
650
        self.font_chain_cache = chains.into_fontconfig_chains();
651
    }
652

            
653
    /// Load parsed font bytes from disk for all fonts referenced in `font_chain_cache`.
654
    ///
655
    /// Thin wrapper that materialises a `ResolvedFontChains` from the
656
    /// cached chain map and delegates the actual disk-load to the
657
    /// shared `FontManager::load_missing_for_chains` helper, so the
658
    /// "collect → diff → load → insert" sequence lives in exactly
659
    /// one place. Failures are silently dropped here (the caller is
660
    /// the warmup path which has no good place to log them); use
661
    /// `FontManager::load_missing_for_chains` directly for diagnostics.
662
    pub fn load_fonts_for_chains(&self) {
663
        use crate::solver3::getters::ResolvedFontChains;
664
        use crate::text3::default::PathLoader;
665

            
666
        let chains_map: HashMap<FontChainKeyOrRef, _> = self
667
            .font_chain_cache
668
            .iter()
669
            .map(|(k, v)| (FontChainKeyOrRef::Chain(k.clone()), v.clone()))
670
            .collect();
671
        let resolved = ResolvedFontChains { chains: chains_map };
672

            
673
        // Borrow our shared `parsed_fonts` Arc as a transient
674
        // FontManager so we can use the helper. `from_arc_shared`
675
        // returns a manager that mutates the same underlying pool.
676
        let manager = match FontManager::<azul_css::props::basic::FontRef>::from_arc_shared(
677
            self.fc_cache.clone(),
678
            self.parsed_fonts.clone(),
679
        ) {
680
            Ok(m) => m,
681
            Err(_) => return,
682
        };
683
        let loader = PathLoader::new();
684
        let _failed = manager
685
            .load_missing_for_chains(&resolved, |bytes, idx| loader.load_font_shared(bytes, idx));
686
    }
687

            
688
    /// Convert into a `FontManager` with all data populated.
689
    /// Carries the `registry` forward so the resulting manager also
690
    /// has the scout-on-demand path available.
691
    pub fn to_font_manager(&self) -> FontManager<azul_css::props::basic::FontRef> {
692
        FontManager {
693
            fc_cache: self.fc_cache.clone(),
694
            parsed_fonts: self.parsed_fonts.clone(),
695
            font_chain_cache: self.font_chain_cache.clone(),
696
            embedded_fonts: Mutex::new(self.embedded_fonts.clone()),
697
            font_hash_to_families: self.font_hash_to_families.clone(),
698
            registry: self.registry.clone(),
699
            last_resolved_font_stacks_sig: None,
700
        }
701
    }
702
}
703

            
704
#[derive(Debug)]
705
pub struct FontManager<T> {
706
    /// The font-path cache. `FcFontCache` in rust-fontconfig 4.1 is
707
    /// already a shared handle internally (`Arc<RwLock<_>>`), so no
708
    /// further `Arc<...>` wrapping is needed — clones are cheap and
709
    /// all clones see builder writes instantly.
710
    pub fc_cache: FcFontCache,
711
    /// Holds the actual parsed font (usually with the font bytes attached).
712
    /// Wrapped in Arc so multiple FontManager instances can share the same
713
    /// pool of already-parsed fonts (avoids re-reading from disk).
714
    pub parsed_fonts: Arc<Mutex<HashMap<FontId, T>>>,
715
    // Cache for font chains - populated by resolve_all_font_chains() before layout
716
    // This is read-only during layout - no locking needed for reads
717
    pub font_chain_cache: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
718
    /// Cache for direct FontRefs (embedded fonts like Material Icons)
719
    /// These are fonts referenced via FontStack::Ref that bypass fontconfig
720
    pub embedded_fonts: Mutex<HashMap<u64, azul_css::props::basic::FontRef>>,
721
    /// Reverse map: font_family_hash → actual StyleFontFamilyVec.
722
    /// Accumulated across DOMs. Used by font collection and text shaping to
723
    /// resolve compact cache hashes without get_property_slow.
724
    pub font_hash_to_families: HashMap<u64, azul_css::props::basic::font::StyleFontFamilyVec>,
725
    /// Optional link back to the live `FcFontRegistry`. When present,
726
    /// chain resolution uses
727
    /// [`rust_fontconfig::registry::FcFontRegistry::request_and_resolve_with_scripts`]
728
    /// which lazy-parses system fonts as the DOM requests them
729
    /// (scout-on-demand). `None` falls back to querying whatever is
730
    /// already in the shared cache.
731
    pub registry: Option<Arc<rust_fontconfig::registry::FcFontRegistry>>,
732
    /// FxHash of the `prev_font_hashes` slice at the moment the last
733
    /// successful `collect_and_resolve_font_chains_with_registration`
734
    /// call populated `font_chain_cache`. Lets repeated layouts of the
735
    /// same DOM skip the ~1.5 ms (cold) / ~0.9 ms (warm) chain resolver
736
    /// when the set of font-family hashes has not changed. Cleared
737
    /// whenever `font_chain_cache` is explicitly emptied.
738
    pub last_resolved_font_stacks_sig: Option<u64>,
739
}
740

            
741
impl<T: ParsedFontTrait> FontManager<T> {
742
4297
    pub fn new(fc_cache: FcFontCache) -> Result<Self, LayoutError> {
743
4297
        Ok(Self {
744
4297
            fc_cache,
745
4297
            parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
746
4297
            font_chain_cache: HashMap::new(),
747
4297
            embedded_fonts: Mutex::new(HashMap::new()),
748
4297
            font_hash_to_families: HashMap::new(),
749
4297
            registry: None,
750
4297
            last_resolved_font_stacks_sig: None,
751
4297
        })
752
4297
    }
753

            
754
    /// Create a FontManager sharing the font-path cache handle.
755
    ///
756
    /// The parsed_fonts pool starts empty. Fonts loaded during the first
757
    /// layout pass are cached and will be available on subsequent calls
758
    /// if you clone the `parsed_fonts` Arc before creating the next instance.
759
    /// For full sharing, prefer `from_arc_shared()`.
760
    pub fn from_shared(fc_cache: FcFontCache) -> Result<Self, LayoutError> {
761
        Ok(Self {
762
            fc_cache,
763
            parsed_fonts: Arc::new(Mutex::new(HashMap::new())),
764
            font_chain_cache: HashMap::new(),
765
            embedded_fonts: Mutex::new(HashMap::new()),
766
            font_hash_to_families: HashMap::new(),
767
            registry: None,
768
            last_resolved_font_stacks_sig: None,
769
        })
770
    }
771

            
772
    /// Create a FontManager sharing both the font-path cache and the
773
    /// already-parsed font data with another FontManager.
774
    ///
775
    /// This avoids re-reading and re-parsing font files from disk when
776
    /// rendering multiple documents that use the same fonts.
777
748
    pub fn from_arc_shared(
778
748
        fc_cache: FcFontCache,
779
748
        parsed_fonts: Arc<Mutex<HashMap<FontId, T>>>,
780
748
    ) -> Result<Self, LayoutError> {
781
748
        Ok(Self {
782
748
            fc_cache,
783
748
            parsed_fonts,
784
748
            font_chain_cache: HashMap::new(),
785
748
            embedded_fonts: Mutex::new(HashMap::new()),
786
748
            font_hash_to_families: HashMap::new(),
787
748
            registry: None,
788
748
            last_resolved_font_stacks_sig: None,
789
748
        })
790
748
    }
791

            
792
    /// Attach a `FcFontRegistry` to this FontManager so subsequent
793
    /// chain-resolution calls use the on-demand path
794
    /// ([`rust_fontconfig::registry::FcFontRegistry::request_and_resolve_with_scripts`]).
795
    pub fn with_registry(
796
        mut self,
797
        registry: Arc<rust_fontconfig::registry::FcFontRegistry>,
798
    ) -> Self {
799
        self.registry = Some(registry);
800
        self
801
    }
802

            
803
    /// Get a shareable handle to the parsed-font pool.
804
    ///
805
    /// Pass this to `from_arc_shared()` to create a new FontManager that
806
    /// reuses already-parsed fonts.
807
    pub fn shared_parsed_fonts(&self) -> Arc<Mutex<HashMap<FontId, T>>> {
808
        Arc::clone(&self.parsed_fonts)
809
    }
810

            
811
    /// Set the font chain cache from externally resolved chains
812
    ///
813
    /// This should be called with the result of `resolve_font_chains()` or
814
    /// `collect_and_resolve_font_chains()` from `solver3::getters`.
815
809
    pub fn set_font_chain_cache(
816
809
        &mut self,
817
809
        chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
818
809
    ) {
819
809
        self.font_chain_cache = chains;
820
809
        self.last_resolved_font_stacks_sig = None;
821
809
    }
822

            
823
    /// Set the font chain cache and record the input signature so
824
    /// subsequent layouts with the same `prev_font_hashes` skip the
825
    /// resolver. Pass `sig = None` if the caller cannot compute a
826
    /// reliable signature — equivalent to the single-arg
827
    /// `set_font_chain_cache`.
828
3872
    pub fn set_font_chain_cache_with_sig(
829
3872
        &mut self,
830
3872
        chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
831
3872
        sig: Option<u64>,
832
3872
    ) {
833
        // (2026-06-10: reverted to HashMap — the empty-map RawIter hang behind the 2026-06-05
834
        // BTreeMap migration was the un-mirrored hashbrown EMPTY_GROUP static, fixed
835
        // transpiler-side.)
836
3872
        self.font_chain_cache = chains;
837
3872
        self.last_resolved_font_stacks_sig = sig;
838
3872
    }
839

            
840
    /// Merge additional font chains into the existing cache
841
    ///
842
    /// Useful when processing multiple DOMs that may have different font requirements.
843
    pub fn merge_font_chain_cache(
844
        &mut self,
845
        chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
846
    ) {
847
        self.font_chain_cache.extend(chains);
848
    }
849

            
850
    /// Get a reference to the font chain cache
851
572
    pub fn get_font_chain_cache(
852
572
        &self,
853
572
    ) -> &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain> {
854
572
        &self.font_chain_cache
855
572
    }
856

            
857
    /// Get an embedded font by its hash (used for WebRender registration)
858
    /// Returns the FontRef if it exists in the embedded_fonts cache.
859
    pub fn get_embedded_font_by_hash(&self, font_hash: u64) -> Option<azul_css::props::basic::FontRef> {
860
        let embedded = self.embedded_fonts.lock().unwrap();
861
        embedded.get(&font_hash).cloned()
862
    }
863

            
864
    /// Get a parsed font by its hash (used for WebRender registration)
865
    /// Returns the parsed font if it exists in the parsed_fonts cache.
866
2114
    pub fn get_font_by_hash(&self, font_hash: u64) -> Option<T> {
867
2114
        let parsed = self.parsed_fonts.lock().unwrap();
868
        // Linear search through all cached fonts to find one with matching hash
869
8010
        for (_, font) in parsed.iter() {
870
8010
            if font.get_hash() == font_hash {
871
2113
                return Some(font.clone());
872
5897
            }
873
        }
874
1
        None
875
2114
    }
876

            
877
    /// Register an embedded FontRef for later lookup by hash
878
    /// This is called when using FontStack::Ref during shaping
879
    pub fn register_embedded_font(&self, font_ref: &azul_css::props::basic::FontRef) {
880
        let hash = font_ref.get_hash();
881
        let mut embedded = self.embedded_fonts.lock().unwrap();
882
        embedded.insert(hash, font_ref.clone());
883
    }
884

            
885
    /// Get a snapshot of all currently loaded fonts
886
    ///
887
    /// This returns a copy of all parsed fonts, which can be passed to the shaper.
888
    /// No locking is required after this call - the returned HashMap is independent.
889
    ///
890
    /// NOTE: This should be called AFTER loading all required fonts for a layout pass.
891
67833
    pub fn get_loaded_fonts(&self) -> LoadedFonts<T> {
892
67833
        let parsed = self.parsed_fonts.lock().unwrap();
893
67833
        parsed
894
67833
            .iter()
895
441202
            .map(|(id, font)| (id.clone(), font.shallow_clone()))
896
67833
            .collect()
897
67833
    }
898

            
899
    /// Get the set of FontIds that are currently loaded
900
    ///
901
    /// This is useful for computing which fonts need to be loaded
902
    /// (diff with required fonts).
903
4681
    pub fn get_loaded_font_ids(&self) -> std::collections::HashSet<FontId> {
904
4681
        let parsed = self.parsed_fonts.lock().unwrap();
905
        // M12.7: skip hashbrown's RawIterRange on an empty map — its NEON
906
        // control-byte group-scan mis-lifts to wasm and iterates forever
907
        // (the headless web layout uses an empty font cache → parsed is
908
        // empty here). is_empty() is len-based (no iteration), so it is safe.
909
4681
        if parsed.is_empty() {
910
4285
            return std::collections::HashSet::new();
911
396
        }
912
396
        unsafe { crate::az_mark(0x60788, 0xA1) };
913
396
        let out = parsed.keys().cloned().collect();
914
396
        unsafe { crate::az_mark(0x6078C, 0xA2) };
915
396
        out
916
4681
    }
917

            
918
    /// Insert a loaded font into the cache
919
    ///
920
    /// Returns the old font if one was already present for this FontId.
921
1
    pub fn insert_font(&self, font_id: FontId, font: T) -> Option<T> {
922
1
        let mut parsed = self.parsed_fonts.lock().unwrap();
923
1
        parsed.insert(font_id, font)
924
1
    }
925

            
926
    /// Insert multiple loaded fonts into the cache
927
    ///
928
    /// This is more efficient than calling `insert_font` multiple times
929
    /// because it only acquires the lock once.
930
2695
    pub fn insert_fonts(&self, fonts: impl IntoIterator<Item = (FontId, T)>) {
931
2695
        let mut parsed = self.parsed_fonts.lock().unwrap();
932
9933
        for (font_id, font) in fonts {
933
7238
            parsed.insert(font_id, font);
934
7238
        }
935
2695
    }
936

            
937
    /// One-shot helper that resolves "what fonts does `chains` need
938
    /// that this manager hasn't loaded yet" and loads them via the
939
    /// supplied `load_fn` closure (typically
940
    /// `PathLoader::load_font_shared` for the production lazy-decode
941
    /// path). Updates `parsed_fonts` in place and returns any failures
942
    /// for the caller to log.
943
    ///
944
    /// Replaces the same four-step `collect → compute_diff →
945
    /// load_from_disk → insert_fonts` dance previously inlined in
946
    /// `LayoutWindow::layout_document`, the CPU rasterizer pre-fill
947
    /// in `cpurender.rs`, and `FontContext::load_fonts_for_chains`.
948
4620
    pub fn load_missing_for_chains<F>(
949
4620
        &self,
950
4620
        chains: &crate::solver3::getters::ResolvedFontChains,
951
4620
        load_fn: F,
952
4620
    ) -> Vec<(FontId, String)>
953
4620
    where
954
4620
        F: Fn(std::sync::Arc<rust_fontconfig::FontBytes>, usize) -> Result<T, LayoutError>,
955
    {
956
        use crate::solver3::getters::{
957
            collect_font_ids_from_chains, compute_fonts_to_load, load_fonts_from_disk,
958
        };
959
4620
        let required = collect_font_ids_from_chains(chains);
960
4620
        let already = self.get_loaded_font_ids();
961
4620
        let to_load = compute_fonts_to_load(&required, &already);
962
4620
        if to_load.is_empty() {
963
1980
            return Vec::new();
964
2640
        }
965
2640
        let result = load_fonts_from_disk(&to_load, &self.fc_cache, load_fn);
966
2640
        self.insert_fonts(result.loaded);
967
2640
        result.failed
968
4620
    }
969

            
970
    /// Remove a font from the cache
971
    ///
972
    /// Returns the removed font if it was present.
973
    pub fn remove_font(&self, font_id: &FontId) -> Option<T> {
974
        let mut parsed = self.parsed_fonts.lock().unwrap();
975
        parsed.remove(font_id)
976
    }
977
}
978

            
979
// Error handling
980
// [g119 az-web-lift FIX] `#[repr(C, u8)]` (was repr(Rust)): the String/FontSelector payloads give
981
// `Result<T, LayoutError>` (e.g. measure_intrinsic_widths' return + reorder/shape/orientation `?`)
982
// a POINTER-niche disc the web lift mis-reads → Ok→Err. Explicit u8 tag = simple-compare niche the
983
// lift handles. Also nested in solver3::LayoutError::Text (so both must be repr(C,u8)). Not FFI-exposed.
984
#[derive(Debug, thiserror::Error)]
985
#[repr(C, u8)]
986
pub enum LayoutError {
987
    #[error("Bidi analysis failed: {0}")]
988
    BidiError(String),
989
    #[error("Shaping failed: {0}")]
990
    ShapingError(String),
991
    #[error("Font not found: {0:?}")]
992
    FontNotFound(FontSelector),
993
    #[error("Invalid text input: {0}")]
994
    InvalidText(String),
995
    #[error("Hyphenation failed: {0}")]
996
    HyphenationError(String),
997
}
998

            
999
/// Text boundary types for cursor movement
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextBoundary {
    /// Reached top of text (first line)
    Top,
    /// Reached bottom of text (last line)
    Bottom,
    /// Reached start of text (first character)
    Start,
    /// Reached end of text (last character)
    End,
}
/// Error returned when cursor movement hits a boundary
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CursorBoundsError {
    /// The boundary that was hit
    pub boundary: TextBoundary,
    /// The cursor position (unchanged from input)
    pub cursor: TextCursor,
}
/// Unified constraints combining all layout features
///
/// # CSS Inline Layout Module Level 3: Constraint Mapping
///
/// This structure maps CSS properties to layout constraints:
///
/// ## \u00a7 2.1 Layout of Line Boxes
/// - `available_width`: \u26a0\ufe0f CRITICAL - Should equal containing block's inner width
///   * Currently defaults to 0.0 which causes immediate line breaking
///   * Per spec: "logical width of a line box is equal to the inner logical width of its containing
///     block"
/// - `available_height`: For block-axis constraints (max-height)
///
/// ## \u00a7 2.2 Layout Within Line Boxes
/// - `text_align`: \u2705 Horizontal alignment (start, end, center, justify)
/// - `vertical_align`: \u26a0\ufe0f PARTIAL - Only baseline supported, missing:
///   * top, bottom, middle, text-top, text-bottom
///   * <length>, <percentage> values
///   * sub, super positions
/// - `line_height`: \u2705 Distance between baselines
///
/// ## \u00a7 3 Baselines and Alignment Metrics
/// - `text_orientation`: \u2705 For vertical writing (sideways, upright)
/// - `writing_mode`: \u2705 horizontal-tb, vertical-rl, vertical-lr
/// - `direction`: \u2705 ltr, rtl for BiDi
///
/// ## \u00a7 4 Baseline Alignment (vertical-align property)
/// \u26a0\ufe0f INCOMPLETE: Only basic baseline alignment implemented
///
/// ## \u00a7 5 Line Spacing (line-height property)
/// - `line_height`: \u2705 Implemented
/// - \u274c MISSING: line-fit-edge for controlling which edges contribute to line height
/// +spec:box-model:51342f - inline box margins/borders/padding do not affect line box height (default leading mode)
/// +spec:font-metrics:618776 - line-fit-edge (cap, ex, ideographic, alphabetic edge selection) not yet implemented
///
/// ## \u00a7 6 Trimming Leading (text-box-trim)
/// - \u274c NOT IMPLEMENTED: text-box-trim property
/// - \u274c NOT IMPLEMENTED: text-box-edge property
/// +spec:box-model:c09331 - text-box-trim trims block container first/last line to font metrics
/// // +spec:overflow:dc2196 - text-box-trim overflow handled as normal overflow (no special handling needed)
///
/// ## CSS Text Module Level 3
/// - `text_indent`: \u2705 First line indentation
/// - `text_justify`: \u2705 Justification algorithm (auto, inter-word, inter-character)
/// - `hyphenation`: \u2705 Hyphens property (none / manual / auto)
/// - `hanging_punctuation`: \u2705 Hanging punctuation at line edges
///
/// ## CSS Text Level 4
/// - `text_wrap`: \u2705 balance, pretty, stable
/// - `line_clamp`: \u2705 Max number of lines
///
/// ## CSS Writing Modes Level 4
/// - `text_combine_upright`: \u2705 Tate-chu-yoko for vertical text
///
/// ## CSS Shapes Module
/// - `shape_boundaries`: \u2705 Custom line box shapes
/// - `shape_exclusions`: \u2705 Exclusion areas (float-like behavior)
/// - `exclusion_margin`: \u2705 Margin around exclusions
///
/// ## Multi-column Layout
/// - `columns`: \u2705 Number of columns
/// - `column_gap`: \u2705 Gap between columns
///
/// # Known Issues:
/// 1. [ISSUE] available_width defaults to Definite(0.0) instead of containing block width
/// 2. [ISSUE] vertical_align only supports baseline
/// 3. [TODO] initial-letter (drop caps) not implemented
// +spec:box-model:415ef3 - initial letters use standard margin/padding/border box model; exclusion area = margin box
// +spec:box-model:d53ea3 - when block-start padding+border are zero, content edge coincides with over alignment point
/// +spec:positioning:fb233a - initial letter block-axis: if size < sink, use over alignment
#[derive(Debug, Clone)]
pub struct UnifiedConstraints {
    // Shape definition
    pub shape_boundaries: Vec<ShapeBoundary>,
    pub shape_exclusions: Vec<ShapeBoundary>,
    // Basic layout - using AvailableSpace for proper indefinite handling
    pub available_width: AvailableSpace,
    pub available_height: Option<f32>,
    // Text layout
    pub writing_mode: Option<WritingMode>,
    // +spec:writing-modes:6c5ab9 - blocks inherit base direction from parent via CSS direction property
    // Base direction from CSS, overrides auto-detection
    pub direction: Option<BidiDirection>,
    pub text_orientation: TextOrientation,
    pub text_align: TextAlign,
    pub text_justify: JustifyContent,
    // +spec:display-property:3bcac8 - inline boxes sized in block axis based on font metrics (ascent/descent)
    pub line_height: LineHeight,
    pub vertical_align: VerticalAlign,
    // block container's first available font, used for minimum line box height
    pub strut_ascent: f32,
    pub strut_descent: f32,
    // x-height of the strut font (scaled to font_size), for vertical-align: middle
    pub strut_x_height: f32,
    // Width of '0' (zero) character in px, used for ch unit and tab-size.
    // Approximated as space_width from the first available font, or 0.5 * font_size fallback.
    pub ch_width: f32,
    // Overflow handling
    pub overflow: OverflowBehavior,
    pub segment_alignment: SegmentAlignment,
    // Advanced features
    pub text_combine_upright: Option<TextCombineUpright>,
    pub exclusion_margin: f32,
    pub hyphenation: Hyphens,
    pub hyphenation_language: Option<Language>,
    pub text_indent: f32,
    pub text_indent_each_line: bool,
    pub text_indent_hanging: bool,
    pub initial_letter: Option<InitialLetter>,
    pub line_clamp: Option<NonZeroUsize>,
    // text-wrap: balance
    pub text_wrap: TextWrap,
    pub columns: u32,
    pub column_gap: f32,
    pub hanging_punctuation: bool,
    pub overflow_wrap: OverflowWrap,
    pub text_align_last: TextAlign,
    // §5.2 word-break property on constraints
    pub word_break: WordBreak,
    pub white_space_mode: WhiteSpaceMode,
    pub line_break: LineBreakStrictness,
    // CSS unicode-bidi property; Plaintext causes per-paragraph auto-detection
    pub unicode_bidi: UnicodeBidi,
}
impl Default for UnifiedConstraints {
69828
    fn default() -> Self {
69828
        Self {
69828
            shape_boundaries: Vec::new(),
69828
            shape_exclusions: Vec::new(),
69828

            
69828
            // Use MaxContent as default to avoid premature line breaking.
69828
            // MaxContent means "use intrinsic width" which is appropriate when
69828
            // the containing block's width is not yet known.
69828
            // Previously this was Definite(0.0) which caused each character to
69828
            // wrap to its own line. The actual width should be passed from the 
69828
            // box layout solver (fc.rs) when creating UnifiedConstraints.
69828
            available_width: AvailableSpace::MaxContent,
69828
            available_height: None,
69828
            writing_mode: None,
69828
            direction: None, // Will default to LTR if not specified
69828
            text_orientation: TextOrientation::default(),
69828
            text_align: TextAlign::default(),
69828
            text_justify: JustifyContent::default(),
69828
            line_height: LineHeight::Normal,
69828
            vertical_align: VerticalAlign::default(),
69828
            strut_ascent: 12.8, // 80% of default line-height (typical ratio)
69828
            strut_descent: 3.2, // 20% of default line-height
69828
            strut_x_height: 8.0, // 0.5 * default font_size (16.0), spec fallback
69828
            ch_width: 8.0, // 0.5 * default font_size (16.0)
69828
            overflow: OverflowBehavior::default(),
69828
            segment_alignment: SegmentAlignment::default(),
69828
            text_combine_upright: None,
69828
            exclusion_margin: 0.0,
69828
            hyphenation: Hyphens::default(),
69828
            hyphenation_language: None,
69828
            columns: 1,
69828
            column_gap: 0.0,
69828
            hanging_punctuation: false,
69828
            text_indent: 0.0,
69828
            text_indent_each_line: false,
69828
            text_indent_hanging: false,
69828
            initial_letter: None,
69828
            line_clamp: None,
69828
            text_wrap: TextWrap::default(),
69828
            overflow_wrap: OverflowWrap::default(),
69828
            text_align_last: TextAlign::default(),
69828
            word_break: WordBreak::default(),
69828
            white_space_mode: WhiteSpaceMode::default(),
69828
            line_break: LineBreakStrictness::default(),
69828
            unicode_bidi: UnicodeBidi::default(),
69828
        }
69828
    }
}
// UnifiedConstraints
impl Hash for UnifiedConstraints {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.shape_boundaries.hash(state);
        self.shape_exclusions.hash(state);
        self.available_width.hash(state);
        self.available_height
            .map(|h| h.round() as usize)
            .hash(state);
        self.writing_mode.hash(state);
        self.direction.hash(state);
        self.text_orientation.hash(state);
        self.text_align.hash(state);
        self.text_justify.hash(state);
        self.line_height.hash(state);
        self.vertical_align.hash(state);
        (self.strut_ascent.round() as usize).hash(state);
        (self.strut_descent.round() as usize).hash(state);
        (self.strut_x_height.round() as usize).hash(state);
        (self.ch_width.round() as usize).hash(state);
        self.overflow.hash(state);
        self.segment_alignment.hash(state);
        self.text_combine_upright.hash(state);
        (self.exclusion_margin.round() as usize).hash(state);
        self.hyphenation.hash(state);
        self.hyphenation_language.hash(state);
        (self.text_indent.round() as usize).hash(state);
        self.text_indent_each_line.hash(state);
        self.text_indent_hanging.hash(state);
        self.initial_letter.hash(state);
        self.line_clamp.hash(state);
        self.columns.hash(state);
        (self.column_gap.round() as usize).hash(state);
        self.hanging_punctuation.hash(state);
        self.overflow_wrap.hash(state);
        self.text_align_last.hash(state);
        self.word_break.hash(state);
        self.white_space_mode.hash(state);
        self.line_break.hash(state);
        self.unicode_bidi.hash(state);
    }
}
impl PartialEq for UnifiedConstraints {
    fn eq(&self, other: &Self) -> bool {
        self.shape_boundaries == other.shape_boundaries
            && self.shape_exclusions == other.shape_exclusions
            && self.available_width == other.available_width
            && match (self.available_height, other.available_height) {
                (None, None) => true,
                (Some(h1), Some(h2)) => round_eq(h1, h2),
                _ => false,
            }
            && self.writing_mode == other.writing_mode
            && self.direction == other.direction
            && self.text_orientation == other.text_orientation
            && self.text_align == other.text_align
            && self.text_justify == other.text_justify
            && self.line_height == other.line_height
            && self.vertical_align == other.vertical_align
            && round_eq(self.strut_ascent, other.strut_ascent)
            && round_eq(self.strut_descent, other.strut_descent)
            && round_eq(self.strut_x_height, other.strut_x_height)
            && round_eq(self.ch_width, other.ch_width)
            && self.overflow == other.overflow
            && self.segment_alignment == other.segment_alignment
            && self.text_combine_upright == other.text_combine_upright
            && round_eq(self.exclusion_margin, other.exclusion_margin)
            && self.hyphenation == other.hyphenation
            && self.hyphenation_language == other.hyphenation_language
            && round_eq(self.text_indent, other.text_indent)
            && self.text_indent_each_line == other.text_indent_each_line
            && self.text_indent_hanging == other.text_indent_hanging
            && self.initial_letter == other.initial_letter
            && self.line_clamp == other.line_clamp
            && self.columns == other.columns
            && round_eq(self.column_gap, other.column_gap)
            && self.hanging_punctuation == other.hanging_punctuation
            && self.overflow_wrap == other.overflow_wrap
            && self.text_align_last == other.text_align_last
            && self.word_break == other.word_break
            && self.white_space_mode == other.white_space_mode
            && self.line_break == other.line_break
            && self.unicode_bidi == other.unicode_bidi
    }
}
impl Eq for UnifiedConstraints {}
impl UnifiedConstraints {
    /// Resolve `line_height` to a pixel value using the strut metrics as a font-size proxy.
    /// `strut_ascent + strut_descent` approximates `font_size` (the block container's font).
255860
    pub fn resolved_line_height(&self) -> f32 {
255860
        let font_size_approx = self.strut_ascent + self.strut_descent;
255860
        self.line_height.resolve(font_size_approx, 0.0, 0.0, 0.0, 0)
255860
    }
    fn direction(&self, fallback: BidiDirection) -> BidiDirection {
        match self.writing_mode {
            Some(s) => s.get_direction().unwrap_or(fallback),
            None => fallback,
        }
    }
155056
    fn is_vertical(&self) -> bool {
155056
        matches!(
134244
            self.writing_mode,
            Some(WritingMode::VerticalRl) | Some(WritingMode::VerticalLr)
        )
155056
    }
}
/// Line constraints with multi-segment support
#[derive(Debug, Clone)]
pub struct LineConstraints {
    pub segments: Vec<LineSegment>,
    pub total_available: f32,
}
impl WritingMode {
    fn get_direction(&self) -> Option<BidiDirection> {
        match self {
            // determined by text content
            WritingMode::HorizontalTb => None,
            WritingMode::VerticalRl => Some(BidiDirection::Rtl),
            WritingMode::VerticalLr => Some(BidiDirection::Ltr),
            WritingMode::SidewaysRl => Some(BidiDirection::Rtl),
            WritingMode::SidewaysLr => Some(BidiDirection::Ltr),
        }
    }
}
// Stage 1: Collection - Styled runs from DOM traversal
#[derive(Debug, Clone, Hash)]
pub struct StyledRun {
    pub text: String,
    pub style: Arc<StyleProperties>,
    /// Byte index in the original logical paragraph text
    pub logical_start_byte: usize,
    /// The DOM NodeId of the Text node this run came from.
    /// None for generated content (e.g., list markers, ::before/::after).
    pub source_node_id: Option<NodeId>,
}
// Stage 2: Bidi Analysis - Visual runs in display order
#[derive(Debug, Clone)]
pub struct VisualRun<'a> {
    pub text_slice: &'a str,
    pub style: Arc<StyleProperties>,
    pub logical_start_byte: usize,
    pub bidi_level: BidiLevel,
    pub script: Script,
    pub language: Language,
}
// Font and styling types
/// A selector for loading fonts from the font cache.
/// Used by FontManager to query fontconfig and load font files.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FontSelector {
    pub family: String,
    pub weight: FcWeight,
    pub style: FontStyle,
    pub unicode_ranges: Vec<UnicodeRange>,
}
impl Default for FontSelector {
92268
    fn default() -> Self {
92268
        Self {
92268
            family: "serif".to_string(),
92268
            weight: FcWeight::Normal,
92268
            style: FontStyle::Normal,
92268
            unicode_ranges: Vec::new(),
92268
        }
92268
    }
}
/// Font stack that can be either a list of font selectors (resolved via fontconfig)
/// or a direct FontRef (bypasses fontconfig entirely).
///
/// When a `FontRef` is used, it bypasses fontconfig resolution entirely
/// and uses the pre-parsed font data directly. This is used for embedded
/// fonts like Material Icons.
// [g121 az-web-lift] `#[repr(C, u8)]` — same disc-mis-lift guard as the other text3 enums; matched in
// shape_visual_items (`match &style.font_stack { Ref => shape, Stack => resolve }`). repr(Rust) niche
// (from the Vec/FontRef payloads) could mis-route. Explicit u8 tag = simple load. Internal to text3.
#[derive(Debug, Clone)]
#[repr(C, u8)]
pub enum FontStack {
    /// A stack of font selectors to be resolved via fontconfig
    /// First font is primary, rest are fallbacks
    Stack(Vec<FontSelector>),
    /// A direct reference to a pre-parsed font (e.g., embedded icon fonts)
    /// This font covers the entire Unicode range and has no fallbacks.
    Ref(azul_css::props::basic::font::FontRef),
}
impl Default for FontStack {
92268
    fn default() -> Self {
92268
        FontStack::Stack(vec![FontSelector::default()])
92268
    }
}
impl FontStack {
    /// Returns true if this is a direct FontRef
    pub fn is_ref(&self) -> bool {
        matches!(self, FontStack::Ref(_))
    }
    /// Returns the FontRef if this is a Ref variant
    pub fn as_ref(&self) -> Option<&azul_css::props::basic::font::FontRef> {
        match self {
            FontStack::Ref(r) => Some(r),
            _ => None,
        }
    }
    /// Returns the font selectors if this is a Stack variant
    pub fn as_stack(&self) -> Option<&[FontSelector]> {
        match self {
            FontStack::Stack(s) => Some(s),
            _ => None,
        }
    }
    /// Returns the first FontSelector if this is a Stack variant, None if Ref
    pub fn first_selector(&self) -> Option<&FontSelector> {
        match self {
            FontStack::Stack(s) => s.first(),
            FontStack::Ref(_) => None,
        }
    }
    /// Returns the first font family name (for Stack) or a placeholder (for Ref)
    pub fn first_family(&self) -> &str {
        match self {
            FontStack::Stack(s) => s.first().map(|f| f.family.as_str()).unwrap_or("serif"),
            FontStack::Ref(_) => "<embedded-font>",
        }
    }
}
impl PartialEq for FontStack {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (FontStack::Stack(a), FontStack::Stack(b)) => a == b,
            (FontStack::Ref(a), FontStack::Ref(b)) => a.parsed == b.parsed,
            _ => false,
        }
    }
}
impl Eq for FontStack {}
impl Hash for FontStack {
233547
    fn hash<H: Hasher>(&self, state: &mut H) {
233547
        core::mem::discriminant(self).hash(state);
233547
        match self {
233547
            FontStack::Stack(s) => s.hash(state),
            FontStack::Ref(r) => (r.parsed as usize).hash(state),
        }
233547
    }
}
/// A reference to a font for rendering, identified by its hash.
/// This hash corresponds to ParsedFont::hash and is used to look up
/// the actual font data in the renderer's font cache.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FontHash {
    /// The hash of the ParsedFont. 0 means invalid/unknown font.
    pub font_hash: u64,
}
impl FontHash {
    pub fn invalid() -> Self {
        Self { font_hash: 0 }
    }
60544
    pub fn from_hash(font_hash: u64) -> Self {
60544
        Self { font_hash }
60544
    }
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum FontStyle {
    Normal,
    Italic,
    Oblique,
}
/// Defines how text should be aligned when a line contains multiple disjoint segments.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SegmentAlignment {
    /// Align text within the first available segment on the line.
    #[default]
    First,
    /// Align text relative to the total available width of all
    /// segments on the line combined.
    Total,
}
#[derive(Debug, Clone)]
pub struct VerticalMetrics {
    pub advance: f32,
    pub bearing_x: f32,
    pub bearing_y: f32,
    pub origin_y: f32,
}
// +spec:font-metrics:df51b1 - font metrics (ascent, descent, line_gap) used as baselines for inline layout alignment and box sizing
/// Layout-specific font metrics extracted from FontMetrics
/// Contains only the metrics needed for text layout and rendering
// +spec:box-model:a2f1c1 - inline box content area sized from first available font metrics (ascent/descent)
// +spec:font-metrics:9c2ca5 - ascent and descent metrics per font for inline layout
// +spec:font-metrics:797593 - font metrics (ascent, descent, line-gap) used for baseline calculations
// +spec:font-metrics:842d6a - font metrics (ascent, descent) used for precise spacing control
// +spec:font-metrics:eb97e0 - Font baseline metrics (ascent/descent) from font tables used for baseline alignment
// +spec:font-metrics:f2cd75 - em-over/em-under baselines intentionally not included (not used by CSS per spec)
// +spec:inline-formatting-context:76cd57 - ascent/descent font metrics for inline formatting context layout
// +spec:font-metrics:207e6b - ascent/descent metrics used for baseline calculations
#[derive(Debug, Clone)]
pub struct LayoutFontMetrics {
    pub ascent: f32,
    pub descent: f32,
    pub line_gap: f32,
    pub units_per_em: u16,
    /// OS/2 sxHeight: distance from baseline to top of lowercase 'x' (in font units).
    /// Used for `vertical-align: middle` per CSS Inline 3 §4.1.
    pub x_height: Option<f32>,
    /// OS/2 sCapHeight: height of capital letters from baseline (in font units).
    /// Used for drop cap / initial-letter alignment per CSS Inline 3 §7.1.1.
    pub cap_height: Option<f32>,
}
impl LayoutFontMetrics {
    // +spec:font-metrics:006bd8 - baseline position from font design coordinates, scaled with font size
    // +spec:font-metrics:910c0a - dominant-baseline: auto resolves to alphabetic for horizontal text
    // +spec:writing-modes:098958 - baseline is along the inline axis, used to align glyphs
64944
    pub fn baseline_scaled(&self, font_size: f32) -> f32 {
64944
        let scale = font_size / self.units_per_em as f32;
64944
        self.ascent * scale
64944
    }
    /// Returns the x-height scaled to the given font size in px.
    /// Falls back to 0.5em when the font doesn't provide sxHeight.
    pub fn x_height_scaled(&self, font_size: f32) -> f32 {
        let scale = font_size / self.units_per_em as f32;
        match self.x_height {
            Some(xh) => xh * scale,
            None => font_size * 0.5,
        }
    }
    /// Returns the cap height scaled to the given font size in px.
    /// Falls back to ascent when the font doesn't provide sCapHeight.
    pub fn cap_height_scaled(&self, font_size: f32) -> f32 {
        let scale = font_size / self.units_per_em as f32;
        self.cap_height.unwrap_or(self.ascent) * scale
    }
    // +spec:line-height:471816 - line gap metric extracted from font for optional use when line-height is normal
    /// Convert from full FontMetrics to layout-specific metrics.
    ///
    // +spec:font-metrics:05193a - prefer OS/2 sTypoAscender/sTypoDescender, fall back to HHEA
    // +spec:font-metrics:17a71c - prefer OS/2 sTypoAscender/sTypoDescender, fall back to HHEA
    // +spec:font-metrics:62c659 - prefer OS/2 sTypoAscender/sTypoDescender, fall back to HHEA
    // +spec:writing-modes:451a3e - ascent/descent/line-gap metrics: prefer OS/2, fallback HHEA, floor line_gap at 0
    /// Per CSS 2.2 §10.8.1: prefer OS/2 sTypoAscender/sTypoDescender,
    /// fall back to HHEA Ascent/Descent if OS/2 metrics are absent.
    // +spec:font-metrics:3dc8c1 - text-over/text-under baselines from font ascent/descent metrics
    // +spec:font-metrics:332c16 - text-over/text-under baseline metrics derived from font ascent/descent
    // +spec:font-metrics:9895e2 - baseline table is a font-level property; metrics apply uniformly to all glyphs
    // +spec:font-metrics:e05c40 - font ascent/descent metric extraction (text edge metrics)
    // +spec:font-metrics:21a3de - ascent/descent used as basis for em-over/em-under normalization
    // +spec:font-metrics:1257b7 - font ascent/descent ensure text fits within line box
    // +spec:table-layout:6bbd10 - use sTypoAscender/sTypoDescender as ascent/descent metrics per spec recommendation
    // +spec:font-metrics:5346d2 - prefer OS/2 sTypoAscender/sTypoDescender, fall back to HHEA
    // +spec:font-metrics:e16941 - line gap metric floored at zero per spec
    // +spec:font-metrics:a55c05 - metrics taken from font, synthesized if missing (prefers OS/2, falls back to HHEA)
    pub fn from_font_metrics(metrics: &azul_css::props::basic::FontMetrics) -> Self {
        let ascent = metrics.s_typo_ascender
            .as_option()
            .map(|v| *v as f32)
            .unwrap_or(metrics.ascender as f32);
        let descent = metrics.s_typo_descender
            .as_option()
            .map(|v| *v as f32)
            .unwrap_or(metrics.descender as f32);
        // UAs must floor the line gap metric at zero (css-inline-3 §3.2.2)
        // Spec: "UAs must floor the line gap metric at zero."
        let line_gap = metrics.s_typo_line_gap
            .as_option()
            .map(|v| *v as f32)
            .unwrap_or(metrics.line_gap as f32)
            .max(0.0);
        let x_height = metrics.sx_height
            .as_option()
            .map(|v| *v as f32);
        let cap_height = metrics.s_cap_height
            .as_option()
            .map(|v| *v as f32);
        Self {
            ascent,
            descent,
            line_gap,
            units_per_em: metrics.units_per_em,
            x_height,
            cap_height,
        }
    }
    // +spec:font-metrics:1eda6b - em-over is 0.5em over central baseline, em-under is 0.5em under
    /// Synthesize em-over baseline offset (in font units).
    /// Per CSS Inline 3 Appendix A.1: em-over = central baseline + 0.5em.
    /// Central baseline is synthesized as midpoint of ascent and descent.
    pub fn em_over(&self) -> f32 {
        let central = self.central_baseline();
        central + (self.units_per_em as f32 / 2.0)
    }
    /// Synthesize em-under baseline offset (in font units).
    /// Per CSS Inline 3 Appendix A.1: em-under = central baseline - 0.5em.
    pub fn em_under(&self) -> f32 {
        let central = self.central_baseline();
        central - (self.units_per_em as f32 / 2.0)
    }
    /// Synthesize central baseline (in font units).
    /// Midpoint between ascent and descent when not provided by the font.
    pub fn central_baseline(&self) -> f32 {
        (self.ascent + self.descent) / 2.0
    }
}
#[derive(Debug, Clone)]
pub struct LineSegment {
    pub start_x: f32,
    pub width: f32,
    // For choosing best segment when multiple available
    pub priority: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
pub enum TextWrap {
    #[default]
    Wrap,
    Balance,
    NoWrap,
}
/// CSS `overflow-wrap` (aka `word-wrap`) property.
///
/// Controls whether an otherwise unbreakable sequence of characters
/// may be broken at an arbitrary point to prevent overflow.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum OverflowWrap {
    /// No special break opportunities are introduced.
    #[default]
    Normal,
    /// Break at arbitrary points if no other break points exist.
    /// Soft wrap opportunities from `anywhere` ARE considered
    /// when calculating min-content intrinsic sizes.
    Anywhere,
    /// Same as `anywhere` except soft wrap opportunities introduced
    /// by `break-word` are NOT considered when calculating
    /// min-content intrinsic sizes.
    BreakWord,
}
// +spec:line-breaking:841a87 - hyphens property: manual (U+00AD/U+2010 only) and auto (language-aware automatic hyphenation)
// +spec:line-breaking:68c6ad - hyphens property controls hyphenation opportunities (none/manual/auto)
/// Controls whether hyphenation is allowed to create soft wrap opportunities.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Hyphens {
    /// No hyphenation: U+00AD soft hyphens are not treated as break points.
    None,
    /// Only break at manually-inserted soft hyphens (U+00AD) or explicit hyphens.
    #[default]
    Manual,
    /// The UA may automatically hyphenate words in addition to manual opportunities.
    Auto,
}
// +spec:line-breaking:ce5258 - white-space property controls collapsing, wrapping, and forced breaks
// +spec:line-breaking:35817b - normal/pre/nowrap/pre-wrap/break-spaces/pre-line behaviors
// +spec:white-space-processing:dec7aa - White space not removed/collapsed is "preserved white space"
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
pub enum WhiteSpaceMode {
    #[default]
    Normal,
    Nowrap,
    Pre,
    PreWrap,
    PreLine,
    BreakSpaces,
}
// CSS Text Level 3 §5.3: The line-break property controls strictness of line breaking rules.
// - Auto: UA-dependent, typically normal for CJK, loose for non-CJK
// - Loose: least restrictive, allows breaks before small kana, CJK hyphens, etc.
// - Normal: default CJK rules, allows breaks before CJK hyphen-like chars for CJK text
// - Strict: most restrictive, forbids breaks before small kana and CJK punctuation
// - Anywhere: allows soft wrap opportunities around every typographic character unit
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
pub enum LineBreakStrictness {
    #[default]
    Auto,
    Loose,
    Normal,
    Strict,
    /// Soft wrap opportunity around every typographic character unit.
    /// Hyphenation is not applied.
    Anywhere,
}
// §5.2 word-break property: normal, break-all, keep-all
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub enum WordBreak {
    /// Normal break rules: CJK characters break between each other,
    /// non-CJK text only breaks at spaces/hyphens.
    #[default]
    Normal,
    /// Allow breaks between any two characters, including within Latin words.
    BreakAll,
    /// Suppress breaks between CJK characters (treat them like Latin words,
    /// only breaking at spaces). Sequences of CJK characters do not break.
    KeepAll,
}
// +spec:display-property:162c99 - Initial letter box: in-flow inline-level box with special layout behavior
// +spec:display-property:72a797 - Initial letter handled like inline-level content in originating line box
// initial-letter
// +spec:containing-block:46a499 - subsequent block must clear previous block's initial letter if it starts with its own initial letter, establishes independent FC, or specifies clear in initial letter's CB start direction
// +spec:font-metrics:1e5325 - drop initial cap-height = (N-1)*line_height + surrounding cap-height
// +spec:font-metrics:3aa518 - initial-letter-align: cap-height/ideographic/hanging/leading/border-box baseline alignment
// +spec:writing-modes:9698b0 - Han-derived scripts: initial letter extends from block-start to block-end of Nth line
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct InitialLetter {
    /// How many lines tall the initial letter should be.
    pub size: f32,
    // +spec:font-metrics:dc0632 - raised initial "sinks" to first text baseline (sink=1)
    /// How many lines the letter should sink into.
    pub sink: u32,
    /// How many characters to apply this styling to.
    pub count: NonZeroUsize,
    // +spec:display-property:4c69bf - alignment points for sizing/positioning initial letter
    /// Alignment mode for the initial letter (over/under alignment points
    /// matched to corresponding points of the root inline box).
    pub align: InitialLetterAlign,
}
/// Alignment mode for initial letters, controlling which alignment points
/// are used to size and position the letter relative to the root inline box.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InitialLetterAlign {
    /// UA chooses based on script
    Auto,
    /// Alphabetic baseline alignment
    Alphabetic,
    /// Hanging baseline alignment
    Hanging,
    /// Ideographic baseline alignment
    Ideographic,
}
// A type that implements `Hash` must also implement `Eq`.
// Since f32 does not implement `Eq`, we provide a manual implementation.
// This is a marker trait, indicating that `a == b` is a true equivalence
// relation. The derived `PartialEq` already satisfies this.
impl Eq for InitialLetter {}
impl Hash for InitialLetter {
    fn hash<H: Hasher>(&self, state: &mut H) {
        // Per the request, round the f32 to a usize for hashing.
        // This is a lossy conversion; values like 2.3 and 2.4 will produce
        // the same hash value for this field. This is acceptable as long as
        // the `PartialEq` implementation correctly distinguishes them.
        (self.size.round() as usize).hash(state);
        self.sink.hash(state);
        self.count.hash(state);
        self.align.hash(state);
    }
}
// Path and shape definitions
#[derive(Debug, Clone, PartialOrd)]
pub enum PathSegment {
    MoveTo(Point),
    LineTo(Point),
    CurveTo {
        control1: Point,
        control2: Point,
        end: Point,
    },
    QuadTo {
        control: Point,
        end: Point,
    },
    Arc {
        center: Point,
        radius: f32,
        start_angle: f32,
        end_angle: f32,
    },
    Close,
}
// PathSegment
impl Hash for PathSegment {
    fn hash<H: Hasher>(&self, state: &mut H) {
        // Hash the enum variant's discriminant first to distinguish them
        discriminant(self).hash(state);
        match self {
            PathSegment::MoveTo(p) => p.hash(state),
            PathSegment::LineTo(p) => p.hash(state),
            PathSegment::CurveTo {
                control1,
                control2,
                end,
            } => {
                control1.hash(state);
                control2.hash(state);
                end.hash(state);
            }
            PathSegment::QuadTo { control, end } => {
                control.hash(state);
                end.hash(state);
            }
            PathSegment::Arc {
                center,
                radius,
                start_angle,
                end_angle,
            } => {
                center.hash(state);
                (radius.round() as usize).hash(state);
                (start_angle.round() as usize).hash(state);
                (end_angle.round() as usize).hash(state);
            }
            PathSegment::Close => {} // No data to hash
        }
    }
}
impl PartialEq for PathSegment {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (PathSegment::MoveTo(a), PathSegment::MoveTo(b)) => a == b,
            (PathSegment::LineTo(a), PathSegment::LineTo(b)) => a == b,
            (
                PathSegment::CurveTo {
                    control1: c1a,
                    control2: c2a,
                    end: ea,
                },
                PathSegment::CurveTo {
                    control1: c1b,
                    control2: c2b,
                    end: eb,
                },
            ) => c1a == c1b && c2a == c2b && ea == eb,
            (
                PathSegment::QuadTo {
                    control: ca,
                    end: ea,
                },
                PathSegment::QuadTo {
                    control: cb,
                    end: eb,
                },
            ) => ca == cb && ea == eb,
            (
                PathSegment::Arc {
                    center: ca,
                    radius: ra,
                    start_angle: sa_a,
                    end_angle: ea_a,
                },
                PathSegment::Arc {
                    center: cb,
                    radius: rb,
                    start_angle: sa_b,
                    end_angle: ea_b,
                },
            ) => ca == cb && round_eq(*ra, *rb) && round_eq(*sa_a, *sa_b) && round_eq(*ea_a, *ea_b),
            (PathSegment::Close, PathSegment::Close) => true,
            _ => false, // Variants are different
        }
    }
}
impl Eq for PathSegment {}
// Enhanced content model supporting mixed inline content
// [g117 az-web-lift FIX] `#[repr(C, u8)]` (was repr(Rust)): the web lift MIS-READS a repr(Rust)
// niche/compiler-placed discriminant — `<InlineContent as Clone>::clone` and create_logical_items'
// match both mis-route a Text(disc 0) to a Vec-bearing variant → clone reads a heap ptr as a Vec len
// → ~789MB alloc → OOB (g111/g115/g116 named stack = InlineContent::clone ← create_logical_items;
// content is CLEAN: len=1, ptr ok, disc-at-0=0). An explicit u8 tag at offset 0 (no niche) lowers to
// a simple load the lift handles correctly — the layout other (repr(C,u8)) enums use. Not FFI-exposed
// (internal to text3; only native shell code matches it), so the repr change is layout-safe.
#[derive(Debug, Clone, Hash)]
#[repr(C, u8)]
pub enum InlineContent {
    Text(StyledRun),
    Image(InlineImage),
    Shape(InlineShape),
    Space(InlineSpace),
    LineBreak(InlineBreak),
    /// Tab character - rendered with width based on tab-size CSS property
    Tab {
        style: Arc<StyleProperties>,
    },
    /// List marker (::marker pseudo-element)
    /// Markers with list-style-position: outside are positioned
    /// in the padding gutter of the list container
    Marker {
        run: StyledRun,
        /// Whether marker is positioned outside (in padding) or inside (inline)
        position_outside: bool,
    },
    // Ruby annotation
    Ruby {
        base: Vec<InlineContent>,
        text: Vec<InlineContent>,
        // Style for the ruby text itself
        style: Arc<StyleProperties>,
    },
}
#[derive(Debug, Clone)]
pub struct InlineImage {
    pub source: ImageSource,
    pub intrinsic_size: Size,
    pub display_size: Option<Size>,
    // How much to shift baseline
    pub baseline_offset: f32,
    pub alignment: VerticalAlign,
    pub object_fit: ObjectFit,
}
impl PartialEq for InlineImage {
    fn eq(&self, other: &Self) -> bool {
        self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
            && self.source == other.source
            && self.intrinsic_size == other.intrinsic_size
            && self.display_size == other.display_size
            && self.alignment == other.alignment
            && self.object_fit == other.object_fit
    }
}
impl Eq for InlineImage {}
impl Hash for InlineImage {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.source.hash(state);
        self.intrinsic_size.hash(state);
        self.display_size.hash(state);
        self.baseline_offset.to_bits().hash(state);
        self.alignment.hash(state);
        self.object_fit.hash(state);
    }
}
impl PartialOrd for InlineImage {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}
impl Ord for InlineImage {
    fn cmp(&self, other: &Self) -> Ordering {
        self.source
            .cmp(&other.source)
            .then_with(|| self.intrinsic_size.cmp(&other.intrinsic_size))
            .then_with(|| self.display_size.cmp(&other.display_size))
            .then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
            .then_with(|| self.alignment.cmp(&other.alignment))
            .then_with(|| self.object_fit.cmp(&other.object_fit))
    }
}
/// Enhanced glyph with all features
#[derive(Debug, Clone)]
pub struct Glyph {
    // Core glyph data
    pub glyph_id: u16,
    pub codepoint: char,
    /// Hash of the font - use LoadedFonts to look up the actual font when needed
    pub font_hash: u64,
    /// Cached font metrics to avoid font lookup for common operations
    pub font_metrics: LayoutFontMetrics,
    pub style: Arc<StyleProperties>,
    pub source: GlyphSource,
    // Text mapping
    pub logical_byte_index: usize,
    pub logical_byte_len: usize,
    pub content_index: usize,
    pub cluster: u32,
    // Metrics
    pub advance: f32,
    pub kerning: f32,
    pub offset: Point,
    // Vertical text support
    pub vertical_advance: f32,
    pub vertical_origin_y: f32, // from VORG
    pub vertical_bearing: Point,
    pub orientation: GlyphOrientation,
    // Layout properties
    pub script: Script,
    pub bidi_level: BidiLevel,
}
impl Glyph {
    #[inline]
    fn bounds(&self) -> Rect {
        Rect {
            x: 0.0,
            y: 0.0,
            width: self.advance,
            height: self.style.line_height.resolve_with_metrics(self.style.font_size_px, &self.font_metrics),
        }
    }
    #[inline]
    fn character_class(&self) -> CharacterClass {
        classify_character(self.codepoint as u32)
    }
    #[inline]
    fn is_whitespace(&self) -> bool {
        self.character_class() == CharacterClass::Space
    }
    #[inline]
    fn can_justify(&self) -> bool {
        !self.codepoint.is_whitespace() && self.character_class() != CharacterClass::Combining
    }
    #[inline]
    fn justification_priority(&self) -> u8 {
        get_justification_priority(self.character_class())
    }
    #[inline]
    fn break_opportunity_after(&self) -> bool {
        let is_whitespace = self.codepoint.is_whitespace();
        let is_soft_hyphen = self.codepoint == '\u{00AD}';
        let is_hyphen_minus = self.codepoint == '\u{002D}';
        let is_hyphen = self.codepoint == '\u{2010}';
        is_whitespace || is_soft_hyphen || is_hyphen_minus || is_hyphen
    }
}
// Information about text runs after initial analysis
#[derive(Debug, Clone)]
pub struct TextRunInfo<'a> {
    pub text: &'a str,
    pub style: Arc<StyleProperties>,
    pub logical_start: usize,
    pub content_index: usize,
}
#[derive(Debug, Clone)]
pub enum ImageSource {
    /// Direct reference to decoded image (from DOM NodeType::Image)
    Ref(ImageRef),
    /// CSS url reference (from background-image, needs ImageCache lookup)
    Url(String),
    /// Raw image data
    Data(Arc<[u8]>),
    /// SVG source
    Svg(Arc<str>),
    /// Placeholder for layout without actual image
    Placeholder(Size),
}
impl PartialEq for ImageSource {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (ImageSource::Ref(a), ImageSource::Ref(b)) => a.get_hash() == b.get_hash(),
            (ImageSource::Url(a), ImageSource::Url(b)) => a == b,
            (ImageSource::Data(a), ImageSource::Data(b)) => Arc::ptr_eq(a, b),
            (ImageSource::Svg(a), ImageSource::Svg(b)) => Arc::ptr_eq(a, b),
            (ImageSource::Placeholder(a), ImageSource::Placeholder(b)) => {
                a.width.to_bits() == b.width.to_bits() && a.height.to_bits() == b.height.to_bits()
            }
            _ => false,
        }
    }
}
impl Eq for ImageSource {}
impl std::hash::Hash for ImageSource {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        core::mem::discriminant(self).hash(state);
        match self {
            ImageSource::Ref(r) => r.get_hash().hash(state),
            ImageSource::Url(s) => s.hash(state),
            ImageSource::Data(d) => (Arc::as_ptr(d) as *const u8 as usize).hash(state),
            ImageSource::Svg(s) => (Arc::as_ptr(s) as *const u8 as usize).hash(state),
            ImageSource::Placeholder(sz) => {
                sz.width.to_bits().hash(state);
                sz.height.to_bits().hash(state);
            }
        }
    }
}
impl PartialOrd for ImageSource {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}
impl Ord for ImageSource {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        fn variant_index(s: &ImageSource) -> u8 {
            match s {
                ImageSource::Ref(_) => 0,
                ImageSource::Url(_) => 1,
                ImageSource::Data(_) => 2,
                ImageSource::Svg(_) => 3,
                ImageSource::Placeholder(_) => 4,
            }
        }
        match (self, other) {
            (ImageSource::Ref(a), ImageSource::Ref(b)) => a.get_hash().cmp(&b.get_hash()),
            (ImageSource::Url(a), ImageSource::Url(b)) => a.cmp(b),
            (ImageSource::Data(a), ImageSource::Data(b)) => {
                (Arc::as_ptr(a) as *const u8 as usize).cmp(&(Arc::as_ptr(b) as *const u8 as usize))
            }
            (ImageSource::Svg(a), ImageSource::Svg(b)) => {
                (Arc::as_ptr(a) as *const u8 as usize).cmp(&(Arc::as_ptr(b) as *const u8 as usize))
            }
            (ImageSource::Placeholder(a), ImageSource::Placeholder(b)) => {
                (a.width.to_bits(), a.height.to_bits())
                    .cmp(&(b.width.to_bits(), b.height.to_bits()))
            }
            // Different variants: compare by variant index
            _ => variant_index(self).cmp(&variant_index(other)),
        }
    }
}
// +spec:font-metrics:fa104e - vertical-align values; baseline-source defaults to auto (first baseline)
// +spec:inline-formatting-context:340729 - alignment-baseline values for IFC baseline alignment (only baseline/top/bottom/middle implemented)
// CSS 2.2 §10.8.1 vertical-align property values
// +spec:display-property:0b1deb - inline boxes use dominant baseline to align text and inline-level children
// +spec:inline-formatting-context:3996a6 - dominant-baseline defaults to alphabetic in horizontal mode; vertical-align handles baseline alignment and super/sub shifting
#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum VerticalAlign {
    // Align baseline of box with baseline of parent box
    #[default]
    Baseline,
    // Align bottom of aligned subtree with bottom of line box
    Bottom,
    // Align top of aligned subtree with top of line box
    Top,
    // Align vertical midpoint of box with baseline of parent plus half x-height
    Middle,
    // Align top of box with top of parent's content area (§10.6.1)
    TextTop,
    // Align bottom of box with bottom of parent's content area (§10.6.1)
    TextBottom,
    // Lower baseline to proper subscript position
    Sub,
    // Raise baseline to proper superscript position
    Super,
    // +spec:font-metrics:152df3 - Raise (positive) or lower (negative) by this distance; 0 = baseline
    Offset(f32),
}
impl std::hash::Hash for VerticalAlign {
572
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
572
        core::mem::discriminant(self).hash(state);
572
        if let VerticalAlign::Offset(f) = self {
            f.to_bits().hash(state);
572
        }
572
    }
}
impl Eq for VerticalAlign {}
impl Ord for VerticalAlign {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
    }
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum ObjectFit {
    // Stretch to fit display size
    Fill,
    // Scale to fit within display size
    Contain,
    // Scale to cover display size
    Cover,
    // Use intrinsic size
    None,
    // Like contain but never scale up
    ScaleDown,
}
/// Border information for inline elements (display: inline, inline-block)
///
/// This stores the resolved border properties needed for rendering inline element borders.
/// Unlike block elements which render borders via paint_node_background_and_border(),
/// inline element borders must be rendered per glyph-run to handle line breaks correctly.
#[derive(Debug, Clone, PartialEq)]
pub struct InlineBorderInfo {
    /// Border widths in pixels for each side
    pub top: f32,
    pub right: f32,
    pub bottom: f32,
    pub left: f32,
    /// Border colors for each side
    pub top_color: ColorU,
    pub right_color: ColorU,
    pub bottom_color: ColorU,
    pub left_color: ColorU,
    /// Border radius (if any)
    pub radius: Option<f32>,
    /// Padding widths in pixels for each side (needed to expand background rect)
    pub padding_top: f32,
    pub padding_right: f32,
    pub padding_bottom: f32,
    pub padding_left: f32,
    // +spec:box-model:c5723b - inline box split: suppress margin/border/padding at split points
    /// CSS 2.2 §9.4.2 / §8.6: when an inline box is split across line boxes,
    /// margins, borders, and padding have no visible effect at the split points.
    /// True if this is the first fragment of the inline box.
    pub is_first_fragment: bool,
    /// True if this is the last fragment of the inline box.
    pub is_last_fragment: bool,
    /// CSS 2.2 §8.6: direction flag for visual-order rendering in bidi context.
    /// LTR: first fragment gets left edge, last gets right edge.
    /// RTL: first fragment gets right edge, last gets left edge.
    pub is_rtl: bool,
}
impl Default for InlineBorderInfo {
    fn default() -> Self {
        Self {
            top: 0.0,
            right: 0.0,
            bottom: 0.0,
            left: 0.0,
            top_color: ColorU::TRANSPARENT,
            right_color: ColorU::TRANSPARENT,
            bottom_color: ColorU::TRANSPARENT,
            left_color: ColorU::TRANSPARENT,
            radius: None,
            padding_top: 0.0,
            padding_right: 0.0,
            padding_bottom: 0.0,
            padding_left: 0.0,
            is_first_fragment: true,
            is_last_fragment: true,
            is_rtl: false,
        }
    }
}
impl InlineBorderInfo {
    /// Returns true if any border has a non-zero width
    pub fn has_border(&self) -> bool {
        self.top > 0.0 || self.right > 0.0 || self.bottom > 0.0 || self.left > 0.0
    }
    /// Returns true if any border or padding is present
    pub fn has_chrome(&self) -> bool {
        self.has_border()
            || self.padding_top > 0.0
            || self.padding_right > 0.0
            || self.padding_bottom > 0.0
            || self.padding_left > 0.0
    }
    // +spec:box-model:da0ba2 - RTL bidi inline box split: left/right edges assigned to correct fragments
    // +spec:box-model:e9144f - visual-order margin/border/padding for inline boxes in bidi context
    // +spec:box-model:fac66f - Assigns margins/borders/padding in visual order for bidi inline fragments
    // +spec:box-model:720688 - LTR: left on first, right on last; RTL: right on first, left on last
    // +spec:positioning:1fcad6 - bidi-aware margin/border/padding on inline box fragments per visual order
    /// Total left inset (border + padding), suppressed at split points per §8.6.
    /// In LTR: left edge drawn on first fragment. In RTL: left edge drawn on last fragment.
    // +spec:box-model:bae97f - visual-order margin/border/padding assignment for bidi inline fragments
    pub fn left_inset(&self) -> f32 {
        let show = if self.is_rtl { self.is_last_fragment } else { self.is_first_fragment };
        if show { self.left + self.padding_left } else { 0.0 }
    }
    /// Total right inset (border + padding), suppressed at split points per §8.6.
    /// In LTR: right edge drawn on last fragment. In RTL: right edge drawn on first fragment.
    pub fn right_inset(&self) -> f32 {
        let show = if self.is_rtl { self.is_first_fragment } else { self.is_last_fragment };
        if show { self.right + self.padding_right } else { 0.0 }
    }
    /// Total top inset (border + padding)
    pub fn top_inset(&self) -> f32 { self.top + self.padding_top }
    /// Total bottom inset (border + padding)
    pub fn bottom_inset(&self) -> f32 { self.bottom + self.padding_bottom }
}
#[derive(Debug, Clone)]
pub struct InlineShape {
    pub shape_def: ShapeDefinition,
    pub fill: Option<ColorU>,
    pub stroke: Option<Stroke>,
    pub baseline_offset: f32,
    /// Per-item vertical alignment (CSS `vertical-align` on the inline-block element).
    /// This overrides the global `TextStyleOptions::vertical_align` for this shape.
    pub alignment: VerticalAlign,
    /// The NodeId of the element that created this shape
    /// (e.g., inline-block) - this allows us to look up
    /// styling information (background, border) when rendering
    pub source_node_id: Option<azul_core::dom::NodeId>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum OverflowBehavior {
    // Content extends outside shape
    Visible,
    // Content is clipped to shape
    Hidden,
    // Scrollable overflow
    Scroll,
    // Browser/system decides
    #[default]
    Auto,
    // Break into next shape/page
    Break,
}
#[derive(Debug, Clone)]
pub struct MeasuredImage {
    pub source: ImageSource,
    pub size: Size,
    pub baseline_offset: f32,
    pub alignment: VerticalAlign,
    pub content_index: usize,
}
#[derive(Debug, Clone)]
pub struct MeasuredShape {
    pub shape_def: ShapeDefinition,
    pub size: Size,
    pub baseline_offset: f32,
    pub alignment: VerticalAlign,
    pub content_index: usize,
}
#[derive(Debug, Clone)]
pub struct InlineSpace {
    pub width: f32,
    pub is_breaking: bool, // Can line break here
    pub is_stretchy: bool, // Can be expanded for justification
}
impl PartialEq for InlineSpace {
    fn eq(&self, other: &Self) -> bool {
        self.width.to_bits() == other.width.to_bits()
            && self.is_breaking == other.is_breaking
            && self.is_stretchy == other.is_stretchy
    }
}
impl Eq for InlineSpace {}
impl Hash for InlineSpace {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.width.to_bits().hash(state);
        self.is_breaking.hash(state);
        self.is_stretchy.hash(state);
    }
}
impl PartialOrd for InlineSpace {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}
impl Ord for InlineSpace {
    fn cmp(&self, other: &Self) -> Ordering {
        self.width
            .total_cmp(&other.width)
            .then_with(|| self.is_breaking.cmp(&other.is_breaking))
            .then_with(|| self.is_stretchy.cmp(&other.is_stretchy))
    }
}
impl PartialEq for InlineShape {
    fn eq(&self, other: &Self) -> bool {
        self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
            && self.shape_def == other.shape_def
            && self.fill == other.fill
            && self.stroke == other.stroke
            && self.alignment == other.alignment
            && self.source_node_id == other.source_node_id
    }
}
impl Eq for InlineShape {}
impl Hash for InlineShape {
572
    fn hash<H: Hasher>(&self, state: &mut H) {
572
        self.shape_def.hash(state);
572
        self.fill.hash(state);
572
        self.stroke.hash(state);
572
        self.baseline_offset.to_bits().hash(state);
572
        self.alignment.hash(state);
572
        self.source_node_id.hash(state);
572
    }
}
impl PartialOrd for InlineShape {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(
            self.shape_def
                .partial_cmp(&other.shape_def)?
                .then_with(|| self.fill.cmp(&other.fill))
                .then_with(|| {
                    self.stroke
                        .partial_cmp(&other.stroke)
                        .unwrap_or(Ordering::Equal)
                })
                .then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
                .then_with(|| self.alignment.cmp(&other.alignment))
                .then_with(|| self.source_node_id.cmp(&other.source_node_id)),
        )
    }
}
#[derive(Debug, Default, Clone, Copy)]
pub struct Rect {
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
}
impl PartialEq for Rect {
    fn eq(&self, other: &Self) -> bool {
        round_eq(self.x, other.x)
            && round_eq(self.y, other.y)
            && round_eq(self.width, other.width)
            && round_eq(self.height, other.height)
    }
}
impl Eq for Rect {}
impl Hash for Rect {
    fn hash<H: Hasher>(&self, state: &mut H) {
        // The order in which you hash the fields matters.
        // A consistent order is crucial.
        (self.x.round() as usize).hash(state);
        (self.y.round() as usize).hash(state);
        (self.width.round() as usize).hash(state);
        (self.height.round() as usize).hash(state);
    }
}
#[derive(Debug, Default, Clone, Copy, PartialOrd)]
pub struct Size {
    pub width: f32,
    pub height: f32,
}
impl Ord for Size {
    fn cmp(&self, other: &Self) -> Ordering {
        (self.width.round() as usize)
            .cmp(&(other.width.round() as usize))
            .then_with(|| (self.height.round() as usize).cmp(&(other.height.round() as usize)))
    }
}
// Size
impl Hash for Size {
572
    fn hash<H: Hasher>(&self, state: &mut H) {
572
        (self.width.round() as usize).hash(state);
572
        (self.height.round() as usize).hash(state);
572
    }
}
impl PartialEq for Size {
    fn eq(&self, other: &Self) -> bool {
        round_eq(self.width, other.width) && round_eq(self.height, other.height)
    }
}
impl Eq for Size {}
impl Size {
    pub const fn zero() -> Self {
        Self::new(0.0, 0.0)
    }
    pub const fn new(width: f32, height: f32) -> Self {
        Self { width, height }
    }
}
#[derive(Debug, Default, Clone, Copy, PartialOrd)]
pub struct Point {
    pub x: f32,
    pub y: f32,
}
// Point
impl Hash for Point {
    fn hash<H: Hasher>(&self, state: &mut H) {
        (self.x.round() as usize).hash(state);
        (self.y.round() as usize).hash(state);
    }
}
impl PartialEq for Point {
    fn eq(&self, other: &Self) -> bool {
        round_eq(self.x, other.x) && round_eq(self.y, other.y)
    }
}
impl Eq for Point {}
#[derive(Debug, Clone, PartialOrd)]
pub enum ShapeDefinition {
    Rectangle {
        size: Size,
        corner_radius: Option<f32>,
    },
    Circle {
        radius: f32,
    },
    Ellipse {
        radii: Size,
    },
    Polygon {
        points: Vec<Point>,
    },
    Path {
        segments: Vec<PathSegment>,
    },
}
// ShapeDefinition
impl Hash for ShapeDefinition {
572
    fn hash<H: Hasher>(&self, state: &mut H) {
572
        discriminant(self).hash(state);
572
        match self {
            ShapeDefinition::Rectangle {
572
                size,
572
                corner_radius,
            } => {
572
                size.hash(state);
572
                corner_radius.map(|r| r.round() as usize).hash(state);
            }
            ShapeDefinition::Circle { radius } => {
                (radius.round() as usize).hash(state);
            }
            ShapeDefinition::Ellipse { radii } => {
                radii.hash(state);
            }
            ShapeDefinition::Polygon { points } => {
                // Since Point implements Hash, we can hash the Vec directly.
                points.hash(state);
            }
            ShapeDefinition::Path { segments } => {
                // Same for Vec<PathSegment>
                segments.hash(state);
            }
        }
572
    }
}
impl PartialEq for ShapeDefinition {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (
                ShapeDefinition::Rectangle {
                    size: s1,
                    corner_radius: r1,
                },
                ShapeDefinition::Rectangle {
                    size: s2,
                    corner_radius: r2,
                },
            ) => {
                s1 == s2
                    && match (r1, r2) {
                        (None, None) => true,
                        (Some(v1), Some(v2)) => round_eq(*v1, *v2),
                        _ => false,
                    }
            }
            (ShapeDefinition::Circle { radius: r1 }, ShapeDefinition::Circle { radius: r2 }) => {
                round_eq(*r1, *r2)
            }
            (ShapeDefinition::Ellipse { radii: r1 }, ShapeDefinition::Ellipse { radii: r2 }) => {
                r1 == r2
            }
            (ShapeDefinition::Polygon { points: p1 }, ShapeDefinition::Polygon { points: p2 }) => {
                p1 == p2
            }
            (ShapeDefinition::Path { segments: s1 }, ShapeDefinition::Path { segments: s2 }) => {
                s1 == s2
            }
            _ => false,
        }
    }
}
impl Eq for ShapeDefinition {}
impl ShapeDefinition {
    /// Calculates the bounding box size for the shape.
396
    pub fn get_size(&self) -> Size {
396
        match self {
            // The size is explicitly defined.
396
            ShapeDefinition::Rectangle { size, .. } => *size,
            // The bounding box of a circle is a square with sides equal to the diameter.
            ShapeDefinition::Circle { radius } => {
                let diameter = radius * 2.0;
                Size::new(diameter, diameter)
            }
            // The bounding box of an ellipse has width and height equal to twice its radii.
            ShapeDefinition::Ellipse { radii } => Size::new(radii.width * 2.0, radii.height * 2.0),
            // For a polygon, we must find the min/max coordinates to get the bounds.
            ShapeDefinition::Polygon { points } => calculate_bounding_box_size(points),
            // For a path, we find the bounding box of all its anchor and control points.
            //
            // NOTE: This is a common and fast approximation. The true bounding box of
            // bezier curves can be slightly smaller than the box containing their control
            // points. For pixel-perfect results, one would need to calculate the
            // curve's extrema.
            ShapeDefinition::Path { segments } => {
                let mut points = Vec::new();
                let mut current_pos = Point { x: 0.0, y: 0.0 };
                for segment in segments {
                    match segment {
                        PathSegment::MoveTo(p) | PathSegment::LineTo(p) => {
                            points.push(*p);
                            current_pos = *p;
                        }
                        PathSegment::QuadTo { control, end } => {
                            points.push(current_pos);
                            points.push(*control);
                            points.push(*end);
                            current_pos = *end;
                        }
                        PathSegment::CurveTo {
                            control1,
                            control2,
                            end,
                        } => {
                            points.push(current_pos);
                            points.push(*control1);
                            points.push(*control2);
                            points.push(*end);
                            current_pos = *end;
                        }
                        PathSegment::Arc {
                            center,
                            radius,
                            start_angle,
                            end_angle,
                        } => {
                            // 1. Calculate and add the arc's start and end points to the list.
                            let start_point = Point {
                                x: center.x + radius * start_angle.cos(),
                                y: center.y + radius * start_angle.sin(),
                            };
                            let end_point = Point {
                                x: center.x + radius * end_angle.cos(),
                                y: center.y + radius * end_angle.sin(),
                            };
                            points.push(start_point);
                            points.push(end_point);
                            // 2. Normalize the angles to handle cases where the arc crosses the
                            //    0-radian line.
                            // This ensures we can iterate forward from a start to an end angle.
                            let mut normalized_end = *end_angle;
                            while normalized_end < *start_angle {
                                normalized_end += 2.0 * std::f32::consts::PI;
                            }
                            // 3. Find the first cardinal point (multiples of PI/2) at or after the
                            //    start angle.
                            let mut check_angle = (*start_angle / std::f32::consts::FRAC_PI_2)
                                .ceil()
                                * std::f32::consts::FRAC_PI_2;
                            // 4. Iterate through all cardinal points that fall within the arc's
                            //    sweep and add them.
                            // These points define the maximum extent of the arc's bounding box.
                            while check_angle < normalized_end {
                                points.push(Point {
                                    x: center.x + radius * check_angle.cos(),
                                    y: center.y + radius * check_angle.sin(),
                                });
                                check_angle += std::f32::consts::FRAC_PI_2;
                            }
                            // 5. The end of the arc is the new current position for subsequent path
                            //    segments.
                            current_pos = end_point;
                        }
                        PathSegment::Close => {
                            // No new points are added for closing the path
                        }
                    }
                }
                calculate_bounding_box_size(&points)
            }
        }
396
    }
}
// +spec:text-alignment-spacing:25e82a - text-align shorthand resolves text-align-all / text-align-last
/// Resolve effective text alignment for a line, handling text-align-last per CSS Text §6.3.
/// For the last line (or lines before forced breaks), text-align-last overrides text-align.
/// When text-align-last is auto (default), justify falls back to start; others use text-align.
// +spec:text-alignment-spacing:bca77d - text-align-last auto falls back to text-align-all, justify→start
// +spec:line-breaking:9b10d2 - text-align-last applies to last line and lines before forced breaks
/// +spec:text-alignment-spacing:8d88ce - text-align-last overrides justify on last line/forced break
85228
pub(crate) fn resolve_effective_alignment(
85228
    text_align: TextAlign,
85228
    text_align_last: TextAlign,
85228
    is_last_or_forced: bool,
85228
) -> TextAlign {
85228
    if is_last_or_forced {
44880
        if text_align_last == TextAlign::default() {
44880
            if text_align == TextAlign::Justify { TextAlign::Start } else { text_align }
        } else {
            text_align_last
        }
    } else {
40348
        text_align
    }
85228
}
/// Helper function to calculate the size of the bounding box enclosing a set of points.
fn calculate_bounding_box_size(points: &[Point]) -> Size {
    if points.is_empty() {
        return Size::zero();
    }
    let mut min_x = f32::MAX;
    let mut max_x = f32::MIN;
    let mut min_y = f32::MAX;
    let mut max_y = f32::MIN;
    for point in points {
        min_x = min_x.min(point.x);
        max_x = max_x.max(point.x);
        min_y = min_y.min(point.y);
        max_y = max_y.max(point.y);
    }
    // Handle case where points might be collinear or a single point
    if min_x > max_x || min_y > max_y {
        return Size::zero();
    }
    Size::new(max_x - min_x, max_y - min_y)
}
#[derive(Debug, Clone, PartialOrd)]
pub struct Stroke {
    pub color: ColorU,
    pub width: f32,
    pub dash_pattern: Option<Vec<f32>>,
}
// Stroke
impl Hash for Stroke {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.color.hash(state);
        (self.width.round() as usize).hash(state);
        // Manual hashing for Option<Vec<f32>>
        match &self.dash_pattern {
            None => 0u8.hash(state), // Hash a discriminant for None
            Some(pattern) => {
                1u8.hash(state); // Hash a discriminant for Some
                pattern.len().hash(state); // Hash the length
                for &val in pattern {
                    (val.round() as usize).hash(state); // Hash each rounded value
                }
            }
        }
    }
}
impl PartialEq for Stroke {
    fn eq(&self, other: &Self) -> bool {
        if self.color != other.color || !round_eq(self.width, other.width) {
            return false;
        }
        match (&self.dash_pattern, &other.dash_pattern) {
            (None, None) => true,
            (Some(p1), Some(p2)) => {
                p1.len() == p2.len() && p1.iter().zip(p2.iter()).all(|(a, b)| round_eq(*a, *b))
            }
            _ => false,
        }
    }
}
impl Eq for Stroke {}
// Helper function to round f32 for comparison
fn round_eq(a: f32, b: f32) -> bool {
    (a.round() as isize) == (b.round() as isize)
}
#[derive(Debug, Clone)]
pub enum ShapeBoundary {
    Rectangle(Rect),
    Circle { center: Point, radius: f32 },
    Ellipse { center: Point, radii: Size },
    Polygon { points: Vec<Point> },
    Path { segments: Vec<PathSegment> },
}
impl ShapeBoundary {
    pub fn inflate(&self, margin: f32) -> Self {
        if margin == 0.0 {
            return self.clone();
        }
        match self {
            Self::Rectangle(rect) => Self::Rectangle(Rect {
                x: rect.x - margin,
                y: rect.y - margin,
                width: (rect.width + margin * 2.0).max(0.0),
                height: (rect.height + margin * 2.0).max(0.0),
            }),
            Self::Circle { center, radius } => Self::Circle {
                center: *center,
                radius: radius + margin,
            },
            // For simplicity, Polygon and Path inflation is not implemented here.
            // A full implementation would require a geometry library to offset the path.
            _ => self.clone(),
        }
    }
}
// ShapeBoundary
impl Hash for ShapeBoundary {
    fn hash<H: Hasher>(&self, state: &mut H) {
        discriminant(self).hash(state);
        match self {
            ShapeBoundary::Rectangle(rect) => rect.hash(state),
            ShapeBoundary::Circle { center, radius } => {
                center.hash(state);
                (radius.round() as usize).hash(state);
            }
            ShapeBoundary::Ellipse { center, radii } => {
                center.hash(state);
                radii.hash(state);
            }
            ShapeBoundary::Polygon { points } => points.hash(state),
            ShapeBoundary::Path { segments } => segments.hash(state),
        }
    }
}
impl PartialEq for ShapeBoundary {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (ShapeBoundary::Rectangle(r1), ShapeBoundary::Rectangle(r2)) => r1 == r2,
            (
                ShapeBoundary::Circle {
                    center: c1,
                    radius: r1,
                },
                ShapeBoundary::Circle {
                    center: c2,
                    radius: r2,
                },
            ) => c1 == c2 && round_eq(*r1, *r2),
            (
                ShapeBoundary::Ellipse {
                    center: c1,
                    radii: r1,
                },
                ShapeBoundary::Ellipse {
                    center: c2,
                    radii: r2,
                },
            ) => c1 == c2 && r1 == r2,
            (ShapeBoundary::Polygon { points: p1 }, ShapeBoundary::Polygon { points: p2 }) => {
                p1 == p2
            }
            (ShapeBoundary::Path { segments: s1 }, ShapeBoundary::Path { segments: s2 }) => {
                s1 == s2
            }
            _ => false,
        }
    }
}
impl Eq for ShapeBoundary {}
impl ShapeBoundary {
    /// Converts a CSS shape (from azul-css) to a layout engine ShapeBoundary
    ///
    /// # Arguments
    /// * `css_shape` - The parsed CSS shape from azul-css
    /// * `reference_box` - The containing box for resolving coordinates (from layout solver)
    ///
    /// # Returns
    /// A ShapeBoundary ready for use in the text layout engine
    pub fn from_css_shape(
        css_shape: &azul_css::shape::CssShape,
        reference_box: Rect,
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
    ) -> Self {
        use azul_css::shape::CssShape;
        if let Some(msgs) = debug_messages {
            msgs.push(LayoutDebugMessage::info(format!(
                "[ShapeBoundary::from_css_shape] Input CSS shape: {:?}",
                css_shape
            )));
            msgs.push(LayoutDebugMessage::info(format!(
                "[ShapeBoundary::from_css_shape] Reference box: {:?}",
                reference_box
            )));
        }
        let result = match css_shape {
            CssShape::Circle(circle) => {
                let center = Point {
                    x: reference_box.x + circle.center.x,
                    y: reference_box.y + circle.center.y,
                };
                if let Some(msgs) = debug_messages {
                    msgs.push(LayoutDebugMessage::info(format!(
                        "[ShapeBoundary::from_css_shape] Circle - CSS center: ({}, {}), radius: {}",
                        circle.center.x, circle.center.y, circle.radius
                    )));
                    msgs.push(LayoutDebugMessage::info(format!(
                        "[ShapeBoundary::from_css_shape] Circle - Absolute center: ({}, {}), \
                         radius: {}",
                        center.x, center.y, circle.radius
                    )));
                }
                ShapeBoundary::Circle {
                    center,
                    radius: circle.radius,
                }
            }
            CssShape::Ellipse(ellipse) => {
                let center = Point {
                    x: reference_box.x + ellipse.center.x,
                    y: reference_box.y + ellipse.center.y,
                };
                let radii = Size {
                    width: ellipse.radius_x,
                    height: ellipse.radius_y,
                };
                if let Some(msgs) = debug_messages {
                    msgs.push(LayoutDebugMessage::info(format!(
                        "[ShapeBoundary::from_css_shape] Ellipse - center: ({}, {}), radii: ({}, \
                         {})",
                        center.x, center.y, radii.width, radii.height
                    )));
                }
                ShapeBoundary::Ellipse { center, radii }
            }
            CssShape::Polygon(polygon) => {
                let points = polygon
                    .points
                    .as_ref()
                    .iter()
                    .map(|pt| Point {
                        x: reference_box.x + pt.x,
                        y: reference_box.y + pt.y,
                    })
                    .collect();
                if let Some(msgs) = debug_messages {
                    msgs.push(LayoutDebugMessage::info(format!(
                        "[ShapeBoundary::from_css_shape] Polygon - {} points",
                        polygon.points.as_ref().len()
                    )));
                }
                ShapeBoundary::Polygon { points }
            }
            CssShape::Inset(inset) => {
                // Inset defines distances from reference box edges
                let x = reference_box.x + inset.inset_left;
                let y = reference_box.y + inset.inset_top;
                let width = reference_box.width - inset.inset_left - inset.inset_right;
                let height = reference_box.height - inset.inset_top - inset.inset_bottom;
                if let Some(msgs) = debug_messages {
                    msgs.push(LayoutDebugMessage::info(format!(
                        "[ShapeBoundary::from_css_shape] Inset - insets: ({}, {}, {}, {})",
                        inset.inset_top, inset.inset_right, inset.inset_bottom, inset.inset_left
                    )));
                    msgs.push(LayoutDebugMessage::info(format!(
                        "[ShapeBoundary::from_css_shape] Inset - resulting rect: x={}, y={}, \
                         w={}, h={}",
                        x, y, width, height
                    )));
                }
                ShapeBoundary::Rectangle(Rect {
                    x,
                    y,
                    width: width.max(0.0),
                    height: height.max(0.0),
                })
            }
            CssShape::Path(path) => {
                if let Some(msgs) = debug_messages {
                    msgs.push(LayoutDebugMessage::info(
                        "[ShapeBoundary::from_css_shape] Path - fallback to rectangle".to_string(),
                    ));
                }
                // TODO: Parse SVG path data into PathSegments
                // For now, fall back to rectangle
                ShapeBoundary::Rectangle(reference_box)
            }
        };
        if let Some(msgs) = debug_messages {
            msgs.push(LayoutDebugMessage::info(format!(
                "[ShapeBoundary::from_css_shape] Result: {:?}",
                result
            )));
        }
        result
    }
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InlineBreak {
    pub break_type: BreakType,
    pub clear: ClearType,
    pub content_index: usize,
}
// +spec:line-breaking:d70ffd - Defines forced line break (Hard) vs soft wrap break (Soft) types
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum BreakType {
    Soft,   // Soft wrap break: UA creates unforced line breaks to fit content within the measure
    Hard,   // Forced line break: explicit line-breaking controls (preserved newline, <br>)
    Page,   // Page break
    Column, // Column break
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ClearType {
    None,
    Left,
    Right,
    Both,
}
// Complex shape constraints for non-rectangular text flow
#[derive(Debug, Clone)]
pub struct ShapeConstraints {
    pub boundaries: Vec<ShapeBoundary>,
    pub exclusions: Vec<ShapeBoundary>,
    pub writing_mode: WritingMode,
    pub text_align: TextAlign,
    pub line_height: LineHeight,
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
pub enum WritingMode {
    #[default]
    HorizontalTb, // horizontal-tb (normal horizontal)
    VerticalRl, // +spec:writing-modes:6e22a7 - vertical-rl (vertical right-to-left, commonly used in East Asia)
    VerticalLr, // vertical-lr (vertical left-to-right)
    SidewaysRl, // sideways-rl (rotated horizontal in vertical context)
    SidewaysLr, // sideways-lr (rotated horizontal in vertical context)
}
impl WritingMode {
    /// Necessary to determine if the glyphs are advancing in a horizontal direction
    pub fn is_advance_horizontal(&self) -> bool {
        matches!(
            self,
            WritingMode::HorizontalTb | WritingMode::SidewaysRl | WritingMode::SidewaysLr
        )
    }
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
pub enum JustifyContent {
    #[default]
    None,
    InterWord,      // Expand spaces between words
    InterCharacter, // Expand spaces between all characters (for CJK)
    Distribute,     // Distribute space evenly including start/end
    Kashida,        // Stretch Arabic text using kashidas
}
// Enhanced text alignment with logical directions
#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
pub enum TextAlign {
    #[default]
    Left,
    Right,
    Center,
    Justify,
    Start,
    End,        // Logical start/end
    JustifyAll, // Justify including last line
}
// +spec:block-formatting-context:458d31 - vertical text orientation: upright for horizontal scripts, intrinsic for vertical scripts
// Vertical text orientation for individual characters
#[derive(Debug, Clone, Copy, PartialEq, Default, Eq, PartialOrd, Ord, Hash)]
pub enum TextOrientation {
    #[default]
    Mixed, // Default: upright for scripts, rotated for others
    Upright,  // All characters upright
    Sideways, // All characters rotated 90 degrees
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TextDecoration {
    pub underline: bool,
    pub strikethrough: bool,
    pub overline: bool,
}
impl Default for TextDecoration {
173668
    fn default() -> Self {
173668
        TextDecoration {
173668
            underline: false,
173668
            overline: false,
173668
            strikethrough: false,
173668
        }
173668
    }
}
impl TextDecoration {
    /// Convert from CSS StyleTextDecoration enum to our internal representation.
    /// 
    /// Note: CSS text-decoration can have multiple values (underline line-through),
    /// but the current azul-css parser only supports single values. This can be
    /// extended in the future if CSS parsing is updated.
10868
    pub fn from_css(css: azul_css::props::style::text::StyleTextDecoration) -> Self {
        use azul_css::props::style::text::StyleTextDecoration;
10868
        match css {
            StyleTextDecoration::None => Self::default(),
10868
            StyleTextDecoration::Underline => Self {
10868
                underline: true,
10868
                strikethrough: false,
10868
                overline: false,
10868
            },
            StyleTextDecoration::Overline => Self {
                underline: false,
                strikethrough: false,
                overline: true,
            },
            StyleTextDecoration::LineThrough => Self {
                underline: false,
                strikethrough: true,
                overline: false,
            },
        }
10868
    }
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
pub enum TextTransform {
    #[default]
    None,
    Uppercase,
    Lowercase,
    Capitalize,
    // only within preserved white space (non-preserved spaces already collapsed in Phase I)
    FullWidth,
}
// Type alias for OpenType feature tags
pub type FourCc = [u8; 4];
// Enum for relative or absolute spacing
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum Spacing {
    Px(i32), // Use integer pixels to simplify hashing and equality
    Em(f32),
}
// A type that implements `Hash` must also implement `Eq`.
// Since f32 does not implement `Eq`, we provide a manual implementation.
// The derived `PartialEq` is sufficient for this marker trait.
impl Eq for Spacing {}
impl Hash for Spacing {
467094
    fn hash<H: Hasher>(&self, state: &mut H) {
        // First, hash the enum variant to distinguish between Px and Em.
467094
        discriminant(self).hash(state);
467094
        match self {
467094
            Spacing::Px(val) => val.hash(state),
            // For hashing floats, convert them to their raw bit representation.
            // This ensures that identical float values produce identical hashes.
            Spacing::Em(val) => val.to_bits().hash(state),
        }
467094
    }
}
impl Default for Spacing {
184536
    fn default() -> Self {
184536
        Spacing::Px(0)
184536
    }
}
impl Default for FontHash {
    fn default() -> Self {
        Self::invalid()
    }
}
/// Style properties with vertical text support
#[derive(Debug, Clone, PartialEq)]
pub struct StyleProperties {
    /// Font stack for fallback support (priority order)
    /// Can be either a list of FontSelectors (resolved via fontconfig)
    /// or a direct FontRef (bypasses fontconfig entirely).
    pub font_stack: FontStack,
    pub font_size_px: f32,
    pub color: ColorU,
    /// Background color for inline elements (e.g., `<span style="background-color: yellow">`)
    ///
    /// This is propagated from CSS through the style system and eventually used by
    /// the PDF renderer to draw filled rectangles behind text. The value is `None`
    /// for transparent backgrounds (the default).
    ///
    /// The propagation chain is:
    /// CSS -> `get_style_properties()` -> `StyleProperties` -> `ShapedGlyph` -> `PdfGlyphRun`
    ///
    /// See `PdfGlyphRun::background_color` for how this is used in PDF rendering.
    pub background_color: Option<ColorU>,
    /// Full background content layers (for gradients, images, etc.)
    /// This extends background_color to support CSS gradients on inline elements.
    pub background_content: Vec<StyleBackgroundContent>,
    /// Border information for inline elements
    pub border: Option<InlineBorderInfo>,
    // +spec:text-alignment-spacing:b39a04 - word-spacing and letter-spacing control text spacing
    pub letter_spacing: Spacing,
    pub word_spacing: Spacing,
    pub line_height: LineHeight,
    pub text_decoration: TextDecoration,
    // Represents CSS font-feature-settings like `"liga"`, `"smcp=1"`.
    pub font_features: Vec<String>,
    // Variable fonts
    pub font_variations: Vec<(FourCc, f32)>,
    // Multiplier of the space width
    pub tab_size: f32,
    // text-transform
    pub text_transform: TextTransform,
    // Vertical text properties
    pub writing_mode: WritingMode,
    pub text_orientation: TextOrientation,
    // Tate-chu-yoko
    pub text_combine_upright: Option<TextCombineUpright>,
    // Variant handling
    pub font_variant_caps: FontVariantCaps,
    pub font_variant_numeric: FontVariantNumeric,
    pub font_variant_ligatures: FontVariantLigatures,
    pub font_variant_east_asian: FontVariantEastAsian,
}
impl Default for StyleProperties {
92268
    fn default() -> Self {
        const FONT_SIZE: f32 = 16.0;
        const TAB_SIZE: f32 = 8.0;
92268
        Self {
92268
            font_stack: FontStack::default(),
92268
            font_size_px: FONT_SIZE,
92268
            color: ColorU::default(),
92268
            background_color: None,
92268
            background_content: Vec::new(),
92268
            border: None,
92268
            letter_spacing: Spacing::default(), // Px(0)
92268
            word_spacing: Spacing::default(),   // Px(0)
92268
            line_height: LineHeight::Normal,
92268
            text_decoration: TextDecoration::default(),
92268
            font_features: Vec::new(),
92268
            font_variations: Vec::new(),
92268
            tab_size: TAB_SIZE, // CSS default
92268
            text_transform: TextTransform::default(),
92268
            writing_mode: WritingMode::default(),
92268
            text_orientation: TextOrientation::default(),
92268
            text_combine_upright: None,
92268
            font_variant_caps: FontVariantCaps::default(),
92268
            font_variant_numeric: FontVariantNumeric::default(),
92268
            font_variant_ligatures: FontVariantLigatures::default(),
92268
            font_variant_east_asian: FontVariantEastAsian::default(),
92268
        }
92268
    }
}
impl Hash for StyleProperties {
201603
    fn hash<H: Hasher>(&self, state: &mut H) {
201603
        self.font_stack.hash(state);
201603
        self.color.hash(state);
201603
        self.background_color.hash(state);
201603
        self.text_decoration.hash(state);
201603
        self.font_features.hash(state);
201603
        self.writing_mode.hash(state);
201603
        self.text_orientation.hash(state);
201603
        self.text_combine_upright.hash(state);
201603
        self.letter_spacing.hash(state);
201603
        self.word_spacing.hash(state);
        // For f32 fields, round and cast to usize before hashing.
201603
        (self.font_size_px.round() as usize).hash(state);
201603
        self.line_height.hash(state);
201603
    }
}
impl StyleProperties {
    /// Returns a hash that only includes properties that affect text layout.
    /// 
    /// Properties that DON'T affect layout (only rendering):
    /// - color, background_color, background_content
    /// - text_decoration (underline, etc.)
    /// - border (for inline elements)
    ///
    /// Properties that DO affect layout:
    /// - font_stack, font_size_px, font_features, font_variations
    /// - letter_spacing, word_spacing, line_height, tab_size
    /// - writing_mode, text_orientation, text_combine_upright
    /// - text_transform
    /// - font_variant_* (affects glyph selection)
    ///
    /// This allows the layout cache to reuse layouts when only rendering
    /// properties change (e.g., color changes on hover).
    // (family, weight, style) so that shaping runs break at element boundaries where font
    // properties differ, preventing impossible cross-boundary ligatures (e.g. "and" → "&").
31944
    pub fn layout_hash(&self) -> u64 {
        use std::hash::Hasher;
31944
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        // Font selection (affects shaping and metrics)
31944
        self.font_stack.hash(&mut hasher);
31944
        (self.font_size_px.round() as usize).hash(&mut hasher);
31944
        self.font_features.hash(&mut hasher);
        // font_variations affects glyph outlines
31944
        for (tag, value) in &self.font_variations {
            tag.hash(&mut hasher);
            (value.round() as i32).hash(&mut hasher);
        }
        // Spacing (affects glyph positions)
31944
        self.letter_spacing.hash(&mut hasher);
31944
        self.word_spacing.hash(&mut hasher);
31944
        self.line_height.hash(&mut hasher);
31944
        (self.tab_size.round() as usize).hash(&mut hasher);
        // Writing mode (affects layout direction)
31944
        self.writing_mode.hash(&mut hasher);
31944
        self.text_orientation.hash(&mut hasher);
31944
        self.text_combine_upright.hash(&mut hasher);
        // Text transform (affects which characters are used)
31944
        self.text_transform.hash(&mut hasher);
        // Font variants (affect glyph selection)
31944
        self.font_variant_caps.hash(&mut hasher);
31944
        self.font_variant_numeric.hash(&mut hasher);
31944
        self.font_variant_ligatures.hash(&mut hasher);
31944
        self.font_variant_east_asian.hash(&mut hasher);
31944
        hasher.finish()
31944
    }
    /// Check if two StyleProperties have the same layout-affecting properties.
    ///
    /// Returns true if the layouts would be identical (only rendering differs).
    ///
    /// **Note:** This is a fast-path comparison using 64-bit hashes.  Hash
    /// collisions are theoretically possible, which could cause the cache to
    /// serve a stale layout.  In practice the probability is negligible for
    /// the number of distinct `StyleProperties` values in a single document.
    pub fn layout_eq(&self, other: &Self) -> bool {
        self.layout_hash() == other.layout_hash()
    }
}
#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
pub enum TextCombineUpright {
    None,
    All,        // Combine all characters in horizontal layout
    Digits(u8), // Combine up to N digits
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GlyphSource {
    /// Glyph generated from a character in the source text.
    Char,
    /// Glyph inserted dynamically by the layout engine (e.g., a hyphen).
    Hyphen,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CharacterClass {
    Space,       // Regular spaces - highest justification priority
    Punctuation, // Can sometimes be adjusted
    Letter,      // Normal letters
    Ideograph,   // CJK characters - can be justified between
    Symbol,      // Symbols, emojis
    Combining,   // Combining marks - never justified
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GlyphOrientation {
    Horizontal, // Keep horizontal (normal in horizontal text)
    Vertical,   // Rotate to vertical (normal in vertical text)
    Upright,    // Keep upright regardless of writing mode
    Mixed,      // Use script-specific default orientation
}
// Bidi and script detection
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum BidiDirection {
    Ltr,
    Rtl,
}
impl BidiDirection {
20064
    pub fn is_rtl(&self) -> bool {
20064
        matches!(self, BidiDirection::Rtl)
20064
    }
}
/// CSS `unicode-bidi` property values relevant to layout.
/// When `Plaintext`, the bidi algorithm uses P2/P3 heuristics to auto-detect
/// paragraph direction from text content, instead of the HL1 override from
/// the CSS `direction` property.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum UnicodeBidi {
    Normal,
    Embed,
    Isolate,
    BidiOverride,
    IsolateOverride,
    Plaintext,
}
impl Default for UnicodeBidi {
69828
    fn default() -> Self {
69828
        UnicodeBidi::Normal
69828
    }
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
pub enum FontVariantCaps {
    #[default]
    Normal,
    SmallCaps,
    AllSmallCaps,
    PetiteCaps,
    AllPetiteCaps,
    Unicase,
    TitlingCaps,
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
pub enum FontVariantNumeric {
    #[default]
    Normal,
    LiningNums,
    OldstyleNums,
    ProportionalNums,
    TabularNums,
    DiagonalFractions,
    StackedFractions,
    Ordinal,
    SlashedZero,
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
pub enum FontVariantLigatures {
    #[default]
    Normal,
    None,
    Common,
    NoCommon,
    Discretionary,
    NoDiscretionary,
    Historical,
    NoHistorical,
    Contextual,
    NoContextual,
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
pub enum FontVariantEastAsian {
    #[default]
    Normal,
    Jis78,
    Jis83,
    Jis90,
    Jis04,
    Simplified,
    Traditional,
    FullWidth,
    ProportionalWidth,
    Ruby,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BidiLevel(u8);
impl BidiLevel {
37268
    pub fn new(level: u8) -> Self {
37268
        Self(level)
37268
    }
15224
    pub fn is_rtl(&self) -> bool {
15224
        self.0 % 2 == 1
15224
    }
17468
    pub fn level(&self) -> u8 {
17468
        self.0
17468
    }
}
// Add this new struct for style overrides
#[derive(Debug, Clone)]
pub struct StyleOverride {
    /// The specific character this override applies to.
    pub target: ContentIndex,
    /// The style properties to apply.
    /// Any `None` value means "inherit from the base style".
    pub style: PartialStyleProperties,
}
#[derive(Debug, Clone, Default)]
pub struct PartialStyleProperties {
    pub font_stack: Option<FontStack>,
    pub font_size_px: Option<f32>,
    pub color: Option<ColorU>,
    pub letter_spacing: Option<Spacing>,
    pub word_spacing: Option<Spacing>,
    pub line_height: Option<LineHeight>,
    pub text_decoration: Option<TextDecoration>,
    pub font_features: Option<Vec<String>>,
    pub font_variations: Option<Vec<(FourCc, f32)>>,
    pub tab_size: Option<f32>,
    pub text_transform: Option<TextTransform>,
    pub writing_mode: Option<WritingMode>,
    pub text_orientation: Option<TextOrientation>,
    pub text_combine_upright: Option<Option<TextCombineUpright>>,
    pub font_variant_caps: Option<FontVariantCaps>,
    pub font_variant_numeric: Option<FontVariantNumeric>,
    pub font_variant_ligatures: Option<FontVariantLigatures>,
    pub font_variant_east_asian: Option<FontVariantEastAsian>,
}
impl Hash for PartialStyleProperties {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.font_stack.hash(state);
        self.font_size_px.map(|f| f.to_bits()).hash(state);
        self.color.hash(state);
        self.letter_spacing.hash(state);
        self.word_spacing.hash(state);
        self.line_height.hash(state);
        self.text_decoration.hash(state);
        self.font_features.hash(state);
        // Manual hashing for Vec<(FourCc, f32)>
        self.font_variations.as_ref().map(|v| {
            for (tag, val) in v {
                tag.hash(state);
                val.to_bits().hash(state);
            }
        });
        self.tab_size.map(|f| f.to_bits()).hash(state);
        self.text_transform.hash(state);
        self.writing_mode.hash(state);
        self.text_orientation.hash(state);
        self.text_combine_upright.hash(state);
        self.font_variant_caps.hash(state);
        self.font_variant_numeric.hash(state);
        self.font_variant_ligatures.hash(state);
        self.font_variant_east_asian.hash(state);
    }
}
impl PartialEq for PartialStyleProperties {
    fn eq(&self, other: &Self) -> bool {
        self.font_stack == other.font_stack &&
        self.font_size_px.map(|f| f.to_bits()) == other.font_size_px.map(|f| f.to_bits()) &&
        self.color == other.color &&
        self.letter_spacing == other.letter_spacing &&
        self.word_spacing == other.word_spacing &&
        self.line_height == other.line_height &&
        self.text_decoration == other.text_decoration &&
        self.font_features == other.font_features &&
        self.font_variations == other.font_variations && // Vec<(FourCc, f32)> is PartialEq
        self.tab_size.map(|f| f.to_bits()) == other.tab_size.map(|f| f.to_bits()) &&
        self.text_transform == other.text_transform &&
        self.writing_mode == other.writing_mode &&
        self.text_orientation == other.text_orientation &&
        self.text_combine_upright == other.text_combine_upright &&
        self.font_variant_caps == other.font_variant_caps &&
        self.font_variant_numeric == other.font_variant_numeric &&
        self.font_variant_ligatures == other.font_variant_ligatures &&
        self.font_variant_east_asian == other.font_variant_east_asian
    }
}
impl Eq for PartialStyleProperties {}
impl StyleProperties {
    fn apply_override(&self, partial: &PartialStyleProperties) -> Self {
        let mut new_style = self.clone();
        if let Some(val) = &partial.font_stack {
            new_style.font_stack = val.clone();
        }
        if let Some(val) = partial.font_size_px {
            new_style.font_size_px = val;
        }
        if let Some(val) = &partial.color {
            new_style.color = val.clone();
        }
        if let Some(val) = partial.letter_spacing {
            new_style.letter_spacing = val;
        }
        if let Some(val) = partial.word_spacing {
            new_style.word_spacing = val;
        }
        if let Some(val) = partial.line_height {
            new_style.line_height = val;
        }
        if let Some(val) = &partial.text_decoration {
            new_style.text_decoration = val.clone();
        }
        if let Some(val) = &partial.font_features {
            new_style.font_features = val.clone();
        }
        if let Some(val) = &partial.font_variations {
            new_style.font_variations = val.clone();
        }
        if let Some(val) = partial.tab_size {
            new_style.tab_size = val;
        }
        if let Some(val) = partial.text_transform {
            new_style.text_transform = val;
        }
        if let Some(val) = partial.writing_mode {
            new_style.writing_mode = val;
        }
        if let Some(val) = partial.text_orientation {
            new_style.text_orientation = val;
        }
        if let Some(val) = &partial.text_combine_upright {
            new_style.text_combine_upright = val.clone();
        }
        if let Some(val) = partial.font_variant_caps {
            new_style.font_variant_caps = val;
        }
        if let Some(val) = partial.font_variant_numeric {
            new_style.font_variant_numeric = val;
        }
        if let Some(val) = partial.font_variant_ligatures {
            new_style.font_variant_ligatures = val;
        }
        if let Some(val) = partial.font_variant_east_asian {
            new_style.font_variant_east_asian = val;
        }
        new_style
    }
}
/// The kind of a glyph, used to distinguish characters from layout-inserted items.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GlyphKind {
    /// A standard glyph representing one or more characters from the source text.
    Character,
    /// A hyphen glyph inserted by the line breaking algorithm.
    Hyphen,
    /// A `.notdef` glyph, indicating a character that could not be found in any font.
    NotDef,
    /// A Kashida justification glyph, inserted to stretch Arabic text.
    Kashida {
        /// The target width of the kashida.
        width: f32,
    },
}
// --- Stage 1: Logical Representation ---
// [g117 az-web-lift FIX] `#[repr(C, u8)]` (was repr(Rust)) — same disc-mis-lift class as InlineContent
// above. LogicalItem is matched in measure Stage-2 (`if let LogicalItem::Text`) + reorder_logical_items;
// a repr(Rust) niche disc mis-lifts on the web. Explicit u8 tag at offset 0 = a simple load the lift
// reads correctly. Internal to text3 (not FFI-exposed). LogicalItem::Object embeds InlineContent inline.
#[derive(Debug, Clone)]
#[repr(C, u8)]
pub enum LogicalItem {
    Text {
        /// A stable ID pointing back to the original source character.
        source: ContentIndex,
        /// The text of this specific logical item (often a single grapheme cluster).
        text: String,
        style: Arc<StyleProperties>,
        /// If this text is a list marker: whether it should be positioned outside
        /// (in the padding gutter) or inside (inline with content).
        /// None for non-marker content.
        marker_position_outside: Option<bool>,
        /// The DOM NodeId of the Text node this item originated from.
        /// None for generated content (list markers, ::before/::after, etc.)
        source_node_id: Option<NodeId>,
    },
    // +spec:display-property:b1533f - text-combine-upright tate-chu-yoko horizontal-in-vertical composition
    /// Tate-chu-yoko: Run of text to be laid out horizontally within a vertical context.
    CombinedText {
        source: ContentIndex,
        text: String,
        style: Arc<StyleProperties>,
    },
    Ruby {
        source: ContentIndex,
        // For the stub, we simplify to strings. A full implementation
        // would need to handle Vec<LogicalItem> for both.
        base_text: String,
        ruby_text: String,
        style: Arc<StyleProperties>,
    },
    Object {
        /// A stable ID pointing back to the original source object.
        source: ContentIndex,
        /// The original non-text object.
        content: InlineContent,
    },
    Tab {
        source: ContentIndex,
        style: Arc<StyleProperties>,
    },
    Break {
        source: ContentIndex,
        break_info: InlineBreak,
    },
}
impl Hash for LogicalItem {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        discriminant(self).hash(state);
        match self {
            LogicalItem::Text {
                source,
                text,
                style,
                marker_position_outside,
                source_node_id,
            } => {
                source.hash(state);
                text.hash(state);
                style.as_ref().hash(state); // Hash the content, not the Arc pointer
                marker_position_outside.hash(state);
                source_node_id.hash(state);
            }
            LogicalItem::CombinedText {
                source,
                text,
                style,
            } => {
                source.hash(state);
                text.hash(state);
                style.as_ref().hash(state);
            }
            LogicalItem::Ruby {
                source,
                base_text,
                ruby_text,
                style,
            } => {
                source.hash(state);
                base_text.hash(state);
                ruby_text.hash(state);
                style.as_ref().hash(state);
            }
            LogicalItem::Object { source, content } => {
                source.hash(state);
                content.hash(state);
            }
            LogicalItem::Tab { source, style } => {
                source.hash(state);
                style.as_ref().hash(state);
            }
            LogicalItem::Break { source, break_info } => {
                source.hash(state);
                break_info.hash(state);
            }
        }
    }
}
// --- Stage 2: Visual Representation ---
#[derive(Debug, Clone)]
pub struct VisualItem {
    /// A reference to the logical item this visual item originated from.
    /// A single LogicalItem can be split into multiple VisualItems.
    pub logical_source: LogicalItem,
    /// The Bidi embedding level for this item.
    pub bidi_level: BidiLevel,
    /// The script detected for this run, crucial for shaping.
    pub script: Script,
    /// The text content for this specific visual run.
    pub text: String,
}
// --- Stage 3: Shaped Representation ---
// [g118 az-web-lift FIX] `#[repr(C, u8)]` (was repr(Rust)) — same disc-mis-lift class as InlineContent
// + LogicalItem (g117). ShapedItem is matched in measure Stage-5 (`match item { ShapedItem::Cluster ..}`)
// + cloned/matched throughout shaping; a repr(Rust) niche disc mis-lifts on the web. Explicit u8 tag at
// offset 0 = a simple load the lift reads correctly. Internal to text3 (not FFI-exposed).
#[derive(Debug, Clone)]
#[repr(C, u8)]
pub enum ShapedItem {
    Cluster(ShapedCluster),
    /// A block of combined text (tate-chu-yoko) that is laid out
    // as a single unbreakable object.
    CombinedBlock {
        source: ContentIndex,
        /// The glyphs to be rendered horizontally within the vertical line.
        glyphs: ShapedGlyphVec,
        bounds: Rect,
        baseline_offset: f32,
    },
    Object {
        source: ContentIndex,
        bounds: Rect,
        baseline_offset: f32,
        // Store original object for rendering
        content: InlineContent,
    },
    Tab {
        source: ContentIndex,
        bounds: Rect,
    },
    Break {
        source: ContentIndex,
        break_info: InlineBreak,
    },
}
impl ShapedItem {
1082752
    pub fn as_cluster(&self) -> Option<&ShapedCluster> {
1082752
        match self {
1081168
            ShapedItem::Cluster(c) => Some(c),
1584
            _ => None,
        }
1082752
    }
    /// Returns the bounding box of the item, relative to its own origin.
    ///
    /// The origin of the returned `Rect` is `(0,0)`, representing the top-left corner
    /// of the item's layout space before final positioning. The size represents the
    /// item's total advance (width in horizontal mode) and its line height (ascent + descent).
1722248
    pub fn bounds(&self) -> Rect {
1722248
        match self {
1718508
            ShapedItem::Cluster(cluster) => {
                // The width of a text cluster is its total advance.
1718508
                let width = cluster.advance;
                // The height is the sum of its ascent and descent, which defines its line box.
                // We use the existing helper function which correctly calculates this from font
                // metrics.
1718508
                let (ascent, descent) = get_item_vertical_metrics_approx(self);
1718508
                let height = ascent + descent;
1718508
                Rect {
1718508
                    x: 0.0,
1718508
                    y: 0.0,
1718508
                    width,
1718508
                    height,
1718508
                }
            }
            // For atomic inline items like objects, combined blocks, and tabs,
            // their bounds have already been calculated during the shaping or measurement phase.
            ShapedItem::CombinedBlock { bounds, .. } => *bounds,
748
            ShapedItem::Object { bounds, .. } => *bounds,
616
            ShapedItem::Tab { bounds, .. } => *bounds,
            // Breaks are control characters and have no visual geometry.
2376
            ShapedItem::Break { .. } => Rect::default(), // A zero-sized rectangle.
        }
1722248
    }
}
/// A group of glyphs that corresponds to one or more source characters (a cluster).
#[derive(Debug, Clone)]
pub struct ShapedCluster {
    /// The original text that this cluster was shaped from.
    /// This is crucial for correct hyphenation.
    pub text: String,
    /// The ID of the grapheme cluster this glyph cluster represents.
    pub source_cluster_id: GraphemeClusterId,
    /// The source `ContentIndex` for mapping back to logical items.
    pub source_content_index: ContentIndex,
    /// The DOM NodeId of the Text node this cluster originated from.
    /// None for generated content (list markers, ::before/::after, etc.)
    pub source_node_id: Option<NodeId>,
    /// The glyphs that make up this cluster. `SmallVec<[T; 1]>` — inline
    /// single-glyph clusters (the common case for Latin text), spill to
    /// heap only for ligatures / combining marks.
    pub glyphs: ShapedGlyphVec,
    /// The total advance width (horizontal) or height (vertical) of the cluster.
    pub advance: f32,
    /// The direction of this cluster, inherited from its `VisualItem`.
    pub direction: BidiDirection,
    /// Font style of this cluster
    pub style: Arc<StyleProperties>,
    /// If this cluster is a list marker: whether it should be positioned outside
    /// (in the padding gutter) or inside (inline with content).
    /// None for non-marker content.
    pub marker_position_outside: Option<bool>,
    /// True if this is the first visual fragment of its inline box.
    /// Used for `box-decoration-break` and split inline border/padding.
    /// When an inline element wraps across lines, only the first fragment
    /// gets the start-edge border/padding.
    pub is_first_fragment: bool,
    /// True if this is the last visual fragment of its inline box.
    /// Only the last fragment gets the end-edge border/padding.
    pub is_last_fragment: bool,
}
/// A single, shaped glyph with its essential metrics.
#[derive(Debug, Clone)]
pub struct ShapedGlyph {
    /// The kind of glyph this is (character, hyphen, etc.).
    pub kind: GlyphKind,
    /// Glyph ID inside of the font
    pub glyph_id: u16,
    /// The byte offset of this glyph's source character(s) within its cluster text.
    pub cluster_offset: u32,
    /// The horizontal advance for this glyph (for horizontal text) - this is the BASE advance
    /// from the font metrics, WITHOUT kerning applied
    pub advance: f32,
    /// The kerning adjustment for this glyph (positive = more space, negative = less space)
    /// This is separate from advance so we can position glyphs absolutely
    pub kerning: f32,
    /// The horizontal offset/bearing for this glyph
    pub offset: Point,
    /// The vertical advance for this glyph (for vertical text).
    pub vertical_advance: f32,
    /// The vertical offset/bearing for this glyph.
    pub vertical_offset: Point,
    pub script: Script,
    pub style: Arc<StyleProperties>,
    /// Hash of the font - use LoadedFonts to look up the actual font when needed
    pub font_hash: u64,
    /// Cached font metrics to avoid font lookup for common operations
    pub font_metrics: LayoutFontMetrics,
}
impl ShapedGlyph {
    pub fn into_glyph_instance<T: ParsedFontTrait>(
        &self,
        writing_mode: WritingMode,
        loaded_fonts: &LoadedFonts<T>,
    ) -> GlyphInstance {
        let size = loaded_fonts
            .get_by_hash(self.font_hash)
            .and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
            .unwrap_or_default();
        let position = if writing_mode.is_advance_horizontal() {
            LogicalPosition {
                x: self.offset.x,
                y: self.offset.y,
            }
        } else {
            LogicalPosition {
                x: self.vertical_offset.x,
                y: self.vertical_offset.y,
            }
        };
        GlyphInstance {
            index: self.glyph_id as u32,
            point: position,
            size,
        }
    }
    /// Convert this ShapedGlyph into a GlyphInstance with an absolute position.
    /// This is used for display list generation where glyphs need their final page coordinates.
    pub fn into_glyph_instance_at<T: ParsedFontTrait>(
        &self,
        writing_mode: WritingMode,
        absolute_position: LogicalPosition,
        loaded_fonts: &LoadedFonts<T>,
    ) -> GlyphInstance {
        let size = loaded_fonts
            .get_by_hash(self.font_hash)
            .and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
            .unwrap_or_default();
        GlyphInstance {
            index: self.glyph_id as u32,
            point: absolute_position,
            size,
        }
    }
    /// Convert this ShapedGlyph into a GlyphInstance with an absolute position.
    /// This version doesn't require fonts - it uses a default size.
    /// Use this when you don't need precise glyph bounds (e.g., display list generation).
198000
    pub fn into_glyph_instance_at_simple(
198000
        &self,
198000
        _writing_mode: WritingMode,
198000
        absolute_position: LogicalPosition,
198000
    ) -> GlyphInstance {
        // Use font metrics to estimate size, or default to zero
        // The actual rendering will use the font directly
198000
        GlyphInstance {
198000
            index: self.glyph_id as u32,
198000
            point: absolute_position,
198000
            size: LogicalSize::default(),
198000
        }
198000
    }
}
// --- Stage 4: Positioned Representation (Final Layout) ---
#[derive(Debug, Clone)]
pub struct PositionedItem {
    pub item: ShapedItem,
    pub position: Point,
    pub line_index: usize,
}
#[derive(Debug, Clone)]
pub struct UnifiedLayout {
    pub items: Vec<PositionedItem>,
    /// Information about content that did not fit.
    pub overflow: OverflowInfo,
}
impl UnifiedLayout {
    /// Calculate the bounding box of all positioned items.
    /// This is computed on-demand rather than cached.
152064
    pub fn bounds(&self) -> Rect {
152064
        if self.items.is_empty() {
10120
            return Rect::default();
141944
        }
141944
        let mut min_x = f32::MAX;
141944
        let mut min_y = f32::MAX;
141944
        let mut max_x = f32::MIN;
141944
        let mut max_y = f32::MIN;
1292236
        for item in &self.items {
1150292
            let item_x = item.position.x;
1150292
            let item_y = item.position.y;
1150292

            
1150292
            // Get item dimensions
1150292
            let item_bounds = item.item.bounds();
1150292
            let item_width = item_bounds.width;
1150292
            let item_height = item_bounds.height;
1150292

            
1150292
            min_x = min_x.min(item_x);
1150292
            min_y = min_y.min(item_y);
1150292
            max_x = max_x.max(item_x + item_width);
1150292
            max_y = max_y.max(item_y + item_height);
1150292
        }
141944
        Rect {
141944
            x: min_x,
141944
            y: min_y,
141944
            width: max_x - min_x,
141944
            height: max_y - min_y,
141944
        }
152064
    }
    pub fn is_empty(&self) -> bool {
        self.items.is_empty()
    }
    pub fn first_baseline(&self) -> Option<f32> {
        self.items
            .iter()
            .find_map(|item| get_baseline_for_item(&item.item))
    }
70312
    pub fn last_baseline(&self) -> Option<f32> {
70312
        self.items
70312
            .iter()
70312
            .rev()
70312
            .find_map(|item| get_baseline_for_item(&item.item))
70312
    }
    /// Takes a point relative to the layout's origin and returns the closest
    /// logical cursor position.
    ///
    /// This is the unified hit-testing implementation. The old `hit_test_to_cursor`
    /// method is deprecated in favor of this one.
    pub fn hittest_cursor(&self, point: LogicalPosition) -> Option<TextCursor> {
        if self.items.is_empty() {
            return None;
        }
        // Find the closest cluster vertically and horizontally
        let mut closest_item_idx = 0;
        let mut closest_distance = f32::MAX;
        for (idx, item) in self.items.iter().enumerate() {
            // Only consider cluster items for cursor placement
            if !matches!(item.item, ShapedItem::Cluster(_)) {
                continue;
            }
            let item_bounds = item.item.bounds();
            let item_center_y = item.position.y + item_bounds.height / 2.0;
            // Distance from click position to item center
            let vertical_distance = (point.y - item_center_y).abs();
            // For horizontal distance, check if we're within the cluster bounds
            let horizontal_distance = if point.x < item.position.x {
                item.position.x - point.x
            } else if point.x > item.position.x + item_bounds.width {
                point.x - (item.position.x + item_bounds.width)
            } else {
                0.0 // Inside the cluster horizontally
            };
            // Combined distance (prioritize vertical proximity)
            let distance = vertical_distance * 2.0 + horizontal_distance;
            if distance < closest_distance {
                closest_distance = distance;
                closest_item_idx = idx;
            }
        }
        // Get the closest cluster
        let closest_item = &self.items[closest_item_idx];
        let cluster = match &closest_item.item {
            ShapedItem::Cluster(c) => c,
            // Objects are treated as a single cluster for selection
            ShapedItem::Object { source, .. } | ShapedItem::CombinedBlock { source, .. } => {
                return Some(TextCursor {
                    cluster_id: GraphemeClusterId {
                        source_run: source.run_index,
                        start_byte_in_run: source.item_index,
                    },
                    affinity: if point.x
                        < closest_item.position.x + (closest_item.item.bounds().width / 2.0)
                    {
                        CursorAffinity::Leading
                    } else {
                        CursorAffinity::Trailing
                    },
                });
            }
            _ => return None,
        };
        // Determine affinity based on which half of the cluster was clicked
        let cluster_mid_x = closest_item.position.x + cluster.advance / 2.0;
        let affinity = if point.x < cluster_mid_x {
            CursorAffinity::Leading
        } else {
            CursorAffinity::Trailing
        };
        Some(TextCursor {
            cluster_id: cluster.source_cluster_id,
            affinity,
        })
    }
    /// Given a logical selection range, returns a vector of visual rectangles
    /// that cover the selected text, in the layout's coordinate space.
    pub fn get_selection_rects(&self, range: &SelectionRange) -> Vec<LogicalRect> {
        // 1. Build a map from the logical cluster ID to the visual PositionedItem for fast lookups.
        let mut cluster_map: HashMap<GraphemeClusterId, &PositionedItem> = HashMap::new();
        for item in &self.items {
            if let Some(cluster) = item.item.as_cluster() {
                cluster_map.insert(cluster.source_cluster_id, item);
            }
        }
        // 2. Normalize the range to ensure start always logically precedes end.
        let (start_cursor, end_cursor) = if range.start.cluster_id > range.end.cluster_id
            || (range.start.cluster_id == range.end.cluster_id
                && range.start.affinity > range.end.affinity)
        {
            (range.end, range.start)
        } else {
            (range.start, range.end)
        };
        // 3. Find the positioned items corresponding to the start and end of the selection.
        let Some(start_item) = cluster_map.get(&start_cursor.cluster_id) else {
            return Vec::new();
        };
        let Some(end_item) = cluster_map.get(&end_cursor.cluster_id) else {
            return Vec::new();
        };
        let mut rects = Vec::new();
        // Helper to get the absolute visual X coordinate of a cursor.
        let get_cursor_x = |item: &PositionedItem, affinity: CursorAffinity| -> f32 {
            match affinity {
                CursorAffinity::Leading => item.position.x,
                CursorAffinity::Trailing => item.position.x + get_item_measure(&item.item, false),
            }
        };
        // Helper to get the visual bounding box of all content on a specific line index.
        let get_line_bounds = |line_index: usize| -> Option<LogicalRect> {
            let items_on_line = self.items.iter().filter(|i| i.line_index == line_index);
            let mut min_x: Option<f32> = None;
            let mut max_x: Option<f32> = None;
            let mut min_y: Option<f32> = None;
            let mut max_y: Option<f32> = None;
            for item in items_on_line {
                // Skip items that don't take up space (like hard breaks)
                let item_bounds = item.item.bounds();
                if item_bounds.width <= 0.0 && item_bounds.height <= 0.0 {
                    continue;
                }
                let item_x_end = item.position.x + item_bounds.width;
                let item_y_end = item.position.y + item_bounds.height;
                min_x = Some(min_x.map_or(item.position.x, |mx| mx.min(item.position.x)));
                max_x = Some(max_x.map_or(item_x_end, |mx| mx.max(item_x_end)));
                min_y = Some(min_y.map_or(item.position.y, |my| my.min(item.position.y)));
                max_y = Some(max_y.map_or(item_y_end, |my| my.max(item_y_end)));
            }
            if let (Some(min_x), Some(max_x), Some(min_y), Some(max_y)) =
                (min_x, max_x, min_y, max_y)
            {
                Some(LogicalRect {
                    origin: LogicalPosition { x: min_x, y: min_y },
                    size: LogicalSize {
                        width: max_x - min_x,
                        height: max_y - min_y,
                    },
                })
            } else {
                None
            }
        };
        // 4. Handle single-line selection.
        if start_item.line_index == end_item.line_index {
            if let Some(line_bounds) = get_line_bounds(start_item.line_index) {
                let start_x = get_cursor_x(start_item, start_cursor.affinity);
                let end_x = get_cursor_x(end_item, end_cursor.affinity);
                // Use min/max and abs to correctly handle selections made from right-to-left.
                rects.push(LogicalRect {
                    origin: LogicalPosition {
                        x: start_x.min(end_x),
                        y: line_bounds.origin.y,
                    },
                    size: LogicalSize {
                        width: (end_x - start_x).abs(),
                        height: line_bounds.size.height,
                    },
                });
            }
        }
        // 5. Handle multi-line selection.
        else {
            // Rectangle for the start line (from cursor to end of line).
            if let Some(start_line_bounds) = get_line_bounds(start_item.line_index) {
                let start_x = get_cursor_x(start_item, start_cursor.affinity);
                let line_end_x = start_line_bounds.origin.x + start_line_bounds.size.width;
                rects.push(LogicalRect {
                    origin: LogicalPosition {
                        x: start_x,
                        y: start_line_bounds.origin.y,
                    },
                    size: LogicalSize {
                        width: line_end_x - start_x,
                        height: start_line_bounds.size.height,
                    },
                });
            }
            // Rectangles for all full lines in between.
            for line_idx in (start_item.line_index + 1)..end_item.line_index {
                if let Some(line_bounds) = get_line_bounds(line_idx) {
                    rects.push(line_bounds);
                }
            }
            // Rectangle for the end line (from start of line to cursor).
            if let Some(end_line_bounds) = get_line_bounds(end_item.line_index) {
                let line_start_x = end_line_bounds.origin.x;
                let end_x = get_cursor_x(end_item, end_cursor.affinity);
                rects.push(LogicalRect {
                    origin: LogicalPosition {
                        x: line_start_x,
                        y: end_line_bounds.origin.y,
                    },
                    size: LogicalSize {
                        width: end_x - line_start_x,
                        height: end_line_bounds.size.height,
                    },
                });
            }
        }
        rects
    }
    /// Calculates the visual rectangle for a cursor at a given logical position.
2288
    pub fn get_cursor_rect(&self, cursor: &TextCursor) -> Option<LogicalRect> {
        // Find the item and glyph corresponding to the cursor's cluster ID.
2288
        let mut last_cluster: Option<(&PositionedItem, &ShapedCluster)> = None;
19536
        for item in &self.items {
17248
            if let ShapedItem::Cluster(cluster) = &item.item {
17248
                if cluster.source_cluster_id == cursor.cluster_id {
                    // Exact match
                    let line_height = item.item.bounds().height;
                    let cursor_x = match cursor.affinity {
                        CursorAffinity::Leading => item.position.x,
                        CursorAffinity::Trailing => item.position.x + cluster.advance,
                    };
                    return Some(LogicalRect {
                        origin: LogicalPosition {
                            x: cursor_x,
                            y: item.position.y,
                        },
                        size: LogicalSize {
                            width: 1.0,
                            height: line_height,
                        },
                    });
17248
                }
17248
                last_cluster = Some((item, cluster));
            }
        }
        // Cursor past end of text: position after the last cluster
2288
        if let Some((item, cluster)) = last_cluster {
2288
            if cursor.cluster_id.source_run == cluster.source_cluster_id.source_run
2288
                && cursor.cluster_id.start_byte_in_run >= cluster.source_cluster_id.start_byte_in_run
            {
2288
                let line_height = item.item.bounds().height;
2288
                return Some(LogicalRect {
2288
                    origin: LogicalPosition {
2288
                        x: item.position.x + cluster.advance,
2288
                        y: item.position.y,
2288
                    },
2288
                    size: LogicalSize {
2288
                        width: 1.0,
2288
                        height: line_height,
2288
                    },
2288
                });
            }
        }
        None
2288
    }
    /// Get a cursor at the first cluster (leading edge) in the layout.
    pub fn get_first_cluster_cursor(&self) -> Option<TextCursor> {
        for item in &self.items {
            if let ShapedItem::Cluster(cluster) = &item.item {
                return Some(TextCursor {
                    cluster_id: cluster.source_cluster_id,
                    affinity: CursorAffinity::Leading,
                });
            }
        }
        None
    }
    /// Get a cursor at the last cluster (trailing edge) in the layout.
    pub fn get_last_cluster_cursor(&self) -> Option<TextCursor> {
        for item in self.items.iter().rev() {
            if let ShapedItem::Cluster(cluster) = &item.item {
                return Some(TextCursor {
                    cluster_id: cluster.source_cluster_id,
                    affinity: CursorAffinity::Trailing,
                });
            }
        }
        None
    }
    /// Moves a cursor one visual unit to the left, handling line wrapping and Bidi text.
    pub fn move_cursor_left(
        &self,
        cursor: TextCursor,
        debug: &mut Option<Vec<String>>,
    ) -> TextCursor {
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_left: starting at byte {}, affinity {:?}",
                cursor.cluster_id.start_byte_in_run, cursor.affinity
            ));
        }
        // Find current item
        let current_item_pos = self.items.iter().position(|i| {
            i.item
                .as_cluster()
                .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
        });
        let Some(current_pos) = current_item_pos else {
            if let Some(d) = debug {
                d.push(format!(
                    "[Cursor] move_cursor_left: cursor not found, staying at byte {}",
                    cursor.cluster_id.start_byte_in_run
                ));
            }
            return cursor;
        };
        // Skip the Trailing→Leading affinity flip for simple cursor movement.
        // Each left arrow press should move to the previous visible character position.
        // Move to previous cluster's trailing edge
        // Search backwards for a cluster on the same line, or any cluster if at line start
        let current_line = self.items[current_pos].line_index;
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_left: at leading edge, current line {}",
                current_line
            ));
        }
        // First, try to find previous item on same line
        for i in (0..current_pos).rev() {
            if let Some(cluster) = self.items[i].item.as_cluster() {
                if self.items[i].line_index == current_line {
                    if let Some(d) = debug {
                        d.push(format!(
                            "[Cursor] move_cursor_left: found previous cluster on same line, byte \
                             {}",
                            cluster.source_cluster_id.start_byte_in_run
                        ));
                    }
                    return TextCursor {
                        cluster_id: cluster.source_cluster_id,
                        affinity: CursorAffinity::Trailing,
                    };
                }
            }
        }
        // If no previous item on same line, try to move to end of previous line
        if current_line > 0 {
            let prev_line = current_line - 1;
            if let Some(d) = debug {
                d.push(format!(
                    "[Cursor] move_cursor_left: trying previous line {}",
                    prev_line
                ));
            }
            for i in (0..current_pos).rev() {
                if let Some(cluster) = self.items[i].item.as_cluster() {
                    if self.items[i].line_index == prev_line {
                        if let Some(d) = debug {
                            d.push(format!(
                                "[Cursor] move_cursor_left: found cluster on previous line, byte \
                                 {}",
                                cluster.source_cluster_id.start_byte_in_run
                            ));
                        }
                        return TextCursor {
                            cluster_id: cluster.source_cluster_id,
                            affinity: CursorAffinity::Trailing,
                        };
                    }
                }
            }
        }
        // At start of text, can't move further
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_left: at start of text, staying at byte {}",
                cursor.cluster_id.start_byte_in_run
            ));
        }
        cursor
    }
    /// Moves a cursor one visual unit to the right.
    pub fn move_cursor_right(
        &self,
        cursor: TextCursor,
        debug: &mut Option<Vec<String>>,
    ) -> TextCursor {
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_right: starting at byte {}, affinity {:?}",
                cursor.cluster_id.start_byte_in_run, cursor.affinity
            ));
        }
        // Find current item
        let current_item_pos = self.items.iter().position(|i| {
            i.item
                .as_cluster()
                .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
        });
        let Some(current_pos) = current_item_pos else {
            if let Some(d) = debug {
                d.push(format!(
                    "[Cursor] move_cursor_right: cursor not found, staying at byte {}",
                    cursor.cluster_id.start_byte_in_run
                ));
            }
            return cursor;
        };
        // Skip the Leading→Trailing affinity flip for simple cursor movement.
        // The affinity distinction matters for selection extension and bidi text,
        // but for basic left/right navigation, the user expects each press to move
        // the cursor to the next/previous visible character position.
        // If at Leading, go directly to the next cluster's Leading.
        // We're at leading or trailing edge, move to next cluster's leading edge
        let current_line = self.items[current_pos].line_index;
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_right: at trailing edge, current line {}",
                current_line
            ));
        }
        // First, try to find next item on same line
        for i in (current_pos + 1)..self.items.len() {
            if let Some(cluster) = self.items[i].item.as_cluster() {
                if self.items[i].line_index == current_line {
                    if let Some(d) = debug {
                        d.push(format!(
                            "[Cursor] move_cursor_right: found next cluster on same line, byte {}",
                            cluster.source_cluster_id.start_byte_in_run
                        ));
                    }
                    return TextCursor {
                        cluster_id: cluster.source_cluster_id,
                        affinity: CursorAffinity::Leading,
                    };
                }
            }
        }
        // If no next item on same line, try to move to start of next line
        let next_line = current_line + 1;
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_right: trying next line {}",
                next_line
            ));
        }
        for i in (current_pos + 1)..self.items.len() {
            if let Some(cluster) = self.items[i].item.as_cluster() {
                if self.items[i].line_index == next_line {
                    if let Some(d) = debug {
                        d.push(format!(
                            "[Cursor] move_cursor_right: found cluster on next line, byte {}",
                            cluster.source_cluster_id.start_byte_in_run
                        ));
                    }
                    return TextCursor {
                        cluster_id: cluster.source_cluster_id,
                        affinity: CursorAffinity::Leading,
                    };
                }
            }
        }
        // At end of text, can't move further
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_right: at end of text, staying at byte {}",
                cursor.cluster_id.start_byte_in_run
            ));
        }
        cursor
    }
    /// Moves a cursor up one line, attempting to preserve the horizontal column.
    pub fn move_cursor_up(
        &self,
        cursor: TextCursor,
        goal_x: &mut Option<f32>,
        debug: &mut Option<Vec<String>>,
    ) -> TextCursor {
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_up: from byte {} (affinity {:?})",
                cursor.cluster_id.start_byte_in_run, cursor.affinity
            ));
        }
        let Some(current_item) = self.items.iter().find(|i| {
            i.item
                .as_cluster()
                .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
        }) else {
            if let Some(d) = debug {
                d.push(format!(
                    "[Cursor] move_cursor_up: cursor not found in items, staying at byte {}",
                    cursor.cluster_id.start_byte_in_run
                ));
            }
            return cursor;
        };
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_up: current line {}, position ({}, {})",
                current_item.line_index, current_item.position.x, current_item.position.y
            ));
        }
        let target_line_idx = current_item.line_index.saturating_sub(1);
        if current_item.line_index == target_line_idx {
            if let Some(d) = debug {
                d.push(format!(
                    "[Cursor] move_cursor_up: already at top line {}, staying put",
                    current_item.line_index
                ));
            }
            return cursor;
        }
        let current_x = goal_x.unwrap_or_else(|| {
            let x = match cursor.affinity {
                CursorAffinity::Leading => current_item.position.x,
                CursorAffinity::Trailing => {
                    current_item.position.x + get_item_measure(&current_item.item, false)
                }
            };
            *goal_x = Some(x);
            x
        });
        // Find the Y coordinate of the middle of the target line
        let target_y = self
            .items
            .iter()
            .find(|i| i.line_index == target_line_idx)
            .map(|i| i.position.y + (i.item.bounds().height / 2.0))
            .unwrap_or(current_item.position.y);
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_up: target line {}, hittesting at ({}, {})",
                target_line_idx, current_x, target_y
            ));
        }
        let result = self
            .hittest_cursor(LogicalPosition {
                x: current_x,
                y: target_y,
            })
            .unwrap_or(cursor);
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_up: result byte {} (affinity {:?})",
                result.cluster_id.start_byte_in_run, result.affinity
            ));
        }
        result
    }
    /// Moves a cursor down one line, attempting to preserve the horizontal column.
    pub fn move_cursor_down(
        &self,
        cursor: TextCursor,
        goal_x: &mut Option<f32>,
        debug: &mut Option<Vec<String>>,
    ) -> TextCursor {
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_down: from byte {} (affinity {:?})",
                cursor.cluster_id.start_byte_in_run, cursor.affinity
            ));
        }
        let Some(current_item) = self.items.iter().find(|i| {
            i.item
                .as_cluster()
                .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
        }) else {
            if let Some(d) = debug {
                d.push(format!(
                    "[Cursor] move_cursor_down: cursor not found in items, staying at byte {}",
                    cursor.cluster_id.start_byte_in_run
                ));
            }
            return cursor;
        };
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_down: current line {}, position ({}, {})",
                current_item.line_index, current_item.position.x, current_item.position.y
            ));
        }
        let max_line = self.items.iter().map(|i| i.line_index).max().unwrap_or(0);
        let target_line_idx = (current_item.line_index + 1).min(max_line);
        if current_item.line_index == target_line_idx {
            if let Some(d) = debug {
                d.push(format!(
                    "[Cursor] move_cursor_down: already at bottom line {}, staying put",
                    current_item.line_index
                ));
            }
            return cursor;
        }
        let current_x = goal_x.unwrap_or_else(|| {
            let x = match cursor.affinity {
                CursorAffinity::Leading => current_item.position.x,
                CursorAffinity::Trailing => {
                    current_item.position.x + get_item_measure(&current_item.item, false)
                }
            };
            *goal_x = Some(x);
            x
        });
        let target_y = self
            .items
            .iter()
            .find(|i| i.line_index == target_line_idx)
            .map(|i| i.position.y + (i.item.bounds().height / 2.0))
            .unwrap_or(current_item.position.y);
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_down: hit testing at ({}, {})",
                current_x, target_y
            ));
        }
        let result = self
            .hittest_cursor(LogicalPosition {
                x: current_x,
                y: target_y,
            })
            .unwrap_or(cursor);
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_down: result byte {}, affinity {:?}",
                result.cluster_id.start_byte_in_run, result.affinity
            ));
        }
        result
    }
    /// Moves a cursor to the visual start of its current line.
    pub fn move_cursor_to_line_start(
        &self,
        cursor: TextCursor,
        debug: &mut Option<Vec<String>>,
    ) -> TextCursor {
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_to_line_start: starting at byte {}, affinity {:?}",
                cursor.cluster_id.start_byte_in_run, cursor.affinity
            ));
        }
        let Some(current_item) = self.items.iter().find(|i| {
            i.item
                .as_cluster()
                .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
        }) else {
            if let Some(d) = debug {
                d.push(format!(
                    "[Cursor] move_cursor_to_line_start: cursor not found, staying at byte {}",
                    cursor.cluster_id.start_byte_in_run
                ));
            }
            return cursor;
        };
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_to_line_start: current line {}, position ({}, {})",
                current_item.line_index, current_item.position.x, current_item.position.y
            ));
        }
        let first_item_on_line = self
            .items
            .iter()
            .filter(|i| i.line_index == current_item.line_index)
            .min_by(|a, b| {
                a.position
                    .x
                    .partial_cmp(&b.position.x)
                    .unwrap_or(Ordering::Equal)
            });
        if let Some(item) = first_item_on_line {
            if let ShapedItem::Cluster(c) = &item.item {
                let result = TextCursor {
                    cluster_id: c.source_cluster_id,
                    affinity: CursorAffinity::Leading,
                };
                if let Some(d) = debug {
                    d.push(format!(
                        "[Cursor] move_cursor_to_line_start: result byte {}, affinity {:?}",
                        result.cluster_id.start_byte_in_run, result.affinity
                    ));
                }
                return result;
            }
        }
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_to_line_start: no first item found, staying at byte {}",
                cursor.cluster_id.start_byte_in_run
            ));
        }
        cursor
    }
    /// Moves a cursor to the visual end of its current line.
    pub fn move_cursor_to_line_end(
        &self,
        cursor: TextCursor,
        debug: &mut Option<Vec<String>>,
    ) -> TextCursor {
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_to_line_end: starting at byte {}, affinity {:?}",
                cursor.cluster_id.start_byte_in_run, cursor.affinity
            ));
        }
        let Some(current_item) = self.items.iter().find(|i| {
            i.item
                .as_cluster()
                .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
        }) else {
            if let Some(d) = debug {
                d.push(format!(
                    "[Cursor] move_cursor_to_line_end: cursor not found, staying at byte {}",
                    cursor.cluster_id.start_byte_in_run
                ));
            }
            return cursor;
        };
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_to_line_end: current line {}, position ({}, {})",
                current_item.line_index, current_item.position.x, current_item.position.y
            ));
        }
        let last_item_on_line = self
            .items
            .iter()
            .filter(|i| i.line_index == current_item.line_index)
            .max_by(|a, b| {
                a.position
                    .x
                    .partial_cmp(&b.position.x)
                    .unwrap_or(Ordering::Equal)
            });
        if let Some(item) = last_item_on_line {
            if let ShapedItem::Cluster(c) = &item.item {
                let result = TextCursor {
                    cluster_id: c.source_cluster_id,
                    affinity: CursorAffinity::Trailing,
                };
                if let Some(d) = debug {
                    d.push(format!(
                        "[Cursor] move_cursor_to_line_end: result byte {}, affinity {:?}",
                        result.cluster_id.start_byte_in_run, result.affinity
                    ));
                }
                return result;
            }
        }
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_to_line_end: no last item found, staying at byte {}",
                cursor.cluster_id.start_byte_in_run
            ));
        }
        cursor
    }
    /// Moves a cursor one word to the left (Ctrl+Left / Option+Left).
    ///
    /// Word boundaries are defined by whitespace: the cursor moves past any
    /// whitespace to the left, then past non-whitespace characters until
    /// the next whitespace or start of text.
    pub fn move_cursor_to_prev_word(
        &self,
        cursor: TextCursor,
        debug: &mut Option<Vec<String>>,
    ) -> TextCursor {
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_to_prev_word: starting at byte {}, affinity {:?}",
                cursor.cluster_id.start_byte_in_run, cursor.affinity
            ));
        }
        let current_pos = match self.items.iter().position(|i| {
            i.item
                .as_cluster()
                .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
        }) {
            Some(pos) => pos,
            None => return cursor,
        };
        // Phase 1: Skip whitespace going left
        let mut pos = if cursor.affinity == CursorAffinity::Leading {
            // Already at leading edge, start from previous item
            current_pos.checked_sub(1)
        } else {
            // At trailing edge, start from current item
            Some(current_pos)
        };
        // Skip whitespace
        while let Some(p) = pos {
            if let Some(cluster) = self.items[p].item.as_cluster() {
                if !cluster.text.chars().all(|c| c.is_whitespace()) {
                    break;
                }
            }
            pos = p.checked_sub(1);
        }
        // Phase 2: Skip non-whitespace going left (the word itself)
        while let Some(p) = pos {
            if let Some(cluster) = self.items[p].item.as_cluster() {
                if cluster.text.chars().all(|c| c.is_whitespace()) {
                    // We've reached whitespace before the word — stop at next cluster
                    if p + 1 < self.items.len() {
                        if let Some(c) = self.items[p + 1].item.as_cluster() {
                            return TextCursor {
                                cluster_id: c.source_cluster_id,
                                affinity: CursorAffinity::Leading,
                            };
                        }
                    }
                    break;
                }
            }
            if p == 0 {
                // Reached start of text — return first cluster
                if let Some(c) = self.items[0].item.as_cluster() {
                    return TextCursor {
                        cluster_id: c.source_cluster_id,
                        affinity: CursorAffinity::Leading,
                    };
                }
                break;
            }
            pos = p.checked_sub(1);
        }
        // If we exhausted the search, go to first cluster
        if pos.is_none() {
            if let Some(first) = self.get_first_cluster_cursor() {
                return first;
            }
        }
        cursor
    }
    /// Moves a cursor one word to the right (Ctrl+Right / Option+Right).
    ///
    /// Word boundaries are defined by whitespace: the cursor moves past any
    /// non-whitespace characters, then past whitespace until the next word
    /// or end of text.
    pub fn move_cursor_to_next_word(
        &self,
        cursor: TextCursor,
        debug: &mut Option<Vec<String>>,
    ) -> TextCursor {
        if let Some(d) = debug {
            d.push(format!(
                "[Cursor] move_cursor_to_next_word: starting at byte {}, affinity {:?}",
                cursor.cluster_id.start_byte_in_run, cursor.affinity
            ));
        }
        let current_pos = match self.items.iter().position(|i| {
            i.item
                .as_cluster()
                .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
        }) {
            Some(pos) => pos,
            None => return cursor,
        };
        let len = self.items.len();
        // Start position: if at leading edge, start from current; if trailing, start from next
        let start = if cursor.affinity == CursorAffinity::Trailing {
            current_pos + 1
        } else {
            current_pos
        };
        if start >= len {
            return cursor;
        }
        let mut pos = start;
        // Phase 1: Skip non-whitespace (current word)
        while pos < len {
            if let Some(cluster) = self.items[pos].item.as_cluster() {
                if cluster.text.chars().all(|c| c.is_whitespace()) {
                    break;
                }
            }
            pos += 1;
        }
        // Phase 2: Skip whitespace after word
        while pos < len {
            if let Some(cluster) = self.items[pos].item.as_cluster() {
                if !cluster.text.chars().all(|c| c.is_whitespace()) {
                    // Found start of next word
                    return TextCursor {
                        cluster_id: cluster.source_cluster_id,
                        affinity: CursorAffinity::Leading,
                    };
                }
            }
            pos += 1;
        }
        // Reached end of text
        if let Some(last) = self.get_last_cluster_cursor() {
            return last;
        }
        cursor
    }
}
65296
fn get_baseline_for_item(item: &ShapedItem) -> Option<f32> {
65296
    match item {
        ShapedItem::CombinedBlock {
            baseline_offset, ..
        } => Some(*baseline_offset),
        ShapedItem::Object {
176
            baseline_offset, ..
176
        } => Some(*baseline_offset),
        // We have to get the clusters font from the last glyph
64944
        ShapedItem::Cluster(ref cluster) => {
64944
            if let Some(last_glyph) = cluster.glyphs.last() {
64944
                Some(
64944
                    last_glyph
64944
                        .font_metrics
64944
                        .baseline_scaled(last_glyph.style.font_size_px),
64944
                )
            } else {
                None
            }
        }
176
        ShapedItem::Break { source, break_info } => {
            // Breaks do not contribute to baseline
176
            None
        }
        ShapedItem::Tab { source, bounds } => {
            // Tabs do not contribute to baseline
            None
        }
    }
65296
}
/// Stores information about content that exceeded the available layout space.
#[derive(Debug, Clone, Default)]
pub struct OverflowInfo {
    /// The items that did not fit within the constraints.
    pub overflow_items: Vec<ShapedItem>,
    /// The total bounds of all content, including overflowing items.
    /// This is useful for `OverflowBehavior::Visible` or `Scroll`.
    pub unclipped_bounds: Rect,
}
impl OverflowInfo {
    pub fn has_overflow(&self) -> bool {
        !self.overflow_items.is_empty()
    }
}
/// Intermediate structure carrying information from the line breaker to the positioner.
#[derive(Debug, Clone)]
pub struct UnifiedLine {
    pub items: Vec<ShapedItem>,
    /// The y-position (for horizontal) or x-position (for vertical) of the line's baseline.
    pub cross_axis_position: f32,
    /// The geometric segments this line must fit into.
    pub constraints: LineConstraints,
    pub is_last: bool,
}
// --- Caching Infrastructure ---
pub type CacheId = u64;
/// Defines a single area for layout, with its own shape and properties.
#[derive(Debug, Clone)]
pub struct LayoutFragment {
    /// A unique identifier for this fragment (e.g., "main-content", "sidebar").
    pub id: String,
    /// The geometric and style constraints for this specific fragment.
    pub constraints: UnifiedConstraints,
}
/// Represents the final layout distributed across multiple fragments.
#[derive(Debug, Clone)]
pub struct FlowLayout {
    /// A map from a fragment's unique ID to the layout it contains.
    pub fragment_layouts: HashMap<String, Arc<UnifiedLayout>>,
    /// Any items that did not fit into the last fragment in the flow chain.
    /// This is useful for pagination or determining if more layout space is needed.
    pub remaining_items: Vec<ShapedItem>,
}
/// Inline-axis intrinsic contributions derived from shaped text, without running
/// the line-breaking stage of the pipeline.
///
/// Callers that only need min/max-content widths for sizing (see
/// `calculate_ifc_root_intrinsic_sizes`) should prefer this over invoking
/// `layout_flow` twice with `AvailableSpace::MinContent`/`MaxContent`. The
/// latter runs the full flow loop — including `BreakCursor::peek_next_unit`,
/// which clones every `ShapedCluster` it inspects — even though no constraint
/// actually limits the line width.
#[derive(Debug, Clone, Default)]
pub struct IntrinsicTextSizes {
    /// CSS min-content = widest unbreakable unit (word) along the inline axis.
    pub min_content_width: f32,
    /// CSS max-content = sum of all advances along the inline axis (single line).
    pub max_content_width: f32,
    /// Height of a single line box: max(ascent + descent) across all items.
    pub max_content_height: f32,
}
/// Cached line break boundaries from a previous layout pass.
/// Enables incremental relayout: when a word changes width,
/// we can check if it still fits on the same line without
/// re-running the full line-breaking algorithm.
#[derive(Clone, Debug)]
pub struct CachedLineBreaks {
    /// Per-line: (first_item_idx, last_item_idx_exclusive) into positioned items.
    pub line_ranges: Vec<(usize, usize)>,
    /// Per-line total width (sum of item advances on that line).
    pub line_widths: Vec<f32>,
    /// The available width constraint used when these breaks were computed.
    pub available_width: f32,
}
/// Result of an incremental relayout attempt.
#[derive(Debug)]
pub enum IncrementalRelayoutResult {
    /// Glyphs changed but advance widths identical — swap in place, no repositioning.
    GlyphSwap,
    /// Width changed but still fits on same line — shift x_offsets of subsequent items.
    LineShift {
        /// Index of the first affected item.
        affected_item: usize,
        /// Width delta (new_advance - old_advance).
        delta: f32,
    },
    /// Line breaks changed — need to reflow from this line onward.
    PartialReflow {
        /// The line index from which to start reflowing.
        reflow_from_line: usize,
    },
    /// Cannot do incremental — fall back to full relayout.
    FullRelayout,
}
/// Extract line break boundaries from a positioned items list.
49588
pub fn extract_line_breaks(
49588
    items: &[PositionedItem],
49588
    available_width: f32,
49588
) -> CachedLineBreaks {
49588
    let mut line_ranges = Vec::new();
49588
    let mut line_widths = Vec::new();
49588
    if items.is_empty() {
4928
        return CachedLineBreaks { line_ranges, line_widths, available_width };
44660
    }
44660
    let mut line_start = 0usize;
44660
    let mut current_line = items[0].line_index;
44660
    let mut line_width = 0.0f32;
358600
    for (i, item) in items.iter().enumerate() {
358600
        if item.line_index != current_line {
40568
            line_ranges.push((line_start, i));
40568
            line_widths.push(line_width);
40568
            line_start = i;
40568
            current_line = item.line_index;
40568
            line_width = 0.0;
318032
        }
358600
        line_width += get_item_measure(&item.item, false);
    }
    // Final line
44660
    line_ranges.push((line_start, items.len()));
44660
    line_widths.push(line_width);
44660
    CachedLineBreaks { line_ranges, line_widths, available_width }
49588
}
/// Attempt incremental relayout given old metrics and new per-item advance widths.
///
/// `dirty_item_indices`: which items in the shaped list changed.
/// `old_advances`: per-item advance widths from the previous layout.
/// `new_advances`: per-item advance widths after reshaping.
/// `line_breaks`: cached line boundaries from previous layout.
21296
pub fn try_incremental_relayout(
21296
    dirty_item_indices: &[usize],
21296
    old_advances: &[f32],
21296
    new_advances: &[f32],
21296
    line_breaks: &CachedLineBreaks,
21296
) -> IncrementalRelayoutResult {
21296
    if dirty_item_indices.is_empty() {
21296
        return IncrementalRelayoutResult::GlyphSwap;
    }
    // Check each dirty item
    for &dirty_idx in dirty_item_indices {
        if dirty_idx >= old_advances.len() || dirty_idx >= new_advances.len() {
            return IncrementalRelayoutResult::FullRelayout;
        }
        let old_adv = old_advances[dirty_idx];
        let new_adv = new_advances[dirty_idx];
        let delta = new_adv - old_adv;
        if delta.abs() < 0.001 {
            // Same width — just swap glyphs (GlyphSwap for this item)
            continue;
        }
        // Width changed — find which line this item is on
        let line_idx = line_breaks.line_ranges.iter()
            .position(|&(start, end)| dirty_idx >= start && dirty_idx < end);
        let Some(line_idx) = line_idx else {
            return IncrementalRelayoutResult::FullRelayout;
        };
        let old_line_width = line_breaks.line_widths[line_idx];
        let new_line_width = old_line_width + delta;
        if new_line_width <= line_breaks.available_width {
            // Still fits on same line — shift subsequent items
            return IncrementalRelayoutResult::LineShift {
                affected_item: dirty_idx,
                delta,
            };
        } else {
            // Overflows line — need to reflow from this line
            return IncrementalRelayoutResult::PartialReflow {
                reflow_from_line: line_idx as usize,
            };
        }
    }
    // All dirty items had same width
    IncrementalRelayoutResult::GlyphSwap
21296
}
/// Cached shaped result for a single visual item (or coalesced group).
/// Enables per-item cache hits when only one word changes in a paragraph.
pub struct PerItemShapedEntry {
    /// The shaped clusters for this single item/group.
    pub clusters: Vec<ShapedItem>,
    /// Sum of advance widths — for fast same-width detection during incremental relayout.
    pub total_advance: f32,
}
pub struct TextShapingCache {
    // Stage 1 Cache: InlineContent -> LogicalItems
    logical_items: HashMap<CacheId, Arc<Vec<LogicalItem>>>,
    // Stage 2 Cache: LogicalItems -> VisualItems
    visual_items: HashMap<CacheId, Arc<Vec<VisualItem>>>,
    // Stage 3 Cache: VisualItems -> ShapedItems (monolithic, for backward compat)
    shaped_items: HashMap<CacheId, Arc<Vec<ShapedItem>>>,
    // Stage 3b Cache: Per-item/coalesce-group shaped results
    // Key: hash(text, bidi_level, script, style.layout_hash())
    per_item_shaped: HashMap<u64, Arc<PerItemShapedEntry>>,
    /// Tracks which per_item_shaped keys were accessed in the current generation.
    per_item_accessed: HashSet<u64>,
    /// Current generation counter, incremented each layout pass.
    generation: u64,
}
/// Approximate heap bytes retained by a [`TextShapingCache`].
#[derive(Debug, Clone, Default)]
pub struct TextCacheMemoryReport {
    pub logical_items_entries: usize,
    pub logical_items_bytes: usize,
    pub visual_items_entries: usize,
    pub visual_items_bytes: usize,
    pub shaped_items_entries: usize,
    pub shaped_items_bytes: usize,
    pub shaped_glyph_bytes: usize,
    pub shaped_cluster_text_bytes: usize,
    pub per_item_shaped_entries: usize,
    pub per_item_shaped_bytes: usize,
}
impl TextCacheMemoryReport {
    pub fn total_bytes(&self) -> usize {
        self.logical_items_bytes
            + self.visual_items_bytes
            + self.shaped_items_bytes
            + self.shaped_glyph_bytes
            + self.shaped_cluster_text_bytes
            + self.per_item_shaped_bytes
    }
}
impl TextShapingCache {
6919
    pub fn new() -> Self {
6919
        Self {
6919
            logical_items: HashMap::new(),
6919
            visual_items: HashMap::new(),
6919
            shaped_items: HashMap::new(),
6919
            per_item_shaped: HashMap::new(),
6919
            per_item_accessed: HashSet::new(),
6919
            generation: 0,
6919
        }
6919
    }
    /// Approximate per-stage heap-byte breakdown.
    pub fn memory_report(&self) -> TextCacheMemoryReport {
        let mut r = TextCacheMemoryReport::default();
        r.logical_items_entries = self.logical_items.len();
        for (_, arc) in &self.logical_items {
            r.logical_items_bytes += arc.capacity() * core::mem::size_of::<LogicalItem>();
        }
        r.visual_items_entries = self.visual_items.len();
        for (_, arc) in &self.visual_items {
            r.visual_items_bytes += arc.capacity() * core::mem::size_of::<VisualItem>();
        }
        r.shaped_items_entries = self.shaped_items.len();
        for (_, arc) in &self.shaped_items {
            r.shaped_items_bytes += arc.capacity() * core::mem::size_of::<ShapedItem>();
            for item in arc.iter() {
                if let ShapedItem::Cluster(c) = item {
                    r.shaped_glyph_bytes += c.glyphs.capacity() * core::mem::size_of::<ShapedGlyph>();
                    r.shaped_cluster_text_bytes += c.text.capacity();
                }
            }
        }
        r.per_item_shaped_entries = self.per_item_shaped.len();
        for (_, arc) in &self.per_item_shaped {
            r.per_item_shaped_bytes += arc.clusters.capacity() * core::mem::size_of::<ShapedItem>();
            for item in arc.clusters.iter() {
                if let ShapedItem::Cluster(c) = item {
                    r.per_item_shaped_bytes += c.glyphs.capacity() * core::mem::size_of::<ShapedGlyph>();
                    r.per_item_shaped_bytes += c.text.capacity();
                }
            }
        }
        r
    }
    /// Call at the start of each layout pass. Evicts per-item shaped entries
    /// not accessed in the previous generation to prevent unbounded growth.
    pub fn begin_generation(&mut self) {
        if self.generation > 0 && !self.per_item_accessed.is_empty() {
            // Evict entries not accessed in this generation
            let accessed = &self.per_item_accessed;
            self.per_item_shaped.retain(|k, _| accessed.contains(k));
        }
        self.per_item_accessed.clear();
        self.generation += 1;
    }
    /// Check if we can reuse an old layout based on layout-affecting parameters.
    /// 
    /// This function compares only the parameters that affect glyph positions,
    /// not rendering-only parameters like color or text-decoration.
    /// 
    /// # Parameters
    /// - `old_constraints`: The constraints used for the cached layout
    /// - `new_constraints`: The constraints for the new layout request
    /// - `old_content`: The content used for the cached layout
    /// - `new_content`: The new content to layout
    /// 
    /// # Returns
    /// - `true` if the old layout can be reused (only rendering changed)
    /// - `false` if a new layout is needed (layout-affecting params changed)
    pub fn use_old_layout(
        old_constraints: &UnifiedConstraints,
        new_constraints: &UnifiedConstraints,
        old_content: &[InlineContent],
        new_content: &[InlineContent],
    ) -> bool {
        // First check: constraints must match exactly for layout purposes
        if old_constraints != new_constraints {
            return false;
        }
        // Second check: content length must match
        if old_content.len() != new_content.len() {
            return false;
        }
        // Third check: each content item must have same layout properties
        for (old, new) in old_content.iter().zip(new_content.iter()) {
            if !Self::inline_content_layout_eq(old, new) {
                return false;
            }
        }
        true
    }
    /// Compare two InlineContent items for layout equality.
    /// 
    /// Returns true if the layouts would be identical (only rendering differs).
    fn inline_content_layout_eq(old: &InlineContent, new: &InlineContent) -> bool {
        use InlineContent::*;
        match (old, new) {
            (Text(old_run), Text(new_run)) => {
                // Text must match exactly, but style only needs layout_eq
                old_run.text == new_run.text 
                    && old_run.style.layout_eq(&new_run.style)
            }
            (Image(old_img), Image(new_img)) => {
                // Images: size affects layout, but not visual properties
                old_img.intrinsic_size == new_img.intrinsic_size
                    && old_img.display_size == new_img.display_size
                    && old_img.baseline_offset == new_img.baseline_offset
                    && old_img.alignment == new_img.alignment
            }
            (Space(old_sp), Space(new_sp)) => old_sp == new_sp,
            (LineBreak(old_br), LineBreak(new_br)) => old_br == new_br,
            (Tab { style: old_style }, Tab { style: new_style }) => old_style.layout_eq(new_style),
            (Marker { run: old_run, position_outside: old_pos },
             Marker { run: new_run, position_outside: new_pos }) => {
                old_pos == new_pos
                    && old_run.text == new_run.text
                    && old_run.style.layout_eq(&new_run.style)
            }
            (Shape(old_shape), Shape(new_shape)) => {
                // Shapes: shape_def affects layout, not fill/stroke
                old_shape.shape_def == new_shape.shape_def
                    && old_shape.baseline_offset == new_shape.baseline_offset
            }
            (Ruby { base: old_base, text: old_text, style: old_style },
             Ruby { base: new_base, text: new_text, style: new_style }) => {
                old_style.layout_eq(new_style)
                    && old_base.len() == new_base.len()
                    && old_text.len() == new_text.len()
                    && old_base.iter().zip(new_base.iter())
                        .all(|(o, n)| Self::inline_content_layout_eq(o, n))
                    && old_text.iter().zip(new_text.iter())
                        .all(|(o, n)| Self::inline_content_layout_eq(o, n))
            }
            // Different variants cannot have same layout
            _ => false,
        }
    }
}
impl Default for TextShapingCache {
    fn default() -> Self {
        Self::new()
    }
}
/// Key for caching the conversion from `InlineContent` to `LogicalItem`s.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct LogicalItemsKey<'a> {
    pub inline_content_hash: u64, // Pre-hash the content for efficiency
    pub default_font_size: u32,   // Affects space widths
    // Add other relevant properties from constraints if they affect this stage
    pub _marker: std::marker::PhantomData<&'a ()>,
}
/// Key for caching the Bidi reordering stage.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct VisualItemsKey {
    pub logical_items_id: CacheId,
    pub base_direction: BidiDirection,
}
/// Key for caching the shaping stage.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct ShapedItemsKey {
    pub visual_items_id: CacheId,
    pub style_hash: u64, // Represents a hash of all font/style properties
}
impl ShapedItemsKey {
69828
    pub fn new(visual_items_id: CacheId, visual_items: &[VisualItem]) -> Self {
69828
        let style_hash = {
69828
            let mut hasher = DefaultHasher::new();
70664
            for item in visual_items.iter() {
                // Hash the style from the logical source, as this is what determines the font.
70664
                match &item.logical_source {
69828
                    LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
69828
                        style.as_ref().hash(&mut hasher);
69828
                    }
836
                    _ => {}
                }
            }
69828
            hasher.finish()
        };
69828
        Self {
69828
            visual_items_id,
69828
            style_hash,
69828
        }
69828
    }
}
/// Key for the final layout stage.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct LayoutKey {
    pub shaped_items_id: CacheId,
    pub constraints: UnifiedConstraints,
}
/// Helper to create a `CacheId` from any `Hash`able type.
200067
fn calculate_id<T: Hash>(item: &T) -> CacheId {
200067
    let mut hasher = DefaultHasher::new();
200067
    item.hash(&mut hasher);
200067
    hasher.finish()
200067
}
// --- Main Layout Pipeline Implementation ---
impl TextShapingCache {
    /// New top-level entry point for flowing layout across multiple regions.
    ///
    /// This function orchestrates the entire layout pipeline, but instead of fitting
    /// content into a single set of constraints, it flows the content through an
    /// ordered sequence of `LayoutFragment`s.
    ///
    /// # CSS Inline Layout Module Level 3: Pipeline Implementation
    ///
    /// This implements the inline formatting context with 5 stages:
    ///
    /// ## Stage 1: Logical Analysis (InlineContent -> LogicalItem)
    /// \u2705 IMPLEMENTED: Parses raw content into logical units
    /// - Handles text runs, inline-blocks, replaced elements
    /// - Applies style overrides at character level
    /// - Implements \u00a7 2.2: Content size contribution calculation
    ///
    /// ## Stage 2: BiDi Reordering (LogicalItem -> VisualItem)
    /// \u2705 IMPLEMENTED: Uses CSS 'direction' property per CSS Writing Modes
    /// - Reorders items for right-to-left text (Arabic, Hebrew)
    /// - Respects containing block direction (not auto-detection)
    /// - Conforms to Unicode BiDi Algorithm (UAX #9)
    ///
    /// ## Stage 3: Shaping (VisualItem -> ShapedItem)
    /// \u2705 IMPLEMENTED: Converts text to glyphs
    /// - Uses HarfBuzz for OpenType shaping
    /// - Handles ligatures, kerning, contextual forms
    /// - Caches shaped results for performance
    ///
    /// ## Stage 4: Text Orientation Transformations
    /// \u26a0\ufe0f PARTIAL: Applies text-orientation for vertical text
    /// - Uses constraints from *first* fragment only
    /// - \u274c TODO: Should re-orient if fragments have different writing modes
    ///
    /// ## Stage 5: Flow Loop (ShapedItem -> PositionedItem)
    /// \u2705 IMPLEMENTED: Breaks lines and positions content
    /// - Calls perform_fragment_layout for each fragment
    /// - Uses BreakCursor to flow content across fragments
    /// - Implements \u00a7 5: Line breaking and hyphenation
    ///
    /// # Missing Features from CSS Inline-3:
    /// - \u00a7 3.3: initial-letter (drop caps)
    /// - \u00a7 4: vertical-align (only baseline supported)
    /// - \u00a7 6: text-box-trim (leading trim)
    /// - \u00a7 7: inline-sizing (aspect-ratio for inline-blocks)
    ///
    /// # Arguments
    /// * `content` - The raw `InlineContent` to be laid out.
    /// * `style_overrides` - Character-level style changes.
    /// * `flow_chain` - An ordered slice of `LayoutFragment` defining the regions (e.g., columns,
    ///   pages) that the content should flow through.
    /// * `font_chain_cache` - Pre-resolved font chains (from FontManager.font_chain_cache)
    /// * `fc_cache` - The fontconfig cache for font lookups
    /// * `loaded_fonts` - Pre-loaded fonts, keyed by FontId
    ///
    /// # Returns
    /// A `FlowLayout` struct containing the positioned items for each fragment that
    /// was filled, and any content that did not fit in the final fragment.
45877
    pub fn layout_flow<T: ParsedFontTrait>(
45877
        &mut self,
45877
        content: &[InlineContent],
45877
        style_overrides: &[StyleOverride],
45877
        flow_chain: &[LayoutFragment],
45877
        font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
45877
        fc_cache: &FcFontCache,
45877
        loaded_fonts: &LoadedFonts<T>,
45877
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
45877
    ) -> Result<FlowLayout, LayoutError> {
        // [g150 az-web-lift DIAG] content data ptr (0x60BD0) + len (0x60BD4) at layout_flow ENTRY.
        #[cfg(feature = "web_lift")]
        unsafe {
            crate::az_mark((0x60BD0) as u32, (content.as_ptr() as usize as u32) as u32);
            crate::az_mark((0x60BD4) as u32, (content.len() as u32 | 0xC0DE0000) as u32);
        }
        // [g218 2026-06-09] The g158 `content.len()` force-materialize (a volatile read of content+16) is
        // DELETED: the within-fn SROA-to-0 of content.len() it worked around is now fixed (NEON-decoder +
        // volatile-guest-load transpiler work). VERIFIED: hello-world lays out without it — counter "5"
        // (label_wrapper 8,16,784,40) + button shape correctly, same rects as before. (The cross-FN Vec-*return*-
        // len mis-lift is a separate, still-present issue handled by the g127/g129/g130 out-param hacks — see
        // g134 marker: callee content.len=1 but the caller's return-read sees 0.)
        // --- Stages 1-3: Preparation ---
        // These stages are independent of the final geometry. We perform them once
        // on the entire content block before flowing. Caching is used at each stage.
        // Cap per-item shaped cache to prevent unbounded growth.
        // When threshold is exceeded, evict entries not accessed this generation.
        const PER_ITEM_CACHE_MAX: usize = 4096;
45877
        if self.per_item_shaped.len() > PER_ITEM_CACHE_MAX {
            self.begin_generation();
45877
        }
        // Stage 1: Logical Analysis (InlineContent -> LogicalItem)
        // [g213 2026-06-09] The web lift uses the real `self.logical_items` HashMap cache (NO bypass).
        // This entry() find-probe USED to spin forever on the lift (g178-g210 mis-diagnosed it many ways).
        // TRUE root cause: hashbrown's portable WIDTH=8 `Group::static_empty()` — `[0xFF; 8]` in libazul's
        // `__TEXT.__const` — was not mirrored into the wasm, so the empty-map ctrl-scan read 0x00, looked
        // ALL-FULL (EMPTY=0xFF), and the probe never terminated. FIXED entirely transpiler-side in
        // `dll/src/web/symbol_table.rs::compute_hashbrown_empty_group_ranges` (signature-scans `__const`
        // for >=8-byte 8-aligned 0xFF runs and mirrors them). Verified: web-nested-text lays out
        // ("Hello" at 8,16,800,20), __remill_error=0. No azul-source workaround needed here.
45877
        let logical_items_id = calculate_id(&content);
45877
        let logical_items = self
45877
            .logical_items
45877
            .entry(logical_items_id)
45877
            .or_insert_with(|| {
6364
                Arc::new(create_logical_items(content, style_overrides, debug_messages))
6364
            })
45877
            .clone();
        // Get the first fragment's constraints to extract the CSS direction property.
        // This is used for BiDi reordering in Stage 2.
45877
        let default_constraints = UnifiedConstraints::default();
45877
        let first_constraints = flow_chain
45877
            .first()
45877
            .map(|f| &f.constraints)
45877
            .unwrap_or(&default_constraints);
        // +spec:containing-block:e7a271 - paragraph embedding level set from containing block's 'direction' property
        // +spec:display-property:7665cb - inline boxes split into multiple visual runs due to bidi text processing
        // +spec:display-property:929d6b - applies Unicode bidi algorithm to inline-level box sequences
        // +spec:display-property:e8584a - Apply Unicode bidi algorithm to inline-level box sequences per CSS Writing Modes §2.4
        // Stage 2: Bidi Reordering (LogicalItem -> VisualItem)
        // +spec:containing-block:961e3c - bidi paragraph level from containing block direction, not UAX9 heuristic
        // +spec:writing-modes:0a5368 - unicode-bidi: plaintext auto-detects direction from text content
        // Per CSS Writing Modes §8.3: when unicode-bidi is plaintext, the paragraph's
        // base direction is determined from text content (first strong character), ignoring
        // the containing block's direction property. Empty paragraphs fall back to
        // the containing block's direction.
45877
        let unicode_bidi_val = first_constraints.unicode_bidi;
45877
        let base_direction = if unicode_bidi_val == UnicodeBidi::Plaintext {
            // Auto-detect from text content; fall back to containing block direction
            let has_strong = logical_items.iter().any(|item| {
                if let LogicalItem::Text { text, .. } = item {
                    matches!(unicode_bidi::get_base_direction(text.as_str()),
                        unicode_bidi::Direction::Ltr | unicode_bidi::Direction::Rtl)
                } else {
                    false
                }
            });
            if has_strong {
                get_base_direction_from_logical(&logical_items)
            } else {
                // Empty paragraph: use containing block's direction
                first_constraints.direction.unwrap_or(BidiDirection::Ltr)
            }
        } else {
            // Normal case: use CSS direction property
45877
            first_constraints.direction.unwrap_or(BidiDirection::Ltr)
        };
45877
        let visual_key = VisualItemsKey {
45877
            logical_items_id,
45877
            base_direction,
45877
        };
45877
        let visual_items_id = calculate_id(&visual_key);
        // [g213] web lift uses the real visual_items HashMap cache (g180 bypass deleted; WIDTH=8
        // EMPTY_GROUP now mirrored — see Stage-1 note + symbol_table.rs).
45877
        let visual_items = self
45877
            .visual_items
45877
            .entry(visual_items_id)
45877
            .or_insert_with(|| {
6364
                Arc::new(
6364
                    reorder_logical_items(&logical_items, base_direction, unicode_bidi_val, debug_messages).unwrap(),
                )
6364
            })
45877
            .clone();
        // Stage 3: Shaping (VisualItem -> ShapedItem)
        // Two-level cache: monolithic (fast path) + per-item (incremental path).
45877
        let shaped_key = ShapedItemsKey::new(visual_items_id, &visual_items);
45877
        let shaped_items_id = calculate_id(&shaped_key);
        // [g213] web lift uses the real shaped_items HashMap cache (g180 bypass deleted).
45877
        let shaped_items = match self.shaped_items.get(&shaped_items_id) {
39513
            Some(cached) => {
                // Monolithic cache hit — all visual items unchanged
39513
                cached.clone()
            }
            None => {
                // Monolithic miss — use per-item cache for incremental reshaping.
                // Items not in per-item cache are shaped; cached items are reused.
6364
                let items = Arc::new(shape_visual_items_with_per_item_cache(
6364
                    &visual_items,
6364
                    &mut self.per_item_shaped,
6364
                    &mut self.per_item_accessed,
6364
                    font_chain_cache,
6364
                    fc_cache,
6364
                    loaded_fonts,
6364
                    debug_messages,
                )?);
6364
                self.shaped_items.insert(shaped_items_id, items.clone());
6364
                items
            }
        };
        // --- Stage 4: Apply Vertical Text Transformations ---
        // Note: first_constraints was already extracted above for BiDi reordering (Stage 2).
        // This orients all text based on the constraints of the *first* fragment.
        // A more advanced system could defer orientation until inside the loop if
        // fragments can have different writing modes.
45877
        let oriented_items = apply_text_orientation(shaped_items, first_constraints)?;
        // --- Stage 5: The Flow Loop ---
45877
        let mut fragment_layouts = HashMap::new();
        // The cursor now manages the stream of items for the entire flow.
        // §5.2 word-break: pass word_break from constraints to cursor
45877
        let mut cursor = BreakCursor::with_word_break(&oriented_items, first_constraints.word_break);
45877
        cursor.hyphens = first_constraints.hyphenation;
45877
        cursor.line_break = first_constraints.line_break;
        // [g147 az-web-lift] Hard safety bound on the Stage-5 flow loop. On the remill lift this
        // `for fragment in flow_chain` (or the `cursor.is_done()` break) mis-lifts for the NESTED IFC
        // and iterates without terminating → solveLayoutReal HANGS (fuel trap in layout_flow). The text
        // is fully laid out on the first iteration(s); cap the iterations so the loop always converges.
        // (native is unaffected — the cap is far above any real fragment count.)
45877
        let mut _az_flow_iters: usize = 0;
50417
        for fragment in flow_chain {
            #[cfg(feature = "web_lift")]
            {
                _az_flow_iters += 1;
                unsafe { crate::az_mark((0x60BC0) as u32, (_az_flow_iters as u32 | 0xC0DE0000) as u32); }
                if _az_flow_iters > 256 {
                    break;
                }
            }
            // Perform layout for this single fragment, consuming items from the cursor.
45877
            let fragment_layout = perform_fragment_layout(
45877
                &mut cursor,
45877
                &logical_items,
45877
                &fragment.constraints,
45877
                debug_messages,
45877
                loaded_fonts,
            )?;
45877
            fragment_layouts.insert(fragment.id.clone(), Arc::new(fragment_layout));
45877
            if cursor.is_done() {
41337
                break; // All content has been laid out.
4540
            }
        }
45877
        Ok(FlowLayout {
45877
            fragment_layouts,
45877
            remaining_items: cursor.drain_remaining(),
45877
        })
45877
    }
    /// Runs stages 1–4 of the layout pipeline (logical analysis, BiDi, shaping,
    /// text orientation) and derives min/max-content widths by scanning the
    /// resulting `ShapedItem`s directly — without running stage 5's line-breaking
    /// `BreakCursor` loop.
    ///
    /// Used by `calculate_ifc_root_intrinsic_sizes` to avoid the 24% CPU spent
    /// cloning `ShapedCluster`s inside `BreakCursor::peek_next_unit` on every
    /// sizing pass. Since stages 1–3 hit the same `per_item_shaped` cache as
    /// `layout_flow`, a subsequent `layout_flow` call for the same content at
    /// a real container width is a pure cache hit for the shaping work.
    ///
    /// The item walk uses the same break-opportunity predicate that the
    /// `BreakCursor` would — min-content accumulates advances between break
    /// opportunities and tracks the maximum; max-content is the sum of all
    /// advances (as if the flow were laid out on a single infinitely-wide line).
20812
    pub fn measure_intrinsic_widths<T: ParsedFontTrait>(
20812
        &mut self,
20812
        content: &[InlineContent],
20812
        style_overrides: &[StyleOverride],
20812
        constraints: &UnifiedConstraints,
20812
        font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
20812
        fc_cache: &FcFontCache,
20812
        loaded_fonts: &LoadedFonts<T>,
20812
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
20812
    ) -> Result<IntrinsicTextSizes, LayoutError> {
        const PER_ITEM_CACHE_MAX: usize = 4096;
20812
        if self.per_item_shaped.len() > PER_ITEM_CACHE_MAX {
            self.begin_generation();
20812
        }
        // Stage 1: Logical Analysis (cached, same as layout_flow — the historic web-lift
        // bypass here was rooted in the un-mirrored hashbrown EMPTY_GROUP, fixed transpiler-side
        // in symbol_table.rs::compute_hashbrown_empty_group_ranges).
20812
        let logical_items_id = calculate_id(&content);
20812
        let logical_items = self
20812
            .logical_items
20812
            .entry(logical_items_id)
20812
            .or_insert_with(|| {
9064
                Arc::new(create_logical_items(content, style_overrides, debug_messages))
9064
            })
20812
            .clone();
        // Stage 2: BiDi (same derivation as layout_flow)
20812
        let unicode_bidi_val = constraints.unicode_bidi;
20812
        let base_direction = if unicode_bidi_val == UnicodeBidi::Plaintext {
            let has_strong = logical_items.iter().any(|item| {
                if let LogicalItem::Text { text, .. } = item {
                    matches!(unicode_bidi::get_base_direction(text.as_str()),
                        unicode_bidi::Direction::Ltr | unicode_bidi::Direction::Rtl)
                } else {
                    false
                }
            });
            if has_strong {
                get_base_direction_from_logical(&logical_items)
            } else {
                constraints.direction.unwrap_or(BidiDirection::Ltr)
            }
        } else {
20812
            constraints.direction.unwrap_or(BidiDirection::Ltr)
        };
20812
        let visual_key = VisualItemsKey {
20812
            logical_items_id,
20812
            base_direction,
20812
        };
20812
        let visual_items_id = calculate_id(&visual_key);
20812
        let visual_items = self
20812
            .visual_items
20812
            .entry(visual_items_id)
20812
            .or_insert_with(|| {
9064
                Arc::new(
9064
                    reorder_logical_items(&logical_items, base_direction, unicode_bidi_val, debug_messages).unwrap(),
                )
9064
            })
20812
            .clone();
        // Stage 3: Shaping (two-level cache, same as layout_flow)
20812
        let shaped_key = ShapedItemsKey::new(visual_items_id, &visual_items);
20812
        let shaped_items_id = calculate_id(&shaped_key);
20812
        let shaped_items = match self.shaped_items.get(&shaped_items_id) {
11748
            Some(cached) => cached.clone(),
            None => {
9064
                let items = Arc::new(shape_visual_items_with_per_item_cache(
9064
                    &visual_items,
9064
                    &mut self.per_item_shaped,
9064
                    &mut self.per_item_accessed,
9064
                    font_chain_cache,
9064
                    fc_cache,
9064
                    loaded_fonts,
9064
                    debug_messages,
                )?);
9064
                self.shaped_items.insert(shaped_items_id, items.clone());
9064
                items
            }
        };
        // Stage 4: Text orientation
20812
        let oriented_items = apply_text_orientation(shaped_items, constraints)?;
        // Stage 5 bypass: scan items for min/max contributions.
20812
        let word_break = constraints.word_break;
20812
        let hyphens = constraints.hyphenation;
20812
        let mut total = 0.0f32;
20812
        let mut max_word = 0.0f32;
20812
        let mut cur_word = 0.0f32;
20812
        let mut max_line_height = 0.0f32;
171160
        for item in oriented_items.iter() {
            // Must match get_item_measure() exactly: a cluster's inline advance
            // INCLUDES per-glyph kerning. Omitting kerning here under-measures
            // max-content, so a shrink-to-fit box (e.g. a flex item sized to its
            // text's max-content) ends up narrower than the kerned text the line
            // breaker lays out — the word then "overflows" its own box and, with
            // overflow-wrap:normal, gets force-broken to its first cluster
            // (the menubar "View" → "V" clip). Summing (advance + kerning) here,
            // in the same order as the breaker, makes the box exactly fit.
171160
            let advance = match item {
170852
                ShapedItem::Cluster(c) => {
170852
                    let total_kerning: f32 = c.glyphs.iter().map(|g| g.kerning).sum();
170852
                    c.advance + total_kerning
                }
                ShapedItem::CombinedBlock { bounds, .. }
308
                | ShapedItem::Object { bounds, .. }
308
                | ShapedItem::Tab { bounds, .. } => bounds.width,
                ShapedItem::Break { .. } => 0.0,
            };
171160
            let adv = advance.max(0.0);
171160
            total += adv;
171160
            let (asc, desc) = get_item_vertical_metrics_approx(item);
171160
            let h = (asc + desc).max(item.bounds().height);
171160
            if h > max_line_height {
20812
                max_line_height = h;
150348
            }
171160
            if is_break_opportunity_with_word_break(item, word_break, hyphens) {
18216
                if cur_word > max_word {
10648
                    max_word = cur_word;
10648
                }
18216
                cur_word = 0.0;
152944
            } else {
152944
                cur_word += adv;
152944
            }
        }
20812
        if cur_word > max_word {
15048
            max_word = cur_word;
15048
        }
20812
        Ok(IntrinsicTextSizes {
20812
            min_content_width: max_word,
20812
            max_content_width: total,
20812
            max_content_height: max_line_height,
20812
        })
20812
    }
}
// --- Stage 1 Implementation ---
17204
pub fn create_logical_items(
17204
    content: &[InlineContent],
17204
    style_overrides: &[StyleOverride],
17204
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
17204
) -> Vec<LogicalItem> {
17204
    if let Some(msgs) = debug_messages {
16632
        msgs.push(LayoutDebugMessage::info(
16632
            "\n--- Entering create_logical_items (Refactored) ---".to_string(),
16632
        ));
16632
        msgs.push(LayoutDebugMessage::info(format!(
16632
            "Input content length: {}",
16632
            content.len()
16632
        )));
16632
        msgs.push(LayoutDebugMessage::info(format!(
16632
            "Input overrides length: {}",
16632
            style_overrides.len()
16632
        )));
16632
    }
17204
    let mut items: Vec<LogicalItem> = Vec::new();
17204
    let mut style_cache: HashMap<u64, Arc<StyleProperties>> = HashMap::new();
    // 1. Organize overrides for fast lookup per run.
17204
    let mut run_overrides: HashMap<u32, HashMap<u32, &PartialStyleProperties>> = HashMap::new();
17204
    for override_item in style_overrides {
        run_overrides
            .entry(override_item.target.run_index)
            .or_default()
            .insert(override_item.target.item_index, &override_item.style);
    }
18040
    for (run_idx, inline_item) in content.iter().enumerate() {
18040
        if let Some(msgs) = debug_messages {
17468
            msgs.push(LayoutDebugMessage::info(format!(
17468
                "Processing content run #{}",
17468
                run_idx
17468
            )));
17468
        }
        // Extract marker information if this is a marker
18040
        let marker_position_outside = match inline_item {
            InlineContent::Marker {
                position_outside, ..
            } => Some(*position_outside),
18040
            _ => None,
        };
        // [az-web-lift FIX 2026-06-06] Handle the common Text/Marker case via a STANDALONE `if let`
        // (a simple discriminant compare) instead of the first arm of the multi-way `match` below.
        // The remill lift mis-routes that multi-way InlineContent switch (LLVM's `subs/csel`-clamp
        // lowering): a Text(disc 0) variant lands in the `_`/Object arm → `inline_item.clone()` →
        // `<InlineContent as Clone>::clone` ALSO mis-routes to its Vec-clone arm → reads a heap ptr
        // as a Vec len → ×8 → ~789 MB alloc → BumpAlloc memset OOB. A standalone if-let lowers to a
        // single cmp/beq the lift handles correctly, so Text reaches its real body. Native unaffected.
18040
        if let InlineContent::Text(run) | InlineContent::Marker { run, .. } = inline_item {
17204
                let text = &run.text;
17204
                if text.is_empty() {
                    if let Some(msgs) = debug_messages {
                        msgs.push(LayoutDebugMessage::info(
                            "  Run is empty, skipping.".to_string(),
                        ));
                    }
                    continue;
17204
                }
17204
                if let Some(msgs) = debug_messages {
16632
                    msgs.push(LayoutDebugMessage::info(format!("  Run text: '{}'", text)));
16632
                }
17204
                let current_run_overrides = run_overrides.get(&(run_idx as u32));
17204
                let mut boundaries = BTreeSet::new();
17204
                boundaries.insert(0);
17204
                boundaries.insert(text.len());
                // --- Stateful Boundary Generation ---
                // web-lift FIX + perf: this scan_cursor walk ONLY inserts boundaries for
                // per-char style overrides (Rule 2) or text-combine-upright digit runs (Rule 1).
                // For plain text (no overrides AND no combine-upright) it inserts NOTHING and just
                // walks char-by-char via `scan_cursor += current_char.len_utf8()` — which the web
                // lift mis-advances (overshoot → slice_start_index_len_fail OOB; stall → infinite
                // loop). Skip the whole walk in that common case so `boundaries` stays {0, len}.
17204
                let needs_scan = current_run_overrides.is_some()
17204
                    || run.style.text_combine_upright.is_some();
17204
                let mut scan_cursor = 0;
17204
                while needs_scan && scan_cursor < text.len() {
                    let style_at_cursor = if let Some(partial) =
                        current_run_overrides.and_then(|o| o.get(&(scan_cursor as u32)))
                    {
                        // Create a temporary, full style to check its properties
                        run.style.apply_override(partial)
                    } else {
                        (*run.style).clone()
                    };
                    let current_char = text[scan_cursor..].chars().next().unwrap();
                    // +spec:containing-block:e4d9de - text-combine-upright digit run rules: digits sharing an ancestor with same value form one sequence across box boundaries
                    // +spec:inline-formatting-context:f65029 - text-combine-upright text run rules: combine consecutive digits not interrupted by box boundary
                    // Rule 1: Multi-character features take precedence.
                    // +spec:containing-block:9a26bd - text-combine-upright digit runs scoped by ancestor style boundaries
                    if let Some(TextCombineUpright::Digits(max_digits)) =
                        style_at_cursor.text_combine_upright
                    {
                        if max_digits > 0 && current_char.is_ascii_digit() {
                            let digit_chunk: String = text[scan_cursor..]
                                .chars()
                                .take(max_digits as usize)
                                .take_while(|c| c.is_ascii_digit())
                                .collect();
                            let end_of_chunk = scan_cursor + digit_chunk.len();
                            boundaries.insert(scan_cursor);
                            boundaries.insert(end_of_chunk);
                            scan_cursor = end_of_chunk; // Jump past the entire sequence
                            continue;
                        }
                    }
                    // Rule 2: If no multi-char feature, check for a normal single-grapheme
                    // override.
                    if current_run_overrides
                        .and_then(|o| o.get(&(scan_cursor as u32)))
                        .is_some()
                    {
                        let grapheme_len = text[scan_cursor..]
                            .graphemes(true)
                            .next()
                            .unwrap_or("")
                            .len();
                        boundaries.insert(scan_cursor);
                        boundaries.insert(scan_cursor + grapheme_len);
                        scan_cursor += grapheme_len;
                        continue;
                    }
                    // Rule 3: No special features or overrides at this point, just advance one
                    // char.
                    scan_cursor += current_char.len_utf8();
                }
17204
                if let Some(msgs) = debug_messages {
16632
                    msgs.push(LayoutDebugMessage::info(format!(
16632
                        "  Boundaries: {:?}",
16632
                        boundaries
16632
                    )));
16632
                }
                // --- Chunk Processing ---
17204
                for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
17204
                    let (start, end) = (*start, *end);
17204
                    if start >= end {
                        continue;
17204
                    }
17204
                    let text_slice = &text[start..end];
17204
                    if let Some(msgs) = debug_messages {
16632
                        msgs.push(LayoutDebugMessage::info(format!(
16632
                            "  Processing chunk from {} to {}: '{}'",
16632
                            start, end, text_slice
16632
                        )));
16632
                    }
17204
                    let style_to_use = if let Some(partial_style) =
17204
                        current_run_overrides.and_then(|o| o.get(&(start as u32)))
                    {
                        if let Some(msgs) = debug_messages {
                            msgs.push(LayoutDebugMessage::info(format!(
                                "  -> Applying override at byte {}",
                                start
                            )));
                        }
                        let mut hasher = DefaultHasher::new();
                        Arc::as_ptr(&run.style).hash(&mut hasher);
                        partial_style.hash(&mut hasher);
                        style_cache
                            .entry(hasher.finish())
                            .or_insert_with(|| Arc::new(run.style.apply_override(partial_style)))
                            .clone()
                    } else {
17204
                        run.style.clone()
                    };
                    // +spec:block-formatting-context:9e7c79 - text-combine-upright combines multiple characters into 1em in vertical writing
                    // +spec:containing-block:2b399b - text-combine-upright digits: combine ASCII digit sequences within max_digits limit; box boundaries implicitly prevent cross-box combination
                    // +spec:display-contents:644c78 - text-combine-upright run boundary check:
                    // if a combinable run boundary is due only to inline box boundaries,
                    // and adjacent chars would form a longer combinable sequence, do not combine
                    // +spec:white-space-processing:409d90 - text-combine-upright combined text: white space at start/end processed as in inline-block
17204
                    let is_combinable_chunk = match &style_to_use.text_combine_upright {
                        Some(TextCombineUpright::All) => !text_slice.is_empty(),
                        Some(TextCombineUpright::Digits(max_digits)) => {
                            *max_digits > 0
                                && !text_slice.is_empty()
                                && text_slice.chars().all(|c| c.is_ascii_digit())
                                && text_slice.chars().count() <= *max_digits as usize
                        }
17204
                        _ => false,
                    };
17204
                    if is_combinable_chunk {
                        // Trim leading/trailing white space like an inline-block
                        let trimmed = text_slice.trim();
                        let combined_text = if trimmed.is_empty() {
                            text_slice.to_string()
                        } else {
                            trimmed.to_string()
                        };
                        items.push(LogicalItem::CombinedText {
                            source: ContentIndex {
                                run_index: run_idx as u32,
                                item_index: start as u32,
                            },
                            text: combined_text,
                            style: style_to_use,
                        });
17204
                    } else {
17204
                        items.push(LogicalItem::Text {
17204
                            source: ContentIndex {
17204
                                run_index: run_idx as u32,
17204
                                item_index: start as u32,
17204
                            },
17204
                            text: text_slice.to_string(),
17204
                            style: style_to_use,
17204
                            marker_position_outside,
17204
                            source_node_id: run.source_node_id,
17204
                        });
17204
                    }
                }
        } else {
836
            match inline_item {
            // line breaking class characters must be treated as forced line breaks
352
            InlineContent::LineBreak(break_info) => {
352
                if let Some(msgs) = debug_messages {
352
                    msgs.push(LayoutDebugMessage::info(format!(
352
                        "  LineBreak: {:?}",
352
                        break_info
352
                    )));
352
                }
352
                items.push(LogicalItem::Break {
352
                    source: ContentIndex {
352
                        run_index: run_idx as u32,
352
                        item_index: 0,
352
                    },
352
                    break_info: break_info.clone(),
352
                });
            }
            // Handle tab characters
88
            InlineContent::Tab { style } => {
88
                if let Some(msgs) = debug_messages {
88
                    msgs.push(LayoutDebugMessage::info("  Tab character".to_string()));
88
                }
88
                items.push(LogicalItem::Tab {
88
                    source: ContentIndex {
88
                        run_index: run_idx as u32,
88
                        item_index: 0,
88
                    },
88
                    style: style.clone(),
88
                });
            }
            // Other cases (Image, Shape, Space, Ruby). Text/Marker are handled by the `if let`
            // above (so they never reach here at runtime); `_` keeps this inner match exhaustive.
            _ => {
396
                if let Some(msgs) = debug_messages {
396
                    msgs.push(LayoutDebugMessage::info(
396
                        "  Run is not text, creating generic LogicalItem.".to_string(),
396
                    ));
396
                }
396
                items.push(LogicalItem::Object {
396
                    source: ContentIndex {
396
                        run_index: run_idx as u32,
396
                        item_index: 0,
396
                    },
396
                    content: inline_item.clone(),
396
                });
            }
            }
        }
    }
17204
    if let Some(msgs) = debug_messages {
16632
        msgs.push(LayoutDebugMessage::info(format!(
16632
            "--- Exiting create_logical_items, created {} items ---",
16632
            items.len()
16632
        )));
16632
    }
17204
    items
17204
}
// --- Stage 2 Implementation ---
// +spec:inline-block:d47971 - unicode-bidi:plaintext uses P2/P3 heuristic for base direction (implemented via get_base_direction)
// +spec:writing-modes:287491 - BiDi reordering and base direction detection (Appendix A text processing order)
// when determining base direction, consistent with their neutral bidi treatment
pub fn get_base_direction_from_logical(logical_items: &[LogicalItem]) -> BidiDirection {
    let first_strong = logical_items.iter().find_map(|item| {
        if let LogicalItem::Text { text, .. } = item {
            Some(unicode_bidi::get_base_direction(text.as_str()))
        } else {
            None
        }
    });
    match first_strong {
        Some(unicode_bidi::Direction::Rtl) => BidiDirection::Rtl,
        _ => BidiDirection::Ltr,
    }
}
// +spec:containing-block:149255 - bidi reordering produces inline box fragments that may separate in wide containing blocks
// +spec:containing-block:c7c08f - bidi reordering produces inline box fragments that may be adjacent in narrow containing blocks
// +spec:containing-block:2936ae - bidi reordering splits inline boxes into visual fragments (CSS Writing Modes 4 §2.4.5)
// +spec:display-property:0cdbd3 - bidi reordering splits inline boxes into visual runs; each run is shaped/formatted independently
// +spec:display-property:0d62a2 - bidi reordering of inline content respects block direction and unicode-bidi embedding
// +spec:display-property:10f9cd - bidi reordering splits and reorders inline box fragments
// +spec:display-property:58b30a - bidi paragraph breaks within inline boxes: each IFC does independent bidi analysis, so splitting an inline box at a paragraph boundary naturally closes/reopens bidi embeddings
// +spec:display-property:ecd935 - inline boxes split and reordered for uniform bidi flow
// +spec:writing-modes:330b8f - text ordered according to Unicode bidi algorithm after white-space processing
// +spec:writing-modes:7a9e7d - bidi control translation: text passed to unicode_bidi for reordering
// +spec:writing-modes:8e7281 - unicode-bidi property: bidi control codes inserted via BidiInfo
17204
pub fn reorder_logical_items(
17204
    logical_items: &[LogicalItem],
17204
    base_direction: BidiDirection,
17204
    unicode_bidi: UnicodeBidi,
17204
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
17204
) -> Result<Vec<VisualItem>, LayoutError> {
17204
    if let Some(msgs) = debug_messages {
16632
        msgs.push(LayoutDebugMessage::info(
16632
            "\n--- Entering reorder_logical_items ---".to_string(),
16632
        ));
16632
        msgs.push(LayoutDebugMessage::info(format!(
16632
            "Input logical items count: {}",
16632
            logical_items.len()
16632
        )));
16632
        msgs.push(LayoutDebugMessage::info(format!(
16632
            "Base direction: {:?}",
16632
            base_direction
16632
        )));
16632
    }
    // +spec:writing-modes:809513 - bidi string built across inline element boundaries; unicode-bidi:normal adds no extra embedding levels
17204
    let mut bidi_str = String::new();
17204
    let mut item_map = Vec::new();
18040
    for (idx, item) in logical_items.iter().enumerate() {
        // +spec:containing-block:1fdc31 - inline boxes with unicode-bidi:normal are transparent to bidi algorithm
        // +spec:display-property:074abf - inline boxes transparent to bidi when unicode-bidi:normal
        // +spec:display-property:354966 - unicode-bidi control code injection for inline boxes
        // +spec:display-property:8409d3 - inline-level elements with unicode-bidi:normal have no effect on bidi ordering; embed creates an embedding
        // +spec:display-property:89464a - inline boxes with unicode-bidi:normal don't open embedding levels, so direction has no effect on bidi reordering
        // +spec:display-property:d47971 - bidi control codes should be injected at inline box boundaries based on unicode-bidi + direction
        // +spec:display-property:de657b - bidi control codes injected for display:inline boxes per unicode-bidi value
        // +spec:display-property:f01a81 - bidi-override should prepend LRO/RLO and append PDF per unicode-bidi CSS property (not yet implemented)
        // are treated as neutral characters in the bidi algorithm. Replaced elements with
        // +spec:display-property:fcb011 - unicode-bidi values on inline boxes insert bidi control codes
        // +spec:display-property:89095f - isolate/bidi-override/isolate-override/plaintext semantics
        // +spec:writing-modes:d490bf - direction only affects reordering when unicode-bidi is embed/override (not yet enforced for inline elements)
        // display:inline are also neutral unless unicode-bidi != normal (not yet implemented).
        // +spec:display-property:b4756e - replaced inline elements treated as neutral bidi chars;
        // embed/bidi-override exception not yet implemented (would make them strong chars).
        // U+FFFC (OBJECT REPLACEMENT CHARACTER) is a neutral bidi character.
        // +spec:display-property:df11ef - atomic inlines treated as neutral bidi characters (U+FFFC)
        // Replaced elements with display:inline are also neutral unless unicode-bidi != normal.
18040
        let text = match item {
17204
            LogicalItem::Text { text, .. } => text.as_str(),
            LogicalItem::CombinedText { text, .. } => text.as_str(),
836
            _ => "\u{FFFC}",
        };
18040
        let start_byte = bidi_str.len();
18040
        bidi_str.push_str(text);
174636
        for _ in start_byte..bidi_str.len() {
174636
            item_map.push(idx);
174636
        }
    }
17204
    if bidi_str.is_empty() {
        if let Some(msgs) = debug_messages {
            msgs.push(LayoutDebugMessage::info(
                "Bidi string is empty, returning.".to_string(),
            ));
        }
        return Ok(Vec::new());
17204
    }
17204
    if let Some(msgs) = debug_messages {
16632
        msgs.push(LayoutDebugMessage::info(format!(
16632
            "Constructed bidi string: '{}'",
16632
            bidi_str
16632
        )));
16632
    }
    // +spec:display-property:1a6075 - paragraph embedding level set from direction property per UAX9 HL1
    // +spec:containing-block:0d4914 - unicode-bidi: plaintext exception
    // When the containing block has unicode-bidi: plaintext, use None so the
    // Unicode bidi algorithm applies P2/P3 heuristics instead of the HL1 override
17204
    let bidi_level = if unicode_bidi == UnicodeBidi::Plaintext {
        None
17204
    } else if base_direction == BidiDirection::Rtl {
        Some(Level::rtl())
    } else {
17204
        Some(Level::ltr())
    };
    // +spec:writing-modes:15bf17 - bidi isolation handled by unicode_bidi UAX #9 implementation
17204
    let bidi_info = BidiInfo::new(&bidi_str, bidi_level);
17204
    let para = &bidi_info.paragraphs[0];
17204
    let (levels, visual_runs) = bidi_info.visual_runs(para, para.range.clone());
17204
    if let Some(msgs) = debug_messages {
16632
        msgs.push(LayoutDebugMessage::info(
16632
            "Bidi visual runs generated:".to_string(),
        ));
16632
        for (i, run_range) in visual_runs.iter().enumerate() {
16632
            let level = levels[run_range.start].number();
16632
            let slice = &bidi_str[run_range.start..run_range.end];
16632
            msgs.push(LayoutDebugMessage::info(format!(
16632
                "  Run {}: range={:?}, level={}, text='{}'",
16632
                i, run_range, level, slice
16632
            )));
16632
        }
572
    }
17204
    let mut visual_items = Vec::new();
34408
    for run_range in visual_runs {
17204
        let bidi_level = BidiLevel::new(levels[run_range.start].number());
17204
        let mut sub_run_start = run_range.start;
157432
        for i in (run_range.start + 1)..run_range.end {
157432
            if item_map[i] != item_map[sub_run_start] {
836
                let logical_idx = item_map[sub_run_start];
836
                let logical_item = &logical_items[logical_idx];
836
                let text_slice = &bidi_str[sub_run_start..i];
836
                visual_items.push(VisualItem {
836
                    logical_source: logical_item.clone(),
836
                    bidi_level,
836
                    script: crate::text3::script::detect_script(text_slice)
836
                        .unwrap_or(Script::Latin),
836
                    text: text_slice.to_string(),
836
                });
836
                sub_run_start = i;
156596
            }
        }
17204
        let logical_idx = item_map[sub_run_start];
17204
        let logical_item = &logical_items[logical_idx];
17204
        let text_slice = &bidi_str[sub_run_start..run_range.end];
17204
        visual_items.push(VisualItem {
17204
            logical_source: logical_item.clone(),
17204
            bidi_level,
17204
            script: crate::text3::script::detect_script(text_slice).unwrap_or(Script::Latin),
17204
            text: text_slice.to_string(),
17204
        });
    }
17204
    if let Some(msgs) = debug_messages {
16632
        msgs.push(LayoutDebugMessage::info(
16632
            "Final visual items produced:".to_string(),
        ));
17468
        for (i, item) in visual_items.iter().enumerate() {
17468
            msgs.push(LayoutDebugMessage::info(format!(
17468
                "  Item {}: level={}, text='{}'",
17468
                i,
17468
                item.bidi_level.level(),
17468
                item.text
17468
            )));
17468
        }
16632
        msgs.push(LayoutDebugMessage::info(
16632
            "--- Exiting reorder_logical_items ---".to_string(),
        ));
572
    }
17204
    Ok(visual_items)
17204
}
// --- Stage 3 Implementation ---
/// Shape visual items into ShapedItems using pre-loaded fonts.
///
/// This function does NOT load any fonts - all fonts must be pre-loaded and passed in.
/// If a required font is not in `loaded_fonts`, the text will be skipped with a warning.
///
/// **Optimization: Inline Run Coalescing**
///
/// // +spec:display-property:9c6d59 - text shaping not broken across inline box boundaries when no effective formatting change
/// // +spec:display-property:cf8917 - text shaping not broken across inline box boundaries
/// When consecutive text `VisualItem`s share the same layout-affecting properties
/// (font, size, spacing, etc.) but differ only in rendering properties (color,
/// background), they are coalesced into a single shaping call. This dramatically
/// reduces the number of `font.shape_text()` invocations for syntax-highlighted
/// code where hundreds of `<span>` elements use the same monospace font but
/// different colors. After shaping, the original per-span styles are restored
/// to each `ShapedCluster` based on byte-range mapping.
/// Shape visual items with per-item caching. For each item (or coalesced group),
/// compute a cache key from (text, bidi_level, script, style_layout_hash). On cache
/// hit, reuse the previously shaped clusters. On miss, shape and store.
///
/// This is the incremental shaping path: when one word changes in a paragraph,
/// only that word's item misses the per-item cache; all other items hit.
15428
pub fn shape_visual_items_with_per_item_cache<T: ParsedFontTrait>(
15428
    visual_items: &[VisualItem],
15428
    per_item_cache: &mut HashMap<u64, Arc<PerItemShapedEntry>>,
15428
    per_item_accessed: &mut HashSet<u64>,
15428
    font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
15428
    fc_cache: &FcFontCache,
15428
    loaded_fonts: &LoadedFonts<T>,
15428
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
15428
) -> Result<Vec<ShapedItem>, LayoutError> {
    // Delegate to the existing shaping logic, but for each coalesce group,
    // check the per-item cache first.
    //
    // Strategy: Identify coalesce groups (adjacent items with same layout_hash,
    // bidi_level, script). For each group, compute a key from the concatenated
    // text + shared properties. Check cache. On miss, shape the group and cache it.
15428
    let mut shaped = Vec::new();
15428
    let mut idx = 0;
30873
    while idx < visual_items.len() {
15445
        let item = &visual_items[idx];
        // Determine coalesce group boundaries (same logic as shape_visual_items)
15445
        let (layout_hash, bidi_level, script) = match &item.logical_source {
15039
            LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
15039
                (style.layout_hash(), item.bidi_level, item.script)
            }
            _ => {
                // Non-text items: shape individually (no coalescing)
406
                let single = shape_visual_items(
406
                    &visual_items[idx..idx+1],
406
                    font_chain_cache, fc_cache, loaded_fonts, debug_messages,
                )?;
406
                shaped.extend(single);
406
                idx += 1;
406
                continue;
            }
        };
15039
        let mut coalesce_end = idx + 1;
15041
        while coalesce_end < visual_items.len() {
9
            let next = &visual_items[coalesce_end];
9
            let next_layout_hash = match &next.logical_source {
2
                LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
2
                    Some(style.layout_hash())
                }
7
                _ => None,
            };
9
            if let Some(nlh) = next_layout_hash {
2
                if nlh == layout_hash
2
                    && next.bidi_level == bidi_level
2
                    && next.script == script
2
                {
2
                    coalesce_end += 1;
2
                } else {
                    break;
                }
            } else {
7
                break;
            }
        }
        // Compute per-group cache key
15039
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        use std::hash::{Hash, Hasher};
15041
        for j in idx..coalesce_end {
15041
            visual_items[j].text.hash(&mut hasher);
15041
        }
15039
        layout_hash.hash(&mut hasher);
15039
        bidi_level.hash(&mut hasher);
15039
        (script as u32).hash(&mut hasher);
15039
        let group_key = hasher.finish();
        // Check per-item cache
15039
        per_item_accessed.insert(group_key);
15039
        if let Some(cached) = per_item_cache.get(&group_key) {
1892
            shaped.extend(cached.clusters.iter().cloned());
1892
        } else {
            // Cache miss — shape this group
13147
            let group_items = shape_visual_items(
13147
                &visual_items[idx..coalesce_end],
13147
                font_chain_cache, fc_cache, loaded_fonts, debug_messages,
            )?;
107267
            let total_advance: f32 = group_items.iter().map(|item| {
107267
                match item {
107267
                    ShapedItem::Cluster(c) => c.advance,
                    _ => 0.0,
                }
107267
            }).sum();
13147
            per_item_cache.insert(group_key, Arc::new(PerItemShapedEntry {
13147
                clusters: group_items.clone(),
13147
                total_advance,
13147
            }));
13147
            shaped.extend(group_items);
        }
15039
        idx = coalesce_end;
    }
15428
    Ok(shaped)
15428
}
/// Split text into segments where consecutive characters resolve to the same font
/// in the fallback chain. Returns Vec<(byte_start, byte_end, FontId)>.
///
/// Characters that can't be resolved to any font are skipped (gap in coverage).
13719
fn split_text_by_font_coverage<T: ParsedFontTrait>(
13719
    text: &str,
13719
    font_chain: &rust_fontconfig::FontFallbackChain,
13719
    fc_cache: &FcFontCache,
13719
    loaded_fonts: &LoadedFonts<T>,
13719
) -> Vec<(usize, usize, FontId)> {
13719
    let mut segments: Vec<(usize, usize, FontId)> = Vec::new();
116160
    for (byte_idx, ch) in text.char_indices() {
116160
        let char_end = byte_idx + ch.len_utf8();
        // Primary: the resolved fallback chain. Its coverage comes from
        // rust-fontconfig's OS/2-derived `unicode_ranges`, which can MISS
        // codepoints a font actually has in its cmap — e.g. Noto Sans CJK's
        // JP face does not advertise the Hangul OS/2 block, so 한국어 resolves
        // to None here even though that face's cmap covers it.
116160
        let font_id = font_chain
116160
            .resolve_char(fc_cache, ch)
116160
            .map(|(id, _)| id)
            // Fallback: probe the actually-loaded fonts by REAL glyph coverage
            // so OS/2-vs-cmap gaps render instead of being silently dropped.
            // The covering CJK face is already loaded (Han/Kana resolved to it),
            // so this reuses it for Hangul rather than mixing in another font.
116160
            .or_else(|| {
                loaded_fonts
                    .iter()
                    .find(|(_, font)| font.has_glyph(ch as u32))
                    .map(|(id, _)| id.clone())
            });
116160
        if let Some(font_id) = font_id {
116160
            match segments.last_mut() {
102441
                Some(last) if last.2 == font_id && last.1 == byte_idx => {
97601
                    // Extend current segment (same font, contiguous)
97601
                    last.1 = char_end;
97601
                }
18559
                _ => {
18559
                    // New segment (different font or gap)
18559
                    segments.push((byte_idx, char_end, font_id));
18559
                }
            }
        }
    }
13719
    segments
13719
}
/// Shape text with per-character font fallback.
///
/// Splits the text into segments by font coverage, shapes each segment with
/// its resolved font, and fixes byte offsets so they're relative to the
/// original `text` (not the segment substring).
13719
fn shape_with_font_fallback<T: ParsedFontTrait>(
13719
    text: &str,
13719
    script: Script,
13719
    language: crate::text3::script::Language,
13719
    direction: BidiDirection,
13719
    style: &Arc<StyleProperties>,
13719
    source_index: ContentIndex,
13719
    source_node_id: Option<NodeId>,
13719
    font_chain: &rust_fontconfig::FontFallbackChain,
13719
    fc_cache: &FcFontCache,
13719
    loaded_fonts: &LoadedFonts<T>,
13719
) -> Result<Vec<ShapedCluster>, LayoutError> {
    // Cache the debug flag in a `OnceLock<bool>` — reading it per-shape
    // (this function fires once per text segment, ~hundreds of times
    // per render of a real DOM) costs ~100 ns per `std::env::var_os`
    // call on macOS (env-lock + hashmap lookup), and even before the
    // lookup finishes the `eprintln!` machinery takes a stderr lock
    // and allocates the formatted string. Both are invisible in
    // release unless `AZ_FONT_FALLBACK_DEBUG=1` is set.
    static FONT_FB_DEBUG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13719
    let dbg = *FONT_FB_DEBUG.get_or_init(|| {
532
        std::env::var_os("AZ_FONT_FALLBACK_DEBUG").is_some()
532
    });
13719
    let segments = split_text_by_font_coverage(text, font_chain, fc_cache, loaded_fonts);
13719
    if dbg && segments.len() > 1 {
        eprintln!(
            "[FONT FALLBACK] text needs {} font segments for '{}' ({}..{} bytes)",
            segments.len(),
            text.chars().take(40).collect::<String>(),
            0, text.len()
        );
13719
    }
13719
    unsafe { crate::az_mark((0x60850) as u32, (segments.len() as u32) as u32); } // [g123] segments count (split_text_by_font_coverage)
13719
    if segments.len() <= 1 {
        // Fast path: all characters use the same font (common case)
9099
        let (seg_start, seg_end, font_id) = match segments.first() {
9099
            Some(s) => { unsafe { crate::az_mark((0x60854) as u32, (0x00000001u32) as u32); } s }
            None => {
                unsafe { crate::az_mark((0x60854) as u32, (0x000000EEu32) as u32); } // [g123] split→0 segments (resolve_char failed all)
                if dbg {
                    eprintln!("[FONT FALLBACK] no font could render any char in '{}'", text.chars().take(20).collect::<String>());
                }
                return Ok(Vec::new());
            }
        };
9099
        let font = match loaded_fonts.get(font_id) {
9099
            Some(f) => { unsafe { crate::az_mark((0x60858) as u32, (0x00000001u32) as u32); } f }
            None => {
                unsafe { crate::az_mark((0x60858) as u32, (0x000000EEu32) as u32); } // [g123] loaded_fonts.get MISS
                if dbg {
                    eprintln!("[FONT FALLBACK] font {:?} not in loaded_fonts for '{}'", font_id, text.chars().take(20).collect::<String>());
                }
                return Ok(Vec::new());
            }
        };
        // If segment covers the full text (overwhelmingly common), skip substr+fixup
9099
        if *seg_start == 0 && *seg_end == text.len() {
9099
            unsafe { crate::az_mark((0x60860) as u32, (0xC0DE0860u32) as u32); } // [g123] reached shape_text_correctly (full-text)
9099
            return shape_text_correctly(
9099
                text, script, language, direction,
9099
                font, style, source_index, source_node_id,
            );
        }
        let mut clusters = shape_text_correctly(
            &text[*seg_start..*seg_end], script, language, direction,
            font, style, source_index, source_node_id,
        )?;
        if *seg_start > 0 {
            for cluster in &mut clusters {
                cluster.source_cluster_id.start_byte_in_run += *seg_start as u32;
            }
        }
        return Ok(clusters);
4620
    }
    // Multiple fonts needed — shape each segment separately
4620
    let mut all_clusters = Vec::new();
14080
    for (seg_start, seg_end, font_id) in &segments {
9460
        let font = match loaded_fonts.get(font_id) {
9460
            Some(f) => f,
            None => {
                if dbg {
                    eprintln!("[FONT FALLBACK] font {:?} NOT loaded, skipping segment bytes {}..{}", font_id, seg_start, seg_end);
                }
                continue;
            }
        };
9460
        let segment_text = &text[*seg_start..*seg_end];
9460
        if dbg {
            eprintln!(
                "[FONT FALLBACK] text='{}' uses font {:?} (bytes {}..{})",
                segment_text, font_id, seg_start, seg_end
            );
9460
        }
9460
        let mut seg_clusters = shape_text_correctly(
9460
            segment_text, script, language, direction,
9460
            font, style, source_index, source_node_id,
        )?;
        // Fix byte offsets: shape_text_correctly produces offsets relative to
        // segment_text, but callers expect offsets relative to the full text.
9460
        if *seg_start > 0 {
44440
            for cluster in &mut seg_clusters {
39600
                cluster.source_cluster_id.start_byte_in_run += *seg_start as u32;
39600
            }
4620
        }
9460
        all_clusters.extend(seg_clusters);
    }
4620
    Ok(all_clusters)
13719
}
14125
pub fn shape_visual_items<T: ParsedFontTrait>(
14125
    visual_items: &[VisualItem],
14125
    font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
14125
    fc_cache: &FcFontCache,
14125
    loaded_fonts: &LoadedFonts<T>,
14125
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
14125
) -> Result<Vec<ShapedItem>, LayoutError> {
14125
    let mut shaped = Vec::new();
14125
    let mut idx = 0;
14125
    let mut _coalesced_runs = 0usize;
14125
    let mut _total_runs = 0usize;
14125
    let mut _shape_calls = 0usize;
    // Log count of visual items for debugging coalescing
28250
    while idx < visual_items.len() {
14125
        let item = &visual_items[idx];
14125
        match &item.logical_source {
            LogicalItem::Text {
13719
                style,
13719
                source,
13719
                marker_position_outside,
13719
                source_node_id,
                ..
            } => {
13719
                let layout_hash = style.layout_hash();
13719
                let bidi_level = item.bidi_level;
13719
                let script = item.script;
                // +spec:display-property:ca95f6 - text shaping breaks at inline box boundaries when layout-affecting properties differ
                // when layout-affecting properties (font weight, family, size, etc.) change
                // across element boundaries, preventing ligatures from forming across such changes.
                // Look ahead: find consecutive text items with the same layout-affecting
                // properties (font, size, spacing) that can be shaped as one merged run.
13719
                let mut coalesce_end = idx + 1;
13721
                while coalesce_end < visual_items.len() {
2
                    let next = &visual_items[coalesce_end];
2
                    if let LogicalItem::Text { style: next_style, .. } = &next.logical_source {
2
                        if next_style.layout_hash() == layout_hash
2
                            && next.bidi_level == bidi_level
2
                            && next.script == script
2
                        {
2
                            coalesce_end += 1;
2
                        } else {
                            break;
                        }
                    } else {
                        break;
                    }
                }
13719
                let coalesce_count = coalesce_end - idx;
13719
                if coalesce_count > 1 {
1
                    _coalesced_runs += coalesce_count;
1
                    _shape_calls += 1;
                    // ── COALESCED PATH ──
                    // Merge N text items into one shaping call, then split results
                    // back per original run to preserve per-span rendering styles.
                    // Build merged text and record byte ranges → original style
1
                    let total_text_len: usize = visual_items[idx..coalesce_end]
1
                        .iter()
3
                        .map(|v| v.text.len())
1
                        .sum();
1
                    let mut merged_text = String::with_capacity(total_text_len);
                    // (byte_start, byte_end, style, source, source_node_id, marker_outside)
1
                    let mut byte_ranges: Vec<(
1
                        usize, usize,
1
                        Arc<StyleProperties>,
1
                        ContentIndex,
1
                        Option<NodeId>,
1
                        Option<bool>,
1
                    )> = Vec::with_capacity(coalesce_count);
3
                    for j in idx..coalesce_end {
3
                        let start = merged_text.len();
3
                        merged_text.push_str(&visual_items[j].text);
3
                        let end = merged_text.len();
                        if let LogicalItem::Text {
3
                            style: s, source: src, source_node_id: nid,
3
                            marker_position_outside: mpo, ..
3
                        } = &visual_items[j].logical_source {
3
                            byte_ranges.push((start, end, s.clone(), *src, *nid, *mpo));
3
                        }
                    }
1
                    if let Some(msgs) = debug_messages {
1
                        msgs.push(LayoutDebugMessage::info(format!(
1
                            "[TextLayout] Coalescing {} text runs ({} bytes) into single shaping call",
1
                            coalesce_count, merged_text.len()
1
                        )));
1
                    }
1
                    let direction = if bidi_level.is_rtl() {
                        BidiDirection::Rtl
                    } else {
1
                        BidiDirection::Ltr
                    };
1
                    let language = script_to_language(script, &merged_text);
                    // Shape the merged text using the first item's font (layout is identical
                    // for all coalesced items since layout_hash matches).
1
                    let shaped_clusters_result: Result<Vec<ShapedCluster>, LayoutError> = match &style.font_stack {
                        FontStack::Ref(font_ref) => {
                            shape_text_correctly(
                                &merged_text, script, language, direction,
                                font_ref, style, *source, *source_node_id,
                            )
                        }
1
                        FontStack::Stack(selectors) => {
1
                            let cache_key = FontChainKey::from_selectors(selectors);
1
                            let font_chain = match font_chain_cache.get(&cache_key) {
1
                                Some(chain) => chain,
                                None => { idx = coalesce_end; continue; }
                            };
                            // Per-character font fallback: split text by font coverage
1
                            shape_with_font_fallback(
1
                                &merged_text, script, language, direction,
1
                                style, *source, *source_node_id,
1
                                font_chain, fc_cache, loaded_fonts,
                            )
                        }
                    };
1
                    let shaped_clusters = shaped_clusters_result?;
                    // Restore original per-span styles to each cluster based on byte position.
                    // Each ShapedCluster's source_cluster_id.start_byte_in_run is the byte
                    // offset within the merged text — we use byte_ranges to find which
                    // original run it belongs to and reassign its style, source info, etc.
12
                    for cluster in shaped_clusters {
11
                        let byte_pos = cluster.source_cluster_id.start_byte_in_run as usize;
                        // Find the original run this cluster's first byte falls into
22
                        let orig = byte_ranges.iter().find(|(start, end, ..)| {
22
                            byte_pos >= *start && byte_pos < *end
22
                        });
11
                        let mut cluster = cluster;
11
                        if let Some((range_start, _, orig_style, orig_source, orig_nid, orig_mpo)) = orig {
                            // Reassign rendering-affecting style (color, background, etc.)
11
                            cluster.style = orig_style.clone();
11
                            cluster.source_content_index = *orig_source;
11
                            cluster.source_node_id = *orig_nid;
                            // Fix the byte offset to be relative to the original run
11
                            cluster.source_cluster_id.source_run = orig_source.run_index;
11
                            cluster.source_cluster_id.start_byte_in_run = (byte_pos - range_start) as u32;
                            // Update glyph styles
22
                            for glyph in &mut cluster.glyphs {
11
                                glyph.style = orig_style.clone();
11
                            }
11
                            if let Some(is_outside) = orig_mpo {
                                cluster.marker_position_outside = Some(*is_outside);
11
                            }
                        }
11
                        shaped.push(ShapedItem::Cluster(cluster));
                    }
1
                    idx = coalesce_end;
1
                    continue;
13718
                }
                // ── SINGLE ITEM PATH (no coalescing) ──
13718
                _total_runs += 1;
13718
                _shape_calls += 1;
13718
                let direction = if item.bidi_level.is_rtl() {
                    BidiDirection::Rtl
                } else {
13718
                    BidiDirection::Ltr
                };
13718
                let language = script_to_language(item.script, &item.text);
                // Shape text using either FontRef directly or fontconfig-resolved font
13718
                let shaped_clusters_result: Result<Vec<ShapedCluster>, LayoutError> = match &style.font_stack {
                    FontStack::Ref(font_ref) => {
                        unsafe { crate::az_mark((0x60820) as u32, (0x00000001u32) as u32); } // [g121] Ref arm
                        // For FontRef, use the font directly without fontconfig
                        if let Some(msgs) = debug_messages {
                            msgs.push(LayoutDebugMessage::info(format!(
                                "[TextLayout] Using direct FontRef for text: '{}'",
                                item.text.chars().take(30).collect::<String>()
                            )));
                        }
                        shape_text_correctly(
                            &item.text,
                            item.script,
                            language,
                            direction,
                            font_ref,
                            style,
                            *source,
                            *source_node_id,
                        )
                    }
13718
                    FontStack::Stack(selectors) => {
13718
                        unsafe { crate::az_mark((0x60820) as u32, (0x00000002u32) as u32); } // [g121] Stack arm
                        // Build FontChainKey and resolve through fontconfig
13718
                        let cache_key = FontChainKey::from_selectors(selectors);
13718
                        unsafe { crate::az_mark((0x60824) as u32, (font_chain_cache.len() as u32) as u32); } // [g121] chain map len
                        // Look up the pre-resolved font chain. (2026-06-10: the g122
                        // by_find/by_only fallback chain is GONE — the historic miss was a
                        // KEY-CONSTRUCTION divergence (duplicated families on the query side,
                        // deduped on the store side), fixed by routing every key build through
                        // FontChainKey::from_selectors. Verified lifted: lookup path = get.)
13718
                        let font_chain = match font_chain_cache.get(&cache_key) {
13718
                            Some(chain) => chain,
                            None => {
                                if let Some(msgs) = debug_messages {
                                    msgs.push(LayoutDebugMessage::warning(format!(
                                        "[TextLayout] Font chain not pre-resolved for {:?} - text will \
                                         not be rendered",
                                        cache_key.font_families
                                    )));
                                }
                                idx += 1;
                                continue;
                            }
                        };
                        // Per-character font fallback: split text by font coverage
13718
                        shape_with_font_fallback(
13718
                            &item.text, item.script, language, direction,
13718
                            style, *source, *source_node_id,
13718
                            font_chain, fc_cache, loaded_fonts,
                        )
                    }
                };
13718
                let mut shaped_clusters = shaped_clusters_result?;
                // Set marker flag on all clusters if this is a marker
13718
                if let Some(is_outside) = marker_position_outside {
                    for cluster in &mut shaped_clusters {
                        cluster.marker_position_outside = Some(*is_outside);
                    }
13718
                }
13718
                shaped.extend(shaped_clusters.into_iter().map(ShapedItem::Cluster));
            }
            // +spec:display-property:df076b - tab-size rendering and inline-level line breaking
            // "If the tab size is zero, preserved tabs are not rendered."
            // "Otherwise, each preserved tab is rendered as a horizontal shift that lines up
            //  the start edge of the next glyph with the next tab stop."
            // "Tab stops occur at points that are multiples of the tab size from the starting
            //  content edge of the preserved tab's nearest block container ancestor."
2
            LogicalItem::Tab { source, style } => {
2
                if style.tab_size == 0.0 {
                    // Tab size zero: tab is not rendered (zero width)
                    shaped.push(ShapedItem::Tab {
                        source: *source,
                        bounds: Rect {
                            x: 0.0,
                            y: 0.0,
                            width: 0.0,
                            height: 0.0,
                        },
                    });
                } else {
                    // TODO: use actual font's space_width via ParsedFontTrait::get_space_width()
                    // once we thread font resolution into the shaping phase for tab stops.
                    // For now, approximate space advance as 0.5 * font_size (typical for Latin fonts).
2
                    let space_advance_approx = style.font_size_px * 0.5;
                    // +spec:text-alignment-spacing:5a5efd - tab-size includes letter-spacing and word-spacing
2
                    let ls = match style.letter_spacing {
2
                        Spacing::Px(px) => px as f32,
                        Spacing::Em(em) => em * style.font_size_px,
                    };
2
                    let ws = match style.word_spacing {
2
                        Spacing::Px(px) => px as f32,
                        Spacing::Em(em) => em * style.font_size_px,
                    };
                    // Tab stop interval: tab_size * (space advance + letter-spacing + word-spacing)
2
                    let tab_interval = style.tab_size * (space_advance_approx + ls + ws);
                    // Calculate current advance to find next tab stop
2
                    let current_advance: f32 = shaped.iter().map(|item| {
                        match item {
                            ShapedItem::Cluster(c) => c.advance,
                            ShapedItem::Tab { bounds, .. } => bounds.width,
                            ShapedItem::Object { bounds, .. } => bounds.width,
                            _ => 0.0,
                        }
2
                    }).sum();
                    // Next tab stop = next multiple of tab_interval from content edge
2
                    let next_tab_stop = ((current_advance / tab_interval).floor() + 1.0) * tab_interval;
2
                    let mut tab_width = next_tab_stop - current_advance;
                    // "If this distance is less than 0.5ch, then the subsequent tab stop is used instead."
2
                    let half_ch = space_advance_approx * 0.5;
2
                    if tab_width < half_ch {
                        tab_width += tab_interval;
2
                    }
2
                    shaped.push(ShapedItem::Tab {
2
                        source: *source,
2
                        bounds: Rect {
2
                            x: 0.0,
2
                            y: 0.0,
2
                            width: tab_width,
2
                            height: 0.0,
2
                        },
2
                    });
                }
            }
            LogicalItem::Ruby {
                source,
                base_text,
                ruby_text,
                style,
            } => {
                let placeholder_width = base_text.chars().count() as f32 * style.font_size_px * 0.6;
                shaped.push(ShapedItem::Object {
                    source: *source,
                    bounds: Rect {
                        x: 0.0,
                        y: 0.0,
                        width: placeholder_width,
                        height: style.line_height.resolve(style.font_size_px, 0.0, 0.0, 0.0, 0) * 1.5,
                    },
                    baseline_offset: 0.0,
                    content: InlineContent::Text(StyledRun {
                        text: base_text.clone(),
                        style: style.clone(),
                        logical_start_byte: 0,
                        source_node_id: None,
                    }),
                });
            }
            LogicalItem::CombinedText {
                style,
                source,
                text,
            } => {
                let language = script_to_language(item.script, &item.text);
                // +spec:width-calculation:657f75 - convert full-width chars to non-full-width before compression
                // +spec:width-calculation:d0a295 - full-width digit conversion example (e.g. "23" stays narrow)
                // When combined text has more than one typographic character unit,
                // full-width characters (U+FF01..U+FF5E) are converted to their
                // ASCII equivalents (U+0021..U+007E) before compression.
                let text = if text.chars().count() > 1 {
                    let converted: String = text.chars().map(|c| {
                        let cp = c as u32;
                        if cp >= 0xFF01 && cp <= 0xFF5E {
                            // Reverse of text-transform: full-width
                            char::from_u32(cp - 0xFF01 + 0x0021).unwrap_or(c)
                        } else {
                            c
                        }
                    }).collect();
                    converted
                } else {
                    text.clone()
                };
                // +spec:width-calculation:1ed84d - OpenType compression (half-width/third-width substitution)
                // is delegated to the font shaping layer via shape_text()
                // Shape CombinedText using either FontRef directly or fontconfig-resolved font
                let glyphs: Vec<Glyph> = match &style.font_stack {
                    FontStack::Ref(font_ref) => {
                        // For FontRef, use the font directly without fontconfig
                        if let Some(msgs) = debug_messages {
                            msgs.push(LayoutDebugMessage::info(format!(
                                "[TextLayout] Using direct FontRef for CombinedText: '{}'",
                                text.chars().take(30).collect::<String>()
                            )));
                        }
                        font_ref.shape_text(
                            &text,
                            item.script,
                            language,
                            BidiDirection::Ltr,
                            style.as_ref(),
                        )?
                    }
                    FontStack::Stack(selectors) => {
                        // Build FontChainKey and resolve through fontconfig
                        let cache_key = FontChainKey::from_selectors(selectors);
                        let font_chain = match font_chain_cache.get(&cache_key) {
                            Some(chain) => chain,
                            None => {
                                if let Some(msgs) = debug_messages {
                                    msgs.push(LayoutDebugMessage::warning(format!(
                                        "[TextLayout] Font chain not pre-resolved for CombinedText {:?}",
                                        cache_key.font_families
                                    )));
                                }
                                idx += 1;
                                continue;
                            }
                        };
                        // Per-character font fallback for CombinedText
                        let segments = split_text_by_font_coverage(&text, font_chain, fc_cache, loaded_fonts);
                        let mut all_glyphs = Vec::new();
                        for (seg_start, seg_end, font_id) in &segments {
                            let font = match loaded_fonts.get(font_id) {
                                Some(f) => f,
                                None => continue,
                            };
                            let segment_text = &text[*seg_start..*seg_end];
                            let mut seg_glyphs = font.shape_text(
                                segment_text,
                                item.script,
                                language,
                                BidiDirection::Ltr,
                                style.as_ref(),
                            )?;
                            // Fix byte offsets for glyphs
                            if *seg_start > 0 {
                                for g in &mut seg_glyphs {
                                    g.logical_byte_index += *seg_start;
                                    g.cluster += *seg_start as u32;
                                }
                            }
                            all_glyphs.extend(seg_glyphs);
                        }
                        if all_glyphs.is_empty() {
                            idx += 1;
                            continue;
                        }
                        all_glyphs
                    }
                };
                let shaped_glyphs: ShapedGlyphVec = glyphs
                    .into_iter()
                    .map(|g| ShapedGlyph {
                        kind: GlyphKind::Character,
                        glyph_id: g.glyph_id,
                        script: g.script,
                        font_hash: g.font_hash,
                        font_metrics: g.font_metrics,
                        style: g.style,
                        cluster_offset: 0,
                        advance: g.advance,
                        kerning: g.kerning,
                        offset: g.offset,
                        vertical_advance: g.vertical_advance,
                        vertical_offset: g.vertical_bearing,
                    })
                    .collect();
                // +spec:block-formatting-context:dc4549 - text-combine-upright compression: UA may scale composition to match 水 advance height
                let total_width: f32 = shaped_glyphs.iter().map(|g| g.advance + g.kerning).sum();
                // +spec:inline-formatting-context:8c5969 - text-combine-upright baseline centering
                // The composition forms a 1em square. Per spec, its baseline must be
                // chosen so the square is centered between the text-over and text-under
                // baselines of the parent inline box. We approximate by using font_size
                // as the square height and centering it (baseline_offset = em_size / 2).
                let em_size = shaped_glyphs.first()
                    .map(|g| g.style.font_size_px)
                    .unwrap_or(style.font_size_px);
                let bounds = Rect {
                    x: 0.0,
                    y: 0.0,
                    width: total_width,
                    height: em_size,
                };
                shaped.push(ShapedItem::CombinedBlock {
                    source: *source,
                    glyphs: shaped_glyphs,
                    bounds,
                    baseline_offset: em_size / 2.0,
                });
            }
            LogicalItem::Object {
396
                content, source, ..
            } => {
396
                let (bounds, baseline) = measure_inline_object(content)?;
396
                shaped.push(ShapedItem::Object {
396
                    source: *source,
396
                    bounds,
396
                    baseline_offset: baseline,
396
                    content: content.clone(),
396
                });
            }
8
            LogicalItem::Break { source, break_info } => {
8
                shaped.push(ShapedItem::Break {
8
                    source: *source,
8
                    break_info: break_info.clone(),
8
                });
8
            }
        }
14124
        idx += 1;
    }
14125
    Ok(shaped)
14125
}
/// Returns true if `c` is a hanging punctuation stop or comma per CSS Text 3 §8.2.1.
// +spec:hanging-punctuation - full stop/comma character list per CSS Text 3 §8.2.1
fn is_hanging_punctuation_char(c: char) -> bool {
    matches!(c,
        ','      | // U+002C COMMA
        '.'      | // U+002E FULL STOP
        '\u{060C}' | // ARABIC COMMA
        '\u{06D4}' | // ARABIC FULL STOP
        '\u{3001}' | // IDEOGRAPHIC COMMA
        '\u{3002}' | // IDEOGRAPHIC FULL STOP
        '\u{FF0C}' | // FULLWIDTH COMMA
        '\u{FF0E}' | // FULLWIDTH FULL STOP
        '\u{FE50}' | // SMALL COMMA
        '\u{FE51}' | // SMALL IDEOGRAPHIC COMMA
        '\u{FE52}' | // SMALL FULL STOP
        '\u{FF61}' | // HALFWIDTH IDEOGRAPHIC FULL STOP
        '\u{FF64}'   // HALFWIDTH IDEOGRAPHIC COMMA
    )
}
/// Helper to check if a cluster contains only hanging punctuation.
// +spec:box-model:8bbcd1 - non-zero inline-axis borders/padding between hangable glyph and line edge prevent hanging
/// +spec:inline-formatting-context:135be2 - hanging punctuation placed outside the line box
/// +spec:intrinsic-sizing:407d8b - hanging glyphs not counted in intrinsic size computation
fn is_hanging_punctuation(item: &ShapedItem) -> bool {
    if let ShapedItem::Cluster(c) = item {
        if c.glyphs.len() == 1 {
            c.text.chars().next().map_or(false, is_hanging_punctuation_char)
        } else {
            false
        }
    } else {
        false
    }
}
18559
fn shape_text_correctly<T: ParsedFontTrait>(
18559
    text: &str,
18559
    script: Script,
18559
    language: crate::text3::script::Language,
18559
    direction: BidiDirection,
18559
    font: &T, // Changed from &Arc<T>
18559
    style: &Arc<StyleProperties>,
18559
    source_index: ContentIndex,
18559
    source_node_id: Option<NodeId>,
18559
) -> Result<Vec<ShapedCluster>, LayoutError> {
18559
    unsafe { crate::az_mark((0x60864) as u32, (0xC0DE0864u32) as u32); } // [g123] shape_text_correctly ENTERED
18559
    let glyphs = font.shape_text(text, script, language, direction, style.as_ref())?;
18559
    unsafe { crate::az_mark((0x60868) as u32, ((glyphs.len() as u32) | 0x80000000u32) as u32); } // [g123] font.shape_text returned (high bit set); low bits = glyph count
18559
    if glyphs.is_empty() {
        return Ok(Vec::new());
18559
    }
18559
    let mut clusters = Vec::new();
    // Group glyphs by cluster ID from the shaper.
18559
    let mut current_cluster_glyphs = Vec::new();
18559
    let mut cluster_id = glyphs[0].cluster;
18559
    let mut cluster_start_byte_in_text = glyphs[0].logical_byte_index;
134267
    for glyph in glyphs {
115708
        if glyph.cluster != cluster_id {
            // Finalize previous cluster
93592
            let advance = current_cluster_glyphs
93592
                .iter()
93592
                .map(|g: &Glyph| g.advance)
93592
                .sum();
            // Safely extract cluster text - handle cases where byte indices may be out of order
            // (can happen with RTL text or complex GSUB reordering)
93592
            let (start, end) = if cluster_start_byte_in_text <= glyph.logical_byte_index {
93280
                (cluster_start_byte_in_text, glyph.logical_byte_index)
            } else {
312
                (glyph.logical_byte_index, cluster_start_byte_in_text)
            };
93592
            let cluster_text = text.get(start..end).unwrap_or("");
93592
            clusters.push(ShapedCluster {
93592
                text: cluster_text.to_string(), // Store original text for hyphenation
93592
                source_cluster_id: GraphemeClusterId {
93592
                    source_run: source_index.run_index,
93592
                    start_byte_in_run: cluster_id,
93592
                },
93592
                source_content_index: source_index,
93592
                source_node_id,
93592
                glyphs: current_cluster_glyphs
93592
                    .iter()
93592
                    .map(|g| {
                        // Calculate cluster_offset safely
93592
                        let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
93592
                            (g.logical_byte_index - cluster_start_byte_in_text) as u32
                        } else {
                            0
                        };
                        ShapedGlyph {
93592
                            kind: if g.glyph_id == 0 {
                                GlyphKind::NotDef
                            } else {
93592
                                GlyphKind::Character
                            },
93592
                            glyph_id: g.glyph_id,
93592
                            script: g.script,
93592
                            font_hash: g.font_hash,
93592
                            font_metrics: g.font_metrics.clone(),
93592
                            style: g.style.clone(),
93592
                            cluster_offset,
93592
                            advance: g.advance,
93592
                            kerning: g.kerning,
93592
                            vertical_advance: g.vertical_advance,
93592
                            vertical_offset: g.vertical_bearing,
93592
                            offset: g.offset,
                        }
93592
                    })
93592
                    .collect(),
93592
                advance,
93592
                direction,
93592
                style: style.clone(),
93592
                marker_position_outside: None,
                is_first_fragment: true,
                is_last_fragment: true,
            });
93592
            current_cluster_glyphs.clear();
93592
            cluster_id = glyph.cluster;
93592
            cluster_start_byte_in_text = glyph.logical_byte_index;
22116
        }
115708
        current_cluster_glyphs.push(glyph);
    }
    // Finalize the last cluster
18559
    if !current_cluster_glyphs.is_empty() {
18559
        let advance = current_cluster_glyphs
18559
            .iter()
18559
            .map(|g: &Glyph| g.advance)
18559
            .sum();
18559
        let cluster_text = text.get(cluster_start_byte_in_text..).unwrap_or("");
18559
        clusters.push(ShapedCluster {
18559
            text: cluster_text.to_string(), // Store original text
18559
            source_cluster_id: GraphemeClusterId {
18559
                source_run: source_index.run_index,
18559
                start_byte_in_run: cluster_id,
18559
            },
18559
            source_content_index: source_index,
18559
            source_node_id,
18559
            glyphs: current_cluster_glyphs
18559
                .iter()
22116
                .map(|g| {
                    // Calculate cluster_offset safely
22116
                    let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
22116
                        (g.logical_byte_index - cluster_start_byte_in_text) as u32
                    } else {
                        0
                    };
                    ShapedGlyph {
22116
                        kind: if g.glyph_id == 0 {
                            GlyphKind::NotDef
                        } else {
22116
                            GlyphKind::Character
                        },
22116
                        glyph_id: g.glyph_id,
22116
                        font_hash: g.font_hash,
22116
                        font_metrics: g.font_metrics.clone(),
22116
                        style: g.style.clone(),
22116
                        script: g.script,
22116
                        vertical_advance: g.vertical_advance,
22116
                        vertical_offset: g.vertical_bearing,
22116
                        cluster_offset,
22116
                        advance: g.advance,
22116
                        kerning: g.kerning,
22116
                        offset: g.offset,
                    }
22116
                })
18559
                .collect(),
18559
            advance,
18559
            direction,
18559
            style: style.clone(),
18559
            marker_position_outside: None,
            is_first_fragment: true,
            is_last_fragment: true,
        });
    }
18559
    Ok(clusters)
18559
}
/// Measures a non-text object, returning its bounds and baseline offset.
396
fn measure_inline_object(item: &InlineContent) -> Result<(Rect, f32), LayoutError> {
396
    match item {
        InlineContent::Image(img) => {
            let size = img.display_size.unwrap_or(img.intrinsic_size);
            Ok((
                Rect {
                    x: 0.0,
                    y: 0.0,
                    width: size.width,
                    height: size.height,
                },
                img.baseline_offset,
            ))
        }
396
        InlineContent::Shape(shape) => Ok({
396
            let size = shape.shape_def.get_size();
396
            (
396
                Rect {
396
                    x: 0.0,
396
                    y: 0.0,
396
                    width: size.width,
396
                    height: size.height,
396
                },
396
                shape.baseline_offset,
396
            )
396
        }),
        InlineContent::Space(space) => Ok((
            Rect {
                x: 0.0,
                y: 0.0,
                width: space.width,
                height: 0.0,
            },
            0.0,
        )),
        InlineContent::Marker { .. } => {
            // Markers are treated as text content, not measurable objects
            Err(LayoutError::InvalidText(
                "Marker is text content, not a measurable object".into(),
            ))
        }
        _ => Err(LayoutError::InvalidText("Not a measurable object".into())),
    }
396
}
// --- Stage 4 Implementation: Vertical Text ---
/// Applies orientation and vertical metrics to glyphs if the writing mode is vertical.
// +spec:block-formatting-context:227171 - vertical glyph orientation with fallback vertical metrics
// +spec:block-formatting-context:df20a5 - mixed vertical orientation dispatch (TextOrientation::Mixed)
69828
fn apply_text_orientation(
69828
    items: Arc<Vec<ShapedItem>>,
69828
    constraints: &UnifiedConstraints,
69828
) -> Result<Arc<Vec<ShapedItem>>, LayoutError> {
69828
    if !constraints.is_vertical() {
69828
        return Ok(items);
    }
    let mut oriented_items = Vec::with_capacity(items.len());
    let writing_mode = constraints.writing_mode.unwrap_or_default();
    for item in items.iter() {
        match item {
            ShapedItem::Cluster(cluster) => {
                let mut new_cluster = cluster.clone();
                let mut total_vertical_advance = 0.0;
                for glyph in &mut new_cluster.glyphs {
                    // Use the vertical metrics already computed during shaping
                    // If they're zero, use fallback values
                    if glyph.vertical_advance > 0.0 {
                        total_vertical_advance += glyph.vertical_advance;
                    } else {
                        // Fallback: use line height for vertical advance
                        let fallback_advance = cluster.style.line_height.resolve_with_metrics(cluster.style.font_size_px, &glyph.font_metrics);
                        glyph.vertical_advance = fallback_advance;
                        // Center the glyph horizontally as a fallback
                        glyph.vertical_offset = Point {
                            x: -glyph.advance / 2.0,
                            y: 0.0,
                        };
                        total_vertical_advance += fallback_advance;
                    }
                }
                // The cluster's `advance` now represents vertical advance.
                new_cluster.advance = total_vertical_advance;
                oriented_items.push(ShapedItem::Cluster(new_cluster));
            }
            // Non-text objects also need their advance axis swapped.
            ShapedItem::Object {
                source,
                bounds,
                baseline_offset,
                content,
            } => {
                let mut new_bounds = *bounds;
                std::mem::swap(&mut new_bounds.width, &mut new_bounds.height);
                oriented_items.push(ShapedItem::Object {
                    source: *source,
                    bounds: new_bounds,
                    baseline_offset: *baseline_offset,
                    content: content.clone(),
                });
            }
            _ => oriented_items.push(item.clone()),
        }
    }
    Ok(Arc::new(oriented_items))
69828
}
// --- Stage 5 & 6 Implementation: Combined Layout Pass ---
// This section replaces the previous simple line breaking and positioning logic.
/// Extracts the per-item vertical-align from a ShapedItem.
///
/// For `Object` items (inline-blocks, images), this returns the alignment stored
/// in the original `InlineContent`. For text clusters and other items, returns `None`
/// to indicate the global `constraints.vertical_align` should be used.
1082752
fn get_item_vertical_align(item: &ShapedItem) -> Option<VerticalAlign> {
1082752
    match item {
264
        ShapedItem::Object { content, .. } => match content {
            InlineContent::Image(img) => Some(img.alignment),
264
            InlineContent::Shape(shape) => Some(shape.alignment),
            _ => None,
        },
1082488
        _ => None,
    }
1082752
}
/// Approximate version of get_item_vertical_metrics for use without constraints (e.g. bounds()).
/// Uses 80/20 ascent/descent ratio as fallback for empty-glyph strut case.
2446576
pub fn get_item_vertical_metrics_approx(item: &ShapedItem) -> (f32, f32) {
    // For non-empty clusters, delegate to the font-metrics-based calculation
2446576
    if let ShapedItem::Cluster(c) = item {
2445212
        if !c.glyphs.is_empty() {
            // Reuse the glyph-based calculation (same as get_item_vertical_metrics)
2445212
            let (asc, desc) = c.glyphs
2445212
                .iter()
2562032
                .fold((0.0f32, 0.0f32), |(max_asc, max_desc), glyph| {
2562032
                    let metrics = &glyph.font_metrics;
2562032
                    if metrics.units_per_em == 0 {
                        return (max_asc, max_desc);
2562032
                    }
2562032
                    let scale = glyph.style.font_size_px / metrics.units_per_em as f32;
2562032
                    let font_ascent = metrics.ascent * scale;
2562032
                    let font_descent = (-metrics.descent * scale).max(0.0);
2562032
                    let ad = font_ascent + font_descent;
2562032
                    let resolved_lh = c.style.line_height.resolve_with_metrics(glyph.style.font_size_px, &glyph.font_metrics);
2562032
                    let half_leading = (resolved_lh - ad) / 2.0;
2562032
                    (max_asc.max(font_ascent + half_leading), max_desc.max(font_descent + half_leading))
2562032
                });
2445212
            return (asc, desc);
        }
1364
    }
    // Fallback for empty glyphs or non-cluster items
1364
    match item {
        ShapedItem::Cluster(c) => {
            let lh = c.style.line_height.resolve(c.style.font_size_px, 0.0, 0.0, 0.0, 0);
            (lh * 0.8, lh * 0.2)
        }
        ShapedItem::CombinedBlock { bounds, .. } => (bounds.height * 0.8, bounds.height * 0.2),
484
        ShapedItem::Object { bounds, .. } => (bounds.height, 0.0),
176
        ShapedItem::Tab { bounds, .. } => (bounds.height * 0.8, bounds.height * 0.2),
704
        ShapedItem::Break { .. } => (0.0, 0.0),
    }
2446576
}
/// Gets the ascent (distance from baseline to top) and descent (distance from baseline to bottom)
/// for a single item, incorporating half-leading from line-height.
// +spec:box-model:37aeb2 - inline box margins/borders/padding do not affect line box height (leading model)
// +spec:display-property:184f0d - Inline box baseline derives from first available font metrics
// +spec:display-property:238bf5 - Inline box layout bounds from own text metrics, not child boxes
// +spec:display-property:29b194 - baseline determination for inline boxes (CSS Box Alignment 3 §9.1)
// +spec:display-property:2987db - per-glyph font metrics impact inline box layout bounds (line-height: normal caveat not yet distinguished)
/// +spec:display-property:fd42a9 - line-height affects line box contribution, not inline box size
// +spec:font-metrics:506abb - A/D from font metrics with half-leading: L = line-height - (A+D), A' = A + L/2, D' = D + L/2
// +spec:font-metrics:773029 - ascent/descent font metrics used for baseline calculations (visual centering depends on these)
// +spec:font-metrics:f42870 - half-leading model: leading = line-height - (ascent + descent), distributed equally above/below
// +spec:writing-modes:531c2e - UAs should use vertical baseline tables in vertical typographic modes
720676
pub fn get_item_vertical_metrics(item: &ShapedItem, constraints: &UnifiedConstraints) -> (f32, f32) {
    // (ascent, descent)
720676
    match item {
719620
        ShapedItem::Cluster(c) => {
719620
            if c.glyphs.is_empty() {
                // +spec:display-property:626c86 - strut for inline box with no glyphs uses first available font metrics
                // +spec:line-height:0078fa - strut: zero-width inline box with element's font/line-height
                // §10.8.1 strut: if inline box contains no glyphs, it is considered to
                // contain a strut with A and D of the element's first available font.
                // Half-leading: L = line-height - (A + D), A' = A + L/2, D' = D + L/2
                let ad = constraints.strut_ascent + constraints.strut_descent;
                let resolved_lh = c.style.line_height.resolve(c.style.font_size_px, 0.0, 0.0, 0.0, 0);
                let half_leading = (resolved_lh - ad) / 2.0;
                return (constraints.strut_ascent + half_leading, constraints.strut_descent + half_leading);
719620
            }
            // +spec:box-model:0b3e1f - inline non-replaced box height uses only line-height, not vertical padding/border/margin
            // +spec:display-property:80b900 - fallback glyphs affect line box size via per-glyph metrics
            // +spec:display-property:d52f26 - layout bounds enclose all glyphs from highest A to deepest D
            // +spec:font-metrics:387751 - content area uses max ascenders/descenders across all fonts
            // +spec:font-metrics:790fd2 - half-leading: L = line-height - (A+D), A' = A + L/2, D' = D + L/2
            // +spec:line-height:1ae6f5 - line-height on non-replaced inline: half-leading model
            // +spec:line-height:0078fa - half-leading: L = line-height - (A+D), distributed equally above/below
            // +spec:line-height:32b3da - half-leading: L = line-height - AD, A' = A + L/2, D' = D + L/2
            // §10.8.1: for each glyph determine A, D from font metrics,
            // then L = line-height - (A + D), and adjust: A' = A + L/2, D' = D + L/2.
            // Note: L may be negative.
            // +spec:height-calculation:eb98b5 - multi-font normal line-height uses max across glyph metrics
719620
            c.glyphs
719620
                .iter()
745932
                .fold((0.0f32, 0.0f32), |(max_asc, max_desc), glyph| {
745932
                    let metrics = &glyph.font_metrics;
745932
                    if metrics.units_per_em == 0 {
                        return (max_asc, max_desc);
745932
                    }
745932
                    let scale = glyph.style.font_size_px / metrics.units_per_em as f32;
745932
                    let a = metrics.ascent * scale;
                    // Descent in OpenType is typically negative, so we negate it to get a positive
                    // distance.
745932
                    let d = (-metrics.descent * scale).max(0.0);
745932
                    let ad = a + d;
745932
                    let resolved_lh = glyph.style.line_height.resolve_with_metrics(glyph.style.font_size_px, &glyph.font_metrics);
745932
                    let leading = resolved_lh - ad;
745932
                    let half_leading = leading / 2.0;
745932
                    let item_asc = a + half_leading;
745932
                    let item_desc = d + half_leading;
745932
                    (max_asc.max(item_asc), max_desc.max(item_desc))
745932
                })
        }
        ShapedItem::Object {
176
            bounds,
176
            baseline_offset,
            ..
        } => {
            // Per analysis, `baseline_offset` is the distance from the bottom.
            // bounds.height already includes margins (set from margin_box_height in fc.rs)
176
            let ascent = bounds.height - *baseline_offset;
176
            let descent = *baseline_offset;
176
            (ascent.max(0.0), descent.max(0.0))
        }
        ShapedItem::CombinedBlock {
            bounds,
            baseline_offset,
            ..
        } => {
            // CORRECTED: Treat baseline_offset consistently as distance from the bottom (descent).
            let ascent = bounds.height - *baseline_offset;
            let descent = *baseline_offset;
            (ascent.max(0.0), descent.max(0.0))
        }
880
        _ => (0.0, 0.0), // Breaks and other non-visible items don't affect line height.
    }
720676
}
// +spec:block-formatting-context:861155 - vertical-align affects vertical positioning inside line box for inline-level elements
/// Calculates the maximum ascent and descent for an entire line of items.
/// This determines the "line box" used for vertical alignment.
/// // +spec:display-contents:66d910 - line box height fitted to contents, controlled by line-height
// +spec:inline-formatting-context:c3fc54 - line box tall enough for all boxes, vertical-align determines alignment within line box
///
/// Per CSS 2.2 §10.8: Inline-level boxes aligned 'top' or 'bottom' must be aligned
/// so as to minimize the line box height. The algorithm is:
/// 1. First pass: compute line box height from baseline-aligned items only
///    (baseline, sub, super, middle, text-top, text-bottom, offset).
/// 2. Second pass: check if any top/bottom-aligned items are taller than the
///    line box from pass 1, and expand if necessary.
// +spec:box-model:c9bcd7 - when line-fit-edge is not leading, layout bounds inflated by margin+border+padding (not yet implemented; default leading behavior is correct)
85228
fn calculate_line_metrics(
85228
    items: &[ShapedItem],
85228
    default_vertical_align: VerticalAlign,
85228
    constraints: &UnifiedConstraints,
85228
) -> (f32, f32) {
    // +spec:font-metrics:95152b - baseline alignment: items with different font sizes aligned by matching alphabetic baselines
    // Pass 1: Compute ascent/descent from baseline-aligned items only
    // (i.e., items that are NOT vertical-align: top or bottom).
85228
    let (mut max_asc, mut max_desc) = items
85228
        .iter()
362076
        .fold((0.0f32, 0.0f32), |(max_asc, max_desc), item| {
362076
            let effective_align = get_item_vertical_align(item)
362076
                .unwrap_or(default_vertical_align);
362076
            match effective_align {
                VerticalAlign::Top | VerticalAlign::Bottom => {
                    // Skip top/bottom items in first pass
                    (max_asc, max_desc)
                }
                _ => {
362076
                    let (item_asc, item_desc) = get_item_vertical_metrics(item, constraints);
362076
                    (max_asc.max(item_asc), max_desc.max(item_desc))
                }
            }
362076
        });
85228
    let baseline_line_height = max_asc + max_desc;
    // Pass 2: Check top/bottom aligned items. If any of them is taller
    // than the current line box, expand the line box to fit.
447304
    for item in items {
362076
        let effective_align = get_item_vertical_align(item)
362076
            .unwrap_or(default_vertical_align);
362076
        match effective_align {
            VerticalAlign::Top | VerticalAlign::Bottom => {
                let (item_asc, item_desc) = get_item_vertical_metrics(item, constraints);
                let item_height = item_asc + item_desc;
                if item_height > baseline_line_height {
                    // To minimize height, expand in the direction the item is aligned to
                    if effective_align == VerticalAlign::Top {
                        // Top-aligned item extends downward from line top
                        max_desc = max_desc.max(item_height - max_asc);
                    } else {
                        // Bottom-aligned item extends upward from line bottom
                        max_asc = max_asc.max(item_height - max_desc);
                    }
                }
            }
362076
            _ => {} // Already handled in first pass
        }
    }
85228
    (max_asc, max_desc)
85228
}
/// Performs layout for a single fragment, consuming items from a `BreakCursor`.
///
/// This function contains the core line-breaking and positioning logic, but is
/// designed to operate on a portion of a larger content stream and within the
/// constraints of a single geometric area (a fragment).
///
/// The loop terminates when either the fragment is filled (e.g., runs out of
/// vertical space) or the content stream managed by the `cursor` is exhausted.
///
/// # CSS Inline Layout Module Level 3 Implementation
///
/// This function implements the inline formatting context as described in:
/// https://www.w3.org/TR/css-inline-3/#inline-formatting-context
///
/// ## § 2.1 Layout of Line Boxes
/// "In general, the line-left edge of a line box touches the line-left edge of its
/// containing block and the line-right edge touches the line-right edge of its
/// containing block, and thus the logical width of a line box is equal to the inner
/// logical width of its containing block."
///
/// [ISSUE] available_width should be set to the containing block's inner width,
/// but is currently defaulting to 0.0 in UnifiedConstraints::default().
/// This causes premature line breaking.
///
/// ## § 2.2 Layout Within Line Boxes
/// The layout process follows these steps:
/// 1. Baseline Alignment: All inline-level boxes are aligned by their baselines
/// 2. Content Size Contribution: Calculate layout bounds for each box
/// 3. Line Box Sizing: Size line box to fit aligned layout bounds
/// 4. Content Positioning: Position boxes within the line box
///
/// ## Missing Features:
/// - § 3 Baselines and Alignment Metrics: Only basic baseline alignment implemented
/// - § 4 Baseline Alignment: vertical-align property not fully supported
/// - § 5 Line Spacing: line-height implemented, but line-fit-edge missing
/// - § 6 Trimming Leading: text-box-trim not implemented
46449
pub fn perform_fragment_layout<T: ParsedFontTrait>(
46449
    cursor: &mut BreakCursor,
46449
    logical_items: &[LogicalItem],
46449
    fragment_constraints: &UnifiedConstraints,
46449
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
46449
    fonts: &LoadedFonts<T>,
46449
) -> Result<UnifiedLayout, LayoutError> {
46449
    if let Some(msgs) = debug_messages {
45877
        msgs.push(LayoutDebugMessage::info(
45877
            "\n--- Entering perform_fragment_layout ---".to_string(),
45877
        ));
45877
        msgs.push(LayoutDebugMessage::info(format!(
45877
            "Constraints: available_width={:?}, available_height={:?}, columns={}, text_wrap={:?}",
45877
            fragment_constraints.available_width,
45877
            fragment_constraints.available_height,
45877
            fragment_constraints.columns,
45877
            fragment_constraints.text_wrap
45877
        )));
45877
    }
    // For TextWrap::Balance, use Knuth-Plass algorithm for optimal line breaking
    // This produces more visually balanced lines at the cost of more computation
46449
    if fragment_constraints.text_wrap == TextWrap::Balance {
        if let Some(msgs) = debug_messages {
            msgs.push(LayoutDebugMessage::info(
                "Using Knuth-Plass algorithm for text-wrap: balance".to_string(),
            ));
        }
        // Get the shaped items from the cursor
        let shaped_items: Vec<ShapedItem> = cursor.drain_remaining();
        // +spec:line-breaking:90c1bd - only auto-hyphenate when language is known and hyphenation resource available
        let hyphenator = if fragment_constraints.hyphenation == Hyphens::Auto {
            fragment_constraints
                .hyphenation_language
                .and_then(|lang| get_hyphenator(lang).ok())
        } else {
            None
        };
        // Use the Knuth-Plass algorithm for optimal line breaking
        return crate::text3::knuth_plass::kp_layout(
            &shaped_items,
            logical_items,
            fragment_constraints,
            hyphenator.as_ref(),
            fonts,
        );
46449
    }
    // +spec:intrinsic-sizing:57e02d - hyphenation opportunities considered in min-content sizing
46449
    let hyphenator = if fragment_constraints.hyphenation == Hyphens::Auto {
        fragment_constraints
            .hyphenation_language
            .and_then(|lang| get_hyphenator(lang).ok())
    } else {
46449
        None
    };
46449
    let mut positioned_items = Vec::new();
46449
    let mut layout_bounds = Rect::default();
46449
    let num_columns = fragment_constraints.columns.max(1);
46449
    let total_column_gap = fragment_constraints.column_gap * (num_columns - 1) as f32;
    // CSS Inline Layout § 2.1: "the logical width of a line box is equal to the inner
    // logical width of its containing block"
    //
    // Handle the different available space modes:
    // - Definite(width): Use the specified width for column calculation
    // - MinContent: Force line breaks at word boundaries, return widest word width
    // - MaxContent: Use a large value to allow content to expand naturally
    //
    // IMPORTANT: For MinContent, we do NOT use 0.0 (which would break after every character).
    // Instead, we use a large width but track the is_min_content flag to force word-level
    // line breaks in the line breaker. The actual min-content width is the width of the
    // widest resulting line (typically the widest word).
46449
    let is_min_content = matches!(fragment_constraints.available_width, AvailableSpace::MinContent);
46449
    let is_max_content = matches!(fragment_constraints.available_width, AvailableSpace::MaxContent);
46449
    let column_width = match fragment_constraints.available_width {
26883
        AvailableSpace::Definite(width) => (width - total_column_gap) / num_columns as f32,
        AvailableSpace::MinContent | AvailableSpace::MaxContent => {
            // For intrinsic sizing, use a large width to measure actual content width.
            // The line breaker will handle MinContent specially by breaking after each word.
19566
            f32::MAX / 2.0
        }
    };
46449
    let mut current_column = 0;
46449
    if let Some(msgs) = debug_messages {
45877
        msgs.push(LayoutDebugMessage::info(format!(
45877
            "Column width calculated: {}",
45877
            column_width
45877
        )));
45877
    }
    // Use the CSS direction from constraints instead of auto-detecting from text
    // This ensures that mixed-direction text (e.g., "مرحبا - Hello") uses the
    // correct paragraph-level direction for alignment purposes.
    // With unicode-bidi: plaintext, direction is auto-detected from text content
    // per CSS Writing Modes §8.3.
46449
    let base_direction = if fragment_constraints.unicode_bidi == UnicodeBidi::Plaintext {
        // Auto-detect from remaining shaped items' text content
        let remaining = &cursor.items[cursor.next_item_index..];
        let text: String = remaining.iter()
            .filter_map(|i| i.as_cluster())
            .map(|c| c.text.as_str())
            .collect();
        match unicode_bidi::get_base_direction(text.as_str()) {
            unicode_bidi::Direction::Ltr => BidiDirection::Ltr,
            unicode_bidi::Direction::Rtl => BidiDirection::Rtl,
            // No strong character: fall back to containing block direction
            unicode_bidi::Direction::Mixed => fragment_constraints.direction.unwrap_or(BidiDirection::Ltr),
        }
    } else {
46449
        fragment_constraints.direction.unwrap_or(BidiDirection::Ltr)
    };
46449
    if let Some(msgs) = debug_messages {
45877
        msgs.push(LayoutDebugMessage::info(format!(
45877
            "[PFLayout] Base direction: {:?} (from CSS), Text align: {:?}",
45877
            base_direction, fragment_constraints.text_align
45877
        )));
45877
    }
92898
    'column_loop: while current_column < num_columns {
46449
        if let Some(msgs) = debug_messages {
45877
            msgs.push(LayoutDebugMessage::info(format!(
45877
                "\n-- Starting Column {} --",
45877
                current_column
45877
            )));
45877
        }
46449
        let column_start_x =
46449
            (column_width + fragment_constraints.column_gap) * current_column as f32;
46449
        let mut line_top_y = 0.0;
46449
        let mut line_index = 0;
46449
        let mut empty_segment_count = 0; // Failsafe counter for infinite loops
46449
        let mut is_after_forced_break = false;
        const MAX_EMPTY_SEGMENTS: usize = 1000; // Maximum allowed consecutive empty segments
        // [g147 az-web-lift] Hard total-iteration cap on the line-build loop. On the remill lift,
        // `cursor.is_done()` (or the empty-segment failsafe) mis-lifts for the NESTED IFC (content.len
        // reads 0 → the cursor is starved but never reports done) → this `while !cursor.is_done()` spins
        // forever → solveLayoutReal HANGS inside perform_fragment_layout. Cap total iterations so the loop
        // always converges (the harness can then read the markers). native is unaffected (far above real
        // line counts). The 0x60BC4 marker exposes the iteration count.
46449
        let mut _az_line_iters: usize = 0;
128538
        while !cursor.is_done() {
            #[cfg(feature = "web_lift")]
            {
                _az_line_iters += 1;
                unsafe { crate::az_mark((0x60BC4) as u32, (_az_line_iters as u32 | 0xC0DE0000) as u32); }
                if _az_line_iters > 4096 {
                    break;
                }
            }
86633
            if let Some(max_height) = fragment_constraints.available_height {
86633
                if line_top_y >= max_height {
4540
                    if let Some(msgs) = debug_messages {
4540
                        msgs.push(LayoutDebugMessage::info(format!(
4540
                            "  Column full (pen {} >= height {}), breaking to next column.",
4540
                            line_top_y, max_height
4540
                        )));
4540
                    }
4540
                    break;
82093
                }
            }
82093
            if let Some(clamp) = fragment_constraints.line_clamp {
                if line_index >= clamp.get() {
                    break;
                }
82093
            }
            // Create constraints specific to the current column for the line breaker.
82093
            let mut column_constraints = fragment_constraints.clone();
            // For MinContent/MaxContent, preserve the semantic type so the line breaker
            // can handle word-level breaking correctly. Only use Definite for actual widths.
82093
            if is_min_content {
7447
                column_constraints.available_width = AvailableSpace::MinContent;
74646
            } else if is_max_content {
7623
                column_constraints.available_width = AvailableSpace::MaxContent;
67023
            } else {
67023
                column_constraints.available_width = AvailableSpace::Definite(column_width);
67023
            }
82093
            let line_constraints = get_line_constraints(
82093
                line_top_y,
82093
                fragment_constraints.resolved_line_height(),
82093
                &column_constraints,
82093
                debug_messages,
            );
82093
            if line_constraints.segments.is_empty() {
                empty_segment_count += 1;
                if let Some(msgs) = debug_messages {
                    msgs.push(LayoutDebugMessage::info(format!(
                        "  No available segments at y={}, skipping to next line. (empty count: \
                         {}/{})",
                        line_top_y, empty_segment_count, MAX_EMPTY_SEGMENTS
                    )));
                }
                // Failsafe: If we've skipped too many lines without content, break out
                if empty_segment_count >= MAX_EMPTY_SEGMENTS {
                    if let Some(msgs) = debug_messages {
                        msgs.push(LayoutDebugMessage::warning(format!(
                            "  [WARN] Reached maximum empty segment count ({}). Breaking to \
                             prevent infinite loop.",
                            MAX_EMPTY_SEGMENTS
                        )));
                        msgs.push(LayoutDebugMessage::warning(
                            "  This likely means the shape constraints are too restrictive or \
                             positioned incorrectly."
                                .to_string(),
                        ));
                        msgs.push(LayoutDebugMessage::warning(format!(
                            "  Current y={}, shape boundaries might be outside this range.",
                            line_top_y
                        )));
                    }
                    break;
                }
                // Additional check: If we have shapes and are far beyond the expected height,
                // also break to avoid infinite loops
                if !fragment_constraints.shape_boundaries.is_empty() && empty_segment_count > 50 {
                    // Calculate maximum shape height
                    let max_shape_y: f32 = fragment_constraints
                        .shape_boundaries
                        .iter()
                        .map(|shape| {
                            match shape {
                                ShapeBoundary::Circle { center, radius } => center.y + radius,
                                ShapeBoundary::Ellipse { center, radii } => center.y + radii.height,
                                ShapeBoundary::Polygon { points } => {
                                    points.iter().map(|p| p.y).fold(0.0, f32::max)
                                }
                                ShapeBoundary::Rectangle(rect) => rect.y + rect.height,
                                ShapeBoundary::Path { .. } => f32::MAX, // Can't determine for path
                            }
                        })
                        .fold(0.0, f32::max);
                    if line_top_y > max_shape_y + 100.0 {
                        if let Some(msgs) = debug_messages {
                            msgs.push(LayoutDebugMessage::info(format!(
                                "  [INFO] Current y={} is far beyond maximum shape extent y={}. \
                                 Breaking layout.",
                                line_top_y, max_shape_y
                            )));
                            msgs.push(LayoutDebugMessage::info(
                                "  Shape boundaries exist but no segments available - text cannot \
                                 fit in shape."
                                    .to_string(),
                            ));
                        }
                        break;
                    }
                }
                line_top_y += fragment_constraints.resolved_line_height();
                continue;
82093
            }
            // Reset counter when we find valid segments
82093
            empty_segment_count = 0;
            // +spec:line-breaking:3bb032 - break-word not considered for min-content intrinsic sizes
            // +spec:overflow:b932c4 - overflow-wrap/word-wrap (normal/break-word/anywhere) and hyphens interaction
            // `anywhere` introduces soft wrap opportunities (min-content = widest cluster),
            // but `break-word` does NOT (min-content = widest unbreakable word).
82093
            let effective_overflow_wrap = if is_min_content && fragment_constraints.overflow_wrap == OverflowWrap::Anywhere {
                OverflowWrap::Anywhere
82093
            } else if is_min_content && fragment_constraints.overflow_wrap == OverflowWrap::BreakWord {
                OverflowWrap::Normal
            } else {
82093
                fragment_constraints.overflow_wrap
            };
            // CSS Text Module Level 3 § 5 Line Breaking and Word Boundaries
            // https://www.w3.org/TR/css-text-3/#line-breaking
            // +spec:display-property:2608cc - inline box splitting across line boxes, overflow for unsplittable boxes
            // +spec:display-property:ea615c - inline boxes split and distributed across line boxes
            // "When an inline box exceeds the logical width of a line box, it is split
            // into several fragments, which are partitioned across multiple line boxes."
82093
            let (mut line_items, was_hyphenated) =
82093
                break_one_line(cursor, &line_constraints, false, hyphenator.as_ref(), fonts, fragment_constraints.line_break, fragment_constraints.white_space_mode, effective_overflow_wrap);
82093
            if line_items.is_empty() {
4
                if let Some(msgs) = debug_messages {
4
                    msgs.push(LayoutDebugMessage::info(
4
                        "  Break returned no items. Ending column.".to_string(),
4
                    ));
4
                }
4
                break;
82089
            }
82089
            let line_text_before_rev: String = line_items
82089
                .iter()
333997
                .filter_map(|i| i.as_cluster())
333899
                .map(|c| c.text.as_str())
82089
                .collect();
82089
            if let Some(msgs) = debug_messages {
81385
                msgs.push(LayoutDebugMessage::info(format!(
81385
                    // FIX: The log message was misleading. Items are in visual order.
81385
                    "[PFLayout] Line items from breaker (visual order): [{}]",
81385
                    line_text_before_rev
81385
                )));
81385
            }
            // +spec:line-breaking:c59944 - forced line breaks detected for bidi-aware alignment
333997
            let line_ends_with_forced_break = line_items.iter().any(|item| matches!(item, ShapedItem::Break { .. }));
            // uses text-align-last (last line of block, or line right before forced break)
82089
            let is_last_line = cursor.is_done() && !was_hyphenated;
82089
            let effective_align = resolve_effective_alignment(
82089
                fragment_constraints.text_align,
82089
                fragment_constraints.text_align_last,
82089
                is_last_line || line_ends_with_forced_break,
            );
82089
            let (mut line_pos_items, line_height) = position_one_line(
82089
                line_items,
82089
                &line_constraints,
82089
                line_top_y,
82089
                line_index,
82089
                effective_align,
82089
                base_direction,
82089
                is_last_line,
82089
                fragment_constraints,
82089
                debug_messages,
82089
                fonts,
82089
                is_after_forced_break,
82089
            );
            // Track whether the next line follows a forced break
82089
            is_after_forced_break = line_ends_with_forced_break;
414416
            for item in &mut line_pos_items {
332327
                item.position.x += column_start_x;
332327
            }
            // +spec:display-property:6c4978 - line-height on block container establishes minimum line box height
82089
            line_top_y += line_height.max(fragment_constraints.resolved_line_height());
82089
            line_index += 1;
82089
            positioned_items.extend(line_pos_items);
        }
46449
        current_column += 1;
    }
46449
    if let Some(msgs) = debug_messages {
45877
        msgs.push(LayoutDebugMessage::info(format!(
45877
            "--- Exiting perform_fragment_layout, positioned {} items ---",
45877
            positioned_items.len()
45877
        )));
45877
    }
46449
    let layout = UnifiedLayout {
46449
        items: positioned_items,
46449
        overflow: OverflowInfo::default(),
46449
    };
    // Calculate bounds on demand via the bounds() method
46449
    let calculated_bounds = layout.bounds();
46449
    if let Some(msgs) = debug_messages {
45877
        msgs.push(LayoutDebugMessage::info(format!(
45877
            "--- Calculated bounds: width={}, height={} ---",
45877
            calculated_bounds.width, calculated_bounds.height
45877
        )));
45877
    }
46449
    Ok(layout)
46449
}
/// Breaks a single line of items to fit within the given geometric constraints,
/// handling multi-segment lines and hyphenation.
/// Break a single line from the current cursor position.
///
/// # CSS Text Module Level 3 \u00a7 5 Line Breaking and Word Boundaries
/// https://www.w3.org/TR/css-text-3/#line-breaking
///
/// Implements the line breaking algorithm:
/// 1. "When an inline box exceeds the logical width of a line box, it is split into several
///    fragments, which are partitioned across multiple line boxes."
///
/// ## \u2705 Implemented Features:
/// - **Break Opportunities**: Identifies word boundaries and break points
/// - **Soft Wraps**: Wraps at spaces between words
/// - **Hard Breaks**: Handles explicit line breaks (\\n)
/// - **Overflow**: If a word is too long, places it anyway to avoid infinite loop
/// - **Hyphenation**: Tries to break long words at hyphenation points (\u00a7 5.4)
///
/// ## \u26a0\ufe0f Known Issues:
/// - If `line_constraints.total_available` is 0.0 (from `available_width: 0.0` bug), every word
///   will overflow, causing single-word lines
/// - This is the symptom visible in the PDF: "List items break extremely early"
///
/// ## \u00a7 5.2 Breaking Rules for Letters
/// \u2705 IMPLEMENTED: Uses Unicode line breaking algorithm
/// - Relies on UAX #14 for break opportunities
/// - Respects non-breaking spaces and zero-width joiners
///
/// ## \u00a7 5.3 Breaking Rules for Punctuation
/// \u26a0\ufe0f PARTIAL: Basic punctuation handling
/// - \u274c TODO: hanging-punctuation is declared in UnifiedConstraints but not used here
/// - \u274c TODO: Should implement punctuation trimming at line edges
/// // +spec:intrinsic-sizing:6085cf - hanging glyphs must be excluded from intrinsic size computation
///
/// ## \u00a7 5.4 Hyphenation
/// \u2705 IMPLEMENTED: Automatic hyphenation with hyphenator library
/// - Tries to hyphenate words that overflow
/// - Inserts hyphen glyph at break point
/// - Carries remainder to next line
///
/// ## \u00a7 5.5 Overflow Wrapping
/// \u2705 IMPLEMENTED: Emergency breaking
/// - If line is empty and word doesn't fit, forces at least one item
/// - Prevents infinite loop
/// - This is "overflow-wrap: break-word" behavior
///
/// # Missing Features:
/// - word-break property (normal, break-all, keep-all) - IMPLEMENTED via BreakCursor.word_break
/// - \u26a0\ufe0f line-break property: anywhere implemented; loose/normal/strict CJK strictness
///   filtering added via `is_cjk_break_allowed_by_strictness` (§5.3)
/// - \u274c overflow-wrap: anywhere vs break-word distinction
/// - \u2705 white-space: break-spaces handling
// around every typographic character unit including preserved white spaces; with break-spaces
// it allows breaking before the first space of a sequence
// +spec:line-breaking:722f3b - wrapping only at soft wrap opportunities, minimizing overflow
82093
pub fn break_one_line<T: ParsedFontTrait>(
82093
    cursor: &mut BreakCursor,
82093
    line_constraints: &LineConstraints,
82093
    is_vertical: bool,
82093
    hyphenator: Option<&Standard>,
82093
    fonts: &LoadedFonts<T>,
82093
    line_break: LineBreakStrictness,
82093
    white_space_mode: WhiteSpaceMode,
82093
    overflow_wrap: OverflowWrap,
82093
) -> (Vec<ShapedItem>, bool) {
82093
    let mut line_items = Vec::new();
82093
    let mut current_width = 0.0;
82093
    if cursor.is_done() {
        return (Vec::new(), false);
82093
    }
    // +spec:white-space-processing:c83dbd - Phase II: collapsible spaces at line start removed, trailing spaces removed, tab stops
    // CSS Text Module Level 3 § 4.1.2: At the beginning of a line, white space
    // is collapsed away. Skip leading whitespace at line start.
    // https://www.w3.org/TR/css-text-3/#white-space-phase-2
82093
    let break_spaces = white_space_mode == WhiteSpaceMode::BreakSpaces;
82093
    if !break_spaces {
88275
        while !cursor.is_done() {
88271
            let next_unit = cursor.peek_next_unit();
88271
            if next_unit.is_empty() {
                break;
88271
            }
88271
            if next_unit.len() == 1 && is_collapsible_whitespace(&next_unit[0]) {
6182
                cursor.consume(1);
6182
            } else {
82089
                break;
            }
        }
    }
    // +spec:line-breaking:35817b - white-space: nowrap/pre prevent soft wrap opportunities
    // CSS Text Level 3 § 3: For nowrap and pre, wrapping is suppressed. All content
    // stays on a single line, overflowing if necessary.
82093
    let no_wrap = matches!(white_space_mode, WhiteSpaceMode::Nowrap | WhiteSpaceMode::Pre);
82093
    if no_wrap {
        // No soft wrapping — consume everything onto one line.
        // Only explicit <br>/newline breaks are honored.
        loop {
1196
            let next_unit = cursor.peek_next_unit();
1196
            if next_unit.is_empty() {
51
                break;
1145
            }
1145
            if let Some(ShapedItem::Break { .. }) = next_unit.first() {
4
                line_items.push(next_unit[0].clone());
4
                cursor.consume(1);
4
                return (line_items, false);
1141
            }
1141
            line_items.extend_from_slice(&next_unit);
1141
            cursor.consume(next_unit.len());
        }
    } else {
    loop {
        // typographic character unit as a soft wrap opportunity; hyphenation is not applied
177940
        let next_unit = if line_break == LineBreakStrictness::Anywhere {
            cursor.peek_next_single_item()
        } else {
177940
            cursor.peek_next_unit()
        };
177940
        if next_unit.is_empty() {
35697
            break; // End of content
142243
        }
142243
        if let Some(ShapedItem::Break { .. }) = next_unit.first() {
4
            line_items.push(next_unit[0].clone());
4
            cursor.consume(1);
4
            return (line_items, false);
142239
        }
142239
        let unit_width: f32 = next_unit
142239
            .iter()
444028
            .map(|item| get_item_measure(item, is_vertical))
142239
            .sum();
142239
        let available_width = line_constraints.total_available - current_width;
        // 2. Can the whole unit fit on the current line?
142239
        if unit_width <= available_width {
95902
            line_items.extend_from_slice(&next_unit);
95902
            current_width += unit_width;
95902
            cursor.consume(next_unit.len());
95902
        } else {
            // 3. The unit overflows. Can we hyphenate it?
46337
            if line_break != LineBreakStrictness::Anywhere {
46337
                if let Some(hyphenator) = hyphenator {
                    if !is_break_opportunity(next_unit.last().unwrap()) {
                        if let Some(hyphenation_result) = try_hyphenate_word_cluster(
                            &next_unit,
                            available_width,
                            is_vertical,
                            hyphenator,
                            fonts,
                        ) {
                            line_items.extend(hyphenation_result.line_part);
                            cursor.consume(next_unit.len());
                            cursor.partial_remainder = hyphenation_result.remainder_part;
                            return (line_items, true);
                        }
                    }
46337
                }
            }
            // an otherwise unbreakable sequence at an arbitrary point when no other
            // break points exist. Grapheme clusters stay together; no hyphen inserted.
            // 4. Cannot hyphenate or fit. The line is finished.
            // If the line is empty, we must force at least one item to avoid an infinite loop.
            // With overflow-wrap: anywhere or break-word, we break the unbreakable
            // unit at an arbitrary cluster boundary. With normal, we only force one
            // item to prevent infinite loops (content will overflow).
46337
            if line_items.is_empty() {
43825
                match overflow_wrap {
                    OverflowWrap::Anywhere | OverflowWrap::BreakWord => {
                        // Emergency break: fit as many clusters as possible on
                        // this line.  Grapheme clusters stay together.
                        //
                        // Per CSS Text 3 §5.5: "an otherwise unbreakable sequence
                        // of characters may be broken at an arbitrary point" when
                        // overflow-wrap is anywhere/break-word.
132
                        let avail = line_constraints.total_available;
1716
                        for item in next_unit.iter() {
1716
                            let item_w = get_item_measure(item, is_vertical);
                            // Break BEFORE this item if adding it would overflow,
                            // but only if we already have at least one item on the
                            // line (must always make progress).
1716
                            if !line_items.is_empty() && avail > 0.0 && current_width + item_w > avail {
132
                                break;
1584
                            }
1584
                            line_items.push(item.clone());
1584
                            current_width += item_w;
                            // If container is zero-width (avail <= 0), place all
                            // items on one line — there's nowhere to break TO,
                            // content just overflows.  This matches browser
                            // behavior for `width: 0` containers.
1584
                            if avail <= 0.0 {
                                continue; // Keep adding — can't break into nothing
1584
                            }
                        }
132
                        let consumed = line_items.len().max(1);
132
                        if line_items.is_empty() {
                            line_items.push(next_unit[0].clone());
132
                        }
132
                        cursor.consume(consumed);
                    }
43693
                    OverflowWrap::Normal => {
43693
                        // No emergency breaking — just force one item to prevent infinite loop
43693
                        line_items.push(next_unit[0].clone());
43693
                        cursor.consume(1);
43693
                    }
                }
2512
            }
46337
            break;
        }
    }
    } // end !no_wrap
    // +spec:white-space-processing:fef250 - Phase II: trailing collapsible spaces and U+1680 removed at line end
    // as well as any trailing U+1680 OGHAM SPACE MARK whose white-space is normal/nowrap/pre-line.
    // Note: pre-wrap and break-spaces have different handling (hanging/preserving)
    // which is not yet implemented here.
84552
    while let Some(last) = line_items.last() {
84548
        if is_collapsible_whitespace(last) {
2467
            line_items.pop();
2467
        } else {
82081
            break;
        }
    }
82085
    (line_items, false)
82093
}
/// Represents a single valid hyphenation point within a word.
#[derive(Clone)]
pub struct HyphenationBreak {
    /// The number of characters from the original word string included on the line.
    pub char_len_on_line: usize,
    /// The total advance width of the line part + the hyphen.
    pub width_on_line: f32,
    /// The cluster(s) that will remain on the current line.
    pub line_part: Vec<ShapedItem>,
    /// The cluster that represents the hyphen character itself.
    pub hyphen_item: ShapedItem,
    /// The cluster(s) that will be carried over to the next line.
    /// CRITICAL FIX: Changed from ShapedItem to Vec<ShapedItem>
    pub remainder_part: Vec<ShapedItem>,
}
/// A "word" is defined as a sequence of one or more adjacent ShapedClusters.
pub fn find_all_hyphenation_breaks<T: ParsedFontTrait>(
    word_clusters: &[ShapedCluster],
    hyphenator: &Standard,
    is_vertical: bool, // Pass this in to use correct metrics
    fonts: &LoadedFonts<T>,
) -> Option<Vec<HyphenationBreak>> {
    if word_clusters.is_empty() {
        return None;
    }
    // --- 1. Concatenate the TRUE text and build a robust map ---
    let mut word_string = String::new();
    let mut char_map = Vec::new();
    let mut current_width = 0.0;
    for (cluster_idx, cluster) in word_clusters.iter().enumerate() {
        for (char_byte_offset, _ch) in cluster.text.char_indices() {
            let glyph_idx = cluster
                .glyphs
                .iter()
                .rposition(|g| g.cluster_offset as usize <= char_byte_offset)
                .unwrap_or(0);
            let glyph = &cluster.glyphs[glyph_idx];
            let num_chars_in_glyph = cluster.text[glyph.cluster_offset as usize..]
                .chars()
                .count();
            let advance_per_char = if is_vertical {
                glyph.vertical_advance
            } else {
                glyph.advance
            } / (num_chars_in_glyph as f32).max(1.0);
            current_width += advance_per_char;
            char_map.push((cluster_idx, glyph_idx, current_width));
        }
        word_string.push_str(&cluster.text);
    }
    // +spec:line-breaking:d7ed93 - language-specific hyphenation rules apply to both auto and explicit (soft hyphen) opportunities
    // --- 2. Get hyphenation opportunities ---
    let opportunities = hyphenator.hyphenate(&word_string);
    if opportunities.breaks.is_empty() {
        return None;
    }
    let last_cluster = word_clusters.last().unwrap();
    let last_glyph = last_cluster.glyphs.last().unwrap();
    let style = last_cluster.style.clone();
    // Look up font from hash
    let font = fonts.get_by_hash(last_glyph.font_hash)?;
    let (hyphen_glyph_id, hyphen_advance) =
        font.get_hyphen_glyph_and_advance(style.font_size_px)?;
    let mut possible_breaks = Vec::new();
    // --- 3. Generate a HyphenationBreak for each valid opportunity ---
    for &break_char_idx in &opportunities.breaks {
        // The break is *before* the character at this index.
        // So the last character on the line is at `break_char_idx - 1`.
        if break_char_idx == 0 || break_char_idx > char_map.len() {
            continue;
        }
        let (_, _, width_at_break) = char_map[break_char_idx - 1];
        // The line part is all clusters *before* the break index.
        let line_part: Vec<ShapedItem> = word_clusters[..break_char_idx]
            .iter()
            .map(|c| ShapedItem::Cluster(c.clone()))
            .collect();
        // The remainder is all clusters *from* the break index onward.
        let remainder_part: Vec<ShapedItem> = word_clusters[break_char_idx..]
            .iter()
            .map(|c| ShapedItem::Cluster(c.clone()))
            .collect();
        let hyphen_item = ShapedItem::Cluster(ShapedCluster {
            text: "-".to_string(),
            source_cluster_id: GraphemeClusterId {
                source_run: u32::MAX,
                start_byte_in_run: u32::MAX,
            },
            source_content_index: ContentIndex {
                run_index: u32::MAX,
                item_index: u32::MAX,
            },
            source_node_id: None, // Hyphen is generated, not from DOM
            glyphs: smallvec![ShapedGlyph {
                kind: GlyphKind::Hyphen,
                glyph_id: hyphen_glyph_id,
                font_hash: last_glyph.font_hash,
                font_metrics: last_glyph.font_metrics.clone(),
                cluster_offset: 0,
                script: Script::Latin,
                advance: hyphen_advance,
                kerning: 0.0,
                offset: Point::default(),
                style: style.clone(),
                vertical_advance: hyphen_advance,
                vertical_offset: Point::default(),
            }],
            advance: hyphen_advance,
            direction: BidiDirection::Ltr,
            style: style.clone(),
            marker_position_outside: None,
            is_first_fragment: true,
            is_last_fragment: true,
        });
        possible_breaks.push(HyphenationBreak {
            char_len_on_line: break_char_idx,
            width_on_line: width_at_break + hyphen_advance,
            line_part,
            hyphen_item,
            remainder_part,
        });
    }
    Some(possible_breaks)
}
/// Tries to find a hyphenation point within a word, returning the line part and remainder.
fn try_hyphenate_word_cluster<T: ParsedFontTrait>(
    word_items: &[ShapedItem],
    remaining_width: f32,
    is_vertical: bool,
    hyphenator: &Standard,
    fonts: &LoadedFonts<T>,
) -> Option<HyphenationResult> {
    let word_clusters: Vec<ShapedCluster> = word_items
        .iter()
        .filter_map(|item| item.as_cluster().cloned())
        .collect();
    if word_clusters.is_empty() {
        return None;
    }
    let all_breaks = find_all_hyphenation_breaks(&word_clusters, hyphenator, is_vertical, fonts)?;
    if let Some(best_break) = all_breaks
        .into_iter()
        .rfind(|b| b.width_on_line <= remaining_width)
    {
        let mut line_part = best_break.line_part;
        line_part.push(best_break.hyphen_item);
        return Some(HyphenationResult {
            line_part,
            remainder_part: best_break.remainder_part,
        });
    }
    None
}
/// Positions a single line of items, handling alignment and justification within segments.
///
/// This function is architecturally critical for cache safety. It does not mutate the
/// `advance` or `bounds` of the input `ShapedItem`s. Instead, it applies justification
/// spacing by adjusting the drawing pen's position (`main_axis_pen`).
///
/// # Returns
/// A tuple containing the `Vec` of positioned items and the calculated height of the line box.
/// Position items on a single line after breaking.
///
/// # CSS Inline Layout Module Level 3 \u00a7 2.2 Layout Within Line Boxes
/// https://www.w3.org/TR/css-inline-3/#layout-within-line-boxes
///
/// Implements the positioning algorithm:
/// 1. "All inline-level boxes are aligned by their baselines"
/// 2. "Calculate layout bounds for each inline box"
/// 3. "Size the line box to fit the aligned layout bounds"
/// 4. "Position all inline boxes within the line box"
///
/// ## \u2705 Implemented Features:
///
/// ### \u00a7 4 Baseline Alignment (vertical-align)
/// \u26a0\ufe0f PARTIAL IMPLEMENTATION:
/// - \u2705 `baseline`: Aligns box baseline with parent baseline (default)
/// - \u2705 `top`: Aligns top of box with top of line box
/// - \u2705 `middle`: Centers box within line box
/// - \u2705 `bottom`: Aligns bottom of box with bottom of line box
/// - \u274c MISSING: `text-top`, `text-bottom`, `sub`, `super`
/// - \u274c MISSING: `<length>`, `<percentage>` values for custom offset
///
/// ### \u00a7 2.2.1 Text Alignment (text-align)
/// +spec:containing-block:8d5146 - text-align aligns within line box, not viewport/containing block
/// \u2705 IMPLEMENTED:
/// - `left`, `right`, `center`: Physical alignment
/// - `start`, `end`: Logical alignment (respects direction: ltr/rtl)
/// - `justify`: Distributes space between words/characters
/// - `justify-all`: Justifies last line too
///
/// ### \u00a7 7.3 Text Justification (text-justify)
/// \u2705 IMPLEMENTED:
/// - `inter-word`: Adds space between words
/// - `inter-character`: Adds space between characters
/// - `kashida`: Arabic kashida elongation
/// - \u274c MISSING: `distribute` (CJK justification)
///
/// ### CSS Text \u00a7 8.1 Text Indentation (text-indent)
/// \u2705 IMPLEMENTED: First line indentation
///
/// ### CSS Text \u00a7 4.1 Word Spacing (word-spacing)
/// \u2705 IMPLEMENTED: Additional space between words
///
/// ### CSS Text \u00a7 4.2 Letter Spacing (letter-spacing)
/// \u2705 IMPLEMENTED: Additional space between characters
///
/// ## Segment-Aware Layout:
/// \u2705 Handles CSS Shapes and multi-column layouts
/// - Breaks line into segments (for shape boundaries)
/// - Calculates justification per segment
/// - Applies alignment within each segment's bounds
///
/// ## Known Issues:
/// - \u26a0\ufe0f If segment.width is infinite (from intrinsic sizing), sets alignment_offset=0 to
///   avoid infinite positioning. This is correct for measurement but documented for clarity.
/// - The function assumes `line_index == 0` means first line for text-indent. A more robust system
///   would track paragraph boundaries.
///
/// # Missing Features:
/// - \u274c \u00a7 6 Trimming Leading (text-box-trim, text-box-edge)
/// - \u274c \u00a7 3.3 Initial Letters (drop caps)
/// // +spec:display-property:265c04 - initial letter exclusion area must continue into subsequent blocks when paragraph is shorter than drop cap
/// - \u274c Full vertical-align support (sub, super, lengths, percentages)
/// - \u274c white-space: break-spaces alignment behavior
// +spec:text-alignment-spacing:c8a926 - order of operations: shaping → letter/word-spacing → justification → alignment
82089
pub fn position_one_line<T: ParsedFontTrait>(
82089
    line_items: Vec<ShapedItem>,
82089
    line_constraints: &LineConstraints,
82089
    line_top_y: f32,
82089
    line_index: usize,
82089
    text_align: TextAlign,
82089
    base_direction: BidiDirection,
82089
    is_last_line: bool,
82089
    constraints: &UnifiedConstraints,
82089
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
82089
    fonts: &LoadedFonts<T>,
82089
    is_after_forced_break: bool,
82089
) -> (Vec<PositionedItem>, f32) {
82089
    let line_text: String = line_items
82089
        .iter()
333997
        .filter_map(|i| i.as_cluster())
333899
        .map(|c| c.text.as_str())
82089
        .collect();
82089
    if let Some(msgs) = debug_messages {
81385
        msgs.push(LayoutDebugMessage::info(format!(
81385
            "\n--- Entering position_one_line for line: [{}] ---",
81385
            line_text
81385
        )));
81385
    }
    // +spec:text-alignment-spacing:13b72d - line box start/end determined by inline base direction
    // +spec:text-alignment-spacing:d497af - line box inline base direction affects text-align resolution
    // +spec:text-alignment-spacing:68332e - bidi direction determines start/end to left/right mapping
82089
    let physical_align = match (text_align, base_direction) {
        (TextAlign::Start, BidiDirection::Ltr) => TextAlign::Left,
        (TextAlign::Start, BidiDirection::Rtl) => TextAlign::Right,
        (TextAlign::End, BidiDirection::Ltr) => TextAlign::Right,
        (TextAlign::End, BidiDirection::Rtl) => TextAlign::Left,
        // Physical alignments are returned as-is, regardless of direction.
82089
        (other, _) => other,
    };
82089
    if let Some(msgs) = debug_messages {
81385
        msgs.push(LayoutDebugMessage::info(format!(
81385
            "[Pos1Line] Physical align: {:?}",
81385
            physical_align
81385
        )));
81385
    }
    // +spec:box-model:847003 - Phantom line boxes: empty lines treated as zero-height
    // +spec:box-model:d781f3 - empty line boxes (no text, no preserved whitespace, no inline elements with non-zero margins/padding/borders, no in-flow content) are treated as zero-height
    // +spec:display-property:90d782 - Phantom line boxes (containing only empty inline boxes, out-of-flow items, or collapsed whitespace) are ignored
82089
    if line_items.is_empty() {
        return (Vec::new(), 0.0);
82089
    }
82089
    let mut positioned = Vec::new();
82089
    let is_vertical = constraints.is_vertical();
    // +spec:line-height:9ca9d9 - line box height = distance from uppermost box top to lowermost box bottom, including strut
    // The line box is calculated once for all items on the line, regardless of segment.
    // Per CSS 2.2 §10.8, top/bottom aligned items are handled in a second pass to
    // minimize line box height; baseline-aligned items determine the initial height.
82089
    let (content_ascent, content_descent) = calculate_line_metrics(&line_items, constraints.vertical_align, constraints);
    // +spec:box-model:e99f7d - strut: each line box starts with zero-width inline box with block container's font/line-height
    // +spec:line-height:29c478 - strut: zero-width inline box with block container's font/line-height
    // inline box with the block container's font and line-height. The strut has A (ascent) and
    // D (descent) from the block container's first available font. Half-leading L/2 is applied:
    // L = line-height - (A + D), strut_above = A + L/2, strut_below = D + L/2.
    // +spec:height-calculation:8e91b2 - specified line-height used in line box height calculation
82089
    let strut_ad = constraints.strut_ascent + constraints.strut_descent;
82089
    let strut_leading_half = (constraints.resolved_line_height() - strut_ad) / 2.0;
82089
    let strut_above = constraints.strut_ascent + strut_leading_half;
82089
    let strut_below = constraints.strut_descent + strut_leading_half;
82089
    let line_ascent = content_ascent.max(strut_above);
82089
    let line_descent = content_descent.max(strut_below);
82089
    let line_box_height = line_ascent + line_descent;
    // The baseline for the entire line is determined by its tallest item.
82089
    let line_baseline_y = line_top_y + line_ascent;
    // --- Segment-Aware Positioning ---
82089
    let mut item_cursor = 0;
82089
    let is_first_line_of_para = line_index == 0; // Simplified assumption
82089
    for (segment_idx, segment) in line_constraints.segments.iter().enumerate() {
82089
        if item_cursor >= line_items.len() {
            break;
82089
        }
        // 1. Collect all items that fit into the current segment.
82089
        let mut segment_items = Vec::new();
82089
        let mut current_segment_width = 0.0;
414416
        while item_cursor < line_items.len() {
332372
            let item = &line_items[item_cursor];
332372
            let item_measure = get_item_measure(item, is_vertical);
            // Put at least one item in the segment to avoid getting stuck.
332372
            if current_segment_width + item_measure > segment.width && !segment_items.is_empty() {
45
                break;
332327
            }
332327
            segment_items.push(item.clone());
332327
            current_segment_width += item_measure;
332327
            item_cursor += 1;
        }
82089
        if segment_items.is_empty() {
            continue;
82089
        }
        // +spec:text-alignment-spacing:b9d88e - justify stretches inline boxes via text-justify; non-collapsible WS may skip justification
        // 2. Calculate justification spacing *for this segment only*.
        // +spec:text-alignment-spacing:30d322 - justify lines with justification opportunities when text-align is justify
        // CSS Text 3 §6: text-justify controls HOW to justify, but only applies
        // when text-align is justify/justify-all. Without this check, ALL text
        // gets justified because text-justify defaults to auto (→ InterWord).
82089
        let (extra_word_spacing, extra_char_spacing) = if (constraints.text_align == TextAlign::Justify
82089
            || constraints.text_align == TextAlign::JustifyAll)
            && constraints.text_justify != JustifyContent::None
            && (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
            && constraints.text_justify != JustifyContent::Kashida
        {
            let segment_line_constraints = LineConstraints {
                segments: vec![segment.clone()],
                total_available: segment.width,
            };
            calculate_justification_spacing(
                &segment_items,
                &segment_line_constraints,
                constraints.text_justify,
                is_vertical,
            )
        } else {
82089
            (0.0, 0.0)
        };
        // Kashida justification needs to be segment-aware if used.
82089
        let justified_segment_items = if constraints.text_justify == JustifyContent::Kashida
            && (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
        {
            let segment_line_constraints = LineConstraints {
                segments: vec![segment.clone()],
                total_available: segment.width,
            };
            justify_kashida_and_rebuild(
                segment_items,
                &segment_line_constraints,
                is_vertical,
                debug_messages,
                fonts,
            )
        } else {
82089
            segment_items
        };
        // Recalculate width in case kashida changed the item list
82089
        let final_segment_width: f32 = justified_segment_items
82089
            .iter()
332327
            .map(|item| get_item_measure(item, is_vertical))
82089
            .sum();
        // +spec:line-breaking:155a96 - pre-wrap hanging spaces: unconditionally hang without forced break, conditionally hang with forced break
        // +spec:white-space-processing:68af09 - Phase II: trailing whitespace hanging/conditional hanging per white-space mode
        // +spec:white-space-processing:75d91e - preserved white space hangs at line end, affecting intrinsic sizing
        // +spec:overflow:a68394 - Hanging trailing whitespace: unconditionally hang (not considered
        // during alignment, may overflow) for lines without forced break; conditionally hang for
        // lines ending with forced break (only hang if would overflow).
        // For normal/nowrap/pre-line: unconditionally hang trailing WS.
        // For pre-wrap: unconditionally hang, unless before forced break (then conditionally hang).
        // For break-spaces: trailing spaces cannot hang.
        // For pre: no hanging (whitespace preserved as-is).
        // +spec:intrinsic-sizing:1db683 - conditionally hanging glyphs excluded from min-content, included in max-content
82089
        let trailing_ws_width = match constraints.white_space_mode {
8
            WhiteSpaceMode::BreakSpaces | WhiteSpaceMode::Pre => 0.0,
            WhiteSpaceMode::Normal | WhiteSpaceMode::Nowrap | WhiteSpaceMode::PreLine => {
82077
                measure_trailing_whitespace(&justified_segment_items, is_vertical)
            }
            // +spec:line-breaking:8aa426 - space before forced break does not hang if it doesn't overflow
            WhiteSpaceMode::PreWrap => {
4
                let has_forced_break = justified_segment_items.last()
4
                    .map(|item| matches!(item, ShapedItem::Break { .. }))
4
                    .unwrap_or(false);
4
                let ws_width = measure_trailing_whitespace(&justified_segment_items, is_vertical);
4
                if has_forced_break {
                    // +spec:display-contents:2704a2 - conditionally hanging chars not considered when measuring line fit
                    // Conditionally hang: only hang if it would overflow
1
                    let content_width = final_segment_width - ws_width;
1
                    if content_width + ws_width > segment.width {
                        ws_width
                    } else {
1
                        0.0
                    }
                } else {
3
                    ws_width // unconditionally hang
                }
            }
        };
82089
        let effective_segment_width = final_segment_width - trailing_ws_width;
        // +spec:text-alignment-spacing:287316 - overflow content is start-aligned; alignment offset within line box
        // 3. Calculate alignment offset *within this segment*.
82089
        let remaining_space = segment.width - effective_segment_width;
        // Handle MaxContent/indefinite width: when available_width is MaxContent (for intrinsic
        // sizing), segment.width will be f32::MAX / 2.0. Alignment calculations would
        // produce huge offsets. In this case, treat as left-aligned (offset = 0) since
        // we're measuring natural content width. We check for both infinite AND very large
        // values (> 1e30) to catch the MaxContent case.
82089
        let is_indefinite_width = segment.width.is_infinite() || segment.width > 1e30;
        // +spec:text-alignment-spacing:ab1d4f - unexpandable justify text aligns as center
82089
        let alignment_offset = if is_indefinite_width {
15070
            0.0 // No alignment offset for indefinite width
        } else {
            match physical_align {
                TextAlign::Center => remaining_space / 2.0,
                TextAlign::Right => remaining_space,
                TextAlign::Justify | TextAlign::JustifyAll
                    if remaining_space > 0.0
                        && extra_word_spacing == 0.0
                        && extra_char_spacing == 0.0 =>
                {
                    // CSS Text §6.4.3: If text cannot be stretched to full width
                    // and text-align-last is justify, align as center.
                    remaining_space / 2.0
                }
67019
                _ => 0.0, // Left, Justify (when justification succeeded)
            }
        };
82089
        let mut main_axis_pen = segment.start_x + alignment_offset;
82089
        if let Some(msgs) = debug_messages {
81385
            msgs.push(LayoutDebugMessage::info(format!(
81385
                "[Pos1Line] Segment width: {}, Item width: {}, Remaining space: {}, Initial pen: \
81385
                 {}",
81385
                segment.width, final_segment_width, remaining_space, main_axis_pen
81385
            )));
81385
        }
        // Default: indent first line only. each-line: also indent after forced breaks.
        // hanging: invert which lines get the indent.
82089
        if segment_idx == 0 {
82089
            let is_indent_target = if constraints.text_indent_each_line {
                // each-line: first line AND each line after a forced break
                is_first_line_of_para || is_after_forced_break
            } else {
                // Default: only the first line of the block
82089
                is_first_line_of_para
            };
            // hanging: inverts which lines are affected
82089
            let should_indent = if constraints.text_indent_hanging {
                !is_indent_target
            } else {
82089
                is_indent_target
            };
82089
            if should_indent {
41951
                main_axis_pen += constraints.text_indent;
41951
            }
        }
        // Calculate total marker width for proper outside marker positioning
        // We need to position all marker clusters together in the padding gutter
82089
        let total_marker_width: f32 = justified_segment_items
82089
            .iter()
332327
            .filter_map(|item| {
332327
                if let ShapedItem::Cluster(c) = item {
332229
                    if c.marker_position_outside == Some(true) {
                        return Some(get_item_measure(item, is_vertical));
332229
                    }
98
                }
332327
                None
332327
            })
82089
            .sum();
        // Track marker pen separately - starts at negative position for outside markers
82089
        let marker_spacing = 4.0; // Small gap between marker and content
82089
        let mut marker_pen = if total_marker_width > 0.0 {
            -(total_marker_width + marker_spacing)
        } else {
82089
            0.0
        };
        // 4. Position the items belonging to this segment.
        //
        // +spec:inline-formatting-context:267438 - Content positioning: position aligned subtree and baseline-shift values within line box
        //
        // Vertical alignment positioning (CSS vertical-align)
        //
        // +spec:font-metrics:cae541 - dominant baseline used for inline alignment
        // Per CSS Inline Layout Level 3 § 4 (Baseline Alignment), each inline
        // element can specify its own `vertical-align`. For Object items
        // (inline-blocks, images), we use their per-item alignment stored in
        // `InlineContent::Shape.alignment` or `InlineContent::Image.alignment`.
        // For text clusters or items without a per-item override, we fall back
        // to the global `constraints.vertical_align` from the containing block.
        //
        // +spec:font-metrics:f29b61 - baseline alignment matches corresponding baseline types (only alphabetic implemented)
        // Reference: https://www.w3.org/TR/css-inline-3/#baseline-alignment
        // +spec:block-formatting-context:26b535 - In vertical typographic mode, central baseline is dominant when text-orientation is mixed/upright; otherwise alphabetic
        // +spec:inline-formatting-context:eb735b - alignment-baseline: inline-level boxes aligned to parent's baseline via vertical-align
        // +spec:inline-formatting-context:da3f34 - baseline alignment of in-flow inline-level boxes in block axis per dominant-baseline/vertical-align
        // +spec:line-height:e2253a - vertical-align positioning within line boxes
        // Pre-compute inline border/padding offsets at span boundaries.
        // Only the FIRST cluster of each inline span gets left_inset, and only
        // the LAST cluster gets right_inset. We detect span boundaries by comparing
        // Arc<StyleProperties> pointers between consecutive clusters.
82089
        let inline_offsets: Vec<(f32, f32)> = {
82089
            let items_slice: &[ShapedItem] = &justified_segment_items;
332327
            items_slice.iter().enumerate().map(|(idx, item)| {
332327
                if let ShapedItem::Cluster(c) = item {
332229
                    if let Some(border) = c.style.border.as_ref() {
                        if border.has_chrome() {
                            let style_ptr = Arc::as_ptr(&c.style);
                            let prev_same_span = idx > 0 && items_slice[idx - 1]
                                .as_cluster()
                                .map(|pc| Arc::as_ptr(&pc.style) == style_ptr)
                                .unwrap_or(false);
                            let next_same_span = idx + 1 < items_slice.len() && items_slice[idx + 1]
                                .as_cluster()
                                .map(|nc| Arc::as_ptr(&nc.style) == style_ptr)
                                .unwrap_or(false);
                            let left = if !prev_same_span { border.left_inset() } else { 0.0 };
                            let right = if !next_same_span { border.right_inset() } else { 0.0 };
                            return (left, right);
                        }
332229
                    }
98
                }
332327
                (0.0, 0.0)
332327
            }).collect()
        };
82089
        let mut inline_offset_idx = 0;
414416
        for item in justified_segment_items {
332327
            let (item_ascent, item_descent) = get_item_vertical_metrics(&item, constraints);
            // Use per-item alignment if available, otherwise fall back to global
332327
            let effective_align = get_item_vertical_align(&item)
332327
                .unwrap_or(constraints.vertical_align);
            // +spec:display-property:328cfc - baseline-shift / aligned subtree vertical alignment (sub, super, top, bottom, center)
            // §10.8.1 vertical-align positioning
            // +spec:line-height:0fcfab - vertical-align property values (baseline, top, middle, bottom, sub, super, text-top, text-bottom, percentage, length)
332327
            let item_baseline_pos = match effective_align {
                // +spec:display-property:8e018d - aligned subtree edges used for top/bottom line box alignment
                // +spec:inline-formatting-context:495672 - line-relative vertical-align (top/center/bottom) and aligned subtree positioning
                // top: align top of aligned subtree with top of line box
                VerticalAlign::Top => line_top_y + item_ascent,
                // +spec:font-metrics:70000d - align vertical midpoint of box with baseline + half x-height of parent
                VerticalAlign::Middle => {
6820
                    let half_x_height = constraints.strut_x_height / 2.0;
6820
                    line_baseline_y + half_x_height - (item_ascent + item_descent) / 2.0 + item_ascent
                }
                // bottom: align bottom of aligned subtree with bottom of line box
                VerticalAlign::Bottom => line_top_y + line_box_height - item_descent,
                // +spec:font-metrics:aa21f7 - sub: lower baseline to proper subscript position
                VerticalAlign::Sub => line_baseline_y + line_ascent * 0.3,
                // +spec:display-property:3b0e76 - baseline-shift super raises by ~1/3 font-size; top/bottom align to line box edges
                // super: raise baseline to proper superscript position (~0.4em)
                VerticalAlign::Super => line_baseline_y - line_ascent * 0.4,
                // text-top: align top of box with top of parent's content area (§10.6.1)
                // Parent's content area top = baseline - strut_ascent
                VerticalAlign::TextTop => (line_baseline_y - constraints.strut_ascent) + item_ascent,
                // text-bottom: align bottom of box with bottom of parent's content area (§10.6.1)
                // Parent's content area bottom = baseline + strut_descent
                VerticalAlign::TextBottom => (line_baseline_y + constraints.strut_descent) - item_descent,
                // <length>/<percentage>: raise (positive) or lower (negative); 0 = baseline
                VerticalAlign::Offset(offset) => line_baseline_y - offset,
                // +spec:display-property:8bf37e - dominant-baseline defaults to alphabetic; baseline alignment matches parent
                // baseline: align baseline of box with baseline of parent box
                // +spec:font-metrics:96bbd3 - baseline: align alphabetic baseline of box with parent's alphabetic baseline
325507
                VerticalAlign::Baseline => line_baseline_y,
            };
            // Calculate item measure (needed for both positioning and pen advance)
332327
            let item_measure = get_item_measure(&item, is_vertical);
            // Advance pen by inline left_inset at span entry (before positioning glyphs)
332327
            let (left_inset, right_inset) = if inline_offset_idx < inline_offsets.len() {
332327
                inline_offsets[inline_offset_idx]
            } else {
                (0.0, 0.0)
            };
332327
            inline_offset_idx += 1;
332327
            main_axis_pen += left_inset;
332327
            let position = if is_vertical {
                Point {
                    x: item_baseline_pos - item_ascent,
                    y: main_axis_pen,
                }
            } else {
332327
                if let Some(msgs) = debug_messages {
327443
                    msgs.push(LayoutDebugMessage::info(format!(
327443
                        "[Pos1Line] is_vertical=false, main_axis_pen={}, item_baseline_pos={}, \
327443
                         item_ascent={}",
327443
                        main_axis_pen, item_baseline_pos, item_ascent
327443
                    )));
327443
                }
                // Check if this is an outside marker - if so, position it in the padding gutter
332327
                let x_position = if let ShapedItem::Cluster(cluster) = &item {
332229
                    if cluster.marker_position_outside == Some(true) {
                        // Use marker_pen for sequential marker positioning
                        let marker_width = item_measure;
                        if let Some(msgs) = debug_messages {
                            msgs.push(LayoutDebugMessage::info(format!(
                                "[Pos1Line] Outside marker detected! width={}, positioning at \
                                 marker_pen={}",
                                marker_width, marker_pen
                            )));
                        }
                        let pos = marker_pen;
                        marker_pen += marker_width; // Advance marker pen for next marker cluster
                        pos
                    } else {
332229
                        main_axis_pen
                    }
                } else {
98
                    main_axis_pen
                };
332327
                Point {
332327
                    y: item_baseline_pos - item_ascent,
332327
                    x: x_position,
332327
                }
            };
            // item_measure is calculated above for marker positioning
332327
            let item_text = item
332327
                .as_cluster()
332327
                .map(|c| c.text.as_str())
332327
                .unwrap_or("[OBJ]");
332327
            if let Some(msgs) = debug_messages {
327443
                msgs.push(LayoutDebugMessage::info(format!(
327443
                    "[Pos1Line] Positioning item '{}' at pen_x={}",
327443
                    item_text, main_axis_pen
327443
                )));
327443
            }
332327
            positioned.push(PositionedItem {
332327
                item: item.clone(),
332327
                position,
332327
                line_index,
332327
            });
            // Outside markers don't advance the pen - they're positioned in the padding gutter
332327
            let is_outside_marker = if let ShapedItem::Cluster(c) = &item {
332229
                c.marker_position_outside == Some(true)
            } else {
98
                false
            };
332327
            if !is_outside_marker {
332327
                main_axis_pen += item_measure;
332327
                // Advance pen by inline right_inset at span exit (after glyph advance)
332327
                main_axis_pen += right_inset;
332327
            }
            // +spec:text-alignment-spacing:e09bd1 - justification space added on top of letter-spacing/word-spacing
            // +spec:text-alignment-spacing:456643 - cursive scripts don't admit inter-character gaps
332327
            let is_cursive = if let ShapedItem::Cluster(c) = &item { is_cursive_script_cluster(c) } else { false };
332327
            if !is_outside_marker && extra_char_spacing > 0.0 && can_justify_after(&item) && !is_cursive {
                main_axis_pen += extra_char_spacing;
332327
            }
            // +spec:display-property:3a833c - consecutive atomic inlines treated as single unit for letter-spacing
            // +spec:display-property:49f04f - letter-spacing applied per innermost inline element
            // +spec:text-alignment-spacing:22bea4 - letter-spacing applied after bidi reordering, additive with kerning and word-spacing; justification may further adjust
332327
            if let ShapedItem::Cluster(c) = &item {
332229
                if !is_outside_marker {
                    // +spec:display-property:756454 - letter-spacing applied between typographic character units
                    // +spec:overflow:e63bc0 - letter-spacing ignores zero-width formatting chars (Cf); handled by shaper merging them into clusters
                    // +spec:text-alignment-spacing:80f9ec - letter-spacing applied per-cluster using innermost element's style (UA-allowed attachment)
                    // +spec:text-alignment-spacing:bdd704 - letter-spacing applied after each cluster, not at line start
                    // +spec:text-alignment-spacing:d3ef6e - single-char element: only trailing space, no inter-char effect
                    // +spec:text-alignment-spacing:d668fc - letter-spacing only affects characters within the element (per-cluster style)
                    // +spec:text-alignment-spacing:8dbb78 - zero letter-spacing behaves as normal (Px(0) adds no spacing)
                    // +spec:text-alignment-spacing:456643 - skip letter-spacing for cursive scripts
332229
                    if !is_cursive_script_cluster(c) {
332229
                    let letter_spacing_px = match c.style.letter_spacing {
332229
                        Spacing::Px(px) => px as f32,
                        Spacing::Em(em) => em * c.style.font_size_px,
                    };
332229
                    main_axis_pen += letter_spacing_px;
                    }
                    // +spec:width-calculation:9447d1 - word-spacing only applied to word separators; zero-width chars like U+200B are excluded
332229
                    if is_word_separator(&item) {
28405
                        let word_spacing_px = match c.style.word_spacing {
28405
                            Spacing::Px(px) => px as f32,
                            Spacing::Em(em) => em * c.style.font_size_px,
                        };
28405
                        main_axis_pen += word_spacing_px;
28405
                        main_axis_pen += extra_word_spacing;
303824
                    }
                }
98
            }
        }
    }
82089
    (positioned, line_box_height)
82089
}
/// Calculates the starting pen offset to achieve the desired text alignment.
fn calculate_alignment_offset(
    items: &[ShapedItem],
    line_constraints: &LineConstraints,
    align: TextAlign,
    is_vertical: bool,
    constraints: &UnifiedConstraints,
) -> f32 {
    // Simplified to use the first segment for alignment.
    if let Some(segment) = line_constraints.segments.first() {
        let total_width: f32 = items
            .iter()
            .map(|item| get_item_measure(item, is_vertical))
            .sum();
        let available_width = if constraints.segment_alignment == SegmentAlignment::Total {
            line_constraints.total_available
        } else {
            segment.width
        };
        if total_width >= available_width {
            return 0.0; // No alignment needed if line is full or overflows
        }
        let remaining_space = available_width - total_width;
        match align {
            TextAlign::Center => remaining_space / 2.0,
            TextAlign::Right => remaining_space,
            _ => 0.0, // Left, Justify, Start, End
        }
    } else {
        0.0
    }
}
/// Calculates the extra spacing needed for justification without modifying the items.
///
/// This function is pure and does not mutate any state, making it safe to use
/// with cached `ShapedItem` data.
///
/// # Arguments
/// * `items` - A slice of items on the line.
/// * `line_constraints` - The geometric constraints for the line.
/// * `text_justify` - The type of justification to calculate.
/// * `is_vertical` - Whether the layout is vertical.
///
/// # Returns
/// A tuple `(extra_per_word, extra_per_char)` containing the extra space in pixels
/// to add at each word or character justification opportunity.
// +spec:display-contents:654278 - distributes remaining space to fill line box when justifying
// +spec:text-alignment-spacing:56c7f4 - equal distribution of justification space within priority level
// +spec:text-alignment-spacing:f17bbc - justification opportunities controlled by text-justify value (inter-word = word separators, inter-character = character juxtaposition)
fn calculate_justification_spacing(
    items: &[ShapedItem],
    line_constraints: &LineConstraints,
    text_justify: JustifyContent,
    is_vertical: bool,
) -> (f32, f32) {
    // (extra_per_word, extra_per_char)
    let total_width: f32 = items
        .iter()
        .map(|item| get_item_measure(item, is_vertical))
        .sum();
    let available_width = line_constraints.total_available;
    if total_width >= available_width || available_width <= 0.0 {
        return (0.0, 0.0);
    }
    let extra_space = available_width - total_width;
    // +spec:text-alignment-spacing:71314a - script categories for justification: inter-word for clustered, kashida for cursive (Arabic), inter-character for block (CJK)
    match text_justify {
        JustifyContent::InterWord => {
            // Count justification opportunities (spaces).
            let space_count = items.iter().filter(|item| is_word_separator(item)).count();
            if space_count > 0 {
                (extra_space / space_count as f32, 0.0)
            } else {
                (0.0, 0.0) // No spaces to expand, do nothing.
            }
        }
        JustifyContent::InterCharacter | JustifyContent::Distribute => {
            // Count justification opportunities (between non-combining characters).
            let gap_count = items
                .iter()
                .enumerate()
                .filter(|(i, item)| *i < items.len() - 1 && can_justify_after(item))
                .count();
            if gap_count > 0 {
                (0.0, extra_space / gap_count as f32)
            } else {
                (0.0, 0.0) // No gaps to expand, do nothing.
            }
        }
        // Kashida justification modifies the item list and is handled by a separate function.
        _ => (0.0, 0.0),
    }
}
/// Rebuilds a line of items, inserting Kashida glyphs for justification.
///
/// This function is non-mutating with respect to its inputs. It takes ownership of the
/// original items and returns a completely new `Vec`. This is necessary because Kashida
/// justification changes the number of items on the line, and must not modify cached data.
pub fn justify_kashida_and_rebuild<T: ParsedFontTrait>(
    items: Vec<ShapedItem>,
    line_constraints: &LineConstraints,
    is_vertical: bool,
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
    fonts: &LoadedFonts<T>,
) -> Vec<ShapedItem> {
    if let Some(msgs) = debug_messages {
        msgs.push(LayoutDebugMessage::info(
            "\n--- Entering justify_kashida_and_rebuild ---".to_string(),
        ));
    }
    let total_width: f32 = items
        .iter()
        .map(|item| get_item_measure(item, is_vertical))
        .sum();
    let available_width = line_constraints.total_available;
    if let Some(msgs) = debug_messages {
        msgs.push(LayoutDebugMessage::info(format!(
            "Total item width: {}, Available width: {}",
            total_width, available_width
        )));
    }
    if total_width >= available_width || available_width <= 0.0 {
        if let Some(msgs) = debug_messages {
            msgs.push(LayoutDebugMessage::info(
                "No justification needed (line is full or invalid).".to_string(),
            ));
        }
        return items;
    }
    let extra_space = available_width - total_width;
    if let Some(msgs) = debug_messages {
        msgs.push(LayoutDebugMessage::info(format!(
            "Extra space to fill: {}",
            extra_space
        )));
    }
    let font_info = items.iter().find_map(|item| {
        if let ShapedItem::Cluster(c) = item {
            if let Some(glyph) = c.glyphs.first() {
                if glyph.script == Script::Arabic {
                    // Look up font from hash
                    if let Some(font) = fonts.get_by_hash(glyph.font_hash) {
                        return Some((
                            font.clone(),
                            glyph.font_hash,
                            glyph.font_metrics.clone(),
                            glyph.style.clone(),
                        ));
                    }
                }
            }
        }
        None
    });
    let (font, font_hash, font_metrics, style) = match font_info {
        Some(info) => {
            if let Some(msgs) = debug_messages {
                msgs.push(LayoutDebugMessage::info(
                    "Found Arabic font for kashida.".to_string(),
                ));
            }
            info
        }
        None => {
            if let Some(msgs) = debug_messages {
                msgs.push(LayoutDebugMessage::info(
                    "No Arabic font found on line. Cannot insert kashidas.".to_string(),
                ));
            }
            return items;
        }
    };
    let (kashida_glyph_id, kashida_advance) =
        match font.get_kashida_glyph_and_advance(style.font_size_px) {
            Some((id, adv)) if adv > 0.0 => {
                if let Some(msgs) = debug_messages {
                    msgs.push(LayoutDebugMessage::info(format!(
                        "Font provides kashida glyph with advance {}",
                        adv
                    )));
                }
                (id, adv)
            }
            _ => {
                if let Some(msgs) = debug_messages {
                    msgs.push(LayoutDebugMessage::info(
                        "Font does not support kashida justification.".to_string(),
                    ));
                }
                return items;
            }
        };
    let opportunity_indices: Vec<usize> = items
        .windows(2)
        .enumerate()
        .filter_map(|(i, window)| {
            if let (ShapedItem::Cluster(cur), ShapedItem::Cluster(next)) = (&window[0], &window[1])
            {
                if is_arabic_cluster(cur)
                    && is_arabic_cluster(next)
                    && !is_word_separator(&window[1])
                {
                    return Some(i + 1);
                }
            }
            None
        })
        .collect();
    if let Some(msgs) = debug_messages {
        msgs.push(LayoutDebugMessage::info(format!(
            "Found {} kashida insertion opportunities at indices: {:?}",
            opportunity_indices.len(),
            opportunity_indices
        )));
    }
    if opportunity_indices.is_empty() {
        if let Some(msgs) = debug_messages {
            msgs.push(LayoutDebugMessage::info(
                "No opportunities found. Exiting.".to_string(),
            ));
        }
        return items;
    }
    let num_kashidas_to_insert = (extra_space / kashida_advance).floor() as usize;
    if let Some(msgs) = debug_messages {
        msgs.push(LayoutDebugMessage::info(format!(
            "Calculated number of kashidas to insert: {}",
            num_kashidas_to_insert
        )));
    }
    if num_kashidas_to_insert == 0 {
        return items;
    }
    let kashidas_per_point = num_kashidas_to_insert / opportunity_indices.len();
    let mut remainder = num_kashidas_to_insert % opportunity_indices.len();
    if let Some(msgs) = debug_messages {
        msgs.push(LayoutDebugMessage::info(format!(
            "Distributing kashidas: {} per point, with {} remainder.",
            kashidas_per_point, remainder
        )));
    }
    let kashida_item = {
        /* ... as before ... */
        let kashida_glyph = ShapedGlyph {
            kind: GlyphKind::Kashida {
                width: kashida_advance,
            },
            glyph_id: kashida_glyph_id,
            font_hash,
            font_metrics: font_metrics.clone(),
            style: style.clone(),
            script: Script::Arabic,
            advance: kashida_advance,
            kerning: 0.0,
            cluster_offset: 0,
            offset: Point::default(),
            vertical_advance: 0.0,
            vertical_offset: Point::default(),
        };
        ShapedItem::Cluster(ShapedCluster {
            text: "\u{0640}".to_string(),
            source_cluster_id: GraphemeClusterId {
                source_run: u32::MAX,
                start_byte_in_run: u32::MAX,
            },
            source_content_index: ContentIndex {
                run_index: u32::MAX,
                item_index: u32::MAX,
            },
            source_node_id: None, // Kashida is generated, not from DOM
            glyphs: smallvec![kashida_glyph],
            advance: kashida_advance,
            direction: BidiDirection::Ltr,
            style,
            marker_position_outside: None,
            is_first_fragment: true,
            is_last_fragment: true,
        })
    };
    let mut new_items = Vec::with_capacity(items.len() + num_kashidas_to_insert);
    let mut last_copy_idx = 0;
    for &point in &opportunity_indices {
        new_items.extend_from_slice(&items[last_copy_idx..point]);
        let mut num_to_insert = kashidas_per_point;
        if remainder > 0 {
            num_to_insert += 1;
            remainder -= 1;
        }
        for _ in 0..num_to_insert {
            new_items.push(kashida_item.clone());
        }
        last_copy_idx = point;
    }
    new_items.extend_from_slice(&items[last_copy_idx..]);
    if let Some(msgs) = debug_messages {
        msgs.push(LayoutDebugMessage::info(format!(
            "--- Exiting justify_kashida_and_rebuild, new item count: {} ---",
            new_items.len()
        )));
    }
    new_items
}
/// Helper to determine if a cluster belongs to the Arabic script.
fn is_arabic_cluster(cluster: &ShapedCluster) -> bool {
    // A cluster is considered Arabic if its first non-NotDef glyph is from the Arabic script.
    // This is a robust heuristic for mixed-script lines.
    cluster.glyphs.iter().any(|g| g.script == Script::Arabic)
}
/// Helper to identify if an item is a word separator (like a space).
84876
fn measure_trailing_whitespace(items: &[ShapedItem], is_vertical: bool) -> f32 {
84876
    let mut trailing_ws = 0.0;
84876
    for item in items.iter().rev() {
84876
        if is_collapsible_whitespace(item) {
            trailing_ws += get_item_measure(item, is_vertical);
        } else {
84876
            break;
        }
    }
84876
    trailing_ws
84876
}
/// Returns true if the item is collapsible whitespace per CSS Text 3 §4.1.2 Phase II.
/// This is used for stripping leading/trailing whitespace at line edges —
/// distinct from `is_word_separator` which is for word-spacing per §7.1.
209176
pub fn is_collapsible_whitespace(item: &ShapedItem) -> bool {
209176
    if let ShapedItem::Cluster(c) = item {
208604
        c.text.chars().all(|ch| matches!(ch,
            ' ' | '\t' | '\u{1680}' // Ogham space mark (collapsible per spec)
        ))
    } else {
572
        false
    }
209176
}
// +spec:text-alignment-spacing:456643 - cursive scripts do not admit letter-spacing gaps
/// Returns true if the cluster's first character belongs to a cursive script
/// (Arabic, Syriac, Mongolian, N'Ko, Mandaic, Phags Pa, Hanifi Rohingya)
/// per CSS Text 3 Appendix D. These scripts should not have letter-spacing applied.
716144
pub fn is_cursive_script_cluster(c: &ShapedCluster) -> bool {
716144
    c.text.chars().next().map_or(false, |ch| is_cursive_script_char(ch))
716144
}
716144
fn is_cursive_script_char(ch: char) -> bool {
716144
    let cp = ch as u32;
    // Arabic (U+0600–U+06FF, U+0750–U+077F, U+08A0–U+08FF, U+FB50–U+FDFF, U+FE70–U+FEFF)
716144
    if (0x0600..=0x06FF).contains(&cp) { return true; }
716144
    if (0x0750..=0x077F).contains(&cp) { return true; }
716144
    if (0x08A0..=0x08FF).contains(&cp) { return true; }
716144
    if (0xFB50..=0xFDFF).contains(&cp) { return true; }
716144
    if (0xFE70..=0xFEFF).contains(&cp) { return true; }
    // Syriac (U+0700–U+074F)
716144
    if (0x0700..=0x074F).contains(&cp) { return true; }
    // Mongolian (U+1800–U+18AF)
716144
    if (0x1800..=0x18AF).contains(&cp) { return true; }
    // N'Ko (U+07C0–U+07FF)
716144
    if (0x07C0..=0x07FF).contains(&cp) { return true; }
    // Mandaic (U+0840–U+085F)
716144
    if (0x0840..=0x085F).contains(&cp) { return true; }
    // Phags Pa (U+A840–U+A87F)
716144
    if (0xA840..=0xA87F).contains(&cp) { return true; }
    // Hanifi Rohingya (U+10D00–U+10D3F)
716144
    if (0x10D00..=0x10D3F).contains(&cp) { return true; }
716144
    false
716144
}
// exclude punctuation and fixed-width spaces (U+3000, U+2000..U+200A)
1364704
pub fn is_word_separator(item: &ShapedItem) -> bool {
1364704
    if let ShapedItem::Cluster(c) = item {
1391720
        c.text.chars().any(|g| is_word_separator_char(g))
    } else {
1540
        false
    }
1364704
}
// +spec:margin-collapsing:6706c1 - fixed-width spaces (U+2000–U+200A, U+3000) excluded from word separators
/// Returns true if the character is a word-separator character per CSS Text §7.1.
/// Punctuation and fixed-width spaces (U+3000, U+2000 through U+200A) are NOT
/// word-separator characters even though they may visually separate words.
// +spec:text-alignment-spacing:3e0655 - word-separator characters for word-spacing
1391720
fn is_word_separator_char(c: char) -> bool {
1391720
    match c {
        // Standard ASCII space
154880
        '\u{0020}' => true,
        // NO-BREAK SPACE
        '\u{00A0}' => true,
        // OGHAM SPACE MARK
        '\u{1680}' => true,
        // ETHIOPIC WORDSPACE (spec §7.1)
        '\u{1361}' => true,
        // Fixed-width spaces: NOT word separators per spec
70092
        '\u{2000}'..='\u{200A}' => false,
        // NARROW NO-BREAK SPACE
        '\u{202F}' => true,
        // MEDIUM MATHEMATICAL SPACE
        '\u{205F}' => true,
        // IDEOGRAPHIC SPACE: NOT a word separator per spec
        '\u{3000}' => false,
        // AEGEAN WORD SEPARATOR LINE (spec §7.1)
        '\u{10100}' => true,
        // AEGEAN WORD SEPARATOR DOT (spec §7.1)
        '\u{10101}' => true,
        // UGARITIC WORD DIVIDER (spec §7.1)
        '\u{1039F}' => true,
        // PHOENICIAN WORD SEPARATOR (spec §7.1)
        '\u{1091F}' => true,
        // Other Unicode whitespace not listed above
1236840
        _ => false,
    }
1391720
}
/// Helper to identify if an item is a zero-width space (U+200B),
/// which provides a soft wrap opportunity with no visible width.
/// Used in scripts like Thai, Lao, and Khmer that don't use spaces between words.
// +spec:line-breaking:fd3164 - U+200B as explicit word delimiter for scripts without space-separated words
882244
pub fn is_zero_width_space(item: &ShapedItem) -> bool {
882244
    if let ShapedItem::Cluster(c) = item {
881584
        c.text.contains('\u{200B}')
    } else {
660
        false
    }
882244
}
/// Helper to identify if space can be added after an item.
fn can_justify_after(item: &ShapedItem) -> bool {
    if let ShapedItem::Cluster(c) = item {
        c.text.chars().last().map_or(false, |g| {
            !g.is_whitespace() && classify_character(g as u32) != CharacterClass::Combining
        })
    } else {
        // Per CSS 2.2 §9.4.2, justification must NOT stretch inline-table and
        // inline-block boxes. Object items represent these atomic inline-level
        // boxes, so we return false to prevent adding justification space after them.
        false
    }
}
// +spec:font-metrics:b8eb97 - Script group classification for justification/letter-spacing behavior
/// Classifies a character for layout purposes (e.g., justification behavior).
/// Copied from `mod.rs`.
fn classify_character(codepoint: u32) -> CharacterClass {
    match codepoint {
        0x0020 | 0x00A0 | 0x3000 => CharacterClass::Space,
        0x0021..=0x002F | 0x003A..=0x0040 | 0x005B..=0x0060 | 0x007B..=0x007E => {
            CharacterClass::Punctuation
        }
        0x4E00..=0x9FFF | 0x3400..=0x4DBF => CharacterClass::Ideograph,
        0x0300..=0x036F | 0x1AB0..=0x1AFF => CharacterClass::Combining,
        // Mongolian script range
        0x1800..=0x18AF => CharacterClass::Letter,
        _ => CharacterClass::Letter,
    }
}
/// Helper to get the primary measure (width or height) of a shaped item.
1903968
pub fn get_item_measure(item: &ShapedItem, is_vertical: bool) -> f32 {
1903968
    match item {
1901768
        ShapedItem::Cluster(c) => {
            // Total width = base advance + kerning adjustments
            // Kerning is stored separately in glyphs for inspection, but the total
            // cluster width must include it for correct layout positioning
1901768
            let total_kerning: f32 = c.glyphs.iter().map(|g| g.kerning).sum();
1901768
            c.advance + total_kerning
        }
440
        ShapedItem::Object { bounds, .. }
        | ShapedItem::CombinedBlock { bounds, .. }
352
        | ShapedItem::Tab { bounds, .. } => {
792
            if is_vertical {
                bounds.height
            } else {
792
                bounds.width
            }
        }
1408
        ShapedItem::Break { .. } => 0.0,
    }
1903968
}
/// Calculates the available horizontal segments for a line at a given vertical position,
/// considering both shape boundaries and exclusions.
85404
fn get_line_constraints(
85404
    line_y: f32,
85404
    line_height: f32,
85404
    constraints: &UnifiedConstraints,
85404
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
85404
) -> LineConstraints {
85404
    if let Some(msgs) = debug_messages {
84700
        msgs.push(LayoutDebugMessage::info(format!(
84700
            "\n--- Entering get_line_constraints for y={} ---",
84700
            line_y
84700
        )));
84700
    }
85404
    let mut available_segments = Vec::new();
85404
    if constraints.shape_boundaries.is_empty() {
        // The segment_width is determined by available_width, NOT by TextWrap.
        // TextWrap::NoWrap only affects whether the LineBreaker can insert soft breaks,
        // it should NOT override a definite width constraint from CSS.
        // +spec:overflow:b06c3e - text overflows when wrapping is prevented (e.g. white-space: nowrap)
        // CSS Text Level 3: For 'white-space: pre/nowrap', text overflows horizontally
        // if it doesn't fit, rather than expanding the container.
        //
        // For MinContent/MaxContent intrinsic sizing: use a large value to let text 
        // lay out fully. The line breaker handles min-content by breaking at word 
        // boundaries. The actual content width is measured from the laid-out lines.
85404
        let segment_width = match constraints.available_width {
69388
            AvailableSpace::Definite(w) => w, // Respect definite width from CSS
8096
            AvailableSpace::MaxContent => f32::MAX / 2.0, // For intrinsic max-content sizing
7920
            AvailableSpace::MinContent => f32::MAX / 2.0, // For intrinsic min-content sizing
        };
        // Note: TextWrap::NoWrap is handled by the LineBreaker in break_one_line()
        // to prevent soft wraps. The text will simply overflow if it exceeds segment_width.
85404
        available_segments.push(LineSegment {
85404
            start_x: 0.0,
85404
            width: segment_width,
85404
            priority: 0,
85404
        });
    } else {
        // ... complex boundary logic ...
    }
85404
    if let Some(msgs) = debug_messages {
84700
        msgs.push(LayoutDebugMessage::info(format!(
84700
            "Initial available segments: {:?}",
84700
            available_segments
84700
        )));
84700
    }
85404
    for (idx, exclusion) in constraints.shape_exclusions.iter().enumerate() {
        if let Some(msgs) = debug_messages {
            msgs.push(LayoutDebugMessage::info(format!(
                "Applying exclusion #{}: {:?}",
                idx, exclusion
            )));
        }
        let exclusion_spans =
            get_shape_horizontal_spans(exclusion, line_y, line_height).unwrap_or_default();
        if let Some(msgs) = debug_messages {
            msgs.push(LayoutDebugMessage::info(format!(
                "  Exclusion spans at y={}: {:?}",
                line_y, exclusion_spans
            )));
        }
        if exclusion_spans.is_empty() {
            continue;
        }
        let mut next_segments = Vec::new();
        for (excl_start, excl_end) in exclusion_spans {
            for segment in &available_segments {
                let seg_start = segment.start_x;
                let seg_end = segment.start_x + segment.width;
                // Create new segments by subtracting the exclusion
                if seg_end > excl_start && seg_start < excl_end {
                    if seg_start < excl_start {
                        // Left part
                        next_segments.push(LineSegment {
                            start_x: seg_start,
                            width: excl_start - seg_start,
                            priority: segment.priority,
                        });
                    }
                    if seg_end > excl_end {
                        // Right part
                        next_segments.push(LineSegment {
                            start_x: excl_end,
                            width: seg_end - excl_end,
                            priority: segment.priority,
                        });
                    }
                } else {
                    next_segments.push(segment.clone()); // No overlap
                }
            }
            available_segments = merge_segments(next_segments);
            next_segments = Vec::new();
        }
        if let Some(msgs) = debug_messages {
            msgs.push(LayoutDebugMessage::info(format!(
                "  Segments after exclusion #{}: {:?}",
                idx, available_segments
            )));
        }
    }
85404
    let total_width = available_segments.iter().map(|s| s.width).sum();
85404
    if let Some(msgs) = debug_messages {
84700
        msgs.push(LayoutDebugMessage::info(format!(
84700
            "Final segments: {:?}, total available width: {}",
84700
            available_segments, total_width
84700
        )));
84700
        msgs.push(LayoutDebugMessage::info(
84700
            "--- Exiting get_line_constraints ---".to_string(),
84700
        ));
84700
    }
85404
    LineConstraints {
85404
        segments: available_segments,
85404
        total_available: total_width,
85404
    }
85404
}
/// Helper function to get the horizontal spans of any shape at a given y-coordinate.
/// Returns a list of (start_x, end_x) tuples.
fn get_shape_horizontal_spans(
    shape: &ShapeBoundary,
    y: f32,
    line_height: f32,
) -> Result<Vec<(f32, f32)>, LayoutError> {
    match shape {
        ShapeBoundary::Rectangle(rect) => {
            // Check for any overlap between the line box [y, y + line_height]
            // and the rectangle's vertical span [rect.y, rect.y + rect.height].
            let line_start = y;
            let line_end = y + line_height;
            let rect_start = rect.y;
            let rect_end = rect.y + rect.height;
            if line_start < rect_end && line_end > rect_start {
                Ok(vec![(rect.x, rect.x + rect.width)])
            } else {
                Ok(vec![])
            }
        }
        ShapeBoundary::Circle { center, radius } => {
            let line_center_y = y + line_height / 2.0;
            let dy = (line_center_y - center.y).abs();
            if dy <= *radius {
                let dx = (radius.powi(2) - dy.powi(2)).sqrt();
                Ok(vec![(center.x - dx, center.x + dx)])
            } else {
                Ok(vec![])
            }
        }
        ShapeBoundary::Ellipse { center, radii } => {
            let line_center_y = y + line_height / 2.0;
            let dy = line_center_y - center.y;
            if dy.abs() <= radii.height {
                // Formula: (x-h)^2/a^2 + (y-k)^2/b^2 = 1
                let y_term = dy / radii.height;
                let x_term_squared = 1.0 - y_term.powi(2);
                if x_term_squared >= 0.0 {
                    let dx = radii.width * x_term_squared.sqrt();
                    Ok(vec![(center.x - dx, center.x + dx)])
                } else {
                    Ok(vec![])
                }
            } else {
                Ok(vec![])
            }
        }
        ShapeBoundary::Polygon { points } => {
            let segments = polygon_line_intersection(points, y, line_height)?;
            Ok(segments
                .iter()
                .map(|s| (s.start_x, s.start_x + s.width))
                .collect())
        }
        ShapeBoundary::Path { .. } => Ok(vec![]), // TODO!
    }
}
/// Merges overlapping or adjacent line segments into larger ones.
fn merge_segments(mut segments: Vec<LineSegment>) -> Vec<LineSegment> {
    if segments.len() <= 1 {
        return segments;
    }
    segments.sort_by(|a, b| a.start_x.partial_cmp(&b.start_x).unwrap());
    let mut merged = vec![segments[0].clone()];
    for next_seg in segments.iter().skip(1) {
        let last = merged.last_mut().unwrap();
        if next_seg.start_x <= last.start_x + last.width {
            let new_width = (next_seg.start_x + next_seg.width) - last.start_x;
            last.width = last.width.max(new_width);
        } else {
            merged.push(next_seg.clone());
        }
    }
    merged
}
/// Computes horizontal line segments where a polygon intersects a scanline at the given y range.
fn polygon_line_intersection(
    points: &[Point],
    y: f32,
    line_height: f32,
) -> Result<Vec<LineSegment>, LayoutError> {
    if points.len() < 3 {
        return Ok(vec![]);
    }
    let line_center_y = y + line_height / 2.0;
    let mut intersections = Vec::new();
    // Use winding number algorithm for robustness with complex polygons.
    for i in 0..points.len() {
        let p1 = points[i];
        let p2 = points[(i + 1) % points.len()];
        // Skip horizontal edges as they don't intersect a horizontal scanline in a meaningful way.
        if (p2.y - p1.y).abs() < f32::EPSILON {
            continue;
        }
        // Check if our horizontal scanline at `line_center_y` crosses this polygon edge.
        let crosses = (p1.y <= line_center_y && p2.y > line_center_y)
            || (p1.y > line_center_y && p2.y <= line_center_y);
        if crosses {
            // Calculate intersection x-coordinate using linear interpolation.
            let t = (line_center_y - p1.y) / (p2.y - p1.y);
            let x = p1.x + t * (p2.x - p1.x);
            intersections.push(x);
        }
    }
    // Sort intersections by x-coordinate to form spans.
    intersections.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
    // Build segments from paired intersection points.
    let mut segments = Vec::new();
    for chunk in intersections.chunks_exact(2) {
        let start_x = chunk[0];
        let end_x = chunk[1];
        if end_x > start_x {
            segments.push(LineSegment {
                start_x,
                width: end_x - start_x,
                priority: 0,
            });
        }
    }
    Ok(segments)
}
// ADDITION: A helper function to get a hyphenator.
/// Helper to get a hyphenator for a given language.
/// TODO: In a real app, this would be cached.
#[cfg(feature = "text_layout_hyphenation")]
fn get_hyphenator(language: HyphenationLanguage) -> Result<Standard, LayoutError> {
    Standard::from_embedded(language).map_err(|e| LayoutError::HyphenationError(e.to_string()))
}
/// Stub when hyphenation is disabled - always returns an error
#[cfg(not(feature = "text_layout_hyphenation"))]
fn get_hyphenator(_language: Language) -> Result<Standard, LayoutError> {
    Err(LayoutError::HyphenationError("Hyphenation feature not enabled".to_string()))
}
// +spec:inline-block:6e7dd9 - Non-tailorable Unicode line breaking controls take precedence over atomic inline rules (CSS-TEXT-3 recent changes, issue 8972)
1521212
fn is_break_suppressing_control(ch: char) -> bool {
1521212
    matches!(ch,
        '\u{200D}' | // ZERO WIDTH JOINER
        '\u{2060}' | // WORD JOINER
        '\u{FEFF}'   // ZERO WIDTH NO-BREAK SPACE
    )
1521212
}
fn is_break_forcing_control(ch: char) -> bool {
    matches!(ch,
        '\u{200B}' | // ZERO WIDTH SPACE (already handled but included for completeness)
        '\u{2028}' | // LINE SEPARATOR
        '\u{2029}'   // PARAGRAPH SEPARATOR
    )
}
// +spec:line-breaking:495247 - CJK/syllabic writing systems allow breaks between typographic letter units with varying strictness
// §5.2 word-break: determines if a character is CJK ideograph/kana
882244
fn is_cjk_character(ch: char) -> bool {
882244
    let cp = ch as u32;
882244
    matches!(cp,
        // CJK Unified Ideographs
        0x4E00..=0x9FFF |
        // CJK Unified Ideographs Extension A
        0x3400..=0x4DBF |
        // CJK Unified Ideographs Extension B
        0x20000..=0x2A6DF |
        // CJK Compatibility Ideographs
        0xF900..=0xFAFF |
        // Hiragana
        0x3040..=0x309F |
        // Katakana
        0x30A0..=0x30FF |
        // Katakana Phonetic Extensions
        0x31F0..=0x31FF |
        // CJK Symbols and Punctuation
        0x3000..=0x303F |
        // Halfwidth and Fullwidth Forms
        0xFF00..=0xFFEF |
        // Hangul Syllables
        0xAC00..=0xD7AF
    )
882244
}
// §5.2 word-break: checks if a cluster contains CJK characters
881584
fn is_cjk_cluster(cluster: &ShapedCluster) -> bool {
881584
    cluster.text.chars().any(is_cjk_character)
881584
}
// +spec:line-breaking:e1fc9d - word-break normal/break-all/keep-all break opportunity rules
// +spec:line-breaking:73d5fe - word-break break-point determination for CJK and Latin text
// +spec:line-breaking:31ef1a - word-break property controls soft wrap opportunities between letters (NU/AL/AI/ID classes as letter units)
// +spec:line-breaking:798252 - word-break property affects break opportunities (normal/break-all/keep-all)
// +spec:line-breaking:8fed57 - word-break: break-all treats all clusters as break opportunities, keep-all suppresses CJK breaks
// +spec:line-breaking:e2b374 - word-break: normal (only at separators) vs break-all (between all letters incl. Ethiopic)
// +spec:overflow:53a97f - word-break (normal/break-all/keep-all) and line-break strictness rules
// +spec:line-breaking:1c830a - word-break: normal/break-all/keep-all break opportunity rules
// §5.2 word-break property: break opportunity logic
// +spec:line-breaking:a75147 - word-break property: normal (CJK breaks), break-all (every cluster), keep-all (suppress CJK breaks)
// +spec:line-breaking:65ab41 - word-break: normal/break-all/keep-all break opportunity rules
// +spec:line-breaking:7eca16 - U+200B ZERO WIDTH SPACE is always a break opportunity, even with keep-all
1006632
fn is_break_opportunity_with_word_break(item: &ShapedItem, word_break: WordBreak, hyphens: Hyphens) -> bool {
    // Break after spaces or explicit break items (always, regardless of word-break).
1006632
    if is_word_separator(item) {
123508
        return true;
883124
    }
883124
    if let ShapedItem::Break { .. } = item {
880
        return true;
882244
    }
    // +spec:line-breaking:432d5b - hyphens property controls soft wrap opportunities via hyphenation
    // +spec:line-breaking:5a32a1 - soft hyphen (U+00AD) creates break opportunity; glyph styled per surrounding text properties
    // U+200B ZERO WIDTH SPACE is always a soft wrap opportunity regardless of word-break.
    // This allows authors to mark explicit wrap points (e.g. with <wbr> or &#x200B;)
    // even when using word-break: keep-all to suppress other breaks.
882244
    if is_zero_width_space(item) {
        return true;
882244
    }
    // only when hyphens != none. With hyphens:none, soft hyphens do not create break points.
882244
    if hyphens != Hyphens::None {
882244
        if let ShapedItem::Cluster(c) = item {
881584
            if c.text.starts_with('\u{00AD}') {
                return true;
881584
            }
660
        }
    }
    // +spec:line-breaking:2bbda0 - word-break does not affect soft wrap opportunities around punctuation
882244
    match word_break {
        WordBreak::Normal => {
            // CJK characters are implicit break opportunities in normal mode.
882244
            if let ShapedItem::Cluster(c) = item {
881584
                if is_cjk_cluster(c) {
                    return true;
881584
                }
660
            }
882244
            false
        }
        WordBreak::BreakAll => {
            // Every typographic letter unit is a break opportunity.
            if let ShapedItem::Cluster(_) = item {
                return true;
            }
            false
        }
        WordBreak::KeepAll => {
            // +spec:line-breaking:aa3044 - keep-all suppresses CJK (incl. Korean) inter-character breaks
            // Only break at spaces/hyphens (already handled above).
            false
        }
    }
1006632
}
// +spec:line-breaking:db0289 - line-break strictness: anywhere allows soft wrap around every typographic character unit
// +spec:line-breaking:7d242b - line-break strictness levels: loose/normal/strict/anywhere with CJK punctuation rules
// +spec:line-breaking:67bfe8 - line-break strictness (auto/loose/normal/strict/anywhere) controls
// CSS Text Level 3 §5.3: Determines whether a break opportunity before a character is
// allowed based on the line-break strictness level. The spec defines:
// - strict: forbids breaks before small kana (class CJ), CJK hyphens, and certain punctuation
// - normal: allows breaks before small kana (CJ); allows CJK hyphen breaks for CJK writing systems
// - loose: additionally allows breaks before hyphens U+2010/U+2013 after ID-class chars
// - anywhere: allows soft wrap around every typographic character unit
792264
fn is_cjk_break_allowed_by_strictness(
792264
    ch: char,
792264
    _prev_ch: Option<char>,
792264
    strictness: LineBreakStrictness,
792264
) -> bool {
792264
    match strictness {
        LineBreakStrictness::Anywhere => true,
        LineBreakStrictness::Loose => {
            // Loose allows breaks before hyphens U+2010, U+2013 when preceded by ID-class chars
            // Also allows breaks before small kana (CJ class) and CJK hyphens
            true
        }
        LineBreakStrictness::Normal | LineBreakStrictness::Auto => {
            // Normal forbids breaks before hyphens U+2010/U+2013 for non-CJK text
            // but allows breaks before small kana (CJ) and CJK hyphen-like chars
            // (〜 U+301C, ゠ U+30A0) for CJK writing systems
792264
            match ch {
                '\u{2010}' | '\u{2013}' => false, // hyphens forbidden in normal
792264
                _ => true,
            }
        }
        LineBreakStrictness::Strict => {
            // Strict forbids breaks before:
            // - Small kana and prolonged sound mark (Unicode line break class CJ)
            // - CJK hyphen-like characters: 〜 U+301C, ゠ U+30A0
            // - Hyphens: ‐ U+2010, – U+2013
            match ch {
                '\u{301C}' | '\u{30A0}' => false, // CJK hyphen-like
                '\u{2010}' | '\u{2013}' => false,  // hyphens
                c if is_small_kana(c) => false,
                _ => true,
            }
        }
    }
792264
}
/// Returns true if the character is a Japanese small kana or Katakana-Hiragana prolonged sound mark
/// (Unicode line break class CJ). These are forbidden break points in strict line breaking.
fn is_small_kana(ch: char) -> bool {
    matches!(ch,
        '\u{3041}' | // ぁ HIRAGANA LETTER SMALL A
        '\u{3043}' | // ぃ HIRAGANA LETTER SMALL I
        '\u{3045}' | // ぅ HIRAGANA LETTER SMALL U
        '\u{3047}' | // ぇ HIRAGANA LETTER SMALL E
        '\u{3049}' | // ぉ HIRAGANA LETTER SMALL O
        '\u{3063}' | // っ HIRAGANA LETTER SMALL TU
        '\u{3083}' | // ゃ HIRAGANA LETTER SMALL YA
        '\u{3085}' | // ゅ HIRAGANA LETTER SMALL YU
        '\u{3087}' | // ょ HIRAGANA LETTER SMALL YO
        '\u{308E}' | // ゎ HIRAGANA LETTER SMALL WA
        '\u{3095}' | // ゕ HIRAGANA LETTER SMALL KA
        '\u{3096}' | // ゖ HIRAGANA LETTER SMALL KE
        '\u{30A1}' | // ァ KATAKANA LETTER SMALL A
        '\u{30A3}' | // ィ KATAKANA LETTER SMALL I
        '\u{30A5}' | // ゥ KATAKANA LETTER SMALL U
        '\u{30A7}' | // ェ KATAKANA LETTER SMALL E
        '\u{30A9}' | // ォ KATAKANA LETTER SMALL O
        '\u{30C3}' | // ッ KATAKANA LETTER SMALL TU
        '\u{30E3}' | // ャ KATAKANA LETTER SMALL YA
        '\u{30E5}' | // ュ KATAKANA LETTER SMALL YU
        '\u{30E7}' | // ョ KATAKANA LETTER SMALL YO
        '\u{30EE}' | // ヮ KATAKANA LETTER SMALL WA
        '\u{30F5}' | // ヵ KATAKANA LETTER SMALL KA
        '\u{30F6}' | // ヶ KATAKANA LETTER SMALL KE
        '\u{30FC}'   // ー KATAKANA-HIRAGANA PROLONGED SOUND MARK
    )
}
// for every typographic character unit, disregarding GL/WJ/ZWJ line breaking classes
// replaced element or other atomic inline for web-compat
fn is_break_opportunity(item: &ShapedItem) -> bool {
    // Per CSS Text 3 §5.1: "there is a soft wrap opportunity before and
    // after each replaced element or other atomic inline"
    if matches!(item, ShapedItem::Object { .. } | ShapedItem::CombinedBlock { .. }) {
        return true;
    }
    // over atomic inline rules: break-forcing controls (ZWSP, LS, PS) create break opportunities
    // even adjacent to atomic inlines, while break-suppressing controls (WJ, ZWJ, ZWNBSP)
    // prevent breaks
    if let ShapedItem::Cluster(c) = item {
        // ZW (zero-width space U+200B) is always a break opportunity
        if c.text.contains('\u{200B}') {
            return true;
        }
        // Break-forcing Unicode controls (LS, PS) create break opportunities
        if c.text.chars().any(|ch| is_break_forcing_control(ch)) {
            return true;
        }
        // WJ (word joiner U+2060), ZWJ (U+200D), and GL (NBSP U+00A0) suppress breaks
        if c.text.chars().any(|ch| matches!(ch, '\u{2060}' | '\u{200D}' | '\u{00A0}')) {
            return false;
        }
        // +spec:line-breaking:05e09a - U+002D/U+2010 always create soft wrap opportunities regardless of hyphens property
        // are always visible and create a soft wrap opportunity after them, but are NOT
        // hyphenation opportunities (no extra glyph is inserted at the break).
        if c.text.ends_with('\u{002D}') || c.text.ends_with('\u{2010}') {
            return true;
        }
    }
    is_break_opportunity_with_word_break(item, WordBreak::Normal, Hyphens::Manual)
}
// A cursor to manage the state of the line breaking process.
// This allows us to handle items that are partially consumed by hyphenation.
pub struct BreakCursor<'a> {
    /// A reference to the complete list of shaped items.
    pub items: &'a [ShapedItem],
    /// The index of the next *full* item to be processed from the `items` slice.
    pub next_item_index: usize,
    /// The remainder of an item that was split by hyphenation on the previous line.
    /// This will be the very first piece of content considered for the next line.
    pub partial_remainder: Vec<ShapedItem>,
    // §5.2 word-break property stored on cursor
    pub word_break: WordBreak,
    pub hyphens: Hyphens,
    pub line_break: LineBreakStrictness,
}
impl<'a> BreakCursor<'a> {
572
    pub fn new(items: &'a [ShapedItem]) -> Self {
572
        Self {
572
            items,
572
            next_item_index: 0,
572
            partial_remainder: Vec::new(),
572
            word_break: WordBreak::Normal,
572
            hyphens: Hyphens::default(),
572
            line_break: LineBreakStrictness::default(),
572
        }
572
    }
49016
    pub fn with_word_break(items: &'a [ShapedItem], word_break: WordBreak) -> Self {
49016
        Self {
49016
            items,
49016
            next_item_index: 0,
49016
            partial_remainder: Vec::new(),
49016
            word_break,
49016
            hyphens: Hyphens::default(),
49016
            line_break: LineBreakStrictness::default(),
49016
        }
49016
    }
    /// Checks if the cursor is at the very beginning of the content stream.
    pub fn is_at_start(&self) -> bool {
        self.next_item_index == 0 && self.partial_remainder.is_empty()
    }
    /// Consumes the cursor and returns all remaining items as a `Vec`.
49016
    pub fn drain_remaining(&mut self) -> Vec<ShapedItem> {
49016
        let mut remaining = std::mem::take(&mut self.partial_remainder);
49016
        if self.next_item_index < self.items.len() {
4884
            remaining.extend_from_slice(&self.items[self.next_item_index..]);
44132
        }
49016
        self.next_item_index = self.items.len();
49016
        remaining
49016
    }
    /// Checks if all content, including any partial remainders, has been processed.
446996
    pub fn is_done(&self) -> bool {
446996
        self.next_item_index >= self.items.len() && self.partial_remainder.is_empty()
446996
    }
    /// Consumes a number of items from the cursor's stream.
156948
    pub fn consume(&mut self, count: usize) {
156948
        if count == 0 {
            return;
156948
        }
156948
        let remainder_len = self.partial_remainder.len();
156948
        if count <= remainder_len {
            // Consuming only from the remainder.
            self.partial_remainder.drain(..count);
156948
        } else {
156948
            // Consuming all of the remainder and some from the main list.
156948
            let from_main_list = count - remainder_len;
156948
            self.partial_remainder.clear();
156948
            self.next_item_index += from_main_list;
156948
        }
156948
    }
    /// Looks ahead and returns the next "unbreakable" unit of content.
    /// This is typically a word (a series of non-space clusters) followed by a
    /// space, or just a single space if that's next.
    /// The definition of "unbreakable unit" depends on the word-break property.
    // a single typographic character unit (every character is a soft wrap opportunity), including
    // punctuation and preserved white spaces; currently handled via peek_next_single_item
283360
    pub fn peek_next_unit(&self) -> Vec<ShapedItem> {
283360
        let mut unit = Vec::new();
283360
        let mut source_items = self.partial_remainder.clone();
283360
        source_items.extend_from_slice(&self.items[self.next_item_index..]);
283360
        if source_items.is_empty() {
38500
            return unit;
244860
        }
        // If the first item is a break opportunity (like a space), it's a unit on its own.
244860
        if is_break_opportunity_with_word_break(&source_items[0], self.word_break, self.hyphens) {
42460
            unit.push(source_items[0].clone());
42460
            return unit;
202400
        }
        // Otherwise, collect all items until the next break opportunity.
        // For break-all: each cluster is its own unit.
        // For keep-all: CJK sequences are NOT break opportunities.
        // For normal: CJK characters are individual break opportunities.
        // glue items together: if the last cluster ends with a break-suppressing control,
        // the next item cannot be separated from it.
202400
        let mut suppress_next_break = false;
793012
        for (i, item) in source_items.iter().enumerate() {
            // Also suppress break if this item starts with a break-suppressing control
            // (WJ/ZWJ/ZWNBSP suppress breaks on both sides per Unicode line breaking)
793012
            let starts_with_suppress = if let ShapedItem::Cluster(c) = item {
792264
                c.text.chars().next().map_or(false, |ch| is_break_suppressing_control(ch))
            } else {
748
                false
            };
            // If the item is a CJK cluster, check if the break is allowed by strictness
793012
            let cjk_strictness_suppressed = if let ShapedItem::Cluster(c) = item {
792264
                c.text.chars().next().map_or(false, |ch| {
792264
                    !is_cjk_break_allowed_by_strictness(ch, None, self.line_break)
792264
                })
            } else {
748
                false
            };
793012
            if i > 0 && !suppress_next_break && !starts_with_suppress && !cjk_strictness_suppressed && is_break_opportunity_with_word_break(item, self.word_break, self.hyphens) {
63712
                break;
729300
            }
729300
            suppress_next_break = false;
729300
            unit.push(item.clone());
            // Check if this item ends with a break-suppressing control character
729300
            if let ShapedItem::Cluster(c) = item {
728948
                if let Some(last_ch) = c.text.chars().last() {
728948
                    if is_break_suppressing_control(last_ch) {
                        suppress_next_break = true;
728948
                    }
                }
352
            }
            // For break-all, each non-space cluster is a unit on its own
729300
            if self.word_break == WordBreak::BreakAll {
                if let ShapedItem::Cluster(_) = item {
                    break;
                }
729300
            }
        }
202400
        unit
283360
    }
    pub fn peek_next_single_item(&self) -> Vec<ShapedItem> {
        if !self.partial_remainder.is_empty() {
            return vec![self.partial_remainder[0].clone()];
        }
        if self.next_item_index < self.items.len() {
            return vec![self.items[self.next_item_index].clone()];
        }
        Vec::new()
    }
}
// A structured result from a hyphenation attempt.
struct HyphenationResult {
    /// The items that fit on the current line, including the new hyphen.
    line_part: Vec<ShapedItem>,
    /// The remainder of the split item to be carried over to the next line.
    remainder_part: Vec<ShapedItem>,
}
fn perform_bidi_analysis<'a, 'b: 'a>(
    styled_runs: &'a [TextRunInfo],
    full_text: &'b str,
    force_lang: Option<Language>,
) -> Result<(Vec<VisualRun<'a>>, BidiDirection), LayoutError> {
    if full_text.is_empty() {
        return Ok((Vec::new(), BidiDirection::Ltr));
    }
    let bidi_info = BidiInfo::new(full_text, None);
    let para = &bidi_info.paragraphs[0];
    let base_direction = if para.level.is_rtl() {
        BidiDirection::Rtl
    } else {
        BidiDirection::Ltr
    };
    // Create a map from each byte index to its original styled run.
    let mut byte_to_run_index: Vec<usize> = vec![0; full_text.len()];
    for (run_idx, run) in styled_runs.iter().enumerate() {
        let start = run.logical_start;
        let end = start + run.text.len();
        for i in start..end {
            byte_to_run_index[i] = run_idx;
        }
    }
    let mut final_visual_runs = Vec::new();
    let (levels, visual_run_ranges) = bidi_info.visual_runs(para, para.range.clone());
    for range in visual_run_ranges {
        let bidi_level = levels[range.start];
        let mut sub_run_start = range.start;
        // Iterate through the bytes of the visual run to detect style changes.
        for i in (range.start + 1)..range.end {
            if byte_to_run_index[i] != byte_to_run_index[sub_run_start] {
                // Style boundary found. Finalize the previous sub-run.
                let original_run_idx = byte_to_run_index[sub_run_start];
                let script = crate::text3::script::detect_script(&full_text[sub_run_start..i])
                    .unwrap_or(Script::Latin);
                final_visual_runs.push(VisualRun {
                    text_slice: &full_text[sub_run_start..i],
                    style: styled_runs[original_run_idx].style.clone(),
                    logical_start_byte: sub_run_start,
                    bidi_level: BidiLevel::new(bidi_level.number()),
                    language: force_lang.unwrap_or_else(|| {
                        crate::text3::script::script_to_language(
                            script,
                            &full_text[sub_run_start..i],
                        )
                    }),
                    script,
                });
                // Start a new sub-run.
                sub_run_start = i;
            }
        }
        // Add the last sub-run (or the only one if no style change occurred).
        let original_run_idx = byte_to_run_index[sub_run_start];
        let script = crate::text3::script::detect_script(&full_text[sub_run_start..range.end])
            .unwrap_or(Script::Latin);
        final_visual_runs.push(VisualRun {
            text_slice: &full_text[sub_run_start..range.end],
            style: styled_runs[original_run_idx].style.clone(),
            logical_start_byte: sub_run_start,
            bidi_level: BidiLevel::new(bidi_level.number()),
            script,
            language: force_lang.unwrap_or_else(|| {
                crate::text3::script::script_to_language(
                    script,
                    &full_text[sub_run_start..range.end],
                )
            }),
        });
    }
    Ok((final_visual_runs, base_direction))
}
fn get_justification_priority(class: CharacterClass) -> u8 {
    match class {
        CharacterClass::Space => 0,
        CharacterClass::Punctuation => 64,
        CharacterClass::Ideograph => 128,
        CharacterClass::Letter => 192,
        CharacterClass::Symbol => 224,
        CharacterClass::Combining => 255,
    }
}