Skip to main content

azul_layout/text3/
cache.rs

1use std::{
2    any::{Any, TypeId},
3    cmp::Ordering,
4    collections::{
5        hash_map::{DefaultHasher, Entry, HashMap},
6        BTreeSet,
7    },
8    hash::{Hash, Hasher},
9    mem::discriminant,
10    num::NonZeroUsize,
11    sync::{Arc, Mutex},
12};
13
14pub use azul_core::selection::{ContentIndex, GraphemeClusterId};
15use azul_core::{
16    dom::NodeId,
17    geom::{LogicalPosition, LogicalRect, LogicalSize},
18    resources::ImageRef,
19    selection::{CursorAffinity, SelectionRange, TextCursor},
20    ui_solver::GlyphInstance,
21};
22use azul_css::{
23    corety::LayoutDebugMessage, props::basic::ColorU, props::style::StyleBackgroundContent,
24};
25#[cfg(feature = "text_layout_hyphenation")]
26use hyphenation::{Hyphenator, Language as HyphenationLanguage, Load, Standard};
27use rust_fontconfig::{FcFontCache, FcPattern, FcWeight, FontId, PatternMatch, UnicodeRange};
28use unicode_bidi::{BidiInfo, Level, TextSource};
29use unicode_segmentation::UnicodeSegmentation;
30
31// Stub type when hyphenation is disabled
32#[cfg(not(feature = "text_layout_hyphenation"))]
33pub struct Standard;
34
35#[cfg(not(feature = "text_layout_hyphenation"))]
36impl Standard {
37    /// Stub hyphenate method that returns no breaks
38    pub fn hyphenate<'a>(&'a self, _word: &'a str) -> StubHyphenationBreaks {
39        StubHyphenationBreaks { breaks: Vec::new() }
40    }
41}
42
43/// Result of hyphenation (stub when feature is disabled)
44#[cfg(not(feature = "text_layout_hyphenation"))]
45pub struct StubHyphenationBreaks {
46    pub breaks: alloc::vec::Vec<usize>,
47}
48
49// Always import Language from script module
50use crate::text3::script::{script_to_language, Language, Script};
51
52/// Available space for layout, similar to Taffy's AvailableSpace.
53///
54/// This type explicitly represents the three possible states for available space:
55///
56/// - `Definite(f32)`: A specific pixel width is available
57/// - `MinContent`: Layout should use minimum content width (shrink-wrap)
58/// - `MaxContent`: Layout should use maximum content width (no line breaks unless necessary)
59///
60/// This is critical for proper handling of intrinsic sizing in Flexbox/Grid
61/// where the available space may be indefinite during the measure phase.
62#[derive(Debug, Clone, Copy, PartialEq)]
63pub enum AvailableSpace {
64    /// A specific amount of space is available (in pixels)
65    Definite(f32),
66    /// The node should be laid out under a min-content constraint
67    MinContent,
68    /// The node should be laid out under a max-content constraint  
69    MaxContent,
70}
71
72impl Default for AvailableSpace {
73    fn default() -> Self {
74        AvailableSpace::Definite(0.0)
75    }
76}
77
78impl AvailableSpace {
79    /// Returns true if this is a definite (finite, known) amount of space
80    pub fn is_definite(&self) -> bool {
81        matches!(self, AvailableSpace::Definite(_))
82    }
83
84    /// Returns true if this is an indefinite (min-content or max-content) constraint
85    pub fn is_indefinite(&self) -> bool {
86        !self.is_definite()
87    }
88
89    /// Returns the definite value if available, or a fallback for indefinite constraints
90    pub fn unwrap_or(self, fallback: f32) -> f32 {
91        match self {
92            AvailableSpace::Definite(v) => v,
93            _ => fallback,
94        }
95    }
96
97    /// Returns the definite value, or 0.0 for min-content, or f32::MAX for max-content
98    pub fn to_f32_for_layout(self) -> f32 {
99        match self {
100            AvailableSpace::Definite(v) => v,
101            AvailableSpace::MinContent => 0.0,
102            AvailableSpace::MaxContent => f32::MAX,
103        }
104    }
105
106    /// Create from an f32 value, recognizing special sentinel values.
107    ///
108    /// This function provides backwards compatibility with code that uses f32 for constraints:
109    /// - `f32::INFINITY` or `f32::MAX` → `MaxContent` (no line wrapping)
110    /// - `0.0` → `MinContent` (maximum line wrapping, return longest word width)
111    /// - Other values → `Definite(value)`
112    ///
113    /// Note: Using sentinel values like 0.0 for MinContent is fragile. Prefer using
114    /// `AvailableSpace::MinContent` directly when possible.
115    pub fn from_f32(value: f32) -> Self {
116        if value.is_infinite() || value >= f32::MAX / 2.0 {
117            // Treat very large values (including f32::MAX) as MaxContent
118            AvailableSpace::MaxContent
119        } else if value <= 0.0 {
120            // Treat zero or negative as MinContent (shrink-wrap)
121            AvailableSpace::MinContent
122        } else {
123            AvailableSpace::Definite(value)
124        }
125    }
126}
127
128impl Hash for AvailableSpace {
129    fn hash<H: Hasher>(&self, state: &mut H) {
130        std::mem::discriminant(self).hash(state);
131        if let AvailableSpace::Definite(v) = self {
132            (v.round() as usize).hash(state);
133        }
134    }
135}
136
137// Re-export traits for backwards compatibility
138pub use crate::font_traits::{ParsedFontTrait, ShallowClone};
139
140// --- Core Data Structures for the New Architecture ---
141
142/// Key for caching font chains - based only on CSS properties, not text content
143#[derive(Debug, Clone, PartialEq, Eq, Hash)]
144pub struct FontChainKey {
145    pub font_families: Vec<String>,
146    pub weight: FcWeight,
147    pub italic: bool,
148    pub oblique: bool,
149}
150
151/// Either a FontChainKey (resolved via fontconfig) or a direct FontRef hash.
152/// 
153/// This enum cleanly separates:
154/// - `Chain`: Fonts resolved through fontconfig with fallback support
155/// - `Ref`: Direct FontRef that bypasses fontconfig entirely (e.g., embedded icon fonts)
156#[derive(Debug, Clone, PartialEq, Eq, Hash)]
157pub enum FontChainKeyOrRef {
158    /// Regular font chain resolved via fontconfig
159    Chain(FontChainKey),
160    /// Direct FontRef identified by pointer address (covers entire Unicode range, no fallbacks)
161    Ref(usize),
162}
163
164impl FontChainKeyOrRef {
165    /// Create from a FontStack enum
166    pub fn from_font_stack(font_stack: &FontStack) -> Self {
167        match font_stack {
168            FontStack::Stack(selectors) => FontChainKeyOrRef::Chain(FontChainKey::from_selectors(selectors)),
169            FontStack::Ref(font_ref) => FontChainKeyOrRef::Ref(font_ref.parsed as usize),
170        }
171    }
172    
173    /// Returns true if this is a direct FontRef
174    pub fn is_ref(&self) -> bool {
175        matches!(self, FontChainKeyOrRef::Ref(_))
176    }
177    
178    /// Returns the FontRef pointer if this is a Ref variant
179    pub fn as_ref_ptr(&self) -> Option<usize> {
180        match self {
181            FontChainKeyOrRef::Ref(ptr) => Some(*ptr),
182            _ => None,
183        }
184    }
185    
186    /// Returns the FontChainKey if this is a Chain variant
187    pub fn as_chain(&self) -> Option<&FontChainKey> {
188        match self {
189            FontChainKeyOrRef::Chain(key) => Some(key),
190            _ => None,
191        }
192    }
193}
194
195impl FontChainKey {
196    /// Create a FontChainKey from a slice of font selectors
197    pub fn from_selectors(font_stack: &[FontSelector]) -> Self {
198        let font_families: Vec<String> = font_stack
199            .iter()
200            .map(|s| s.family.clone())
201            .filter(|f| !f.is_empty())
202            .collect();
203
204        let font_families = if font_families.is_empty() {
205            vec!["serif".to_string()]
206        } else {
207            font_families
208        };
209
210        let weight = font_stack
211            .first()
212            .map(|s| s.weight)
213            .unwrap_or(FcWeight::Normal);
214        let is_italic = font_stack
215            .first()
216            .map(|s| s.style == FontStyle::Italic)
217            .unwrap_or(false);
218        let is_oblique = font_stack
219            .first()
220            .map(|s| s.style == FontStyle::Oblique)
221            .unwrap_or(false);
222
223        FontChainKey {
224            font_families,
225            weight,
226            italic: is_italic,
227            oblique: is_oblique,
228        }
229    }
230}
231
232/// A map of pre-loaded fonts, keyed by FontId (from rust-fontconfig)
233///
234/// This is passed to the shaper - no font loading happens during shaping
235/// The fonts are loaded BEFORE layout based on the font chains and text content.
236///
237/// Provides both FontId and hash-based lookup for efficient glyph operations.
238#[derive(Debug, Clone)]
239pub struct LoadedFonts<T> {
240    /// Primary storage: FontId -> Font
241    pub fonts: HashMap<FontId, T>,
242    /// Reverse index: font_hash -> FontId for fast hash-based lookups
243    hash_to_id: HashMap<u64, FontId>,
244}
245
246impl<T: ParsedFontTrait> LoadedFonts<T> {
247    pub fn new() -> Self {
248        Self {
249            fonts: HashMap::new(),
250            hash_to_id: HashMap::new(),
251        }
252    }
253
254    /// Insert a font with its FontId
255    pub fn insert(&mut self, font_id: FontId, font: T) {
256        let hash = font.get_hash();
257        self.hash_to_id.insert(hash, font_id.clone());
258        self.fonts.insert(font_id, font);
259    }
260
261    /// Get a font by FontId
262    pub fn get(&self, font_id: &FontId) -> Option<&T> {
263        self.fonts.get(font_id)
264    }
265
266    /// Get a font by its hash
267    pub fn get_by_hash(&self, hash: u64) -> Option<&T> {
268        self.hash_to_id.get(&hash).and_then(|id| self.fonts.get(id))
269    }
270
271    /// Get the FontId for a hash
272    pub fn get_font_id_by_hash(&self, hash: u64) -> Option<&FontId> {
273        self.hash_to_id.get(&hash)
274    }
275
276    /// Check if a FontId is present
277    pub fn contains_key(&self, font_id: &FontId) -> bool {
278        self.fonts.contains_key(font_id)
279    }
280
281    /// Check if a hash is present
282    pub fn contains_hash(&self, hash: u64) -> bool {
283        self.hash_to_id.contains_key(&hash)
284    }
285
286    /// Iterate over all fonts
287    pub fn iter(&self) -> impl Iterator<Item = (&FontId, &T)> {
288        self.fonts.iter()
289    }
290
291    /// Get the number of loaded fonts
292    pub fn len(&self) -> usize {
293        self.fonts.len()
294    }
295
296    /// Check if empty
297    pub fn is_empty(&self) -> bool {
298        self.fonts.is_empty()
299    }
300}
301
302impl<T: ParsedFontTrait> Default for LoadedFonts<T> {
303    fn default() -> Self {
304        Self::new()
305    }
306}
307
308impl<T: ParsedFontTrait> FromIterator<(FontId, T)> for LoadedFonts<T> {
309    fn from_iter<I: IntoIterator<Item = (FontId, T)>>(iter: I) -> Self {
310        let mut loaded = LoadedFonts::new();
311        for (id, font) in iter {
312            loaded.insert(id, font);
313        }
314        loaded
315    }
316}
317
318/// Enum that wraps either a fontconfig-resolved font (T) or a direct FontRef.
319///
320/// This allows the shaping code to handle both fontconfig-resolved fonts
321/// and embedded fonts (FontRef) uniformly through the ParsedFontTrait interface.
322#[derive(Debug, Clone)]
323pub enum FontOrRef<T> {
324    /// A font loaded via fontconfig
325    Font(T),
326    /// A direct FontRef (embedded font, bypasses fontconfig)
327    Ref(azul_css::props::basic::FontRef),
328}
329
330impl<T: ParsedFontTrait> ShallowClone for FontOrRef<T> {
331    fn shallow_clone(&self) -> Self {
332        match self {
333            FontOrRef::Font(f) => FontOrRef::Font(f.shallow_clone()),
334            FontOrRef::Ref(r) => FontOrRef::Ref(r.clone()),
335        }
336    }
337}
338
339impl<T: ParsedFontTrait> ParsedFontTrait for FontOrRef<T> {
340    fn shape_text(
341        &self,
342        text: &str,
343        script: Script,
344        language: Language,
345        direction: BidiDirection,
346        style: &StyleProperties,
347    ) -> Result<Vec<Glyph>, LayoutError> {
348        match self {
349            FontOrRef::Font(f) => f.shape_text(text, script, language, direction, style),
350            FontOrRef::Ref(r) => r.shape_text(text, script, language, direction, style),
351        }
352    }
353
354    fn get_hash(&self) -> u64 {
355        match self {
356            FontOrRef::Font(f) => f.get_hash(),
357            FontOrRef::Ref(r) => r.get_hash(),
358        }
359    }
360
361    fn get_glyph_size(&self, glyph_id: u16, font_size: f32) -> Option<LogicalSize> {
362        match self {
363            FontOrRef::Font(f) => f.get_glyph_size(glyph_id, font_size),
364            FontOrRef::Ref(r) => r.get_glyph_size(glyph_id, font_size),
365        }
366    }
367
368    fn get_hyphen_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
369        match self {
370            FontOrRef::Font(f) => f.get_hyphen_glyph_and_advance(font_size),
371            FontOrRef::Ref(r) => r.get_hyphen_glyph_and_advance(font_size),
372        }
373    }
374
375    fn get_kashida_glyph_and_advance(&self, font_size: f32) -> Option<(u16, f32)> {
376        match self {
377            FontOrRef::Font(f) => f.get_kashida_glyph_and_advance(font_size),
378            FontOrRef::Ref(r) => r.get_kashida_glyph_and_advance(font_size),
379        }
380    }
381
382    fn has_glyph(&self, codepoint: u32) -> bool {
383        match self {
384            FontOrRef::Font(f) => f.has_glyph(codepoint),
385            FontOrRef::Ref(r) => r.has_glyph(codepoint),
386        }
387    }
388
389    fn get_vertical_metrics(&self, glyph_id: u16) -> Option<VerticalMetrics> {
390        match self {
391            FontOrRef::Font(f) => f.get_vertical_metrics(glyph_id),
392            FontOrRef::Ref(r) => r.get_vertical_metrics(glyph_id),
393        }
394    }
395
396    fn get_font_metrics(&self) -> LayoutFontMetrics {
397        match self {
398            FontOrRef::Font(f) => f.get_font_metrics(),
399            FontOrRef::Ref(r) => r.get_font_metrics(),
400        }
401    }
402
403    fn num_glyphs(&self) -> u16 {
404        match self {
405            FontOrRef::Font(f) => f.num_glyphs(),
406            FontOrRef::Ref(r) => r.num_glyphs(),
407        }
408    }
409}
410
411#[derive(Debug)]
412pub struct FontManager<T> {
413    ///  Cache that holds the **file paths** of the fonts (not any font data itself)
414    pub fc_cache: Arc<FcFontCache>,
415    /// Holds the actual parsed font (usually with the font bytes attached)
416    pub parsed_fonts: Mutex<HashMap<FontId, T>>,
417    // Cache for font chains - populated by resolve_all_font_chains() before layout
418    // This is read-only during layout - no locking needed for reads
419    pub font_chain_cache: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
420    /// Cache for direct FontRefs (embedded fonts like Material Icons)
421    /// These are fonts referenced via FontStack::Ref that bypass fontconfig
422    pub embedded_fonts: Mutex<HashMap<u64, azul_css::props::basic::FontRef>>,
423}
424
425impl<T: ParsedFontTrait> FontManager<T> {
426    pub fn new(fc_cache: FcFontCache) -> Result<Self, LayoutError> {
427        Ok(Self {
428            fc_cache: Arc::new(fc_cache),
429            parsed_fonts: Mutex::new(HashMap::new()),
430            font_chain_cache: HashMap::new(), // Populated via set_font_chain_cache()
431            embedded_fonts: Mutex::new(HashMap::new()),
432        })
433    }
434
435    /// Set the font chain cache from externally resolved chains
436    ///
437    /// This should be called with the result of `resolve_font_chains()` or
438    /// `collect_and_resolve_font_chains()` from `solver3::getters`.
439    pub fn set_font_chain_cache(
440        &mut self,
441        chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
442    ) {
443        self.font_chain_cache = chains;
444    }
445
446    /// Merge additional font chains into the existing cache
447    ///
448    /// Useful when processing multiple DOMs that may have different font requirements.
449    pub fn merge_font_chain_cache(
450        &mut self,
451        chains: HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
452    ) {
453        self.font_chain_cache.extend(chains);
454    }
455
456    /// Get a reference to the font chain cache
457    pub fn get_font_chain_cache(
458        &self,
459    ) -> &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain> {
460        &self.font_chain_cache
461    }
462
463    /// Get an embedded font by its hash (used for WebRender registration)
464    /// Returns the FontRef if it exists in the embedded_fonts cache.
465    pub fn get_embedded_font_by_hash(&self, font_hash: u64) -> Option<azul_css::props::basic::FontRef> {
466        let embedded = self.embedded_fonts.lock().unwrap();
467        embedded.get(&font_hash).cloned()
468    }
469
470    /// Get a parsed font by its hash (used for WebRender registration)
471    /// Returns the parsed font if it exists in the parsed_fonts cache.
472    pub fn get_font_by_hash(&self, font_hash: u64) -> Option<T> {
473        let parsed = self.parsed_fonts.lock().unwrap();
474        // Linear search through all cached fonts to find one with matching hash
475        for (_, font) in parsed.iter() {
476            if font.get_hash() == font_hash {
477                return Some(font.clone());
478            }
479        }
480        None
481    }
482
483    /// Register an embedded FontRef for later lookup by hash
484    /// This is called when using FontStack::Ref during shaping
485    pub fn register_embedded_font(&self, font_ref: &azul_css::props::basic::FontRef) {
486        let hash = font_ref.get_hash();
487        let mut embedded = self.embedded_fonts.lock().unwrap();
488        embedded.insert(hash, font_ref.clone());
489    }
490
491    /// Get a snapshot of all currently loaded fonts
492    ///
493    /// This returns a copy of all parsed fonts, which can be passed to the shaper.
494    /// No locking is required after this call - the returned HashMap is independent.
495    ///
496    /// NOTE: This should be called AFTER loading all required fonts for a layout pass.
497    pub fn get_loaded_fonts(&self) -> LoadedFonts<T> {
498        let parsed = self.parsed_fonts.lock().unwrap();
499        parsed
500            .iter()
501            .map(|(id, font)| (id.clone(), font.shallow_clone()))
502            .collect()
503    }
504
505    /// Get the set of FontIds that are currently loaded
506    ///
507    /// This is useful for computing which fonts need to be loaded
508    /// (diff with required fonts).
509    pub fn get_loaded_font_ids(&self) -> std::collections::HashSet<FontId> {
510        let parsed = self.parsed_fonts.lock().unwrap();
511        parsed.keys().cloned().collect()
512    }
513
514    /// Insert a loaded font into the cache
515    ///
516    /// Returns the old font if one was already present for this FontId.
517    pub fn insert_font(&self, font_id: FontId, font: T) -> Option<T> {
518        let mut parsed = self.parsed_fonts.lock().unwrap();
519        parsed.insert(font_id, font)
520    }
521
522    /// Insert multiple loaded fonts into the cache
523    ///
524    /// This is more efficient than calling `insert_font` multiple times
525    /// because it only acquires the lock once.
526    pub fn insert_fonts(&self, fonts: impl IntoIterator<Item = (FontId, T)>) {
527        let mut parsed = self.parsed_fonts.lock().unwrap();
528        for (font_id, font) in fonts {
529            parsed.insert(font_id, font);
530        }
531    }
532
533    /// Remove a font from the cache
534    ///
535    /// Returns the removed font if it was present.
536    pub fn remove_font(&self, font_id: &FontId) -> Option<T> {
537        let mut parsed = self.parsed_fonts.lock().unwrap();
538        parsed.remove(font_id)
539    }
540}
541
542// Error handling
543#[derive(Debug, thiserror::Error)]
544pub enum LayoutError {
545    #[error("Bidi analysis failed: {0}")]
546    BidiError(String),
547    #[error("Shaping failed: {0}")]
548    ShapingError(String),
549    #[error("Font not found: {0:?}")]
550    FontNotFound(FontSelector),
551    #[error("Invalid text input: {0}")]
552    InvalidText(String),
553    #[error("Hyphenation failed: {0}")]
554    HyphenationError(String),
555}
556
557/// Text boundary types for cursor movement
558#[derive(Debug, Clone, Copy, PartialEq, Eq)]
559pub enum TextBoundary {
560    /// Reached top of text (first line)
561    Top,
562    /// Reached bottom of text (last line)
563    Bottom,
564    /// Reached start of text (first character)
565    Start,
566    /// Reached end of text (last character)
567    End,
568}
569
570/// Error returned when cursor movement hits a boundary
571#[derive(Debug, Clone, Copy, PartialEq, Eq)]
572pub struct CursorBoundsError {
573    /// The boundary that was hit
574    pub boundary: TextBoundary,
575    /// The cursor position (unchanged from input)
576    pub cursor: TextCursor,
577}
578
579/// Unified constraints combining all layout features
580///
581/// # CSS Inline Layout Module Level 3: Constraint Mapping
582///
583/// This structure maps CSS properties to layout constraints:
584///
585/// ## \u00a7 2.1 Layout of Line Boxes
586/// - `available_width`: \u26a0\ufe0f CRITICAL - Should equal containing block's inner width
587///   * Currently defaults to 0.0 which causes immediate line breaking
588///   * Per spec: "logical width of a line box is equal to the inner logical width of its containing
589///     block"
590/// - `available_height`: For block-axis constraints (max-height)
591///
592/// ## \u00a7 2.2 Layout Within Line Boxes
593/// - `text_align`: \u2705 Horizontal alignment (start, end, center, justify)
594/// - `vertical_align`: \u26a0\ufe0f PARTIAL - Only baseline supported, missing:
595///   * top, bottom, middle, text-top, text-bottom
596///   * <length>, <percentage> values
597///   * sub, super positions
598/// - `line_height`: \u2705 Distance between baselines
599///
600/// ## \u00a7 3 Baselines and Alignment Metrics
601/// - `text_orientation`: \u2705 For vertical writing (sideways, upright)
602/// - `writing_mode`: \u2705 horizontal-tb, vertical-rl, vertical-lr
603/// - `direction`: \u2705 ltr, rtl for BiDi
604///
605/// ## \u00a7 4 Baseline Alignment (vertical-align property)
606/// \u26a0\ufe0f INCOMPLETE: Only basic baseline alignment implemented
607///
608/// ## \u00a7 5 Line Spacing (line-height property)
609/// - `line_height`: \u2705 Implemented
610/// - \u274c MISSING: line-fit-edge for controlling which edges contribute to line height
611///
612/// ## \u00a7 6 Trimming Leading (text-box-trim)
613/// - \u274c NOT IMPLEMENTED: text-box-trim property
614/// - \u274c NOT IMPLEMENTED: text-box-edge property
615///
616/// ## CSS Text Module Level 3
617/// - `text_indent`: \u2705 First line indentation
618/// - `text_justify`: \u2705 Justification algorithm (auto, inter-word, inter-character)
619/// - `hyphenation`: \u2705 Automatic hyphenation
620/// - `hanging_punctuation`: \u2705 Hanging punctuation at line edges
621///
622/// ## CSS Text Level 4
623/// - `text_wrap`: \u2705 balance, pretty, stable
624/// - `line_clamp`: \u2705 Max number of lines
625///
626/// ## CSS Writing Modes Level 4
627/// - `text_combine_upright`: \u2705 Tate-chu-yoko for vertical text
628///
629/// ## CSS Shapes Module
630/// - `shape_boundaries`: \u2705 Custom line box shapes
631/// - `shape_exclusions`: \u2705 Exclusion areas (float-like behavior)
632/// - `exclusion_margin`: \u2705 Margin around exclusions
633///
634/// ## Multi-column Layout
635/// - `columns`: \u2705 Number of columns
636/// - `column_gap`: \u2705 Gap between columns
637///
638/// # Known Issues:
639/// 1. [ISSUE] available_width defaults to Definite(0.0) instead of containing block width
640/// 2. [ISSUE] vertical_align only supports baseline
641/// 3. [TODO] initial-letter (drop caps) not implemented
642#[derive(Debug, Clone)]
643pub struct UnifiedConstraints {
644    // Shape definition
645    pub shape_boundaries: Vec<ShapeBoundary>,
646    pub shape_exclusions: Vec<ShapeBoundary>,
647
648    // Basic layout - using AvailableSpace for proper indefinite handling
649    pub available_width: AvailableSpace,
650    pub available_height: Option<f32>,
651
652    // Text layout
653    pub writing_mode: Option<WritingMode>,
654    // Base direction from CSS, overrides auto-detection
655    pub direction: Option<BidiDirection>,
656    pub text_orientation: TextOrientation,
657    pub text_align: TextAlign,
658    pub text_justify: JustifyContent,
659    pub line_height: f32,
660    pub vertical_align: VerticalAlign,
661
662    // Overflow handling
663    pub overflow: OverflowBehavior,
664    pub segment_alignment: SegmentAlignment,
665
666    // Advanced features
667    pub text_combine_upright: Option<TextCombineUpright>,
668    pub exclusion_margin: f32,
669    pub hyphenation: bool,
670    pub hyphenation_language: Option<Language>,
671    pub text_indent: f32,
672    pub initial_letter: Option<InitialLetter>,
673    pub line_clamp: Option<NonZeroUsize>,
674
675    // text-wrap: balance
676    pub text_wrap: TextWrap,
677    pub columns: u32,
678    pub column_gap: f32,
679    pub hanging_punctuation: bool,
680}
681
682impl Default for UnifiedConstraints {
683    fn default() -> Self {
684        Self {
685            shape_boundaries: Vec::new(),
686            shape_exclusions: Vec::new(),
687
688            // Use MaxContent as default to avoid premature line breaking.
689            // MaxContent means "use intrinsic width" which is appropriate when
690            // the containing block's width is not yet known.
691            // Previously this was Definite(0.0) which caused each character to
692            // wrap to its own line. The actual width should be passed from the 
693            // box layout solver (fc.rs) when creating UnifiedConstraints.
694            available_width: AvailableSpace::MaxContent,
695            available_height: None,
696            writing_mode: None,
697            direction: None, // Will default to LTR if not specified
698            text_orientation: TextOrientation::default(),
699            text_align: TextAlign::default(),
700            text_justify: JustifyContent::default(),
701            line_height: 16.0, // A more sensible default
702            vertical_align: VerticalAlign::default(),
703            overflow: OverflowBehavior::default(),
704            segment_alignment: SegmentAlignment::default(),
705            text_combine_upright: None,
706            exclusion_margin: 0.0,
707            hyphenation: false,
708            hyphenation_language: None,
709            columns: 1,
710            column_gap: 0.0,
711            hanging_punctuation: false,
712            text_indent: 0.0,
713            initial_letter: None,
714            line_clamp: None,
715            text_wrap: TextWrap::default(),
716        }
717    }
718}
719
720// UnifiedConstraints
721impl Hash for UnifiedConstraints {
722    fn hash<H: Hasher>(&self, state: &mut H) {
723        self.shape_boundaries.hash(state);
724        self.shape_exclusions.hash(state);
725        self.available_width.hash(state);
726        self.available_height
727            .map(|h| h.round() as usize)
728            .hash(state);
729        self.writing_mode.hash(state);
730        self.direction.hash(state);
731        self.text_orientation.hash(state);
732        self.text_align.hash(state);
733        self.text_justify.hash(state);
734        (self.line_height.round() as usize).hash(state);
735        self.vertical_align.hash(state);
736        self.overflow.hash(state);
737        self.text_combine_upright.hash(state);
738        (self.exclusion_margin.round() as usize).hash(state);
739        self.hyphenation.hash(state);
740        self.hyphenation_language.hash(state);
741        self.columns.hash(state);
742        (self.column_gap.round() as usize).hash(state);
743        self.hanging_punctuation.hash(state);
744    }
745}
746
747impl PartialEq for UnifiedConstraints {
748    fn eq(&self, other: &Self) -> bool {
749        self.shape_boundaries == other.shape_boundaries
750            && self.shape_exclusions == other.shape_exclusions
751            && self.available_width == other.available_width
752            && match (self.available_height, other.available_height) {
753                (None, None) => true,
754                (Some(h1), Some(h2)) => round_eq(h1, h2),
755                _ => false,
756            }
757            && self.writing_mode == other.writing_mode
758            && self.direction == other.direction
759            && self.text_orientation == other.text_orientation
760            && self.text_align == other.text_align
761            && self.text_justify == other.text_justify
762            && round_eq(self.line_height, other.line_height)
763            && self.vertical_align == other.vertical_align
764            && self.overflow == other.overflow
765            && self.text_combine_upright == other.text_combine_upright
766            && round_eq(self.exclusion_margin, other.exclusion_margin)
767            && self.hyphenation == other.hyphenation
768            && self.hyphenation_language == other.hyphenation_language
769            && self.columns == other.columns
770            && round_eq(self.column_gap, other.column_gap)
771            && self.hanging_punctuation == other.hanging_punctuation
772    }
773}
774
775impl Eq for UnifiedConstraints {}
776
777impl UnifiedConstraints {
778    fn direction(&self, fallback: BidiDirection) -> BidiDirection {
779        match self.writing_mode {
780            Some(s) => s.get_direction().unwrap_or(fallback),
781            None => fallback,
782        }
783    }
784    fn is_vertical(&self) -> bool {
785        matches!(
786            self.writing_mode,
787            Some(WritingMode::VerticalRl) | Some(WritingMode::VerticalLr)
788        )
789    }
790}
791
792/// Line constraints with multi-segment support
793#[derive(Debug, Clone)]
794pub struct LineConstraints {
795    pub segments: Vec<LineSegment>,
796    pub total_available: f32,
797}
798
799impl WritingMode {
800    fn get_direction(&self) -> Option<BidiDirection> {
801        match self {
802            // determined by text content
803            WritingMode::HorizontalTb => None,
804            WritingMode::VerticalRl => Some(BidiDirection::Rtl),
805            WritingMode::VerticalLr => Some(BidiDirection::Ltr),
806            WritingMode::SidewaysRl => Some(BidiDirection::Rtl),
807            WritingMode::SidewaysLr => Some(BidiDirection::Ltr),
808        }
809    }
810}
811
812// Stage 1: Collection - Styled runs from DOM traversal
813#[derive(Debug, Clone, Hash)]
814pub struct StyledRun {
815    pub text: String,
816    pub style: Arc<StyleProperties>,
817    /// Byte index in the original logical paragraph text
818    pub logical_start_byte: usize,
819    /// The DOM NodeId of the Text node this run came from.
820    /// None for generated content (e.g., list markers, ::before/::after).
821    pub source_node_id: Option<NodeId>,
822}
823
824// Stage 2: Bidi Analysis - Visual runs in display order
825#[derive(Debug, Clone)]
826pub struct VisualRun<'a> {
827    pub text_slice: &'a str,
828    pub style: Arc<StyleProperties>,
829    pub logical_start_byte: usize,
830    pub bidi_level: BidiLevel,
831    pub script: Script,
832    pub language: Language,
833}
834
835// Font and styling types
836
837/// A selector for loading fonts from the font cache.
838/// Used by FontManager to query fontconfig and load font files.
839#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
840pub struct FontSelector {
841    pub family: String,
842    pub weight: FcWeight,
843    pub style: FontStyle,
844    pub unicode_ranges: Vec<UnicodeRange>,
845}
846
847impl Default for FontSelector {
848    fn default() -> Self {
849        Self {
850            family: "serif".to_string(),
851            weight: FcWeight::Normal,
852            style: FontStyle::Normal,
853            unicode_ranges: Vec::new(),
854        }
855    }
856}
857
858/// Font stack that can be either a list of font selectors (resolved via fontconfig)
859/// or a direct FontRef (bypasses fontconfig entirely).
860///
861/// When a `FontRef` is used, it bypasses fontconfig resolution entirely
862/// and uses the pre-parsed font data directly. This is used for embedded
863/// fonts like Material Icons.
864#[derive(Debug, Clone)]
865pub enum FontStack {
866    /// A stack of font selectors to be resolved via fontconfig
867    /// First font is primary, rest are fallbacks
868    Stack(Vec<FontSelector>),
869    /// A direct reference to a pre-parsed font (e.g., embedded icon fonts)
870    /// This font covers the entire Unicode range and has no fallbacks.
871    Ref(azul_css::props::basic::font::FontRef),
872}
873
874impl Default for FontStack {
875    fn default() -> Self {
876        FontStack::Stack(vec![FontSelector::default()])
877    }
878}
879
880impl FontStack {
881    /// Returns true if this is a direct FontRef
882    pub fn is_ref(&self) -> bool {
883        matches!(self, FontStack::Ref(_))
884    }
885
886    /// Returns the FontRef if this is a Ref variant
887    pub fn as_ref(&self) -> Option<&azul_css::props::basic::font::FontRef> {
888        match self {
889            FontStack::Ref(r) => Some(r),
890            _ => None,
891        }
892    }
893
894    /// Returns the font selectors if this is a Stack variant
895    pub fn as_stack(&self) -> Option<&[FontSelector]> {
896        match self {
897            FontStack::Stack(s) => Some(s),
898            _ => None,
899        }
900    }
901
902    /// Returns the first FontSelector if this is a Stack variant, None if Ref
903    pub fn first_selector(&self) -> Option<&FontSelector> {
904        match self {
905            FontStack::Stack(s) => s.first(),
906            FontStack::Ref(_) => None,
907        }
908    }
909
910    /// Returns the first font family name (for Stack) or a placeholder (for Ref)
911    pub fn first_family(&self) -> &str {
912        match self {
913            FontStack::Stack(s) => s.first().map(|f| f.family.as_str()).unwrap_or("serif"),
914            FontStack::Ref(_) => "<embedded-font>",
915        }
916    }
917}
918
919impl PartialEq for FontStack {
920    fn eq(&self, other: &Self) -> bool {
921        match (self, other) {
922            (FontStack::Stack(a), FontStack::Stack(b)) => a == b,
923            (FontStack::Ref(a), FontStack::Ref(b)) => a.parsed == b.parsed,
924            _ => false,
925        }
926    }
927}
928
929impl Eq for FontStack {}
930
931impl Hash for FontStack {
932    fn hash<H: Hasher>(&self, state: &mut H) {
933        core::mem::discriminant(self).hash(state);
934        match self {
935            FontStack::Stack(s) => s.hash(state),
936            FontStack::Ref(r) => (r.parsed as usize).hash(state),
937        }
938    }
939}
940
941/// A reference to a font for rendering, identified by its hash.
942/// This hash corresponds to ParsedFont::hash and is used to look up
943/// the actual font data in the renderer's font cache.
944#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
945pub struct FontHash {
946    /// The hash of the ParsedFont. 0 means invalid/unknown font.
947    pub font_hash: u64,
948}
949
950impl FontHash {
951    pub fn invalid() -> Self {
952        Self { font_hash: 0 }
953    }
954
955    pub fn from_hash(font_hash: u64) -> Self {
956        Self { font_hash }
957    }
958}
959
960#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
961pub enum FontStyle {
962    Normal,
963    Italic,
964    Oblique,
965}
966
967/// Defines how text should be aligned when a line contains multiple disjoint segments.
968#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
969pub enum SegmentAlignment {
970    /// Align text within the first available segment on the line.
971    #[default]
972    First,
973    /// Align text relative to the total available width of all
974    /// segments on the line combined.
975    Total,
976}
977
978#[derive(Debug, Clone)]
979pub struct VerticalMetrics {
980    pub advance: f32,
981    pub bearing_x: f32,
982    pub bearing_y: f32,
983    pub origin_y: f32,
984}
985
986/// Layout-specific font metrics extracted from FontMetrics
987/// Contains only the metrics needed for text layout and rendering
988#[derive(Debug, Clone)]
989pub struct LayoutFontMetrics {
990    pub ascent: f32,
991    pub descent: f32,
992    pub line_gap: f32,
993    pub units_per_em: u16,
994}
995
996impl LayoutFontMetrics {
997    pub fn baseline_scaled(&self, font_size: f32) -> f32 {
998        let scale = font_size / self.units_per_em as f32;
999        self.ascent * scale
1000    }
1001
1002    /// Convert from full FontMetrics to layout-specific metrics
1003    pub fn from_font_metrics(metrics: &azul_css::props::basic::FontMetrics) -> Self {
1004        Self {
1005            ascent: metrics.ascender as f32,
1006            descent: metrics.descender as f32,
1007            line_gap: metrics.line_gap as f32,
1008            units_per_em: metrics.units_per_em,
1009        }
1010    }
1011}
1012
1013#[derive(Debug, Clone)]
1014pub struct LineSegment {
1015    pub start_x: f32,
1016    pub width: f32,
1017    // For choosing best segment when multiple available
1018    pub priority: u8,
1019}
1020
1021#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
1022pub enum TextWrap {
1023    #[default]
1024    Wrap,
1025    Balance,
1026    NoWrap,
1027}
1028
1029// initial-letter
1030#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
1031pub struct InitialLetter {
1032    /// How many lines tall the initial letter should be.
1033    pub size: f32,
1034    /// How many lines the letter should sink into.
1035    pub sink: u32,
1036    /// How many characters to apply this styling to.
1037    pub count: NonZeroUsize,
1038}
1039
1040// A type that implements `Hash` must also implement `Eq`.
1041// Since f32 does not implement `Eq`, we provide a manual implementation.
1042// This is a marker trait, indicating that `a == b` is a true equivalence
1043// relation. The derived `PartialEq` already satisfies this.
1044impl Eq for InitialLetter {}
1045
1046impl Hash for InitialLetter {
1047    fn hash<H: Hasher>(&self, state: &mut H) {
1048        // Per the request, round the f32 to a usize for hashing.
1049        // This is a lossy conversion; values like 2.3 and 2.4 will produce
1050        // the same hash value for this field. This is acceptable as long as
1051        // the `PartialEq` implementation correctly distinguishes them.
1052        (self.size.round() as usize).hash(state);
1053        self.sink.hash(state);
1054        self.count.hash(state);
1055    }
1056}
1057
1058// Path and shape definitions
1059#[derive(Debug, Clone, PartialOrd)]
1060pub enum PathSegment {
1061    MoveTo(Point),
1062    LineTo(Point),
1063    CurveTo {
1064        control1: Point,
1065        control2: Point,
1066        end: Point,
1067    },
1068    QuadTo {
1069        control: Point,
1070        end: Point,
1071    },
1072    Arc {
1073        center: Point,
1074        radius: f32,
1075        start_angle: f32,
1076        end_angle: f32,
1077    },
1078    Close,
1079}
1080
1081// PathSegment
1082impl Hash for PathSegment {
1083    fn hash<H: Hasher>(&self, state: &mut H) {
1084        // Hash the enum variant's discriminant first to distinguish them
1085        discriminant(self).hash(state);
1086
1087        match self {
1088            PathSegment::MoveTo(p) => p.hash(state),
1089            PathSegment::LineTo(p) => p.hash(state),
1090            PathSegment::CurveTo {
1091                control1,
1092                control2,
1093                end,
1094            } => {
1095                control1.hash(state);
1096                control2.hash(state);
1097                end.hash(state);
1098            }
1099            PathSegment::QuadTo { control, end } => {
1100                control.hash(state);
1101                end.hash(state);
1102            }
1103            PathSegment::Arc {
1104                center,
1105                radius,
1106                start_angle,
1107                end_angle,
1108            } => {
1109                center.hash(state);
1110                (radius.round() as usize).hash(state);
1111                (start_angle.round() as usize).hash(state);
1112                (end_angle.round() as usize).hash(state);
1113            }
1114            PathSegment::Close => {} // No data to hash
1115        }
1116    }
1117}
1118
1119impl PartialEq for PathSegment {
1120    fn eq(&self, other: &Self) -> bool {
1121        match (self, other) {
1122            (PathSegment::MoveTo(a), PathSegment::MoveTo(b)) => a == b,
1123            (PathSegment::LineTo(a), PathSegment::LineTo(b)) => a == b,
1124            (
1125                PathSegment::CurveTo {
1126                    control1: c1a,
1127                    control2: c2a,
1128                    end: ea,
1129                },
1130                PathSegment::CurveTo {
1131                    control1: c1b,
1132                    control2: c2b,
1133                    end: eb,
1134                },
1135            ) => c1a == c1b && c2a == c2b && ea == eb,
1136            (
1137                PathSegment::QuadTo {
1138                    control: ca,
1139                    end: ea,
1140                },
1141                PathSegment::QuadTo {
1142                    control: cb,
1143                    end: eb,
1144                },
1145            ) => ca == cb && ea == eb,
1146            (
1147                PathSegment::Arc {
1148                    center: ca,
1149                    radius: ra,
1150                    start_angle: sa_a,
1151                    end_angle: ea_a,
1152                },
1153                PathSegment::Arc {
1154                    center: cb,
1155                    radius: rb,
1156                    start_angle: sa_b,
1157                    end_angle: ea_b,
1158                },
1159            ) => ca == cb && round_eq(*ra, *rb) && round_eq(*sa_a, *sa_b) && round_eq(*ea_a, *ea_b),
1160            (PathSegment::Close, PathSegment::Close) => true,
1161            _ => false, // Variants are different
1162        }
1163    }
1164}
1165
1166impl Eq for PathSegment {}
1167
1168// Enhanced content model supporting mixed inline content
1169#[derive(Debug, Clone, Hash)]
1170pub enum InlineContent {
1171    Text(StyledRun),
1172    Image(InlineImage),
1173    Shape(InlineShape),
1174    Space(InlineSpace),
1175    LineBreak(InlineBreak),
1176    Tab,
1177    /// List marker (::marker pseudo-element)
1178    /// Markers with list-style-position: outside are positioned
1179    /// in the padding gutter of the list container
1180    Marker {
1181        run: StyledRun,
1182        /// Whether marker is positioned outside (in padding) or inside (inline)
1183        position_outside: bool,
1184    },
1185    // Ruby annotation
1186    Ruby {
1187        base: Vec<InlineContent>,
1188        text: Vec<InlineContent>,
1189        // Style for the ruby text itself
1190        style: Arc<StyleProperties>,
1191    },
1192}
1193
1194#[derive(Debug, Clone)]
1195pub struct InlineImage {
1196    pub source: ImageSource,
1197    pub intrinsic_size: Size,
1198    pub display_size: Option<Size>,
1199    // How much to shift baseline
1200    pub baseline_offset: f32,
1201    pub alignment: VerticalAlign,
1202    pub object_fit: ObjectFit,
1203}
1204
1205impl PartialEq for InlineImage {
1206    fn eq(&self, other: &Self) -> bool {
1207        self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
1208            && self.source == other.source
1209            && self.intrinsic_size == other.intrinsic_size
1210            && self.display_size == other.display_size
1211            && self.alignment == other.alignment
1212            && self.object_fit == other.object_fit
1213    }
1214}
1215
1216impl Eq for InlineImage {}
1217
1218impl Hash for InlineImage {
1219    fn hash<H: Hasher>(&self, state: &mut H) {
1220        self.source.hash(state);
1221        self.intrinsic_size.hash(state);
1222        self.display_size.hash(state);
1223        self.baseline_offset.to_bits().hash(state);
1224        self.alignment.hash(state);
1225        self.object_fit.hash(state);
1226    }
1227}
1228
1229impl PartialOrd for InlineImage {
1230    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1231        Some(self.cmp(other))
1232    }
1233}
1234
1235impl Ord for InlineImage {
1236    fn cmp(&self, other: &Self) -> Ordering {
1237        self.source
1238            .cmp(&other.source)
1239            .then_with(|| self.intrinsic_size.cmp(&other.intrinsic_size))
1240            .then_with(|| self.display_size.cmp(&other.display_size))
1241            .then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
1242            .then_with(|| self.alignment.cmp(&other.alignment))
1243            .then_with(|| self.object_fit.cmp(&other.object_fit))
1244    }
1245}
1246
1247/// Enhanced glyph with all features
1248#[derive(Debug, Clone)]
1249pub struct Glyph {
1250    // Core glyph data
1251    pub glyph_id: u16,
1252    pub codepoint: char,
1253    /// Hash of the font - use LoadedFonts to look up the actual font when needed
1254    pub font_hash: u64,
1255    /// Cached font metrics to avoid font lookup for common operations
1256    pub font_metrics: LayoutFontMetrics,
1257    pub style: Arc<StyleProperties>,
1258    pub source: GlyphSource,
1259
1260    // Text mapping
1261    pub logical_byte_index: usize,
1262    pub logical_byte_len: usize,
1263    pub content_index: usize,
1264    pub cluster: u32,
1265
1266    // Metrics
1267    pub advance: f32,
1268    pub kerning: f32,
1269    pub offset: Point,
1270
1271    // Vertical text support
1272    pub vertical_advance: f32,
1273    pub vertical_origin_y: f32, // from VORG
1274    pub vertical_bearing: Point,
1275    pub orientation: GlyphOrientation,
1276
1277    // Layout properties
1278    pub script: Script,
1279    pub bidi_level: BidiLevel,
1280}
1281
1282impl Glyph {
1283    #[inline]
1284    fn bounds(&self) -> Rect {
1285        Rect {
1286            x: 0.0,
1287            y: 0.0,
1288            width: self.advance,
1289            height: self.style.line_height,
1290        }
1291    }
1292
1293    #[inline]
1294    fn character_class(&self) -> CharacterClass {
1295        classify_character(self.codepoint as u32)
1296    }
1297
1298    #[inline]
1299    fn is_whitespace(&self) -> bool {
1300        self.character_class() == CharacterClass::Space
1301    }
1302
1303    #[inline]
1304    fn can_justify(&self) -> bool {
1305        !self.codepoint.is_whitespace() && self.character_class() != CharacterClass::Combining
1306    }
1307
1308    #[inline]
1309    fn justification_priority(&self) -> u8 {
1310        get_justification_priority(self.character_class())
1311    }
1312
1313    #[inline]
1314    fn break_opportunity_after(&self) -> bool {
1315        let is_whitespace = self.codepoint.is_whitespace();
1316        let is_soft_hyphen = self.codepoint == '\u{00AD}';
1317        is_whitespace || is_soft_hyphen
1318    }
1319}
1320
1321// Information about text runs after initial analysis
1322#[derive(Debug, Clone)]
1323pub struct TextRunInfo<'a> {
1324    pub text: &'a str,
1325    pub style: Arc<StyleProperties>,
1326    pub logical_start: usize,
1327    pub content_index: usize,
1328}
1329
1330#[derive(Debug, Clone)]
1331pub enum ImageSource {
1332    /// Direct reference to decoded image (from DOM NodeType::Image)
1333    Ref(ImageRef),
1334    /// CSS url reference (from background-image, needs ImageCache lookup)
1335    Url(String),
1336    /// Raw image data
1337    Data(Arc<[u8]>),
1338    /// SVG source
1339    Svg(Arc<str>),
1340    /// Placeholder for layout without actual image
1341    Placeholder(Size),
1342}
1343
1344impl PartialEq for ImageSource {
1345    fn eq(&self, other: &Self) -> bool {
1346        match (self, other) {
1347            (ImageSource::Ref(a), ImageSource::Ref(b)) => a.get_hash() == b.get_hash(),
1348            (ImageSource::Url(a), ImageSource::Url(b)) => a == b,
1349            (ImageSource::Data(a), ImageSource::Data(b)) => Arc::ptr_eq(a, b),
1350            (ImageSource::Svg(a), ImageSource::Svg(b)) => Arc::ptr_eq(a, b),
1351            (ImageSource::Placeholder(a), ImageSource::Placeholder(b)) => {
1352                a.width.to_bits() == b.width.to_bits() && a.height.to_bits() == b.height.to_bits()
1353            }
1354            _ => false,
1355        }
1356    }
1357}
1358
1359impl Eq for ImageSource {}
1360
1361impl std::hash::Hash for ImageSource {
1362    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1363        core::mem::discriminant(self).hash(state);
1364        match self {
1365            ImageSource::Ref(r) => r.get_hash().hash(state),
1366            ImageSource::Url(s) => s.hash(state),
1367            ImageSource::Data(d) => (Arc::as_ptr(d) as *const u8 as usize).hash(state),
1368            ImageSource::Svg(s) => (Arc::as_ptr(s) as *const u8 as usize).hash(state),
1369            ImageSource::Placeholder(sz) => {
1370                sz.width.to_bits().hash(state);
1371                sz.height.to_bits().hash(state);
1372            }
1373        }
1374    }
1375}
1376
1377impl PartialOrd for ImageSource {
1378    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1379        Some(self.cmp(other))
1380    }
1381}
1382
1383impl Ord for ImageSource {
1384    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1385        fn variant_index(s: &ImageSource) -> u8 {
1386            match s {
1387                ImageSource::Ref(_) => 0,
1388                ImageSource::Url(_) => 1,
1389                ImageSource::Data(_) => 2,
1390                ImageSource::Svg(_) => 3,
1391                ImageSource::Placeholder(_) => 4,
1392            }
1393        }
1394        match (self, other) {
1395            (ImageSource::Ref(a), ImageSource::Ref(b)) => a.get_hash().cmp(&b.get_hash()),
1396            (ImageSource::Url(a), ImageSource::Url(b)) => a.cmp(b),
1397            (ImageSource::Data(a), ImageSource::Data(b)) => {
1398                (Arc::as_ptr(a) as *const u8 as usize).cmp(&(Arc::as_ptr(b) as *const u8 as usize))
1399            }
1400            (ImageSource::Svg(a), ImageSource::Svg(b)) => {
1401                (Arc::as_ptr(a) as *const u8 as usize).cmp(&(Arc::as_ptr(b) as *const u8 as usize))
1402            }
1403            (ImageSource::Placeholder(a), ImageSource::Placeholder(b)) => {
1404                (a.width.to_bits(), a.height.to_bits())
1405                    .cmp(&(b.width.to_bits(), b.height.to_bits()))
1406            }
1407            // Different variants: compare by variant index
1408            _ => variant_index(self).cmp(&variant_index(other)),
1409        }
1410    }
1411}
1412
1413#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd)]
1414pub enum VerticalAlign {
1415    // Align image baseline with text baseline
1416    #[default]
1417    Baseline,
1418    // Align image bottom with line bottom
1419    Bottom,
1420    // Align image top with line top
1421    Top,
1422    // Align image middle with text middle
1423    Middle,
1424    // Align with tallest text in line
1425    TextTop,
1426    // Align with lowest text in line
1427    TextBottom,
1428    // Subscript alignment
1429    Sub,
1430    // Superscript alignment
1431    Super,
1432    // Custom offset from baseline
1433    Offset(f32),
1434}
1435
1436impl std::hash::Hash for VerticalAlign {
1437    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1438        core::mem::discriminant(self).hash(state);
1439        if let VerticalAlign::Offset(f) = self {
1440            f.to_bits().hash(state);
1441        }
1442    }
1443}
1444
1445impl Eq for VerticalAlign {}
1446
1447impl Ord for VerticalAlign {
1448    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1449        self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
1450    }
1451}
1452
1453#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
1454pub enum ObjectFit {
1455    // Stretch to fit display size
1456    Fill,
1457    // Scale to fit within display size
1458    Contain,
1459    // Scale to cover display size
1460    Cover,
1461    // Use intrinsic size
1462    None,
1463    // Like contain but never scale up
1464    ScaleDown,
1465}
1466
1467/// Border information for inline elements (display: inline, inline-block)
1468///
1469/// This stores the resolved border properties needed for rendering inline element borders.
1470/// Unlike block elements which render borders via paint_node_background_and_border(),
1471/// inline element borders must be rendered per glyph-run to handle line breaks correctly.
1472#[derive(Debug, Clone, PartialEq)]
1473pub struct InlineBorderInfo {
1474    /// Border widths in pixels for each side
1475    pub top: f32,
1476    pub right: f32,
1477    pub bottom: f32,
1478    pub left: f32,
1479    /// Border colors for each side
1480    pub top_color: ColorU,
1481    pub right_color: ColorU,
1482    pub bottom_color: ColorU,
1483    pub left_color: ColorU,
1484    /// Border radius (if any)
1485    pub radius: Option<f32>,
1486}
1487
1488impl Default for InlineBorderInfo {
1489    fn default() -> Self {
1490        Self {
1491            top: 0.0,
1492            right: 0.0,
1493            bottom: 0.0,
1494            left: 0.0,
1495            top_color: ColorU::TRANSPARENT,
1496            right_color: ColorU::TRANSPARENT,
1497            bottom_color: ColorU::TRANSPARENT,
1498            left_color: ColorU::TRANSPARENT,
1499            radius: None,
1500        }
1501    }
1502}
1503
1504impl InlineBorderInfo {
1505    /// Returns true if any border has a non-zero width
1506    pub fn has_border(&self) -> bool {
1507        self.top > 0.0 || self.right > 0.0 || self.bottom > 0.0 || self.left > 0.0
1508    }
1509}
1510
1511#[derive(Debug, Clone)]
1512pub struct InlineShape {
1513    pub shape_def: ShapeDefinition,
1514    pub fill: Option<ColorU>,
1515    pub stroke: Option<Stroke>,
1516    pub baseline_offset: f32,
1517    /// The NodeId of the element that created this shape
1518    /// (e.g., inline-block) - this allows us to look up
1519    /// styling information (background, border) when rendering
1520    pub source_node_id: Option<azul_core::dom::NodeId>,
1521}
1522
1523#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
1524pub enum OverflowBehavior {
1525    // Content extends outside shape
1526    Visible,
1527    // Content is clipped to shape
1528    Hidden,
1529    // Scrollable overflow
1530    Scroll,
1531    // Browser/system decides
1532    #[default]
1533    Auto,
1534    // Break into next shape/page
1535    Break,
1536}
1537
1538#[derive(Debug, Clone)]
1539pub struct MeasuredImage {
1540    pub source: ImageSource,
1541    pub size: Size,
1542    pub baseline_offset: f32,
1543    pub alignment: VerticalAlign,
1544    pub content_index: usize,
1545}
1546
1547#[derive(Debug, Clone)]
1548pub struct MeasuredShape {
1549    pub shape_def: ShapeDefinition,
1550    pub size: Size,
1551    pub baseline_offset: f32,
1552    pub content_index: usize,
1553}
1554
1555#[derive(Debug, Clone)]
1556pub struct InlineSpace {
1557    pub width: f32,
1558    pub is_breaking: bool, // Can line break here
1559    pub is_stretchy: bool, // Can be expanded for justification
1560}
1561
1562impl PartialEq for InlineSpace {
1563    fn eq(&self, other: &Self) -> bool {
1564        self.width.to_bits() == other.width.to_bits()
1565            && self.is_breaking == other.is_breaking
1566            && self.is_stretchy == other.is_stretchy
1567    }
1568}
1569
1570impl Eq for InlineSpace {}
1571
1572impl Hash for InlineSpace {
1573    fn hash<H: Hasher>(&self, state: &mut H) {
1574        self.width.to_bits().hash(state);
1575        self.is_breaking.hash(state);
1576        self.is_stretchy.hash(state);
1577    }
1578}
1579
1580impl PartialOrd for InlineSpace {
1581    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1582        Some(self.cmp(other))
1583    }
1584}
1585
1586impl Ord for InlineSpace {
1587    fn cmp(&self, other: &Self) -> Ordering {
1588        self.width
1589            .total_cmp(&other.width)
1590            .then_with(|| self.is_breaking.cmp(&other.is_breaking))
1591            .then_with(|| self.is_stretchy.cmp(&other.is_stretchy))
1592    }
1593}
1594
1595impl PartialEq for InlineShape {
1596    fn eq(&self, other: &Self) -> bool {
1597        self.baseline_offset.to_bits() == other.baseline_offset.to_bits()
1598            && self.shape_def == other.shape_def
1599            && self.fill == other.fill
1600            && self.stroke == other.stroke
1601            && self.source_node_id == other.source_node_id
1602    }
1603}
1604
1605impl Eq for InlineShape {}
1606
1607impl Hash for InlineShape {
1608    fn hash<H: Hasher>(&self, state: &mut H) {
1609        self.shape_def.hash(state);
1610        self.fill.hash(state);
1611        self.stroke.hash(state);
1612        self.baseline_offset.to_bits().hash(state);
1613        self.source_node_id.hash(state);
1614    }
1615}
1616
1617impl PartialOrd for InlineShape {
1618    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1619        Some(
1620            self.shape_def
1621                .partial_cmp(&other.shape_def)?
1622                .then_with(|| self.fill.cmp(&other.fill))
1623                .then_with(|| {
1624                    self.stroke
1625                        .partial_cmp(&other.stroke)
1626                        .unwrap_or(Ordering::Equal)
1627                })
1628                .then_with(|| self.baseline_offset.total_cmp(&other.baseline_offset))
1629                .then_with(|| self.source_node_id.cmp(&other.source_node_id)),
1630        )
1631    }
1632}
1633
1634#[derive(Debug, Default, Clone, Copy)]
1635pub struct Rect {
1636    pub x: f32,
1637    pub y: f32,
1638    pub width: f32,
1639    pub height: f32,
1640}
1641
1642impl PartialEq for Rect {
1643    fn eq(&self, other: &Self) -> bool {
1644        round_eq(self.x, other.x)
1645            && round_eq(self.y, other.y)
1646            && round_eq(self.width, other.width)
1647            && round_eq(self.height, other.height)
1648    }
1649}
1650impl Eq for Rect {}
1651
1652impl Hash for Rect {
1653    fn hash<H: Hasher>(&self, state: &mut H) {
1654        // The order in which you hash the fields matters.
1655        // A consistent order is crucial.
1656        (self.x.round() as usize).hash(state);
1657        (self.y.round() as usize).hash(state);
1658        (self.width.round() as usize).hash(state);
1659        (self.height.round() as usize).hash(state);
1660    }
1661}
1662
1663#[derive(Debug, Default, Clone, Copy, PartialOrd)]
1664pub struct Size {
1665    pub width: f32,
1666    pub height: f32,
1667}
1668
1669impl Ord for Size {
1670    fn cmp(&self, other: &Self) -> Ordering {
1671        (self.width.round() as usize)
1672            .cmp(&(other.width.round() as usize))
1673            .then_with(|| (self.height.round() as usize).cmp(&(other.height.round() as usize)))
1674    }
1675}
1676
1677// Size
1678impl Hash for Size {
1679    fn hash<H: Hasher>(&self, state: &mut H) {
1680        (self.width.round() as usize).hash(state);
1681        (self.height.round() as usize).hash(state);
1682    }
1683}
1684impl PartialEq for Size {
1685    fn eq(&self, other: &Self) -> bool {
1686        round_eq(self.width, other.width) && round_eq(self.height, other.height)
1687    }
1688}
1689impl Eq for Size {}
1690
1691impl Size {
1692    pub const fn zero() -> Self {
1693        Self::new(0.0, 0.0)
1694    }
1695    pub const fn new(width: f32, height: f32) -> Self {
1696        Self { width, height }
1697    }
1698}
1699
1700#[derive(Debug, Default, Clone, Copy, PartialOrd)]
1701pub struct Point {
1702    pub x: f32,
1703    pub y: f32,
1704}
1705
1706// Point
1707impl Hash for Point {
1708    fn hash<H: Hasher>(&self, state: &mut H) {
1709        (self.x.round() as usize).hash(state);
1710        (self.y.round() as usize).hash(state);
1711    }
1712}
1713
1714impl PartialEq for Point {
1715    fn eq(&self, other: &Self) -> bool {
1716        round_eq(self.x, other.x) && round_eq(self.y, other.y)
1717    }
1718}
1719
1720impl Eq for Point {}
1721
1722#[derive(Debug, Clone, PartialOrd)]
1723pub enum ShapeDefinition {
1724    Rectangle {
1725        size: Size,
1726        corner_radius: Option<f32>,
1727    },
1728    Circle {
1729        radius: f32,
1730    },
1731    Ellipse {
1732        radii: Size,
1733    },
1734    Polygon {
1735        points: Vec<Point>,
1736    },
1737    Path {
1738        segments: Vec<PathSegment>,
1739    },
1740}
1741
1742// ShapeDefinition
1743impl Hash for ShapeDefinition {
1744    fn hash<H: Hasher>(&self, state: &mut H) {
1745        discriminant(self).hash(state);
1746        match self {
1747            ShapeDefinition::Rectangle {
1748                size,
1749                corner_radius,
1750            } => {
1751                size.hash(state);
1752                corner_radius.map(|r| r.round() as usize).hash(state);
1753            }
1754            ShapeDefinition::Circle { radius } => {
1755                (radius.round() as usize).hash(state);
1756            }
1757            ShapeDefinition::Ellipse { radii } => {
1758                radii.hash(state);
1759            }
1760            ShapeDefinition::Polygon { points } => {
1761                // Since Point implements Hash, we can hash the Vec directly.
1762                points.hash(state);
1763            }
1764            ShapeDefinition::Path { segments } => {
1765                // Same for Vec<PathSegment>
1766                segments.hash(state);
1767            }
1768        }
1769    }
1770}
1771
1772impl PartialEq for ShapeDefinition {
1773    fn eq(&self, other: &Self) -> bool {
1774        match (self, other) {
1775            (
1776                ShapeDefinition::Rectangle {
1777                    size: s1,
1778                    corner_radius: r1,
1779                },
1780                ShapeDefinition::Rectangle {
1781                    size: s2,
1782                    corner_radius: r2,
1783                },
1784            ) => {
1785                s1 == s2
1786                    && match (r1, r2) {
1787                        (None, None) => true,
1788                        (Some(v1), Some(v2)) => round_eq(*v1, *v2),
1789                        _ => false,
1790                    }
1791            }
1792            (ShapeDefinition::Circle { radius: r1 }, ShapeDefinition::Circle { radius: r2 }) => {
1793                round_eq(*r1, *r2)
1794            }
1795            (ShapeDefinition::Ellipse { radii: r1 }, ShapeDefinition::Ellipse { radii: r2 }) => {
1796                r1 == r2
1797            }
1798            (ShapeDefinition::Polygon { points: p1 }, ShapeDefinition::Polygon { points: p2 }) => {
1799                p1 == p2
1800            }
1801            (ShapeDefinition::Path { segments: s1 }, ShapeDefinition::Path { segments: s2 }) => {
1802                s1 == s2
1803            }
1804            _ => false,
1805        }
1806    }
1807}
1808impl Eq for ShapeDefinition {}
1809
1810impl ShapeDefinition {
1811    /// Calculates the bounding box size for the shape.
1812    pub fn get_size(&self) -> Size {
1813        match self {
1814            // The size is explicitly defined.
1815            ShapeDefinition::Rectangle { size, .. } => *size,
1816
1817            // The bounding box of a circle is a square with sides equal to the diameter.
1818            ShapeDefinition::Circle { radius } => {
1819                let diameter = radius * 2.0;
1820                Size::new(diameter, diameter)
1821            }
1822
1823            // The bounding box of an ellipse has width and height equal to twice its radii.
1824            ShapeDefinition::Ellipse { radii } => Size::new(radii.width * 2.0, radii.height * 2.0),
1825
1826            // For a polygon, we must find the min/max coordinates to get the bounds.
1827            ShapeDefinition::Polygon { points } => calculate_bounding_box_size(points),
1828
1829            // For a path, we find the bounding box of all its anchor and control points.
1830            //
1831            // NOTE: This is a common and fast approximation. The true bounding box of
1832            // bezier curves can be slightly smaller than the box containing their control
1833            // points. For pixel-perfect results, one would need to calculate the
1834            // curve's extrema.
1835            ShapeDefinition::Path { segments } => {
1836                let mut points = Vec::new();
1837                let mut current_pos = Point { x: 0.0, y: 0.0 };
1838
1839                for segment in segments {
1840                    match segment {
1841                        PathSegment::MoveTo(p) | PathSegment::LineTo(p) => {
1842                            points.push(*p);
1843                            current_pos = *p;
1844                        }
1845                        PathSegment::QuadTo { control, end } => {
1846                            points.push(current_pos);
1847                            points.push(*control);
1848                            points.push(*end);
1849                            current_pos = *end;
1850                        }
1851                        PathSegment::CurveTo {
1852                            control1,
1853                            control2,
1854                            end,
1855                        } => {
1856                            points.push(current_pos);
1857                            points.push(*control1);
1858                            points.push(*control2);
1859                            points.push(*end);
1860                            current_pos = *end;
1861                        }
1862                        PathSegment::Arc {
1863                            center,
1864                            radius,
1865                            start_angle,
1866                            end_angle,
1867                        } => {
1868                            // 1. Calculate and add the arc's start and end points to the list.
1869                            let start_point = Point {
1870                                x: center.x + radius * start_angle.cos(),
1871                                y: center.y + radius * start_angle.sin(),
1872                            };
1873                            let end_point = Point {
1874                                x: center.x + radius * end_angle.cos(),
1875                                y: center.y + radius * end_angle.sin(),
1876                            };
1877                            points.push(start_point);
1878                            points.push(end_point);
1879
1880                            // 2. Normalize the angles to handle cases where the arc crosses the
1881                            //    0-radian line.
1882                            // This ensures we can iterate forward from a start to an end angle.
1883                            let mut normalized_end = *end_angle;
1884                            while normalized_end < *start_angle {
1885                                normalized_end += 2.0 * std::f32::consts::PI;
1886                            }
1887
1888                            // 3. Find the first cardinal point (multiples of PI/2) at or after the
1889                            //    start angle.
1890                            let mut check_angle = (*start_angle / std::f32::consts::FRAC_PI_2)
1891                                .ceil()
1892                                * std::f32::consts::FRAC_PI_2;
1893
1894                            // 4. Iterate through all cardinal points that fall within the arc's
1895                            //    sweep and add them.
1896                            // These points define the maximum extent of the arc's bounding box.
1897                            while check_angle < normalized_end {
1898                                points.push(Point {
1899                                    x: center.x + radius * check_angle.cos(),
1900                                    y: center.y + radius * check_angle.sin(),
1901                                });
1902                                check_angle += std::f32::consts::FRAC_PI_2;
1903                            }
1904
1905                            // 5. The end of the arc is the new current position for subsequent path
1906                            //    segments.
1907                            current_pos = end_point;
1908                        }
1909                        PathSegment::Close => {
1910                            // No new points are added for closing the path
1911                        }
1912                    }
1913                }
1914                calculate_bounding_box_size(&points)
1915            }
1916        }
1917    }
1918}
1919
1920/// Helper function to calculate the size of the bounding box enclosing a set of points.
1921fn calculate_bounding_box_size(points: &[Point]) -> Size {
1922    if points.is_empty() {
1923        return Size::zero();
1924    }
1925
1926    let mut min_x = f32::MAX;
1927    let mut max_x = f32::MIN;
1928    let mut min_y = f32::MAX;
1929    let mut max_y = f32::MIN;
1930
1931    for point in points {
1932        min_x = min_x.min(point.x);
1933        max_x = max_x.max(point.x);
1934        min_y = min_y.min(point.y);
1935        max_y = max_y.max(point.y);
1936    }
1937
1938    // Handle case where points might be collinear or a single point
1939    if min_x > max_x || min_y > max_y {
1940        return Size::zero();
1941    }
1942
1943    Size::new(max_x - min_x, max_y - min_y)
1944}
1945
1946#[derive(Debug, Clone, PartialOrd)]
1947pub struct Stroke {
1948    pub color: ColorU,
1949    pub width: f32,
1950    pub dash_pattern: Option<Vec<f32>>,
1951}
1952
1953// Stroke
1954impl Hash for Stroke {
1955    fn hash<H: Hasher>(&self, state: &mut H) {
1956        self.color.hash(state);
1957        (self.width.round() as usize).hash(state);
1958
1959        // Manual hashing for Option<Vec<f32>>
1960        match &self.dash_pattern {
1961            None => 0u8.hash(state), // Hash a discriminant for None
1962            Some(pattern) => {
1963                1u8.hash(state); // Hash a discriminant for Some
1964                pattern.len().hash(state); // Hash the length
1965                for &val in pattern {
1966                    (val.round() as usize).hash(state); // Hash each rounded value
1967                }
1968            }
1969        }
1970    }
1971}
1972
1973impl PartialEq for Stroke {
1974    fn eq(&self, other: &Self) -> bool {
1975        if self.color != other.color || !round_eq(self.width, other.width) {
1976            return false;
1977        }
1978        match (&self.dash_pattern, &other.dash_pattern) {
1979            (None, None) => true,
1980            (Some(p1), Some(p2)) => {
1981                p1.len() == p2.len() && p1.iter().zip(p2.iter()).all(|(a, b)| round_eq(*a, *b))
1982            }
1983            _ => false,
1984        }
1985    }
1986}
1987
1988impl Eq for Stroke {}
1989
1990// Helper function to round f32 for comparison
1991fn round_eq(a: f32, b: f32) -> bool {
1992    (a.round() as isize) == (b.round() as isize)
1993}
1994
1995#[derive(Debug, Clone)]
1996pub enum ShapeBoundary {
1997    Rectangle(Rect),
1998    Circle { center: Point, radius: f32 },
1999    Ellipse { center: Point, radii: Size },
2000    Polygon { points: Vec<Point> },
2001    Path { segments: Vec<PathSegment> },
2002}
2003
2004impl ShapeBoundary {
2005    pub fn inflate(&self, margin: f32) -> Self {
2006        if margin == 0.0 {
2007            return self.clone();
2008        }
2009        match self {
2010            Self::Rectangle(rect) => Self::Rectangle(Rect {
2011                x: rect.x - margin,
2012                y: rect.y - margin,
2013                width: (rect.width + margin * 2.0).max(0.0),
2014                height: (rect.height + margin * 2.0).max(0.0),
2015            }),
2016            Self::Circle { center, radius } => Self::Circle {
2017                center: *center,
2018                radius: radius + margin,
2019            },
2020            // For simplicity, Polygon and Path inflation is not implemented here.
2021            // A full implementation would require a geometry library to offset the path.
2022            _ => self.clone(),
2023        }
2024    }
2025}
2026
2027// ShapeBoundary
2028impl Hash for ShapeBoundary {
2029    fn hash<H: Hasher>(&self, state: &mut H) {
2030        discriminant(self).hash(state);
2031        match self {
2032            ShapeBoundary::Rectangle(rect) => rect.hash(state),
2033            ShapeBoundary::Circle { center, radius } => {
2034                center.hash(state);
2035                (radius.round() as usize).hash(state);
2036            }
2037            ShapeBoundary::Ellipse { center, radii } => {
2038                center.hash(state);
2039                radii.hash(state);
2040            }
2041            ShapeBoundary::Polygon { points } => points.hash(state),
2042            ShapeBoundary::Path { segments } => segments.hash(state),
2043        }
2044    }
2045}
2046impl PartialEq for ShapeBoundary {
2047    fn eq(&self, other: &Self) -> bool {
2048        match (self, other) {
2049            (ShapeBoundary::Rectangle(r1), ShapeBoundary::Rectangle(r2)) => r1 == r2,
2050            (
2051                ShapeBoundary::Circle {
2052                    center: c1,
2053                    radius: r1,
2054                },
2055                ShapeBoundary::Circle {
2056                    center: c2,
2057                    radius: r2,
2058                },
2059            ) => c1 == c2 && round_eq(*r1, *r2),
2060            (
2061                ShapeBoundary::Ellipse {
2062                    center: c1,
2063                    radii: r1,
2064                },
2065                ShapeBoundary::Ellipse {
2066                    center: c2,
2067                    radii: r2,
2068                },
2069            ) => c1 == c2 && r1 == r2,
2070            (ShapeBoundary::Polygon { points: p1 }, ShapeBoundary::Polygon { points: p2 }) => {
2071                p1 == p2
2072            }
2073            (ShapeBoundary::Path { segments: s1 }, ShapeBoundary::Path { segments: s2 }) => {
2074                s1 == s2
2075            }
2076            _ => false,
2077        }
2078    }
2079}
2080impl Eq for ShapeBoundary {}
2081
2082impl ShapeBoundary {
2083    /// Converts a CSS shape (from azul-css) to a layout engine ShapeBoundary
2084    ///
2085    /// # Arguments
2086    /// * `css_shape` - The parsed CSS shape from azul-css
2087    /// * `reference_box` - The containing box for resolving coordinates (from layout solver)
2088    ///
2089    /// # Returns
2090    /// A ShapeBoundary ready for use in the text layout engine
2091    pub fn from_css_shape(
2092        css_shape: &azul_css::shape::CssShape,
2093        reference_box: Rect,
2094        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
2095    ) -> Self {
2096        use azul_css::shape::CssShape;
2097
2098        if let Some(msgs) = debug_messages {
2099            msgs.push(LayoutDebugMessage::info(format!(
2100                "[ShapeBoundary::from_css_shape] Input CSS shape: {:?}",
2101                css_shape
2102            )));
2103            msgs.push(LayoutDebugMessage::info(format!(
2104                "[ShapeBoundary::from_css_shape] Reference box: {:?}",
2105                reference_box
2106            )));
2107        }
2108
2109        let result = match css_shape {
2110            CssShape::Circle(circle) => {
2111                let center = Point {
2112                    x: reference_box.x + circle.center.x,
2113                    y: reference_box.y + circle.center.y,
2114                };
2115                if let Some(msgs) = debug_messages {
2116                    msgs.push(LayoutDebugMessage::info(format!(
2117                        "[ShapeBoundary::from_css_shape] Circle - CSS center: ({}, {}), radius: {}",
2118                        circle.center.x, circle.center.y, circle.radius
2119                    )));
2120                    msgs.push(LayoutDebugMessage::info(format!(
2121                        "[ShapeBoundary::from_css_shape] Circle - Absolute center: ({}, {}), \
2122                         radius: {}",
2123                        center.x, center.y, circle.radius
2124                    )));
2125                }
2126                ShapeBoundary::Circle {
2127                    center,
2128                    radius: circle.radius,
2129                }
2130            }
2131
2132            CssShape::Ellipse(ellipse) => {
2133                let center = Point {
2134                    x: reference_box.x + ellipse.center.x,
2135                    y: reference_box.y + ellipse.center.y,
2136                };
2137                let radii = Size {
2138                    width: ellipse.radius_x,
2139                    height: ellipse.radius_y,
2140                };
2141                if let Some(msgs) = debug_messages {
2142                    msgs.push(LayoutDebugMessage::info(format!(
2143                        "[ShapeBoundary::from_css_shape] Ellipse - center: ({}, {}), radii: ({}, \
2144                         {})",
2145                        center.x, center.y, radii.width, radii.height
2146                    )));
2147                }
2148                ShapeBoundary::Ellipse { center, radii }
2149            }
2150
2151            CssShape::Polygon(polygon) => {
2152                let points = polygon
2153                    .points
2154                    .as_ref()
2155                    .iter()
2156                    .map(|pt| Point {
2157                        x: reference_box.x + pt.x,
2158                        y: reference_box.y + pt.y,
2159                    })
2160                    .collect();
2161                if let Some(msgs) = debug_messages {
2162                    msgs.push(LayoutDebugMessage::info(format!(
2163                        "[ShapeBoundary::from_css_shape] Polygon - {} points",
2164                        polygon.points.as_ref().len()
2165                    )));
2166                }
2167                ShapeBoundary::Polygon { points }
2168            }
2169
2170            CssShape::Inset(inset) => {
2171                // Inset defines distances from reference box edges
2172                let x = reference_box.x + inset.inset_left;
2173                let y = reference_box.y + inset.inset_top;
2174                let width = reference_box.width - inset.inset_left - inset.inset_right;
2175                let height = reference_box.height - inset.inset_top - inset.inset_bottom;
2176
2177                if let Some(msgs) = debug_messages {
2178                    msgs.push(LayoutDebugMessage::info(format!(
2179                        "[ShapeBoundary::from_css_shape] Inset - insets: ({}, {}, {}, {})",
2180                        inset.inset_top, inset.inset_right, inset.inset_bottom, inset.inset_left
2181                    )));
2182                    msgs.push(LayoutDebugMessage::info(format!(
2183                        "[ShapeBoundary::from_css_shape] Inset - resulting rect: x={}, y={}, \
2184                         w={}, h={}",
2185                        x, y, width, height
2186                    )));
2187                }
2188
2189                ShapeBoundary::Rectangle(Rect {
2190                    x,
2191                    y,
2192                    width: width.max(0.0),
2193                    height: height.max(0.0),
2194                })
2195            }
2196
2197            CssShape::Path(path) => {
2198                if let Some(msgs) = debug_messages {
2199                    msgs.push(LayoutDebugMessage::info(
2200                        "[ShapeBoundary::from_css_shape] Path - fallback to rectangle".to_string(),
2201                    ));
2202                }
2203                // TODO: Parse SVG path data into PathSegments
2204                // For now, fall back to rectangle
2205                ShapeBoundary::Rectangle(reference_box)
2206            }
2207        };
2208
2209        if let Some(msgs) = debug_messages {
2210            msgs.push(LayoutDebugMessage::info(format!(
2211                "[ShapeBoundary::from_css_shape] Result: {:?}",
2212                result
2213            )));
2214        }
2215        result
2216    }
2217}
2218
2219#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2220pub struct InlineBreak {
2221    pub break_type: BreakType,
2222    pub clear: ClearType,
2223    pub content_index: usize,
2224}
2225
2226#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2227pub enum BreakType {
2228    Soft,   // Preferred break (like <wbr>)
2229    Hard,   // Forced break (like <br>)
2230    Page,   // Page break
2231    Column, // Column break
2232}
2233
2234#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
2235pub enum ClearType {
2236    None,
2237    Left,
2238    Right,
2239    Both,
2240}
2241
2242// Complex shape constraints for non-rectangular text flow
2243#[derive(Debug, Clone)]
2244pub struct ShapeConstraints {
2245    pub boundaries: Vec<ShapeBoundary>,
2246    pub exclusions: Vec<ShapeBoundary>,
2247    pub writing_mode: WritingMode,
2248    pub text_align: TextAlign,
2249    pub line_height: f32,
2250}
2251
2252#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
2253pub enum WritingMode {
2254    #[default]
2255    HorizontalTb, // horizontal-tb (normal horizontal)
2256    VerticalRl, // vertical-rl (vertical right-to-left)
2257    VerticalLr, // vertical-lr (vertical left-to-right)
2258    SidewaysRl, // sideways-rl (rotated horizontal in vertical context)
2259    SidewaysLr, // sideways-lr (rotated horizontal in vertical context)
2260}
2261
2262impl WritingMode {
2263    /// Necessary to determine if the glyphs are advancing in a horizontal direction
2264    pub fn is_advance_horizontal(&self) -> bool {
2265        matches!(
2266            self,
2267            WritingMode::HorizontalTb | WritingMode::SidewaysRl | WritingMode::SidewaysLr
2268        )
2269    }
2270}
2271
2272#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
2273pub enum JustifyContent {
2274    #[default]
2275    None,
2276    InterWord,      // Expand spaces between words
2277    InterCharacter, // Expand spaces between all characters (for CJK)
2278    Distribute,     // Distribute space evenly including start/end
2279    Kashida,        // Stretch Arabic text using kashidas
2280}
2281
2282// Enhanced text alignment with logical directions
2283#[derive(Debug, Clone, Copy, PartialEq, Default, Hash, Eq, PartialOrd, Ord)]
2284pub enum TextAlign {
2285    #[default]
2286    Left,
2287    Right,
2288    Center,
2289    Justify,
2290    Start,
2291    End,        // Logical start/end
2292    JustifyAll, // Justify including last line
2293}
2294
2295// Vertical text orientation for individual characters
2296#[derive(Debug, Clone, Copy, PartialEq, Default, Eq, PartialOrd, Ord, Hash)]
2297pub enum TextOrientation {
2298    #[default]
2299    Mixed, // Default: upright for scripts, rotated for others
2300    Upright,  // All characters upright
2301    Sideways, // All characters rotated 90 degrees
2302}
2303
2304#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2305pub struct TextDecoration {
2306    pub underline: bool,
2307    pub strikethrough: bool,
2308    pub overline: bool,
2309}
2310
2311impl Default for TextDecoration {
2312    fn default() -> Self {
2313        TextDecoration {
2314            underline: false,
2315            overline: false,
2316            strikethrough: false,
2317        }
2318    }
2319}
2320
2321impl TextDecoration {
2322    /// Convert from CSS StyleTextDecoration enum to our internal representation.
2323    /// 
2324    /// Note: CSS text-decoration can have multiple values (underline line-through),
2325    /// but the current azul-css parser only supports single values. This can be
2326    /// extended in the future if CSS parsing is updated.
2327    pub fn from_css(css: azul_css::props::style::text::StyleTextDecoration) -> Self {
2328        use azul_css::props::style::text::StyleTextDecoration;
2329        match css {
2330            StyleTextDecoration::None => Self::default(),
2331            StyleTextDecoration::Underline => Self {
2332                underline: true,
2333                strikethrough: false,
2334                overline: false,
2335            },
2336            StyleTextDecoration::Overline => Self {
2337                underline: false,
2338                strikethrough: false,
2339                overline: true,
2340            },
2341            StyleTextDecoration::LineThrough => Self {
2342                underline: false,
2343                strikethrough: true,
2344                overline: false,
2345            },
2346        }
2347    }
2348}
2349
2350#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2351pub enum TextTransform {
2352    #[default]
2353    None,
2354    Uppercase,
2355    Lowercase,
2356    Capitalize,
2357}
2358
2359// Type alias for OpenType feature tags
2360pub type FourCc = [u8; 4];
2361
2362// Enum for relative or absolute spacing
2363#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
2364pub enum Spacing {
2365    Px(i32), // Use integer pixels to simplify hashing and equality
2366    Em(f32),
2367}
2368
2369// A type that implements `Hash` must also implement `Eq`.
2370// Since f32 does not implement `Eq`, we provide a manual implementation.
2371// The derived `PartialEq` is sufficient for this marker trait.
2372impl Eq for Spacing {}
2373
2374impl Hash for Spacing {
2375    fn hash<H: Hasher>(&self, state: &mut H) {
2376        // First, hash the enum variant to distinguish between Px and Em.
2377        discriminant(self).hash(state);
2378        match self {
2379            Spacing::Px(val) => val.hash(state),
2380            // For hashing floats, convert them to their raw bit representation.
2381            // This ensures that identical float values produce identical hashes.
2382            Spacing::Em(val) => val.to_bits().hash(state),
2383        }
2384    }
2385}
2386
2387impl Default for Spacing {
2388    fn default() -> Self {
2389        Spacing::Px(0)
2390    }
2391}
2392
2393impl Default for FontHash {
2394    fn default() -> Self {
2395        Self::invalid()
2396    }
2397}
2398
2399/// Style properties with vertical text support
2400#[derive(Debug, Clone, PartialEq)]
2401pub struct StyleProperties {
2402    /// Font stack for fallback support (priority order)
2403    /// Can be either a list of FontSelectors (resolved via fontconfig)
2404    /// or a direct FontRef (bypasses fontconfig entirely).
2405    pub font_stack: FontStack,
2406    pub font_size_px: f32,
2407    pub color: ColorU,
2408    /// Background color for inline elements (e.g., `<span style="background-color: yellow">`)
2409    ///
2410    /// This is propagated from CSS through the style system and eventually used by
2411    /// the PDF renderer to draw filled rectangles behind text. The value is `None`
2412    /// for transparent backgrounds (the default).
2413    ///
2414    /// The propagation chain is:
2415    /// CSS -> `get_style_properties()` -> `StyleProperties` -> `ShapedGlyph` -> `PdfGlyphRun`
2416    ///
2417    /// See `PdfGlyphRun::background_color` for how this is used in PDF rendering.
2418    pub background_color: Option<ColorU>,
2419    /// Full background content layers (for gradients, images, etc.)
2420    /// This extends background_color to support CSS gradients on inline elements.
2421    pub background_content: Vec<StyleBackgroundContent>,
2422    /// Border information for inline elements
2423    pub border: Option<InlineBorderInfo>,
2424    pub letter_spacing: Spacing,
2425    pub word_spacing: Spacing,
2426
2427    pub line_height: f32,
2428    pub text_decoration: TextDecoration,
2429
2430    // Represents CSS font-feature-settings like `"liga"`, `"smcp=1"`.
2431    pub font_features: Vec<String>,
2432
2433    // Variable fonts
2434    pub font_variations: Vec<(FourCc, f32)>,
2435    // Multiplier of the space width
2436    pub tab_size: f32,
2437    // text-transform
2438    pub text_transform: TextTransform,
2439    // Vertical text properties
2440    pub writing_mode: WritingMode,
2441    pub text_orientation: TextOrientation,
2442    // Tate-chu-yoko
2443    pub text_combine_upright: Option<TextCombineUpright>,
2444
2445    // Variant handling
2446    pub font_variant_caps: FontVariantCaps,
2447    pub font_variant_numeric: FontVariantNumeric,
2448    pub font_variant_ligatures: FontVariantLigatures,
2449    pub font_variant_east_asian: FontVariantEastAsian,
2450}
2451
2452impl Default for StyleProperties {
2453    fn default() -> Self {
2454        const FONT_SIZE: f32 = 16.0;
2455        const TAB_SIZE: f32 = 8.0;
2456        Self {
2457            font_stack: FontStack::default(),
2458            font_size_px: FONT_SIZE,
2459            color: ColorU::default(),
2460            background_color: None,
2461            background_content: Vec::new(),
2462            border: None,
2463            letter_spacing: Spacing::default(), // Px(0)
2464            word_spacing: Spacing::default(),   // Px(0)
2465            line_height: FONT_SIZE * 1.2,
2466            text_decoration: TextDecoration::default(),
2467            font_features: Vec::new(),
2468            font_variations: Vec::new(),
2469            tab_size: TAB_SIZE, // CSS default
2470            text_transform: TextTransform::default(),
2471            writing_mode: WritingMode::default(),
2472            text_orientation: TextOrientation::default(),
2473            text_combine_upright: None,
2474            font_variant_caps: FontVariantCaps::default(),
2475            font_variant_numeric: FontVariantNumeric::default(),
2476            font_variant_ligatures: FontVariantLigatures::default(),
2477            font_variant_east_asian: FontVariantEastAsian::default(),
2478        }
2479    }
2480}
2481
2482impl Hash for StyleProperties {
2483    fn hash<H: Hasher>(&self, state: &mut H) {
2484        self.font_stack.hash(state);
2485        self.color.hash(state);
2486        self.background_color.hash(state);
2487        self.text_decoration.hash(state);
2488        self.font_features.hash(state);
2489        self.writing_mode.hash(state);
2490        self.text_orientation.hash(state);
2491        self.text_combine_upright.hash(state);
2492        self.letter_spacing.hash(state);
2493        self.word_spacing.hash(state);
2494
2495        // For f32 fields, round and cast to usize before hashing.
2496        (self.font_size_px.round() as usize).hash(state);
2497        (self.line_height.round() as usize).hash(state);
2498    }
2499}
2500
2501impl StyleProperties {
2502    /// Returns a hash that only includes properties that affect text layout.
2503    /// 
2504    /// Properties that DON'T affect layout (only rendering):
2505    /// - color, background_color, background_content
2506    /// - text_decoration (underline, etc.)
2507    /// - border (for inline elements)
2508    ///
2509    /// Properties that DO affect layout:
2510    /// - font_stack, font_size_px, font_features, font_variations
2511    /// - letter_spacing, word_spacing, line_height, tab_size
2512    /// - writing_mode, text_orientation, text_combine_upright
2513    /// - text_transform
2514    /// - font_variant_* (affects glyph selection)
2515    ///
2516    /// This allows the layout cache to reuse layouts when only rendering
2517    /// properties change (e.g., color changes on hover).
2518    pub fn layout_hash(&self) -> u64 {
2519        use std::hash::Hasher;
2520        let mut hasher = std::collections::hash_map::DefaultHasher::new();
2521        
2522        // Font selection (affects shaping and metrics)
2523        self.font_stack.hash(&mut hasher);
2524        (self.font_size_px.round() as usize).hash(&mut hasher);
2525        self.font_features.hash(&mut hasher);
2526        // font_variations affects glyph outlines
2527        for (tag, value) in &self.font_variations {
2528            tag.hash(&mut hasher);
2529            (value.round() as i32).hash(&mut hasher);
2530        }
2531        
2532        // Spacing (affects glyph positions)
2533        self.letter_spacing.hash(&mut hasher);
2534        self.word_spacing.hash(&mut hasher);
2535        (self.line_height.round() as usize).hash(&mut hasher);
2536        (self.tab_size.round() as usize).hash(&mut hasher);
2537        
2538        // Writing mode (affects layout direction)
2539        self.writing_mode.hash(&mut hasher);
2540        self.text_orientation.hash(&mut hasher);
2541        self.text_combine_upright.hash(&mut hasher);
2542        
2543        // Text transform (affects which characters are used)
2544        self.text_transform.hash(&mut hasher);
2545        
2546        // Font variants (affect glyph selection)
2547        self.font_variant_caps.hash(&mut hasher);
2548        self.font_variant_numeric.hash(&mut hasher);
2549        self.font_variant_ligatures.hash(&mut hasher);
2550        self.font_variant_east_asian.hash(&mut hasher);
2551        
2552        hasher.finish()
2553    }
2554    
2555    /// Check if two StyleProperties have the same layout-affecting properties.
2556    /// 
2557    /// Returns true if the layouts would be identical (only rendering differs).
2558    pub fn layout_eq(&self, other: &Self) -> bool {
2559        self.layout_hash() == other.layout_hash()
2560    }
2561}
2562
2563#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
2564pub enum TextCombineUpright {
2565    None,
2566    All,        // Combine all characters in horizontal layout
2567    Digits(u8), // Combine up to N digits
2568}
2569
2570#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2571pub enum GlyphSource {
2572    /// Glyph generated from a character in the source text.
2573    Char,
2574    /// Glyph inserted dynamically by the layout engine (e.g., a hyphen).
2575    Hyphen,
2576}
2577
2578#[derive(Debug, Clone, Copy, PartialEq)]
2579pub enum CharacterClass {
2580    Space,       // Regular spaces - highest justification priority
2581    Punctuation, // Can sometimes be adjusted
2582    Letter,      // Normal letters
2583    Ideograph,   // CJK characters - can be justified between
2584    Symbol,      // Symbols, emojis
2585    Combining,   // Combining marks - never justified
2586}
2587
2588#[derive(Debug, Clone, Copy, PartialEq)]
2589pub enum GlyphOrientation {
2590    Horizontal, // Keep horizontal (normal in horizontal text)
2591    Vertical,   // Rotate to vertical (normal in vertical text)
2592    Upright,    // Keep upright regardless of writing mode
2593    Mixed,      // Use script-specific default orientation
2594}
2595
2596// Bidi and script detection
2597#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2598pub enum BidiDirection {
2599    Ltr,
2600    Rtl,
2601}
2602
2603impl BidiDirection {
2604    pub fn is_rtl(&self) -> bool {
2605        matches!(self, BidiDirection::Rtl)
2606    }
2607}
2608
2609#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2610pub enum FontVariantCaps {
2611    #[default]
2612    Normal,
2613    SmallCaps,
2614    AllSmallCaps,
2615    PetiteCaps,
2616    AllPetiteCaps,
2617    Unicase,
2618    TitlingCaps,
2619}
2620
2621#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2622pub enum FontVariantNumeric {
2623    #[default]
2624    Normal,
2625    LiningNums,
2626    OldstyleNums,
2627    ProportionalNums,
2628    TabularNums,
2629    DiagonalFractions,
2630    StackedFractions,
2631    Ordinal,
2632    SlashedZero,
2633}
2634
2635#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2636pub enum FontVariantLigatures {
2637    #[default]
2638    Normal,
2639    None,
2640    Common,
2641    NoCommon,
2642    Discretionary,
2643    NoDiscretionary,
2644    Historical,
2645    NoHistorical,
2646    Contextual,
2647    NoContextual,
2648}
2649
2650#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Default)]
2651pub enum FontVariantEastAsian {
2652    #[default]
2653    Normal,
2654    Jis78,
2655    Jis83,
2656    Jis90,
2657    Jis04,
2658    Simplified,
2659    Traditional,
2660    FullWidth,
2661    ProportionalWidth,
2662    Ruby,
2663}
2664
2665#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2666pub struct BidiLevel(u8);
2667
2668impl BidiLevel {
2669    pub fn new(level: u8) -> Self {
2670        Self(level)
2671    }
2672    pub fn is_rtl(&self) -> bool {
2673        self.0 % 2 == 1
2674    }
2675    pub fn level(&self) -> u8 {
2676        self.0
2677    }
2678}
2679
2680// Add this new struct for style overrides
2681#[derive(Debug, Clone)]
2682pub struct StyleOverride {
2683    /// The specific character this override applies to.
2684    pub target: ContentIndex,
2685    /// The style properties to apply.
2686    /// Any `None` value means "inherit from the base style".
2687    pub style: PartialStyleProperties,
2688}
2689
2690#[derive(Debug, Clone, Default)]
2691pub struct PartialStyleProperties {
2692    pub font_stack: Option<FontStack>,
2693    pub font_size_px: Option<f32>,
2694    pub color: Option<ColorU>,
2695    pub letter_spacing: Option<Spacing>,
2696    pub word_spacing: Option<Spacing>,
2697    pub line_height: Option<f32>,
2698    pub text_decoration: Option<TextDecoration>,
2699    pub font_features: Option<Vec<String>>,
2700    pub font_variations: Option<Vec<(FourCc, f32)>>,
2701    pub tab_size: Option<f32>,
2702    pub text_transform: Option<TextTransform>,
2703    pub writing_mode: Option<WritingMode>,
2704    pub text_orientation: Option<TextOrientation>,
2705    pub text_combine_upright: Option<Option<TextCombineUpright>>,
2706    pub font_variant_caps: Option<FontVariantCaps>,
2707    pub font_variant_numeric: Option<FontVariantNumeric>,
2708    pub font_variant_ligatures: Option<FontVariantLigatures>,
2709    pub font_variant_east_asian: Option<FontVariantEastAsian>,
2710}
2711
2712impl Hash for PartialStyleProperties {
2713    fn hash<H: Hasher>(&self, state: &mut H) {
2714        self.font_stack.hash(state);
2715        self.font_size_px.map(|f| f.to_bits()).hash(state);
2716        self.color.hash(state);
2717        self.letter_spacing.hash(state);
2718        self.word_spacing.hash(state);
2719        self.line_height.map(|f| f.to_bits()).hash(state);
2720        self.text_decoration.hash(state);
2721        self.font_features.hash(state);
2722
2723        // Manual hashing for Vec<(FourCc, f32)>
2724        self.font_variations.as_ref().map(|v| {
2725            for (tag, val) in v {
2726                tag.hash(state);
2727                val.to_bits().hash(state);
2728            }
2729        });
2730
2731        self.tab_size.map(|f| f.to_bits()).hash(state);
2732        self.text_transform.hash(state);
2733        self.writing_mode.hash(state);
2734        self.text_orientation.hash(state);
2735        self.text_combine_upright.hash(state);
2736        self.font_variant_caps.hash(state);
2737        self.font_variant_numeric.hash(state);
2738        self.font_variant_ligatures.hash(state);
2739        self.font_variant_east_asian.hash(state);
2740    }
2741}
2742
2743impl PartialEq for PartialStyleProperties {
2744    fn eq(&self, other: &Self) -> bool {
2745        self.font_stack == other.font_stack &&
2746        self.font_size_px.map(|f| f.to_bits()) == other.font_size_px.map(|f| f.to_bits()) &&
2747        self.color == other.color &&
2748        self.letter_spacing == other.letter_spacing &&
2749        self.word_spacing == other.word_spacing &&
2750        self.line_height.map(|f| f.to_bits()) == other.line_height.map(|f| f.to_bits()) &&
2751        self.text_decoration == other.text_decoration &&
2752        self.font_features == other.font_features &&
2753        self.font_variations == other.font_variations && // Vec<(FourCc, f32)> is PartialEq
2754        self.tab_size.map(|f| f.to_bits()) == other.tab_size.map(|f| f.to_bits()) &&
2755        self.text_transform == other.text_transform &&
2756        self.writing_mode == other.writing_mode &&
2757        self.text_orientation == other.text_orientation &&
2758        self.text_combine_upright == other.text_combine_upright &&
2759        self.font_variant_caps == other.font_variant_caps &&
2760        self.font_variant_numeric == other.font_variant_numeric &&
2761        self.font_variant_ligatures == other.font_variant_ligatures &&
2762        self.font_variant_east_asian == other.font_variant_east_asian
2763    }
2764}
2765
2766impl Eq for PartialStyleProperties {}
2767
2768impl StyleProperties {
2769    fn apply_override(&self, partial: &PartialStyleProperties) -> Self {
2770        let mut new_style = self.clone();
2771        if let Some(val) = &partial.font_stack {
2772            new_style.font_stack = val.clone();
2773        }
2774        if let Some(val) = partial.font_size_px {
2775            new_style.font_size_px = val;
2776        }
2777        if let Some(val) = &partial.color {
2778            new_style.color = val.clone();
2779        }
2780        if let Some(val) = partial.letter_spacing {
2781            new_style.letter_spacing = val;
2782        }
2783        if let Some(val) = partial.word_spacing {
2784            new_style.word_spacing = val;
2785        }
2786        if let Some(val) = partial.line_height {
2787            new_style.line_height = val;
2788        }
2789        if let Some(val) = &partial.text_decoration {
2790            new_style.text_decoration = val.clone();
2791        }
2792        if let Some(val) = &partial.font_features {
2793            new_style.font_features = val.clone();
2794        }
2795        if let Some(val) = &partial.font_variations {
2796            new_style.font_variations = val.clone();
2797        }
2798        if let Some(val) = partial.tab_size {
2799            new_style.tab_size = val;
2800        }
2801        if let Some(val) = partial.text_transform {
2802            new_style.text_transform = val;
2803        }
2804        if let Some(val) = partial.writing_mode {
2805            new_style.writing_mode = val;
2806        }
2807        if let Some(val) = partial.text_orientation {
2808            new_style.text_orientation = val;
2809        }
2810        if let Some(val) = &partial.text_combine_upright {
2811            new_style.text_combine_upright = val.clone();
2812        }
2813        if let Some(val) = partial.font_variant_caps {
2814            new_style.font_variant_caps = val;
2815        }
2816        if let Some(val) = partial.font_variant_numeric {
2817            new_style.font_variant_numeric = val;
2818        }
2819        if let Some(val) = partial.font_variant_ligatures {
2820            new_style.font_variant_ligatures = val;
2821        }
2822        if let Some(val) = partial.font_variant_east_asian {
2823            new_style.font_variant_east_asian = val;
2824        }
2825        new_style
2826    }
2827}
2828
2829/// The kind of a glyph, used to distinguish characters from layout-inserted items.
2830#[derive(Debug, Clone, Copy, PartialEq)]
2831pub enum GlyphKind {
2832    /// A standard glyph representing one or more characters from the source text.
2833    Character,
2834    /// A hyphen glyph inserted by the line breaking algorithm.
2835    Hyphen,
2836    /// A `.notdef` glyph, indicating a character that could not be found in any font.
2837    NotDef,
2838    /// A Kashida justification glyph, inserted to stretch Arabic text.
2839    Kashida {
2840        /// The target width of the kashida.
2841        width: f32,
2842    },
2843}
2844
2845// --- Stage 1: Logical Representation ---
2846
2847#[derive(Debug, Clone)]
2848pub enum LogicalItem {
2849    Text {
2850        /// A stable ID pointing back to the original source character.
2851        source: ContentIndex,
2852        /// The text of this specific logical item (often a single grapheme cluster).
2853        text: String,
2854        style: Arc<StyleProperties>,
2855        /// If this text is a list marker: whether it should be positioned outside
2856        /// (in the padding gutter) or inside (inline with content).
2857        /// None for non-marker content.
2858        marker_position_outside: Option<bool>,
2859        /// The DOM NodeId of the Text node this item originated from.
2860        /// None for generated content (list markers, ::before/::after, etc.)
2861        source_node_id: Option<NodeId>,
2862    },
2863    /// Tate-chu-yoko: Run of text to be laid out horizontally within a vertical context.
2864    CombinedText {
2865        source: ContentIndex,
2866        text: String,
2867        style: Arc<StyleProperties>,
2868    },
2869    Ruby {
2870        source: ContentIndex,
2871        // For the stub, we simplify to strings. A full implementation
2872        // would need to handle Vec<LogicalItem> for both.
2873        base_text: String,
2874        ruby_text: String,
2875        style: Arc<StyleProperties>,
2876    },
2877    Object {
2878        /// A stable ID pointing back to the original source object.
2879        source: ContentIndex,
2880        /// The original non-text object.
2881        content: InlineContent,
2882    },
2883    Tab {
2884        source: ContentIndex,
2885        style: Arc<StyleProperties>,
2886    },
2887    Break {
2888        source: ContentIndex,
2889        break_info: InlineBreak,
2890    },
2891}
2892
2893impl Hash for LogicalItem {
2894    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2895        discriminant(self).hash(state);
2896        match self {
2897            LogicalItem::Text {
2898                source,
2899                text,
2900                style,
2901                marker_position_outside,
2902                source_node_id,
2903            } => {
2904                source.hash(state);
2905                text.hash(state);
2906                style.as_ref().hash(state); // Hash the content, not the Arc pointer
2907                marker_position_outside.hash(state);
2908                source_node_id.hash(state);
2909            }
2910            LogicalItem::CombinedText {
2911                source,
2912                text,
2913                style,
2914            } => {
2915                source.hash(state);
2916                text.hash(state);
2917                style.as_ref().hash(state);
2918            }
2919            LogicalItem::Ruby {
2920                source,
2921                base_text,
2922                ruby_text,
2923                style,
2924            } => {
2925                source.hash(state);
2926                base_text.hash(state);
2927                ruby_text.hash(state);
2928                style.as_ref().hash(state);
2929            }
2930            LogicalItem::Object { source, content } => {
2931                source.hash(state);
2932                content.hash(state);
2933            }
2934            LogicalItem::Tab { source, style } => {
2935                source.hash(state);
2936                style.as_ref().hash(state);
2937            }
2938            LogicalItem::Break { source, break_info } => {
2939                source.hash(state);
2940                break_info.hash(state);
2941            }
2942        }
2943    }
2944}
2945
2946// --- Stage 2: Visual Representation ---
2947
2948#[derive(Debug, Clone)]
2949pub struct VisualItem {
2950    /// A reference to the logical item this visual item originated from.
2951    /// A single LogicalItem can be split into multiple VisualItems.
2952    pub logical_source: LogicalItem,
2953    /// The Bidi embedding level for this item.
2954    pub bidi_level: BidiLevel,
2955    /// The script detected for this run, crucial for shaping.
2956    pub script: Script,
2957    /// The text content for this specific visual run.
2958    pub text: String,
2959}
2960
2961// --- Stage 3: Shaped Representation ---
2962
2963#[derive(Debug, Clone)]
2964pub enum ShapedItem {
2965    Cluster(ShapedCluster),
2966    /// A block of combined text (tate-chu-yoko) that is laid out
2967    // as a single unbreakable object.
2968    CombinedBlock {
2969        source: ContentIndex,
2970        /// The glyphs to be rendered horizontally within the vertical line.
2971        glyphs: Vec<ShapedGlyph>,
2972        bounds: Rect,
2973        baseline_offset: f32,
2974    },
2975    Object {
2976        source: ContentIndex,
2977        bounds: Rect,
2978        baseline_offset: f32,
2979        // Store original object for rendering
2980        content: InlineContent,
2981    },
2982    Tab {
2983        source: ContentIndex,
2984        bounds: Rect,
2985    },
2986    Break {
2987        source: ContentIndex,
2988        break_info: InlineBreak,
2989    },
2990}
2991
2992impl ShapedItem {
2993    pub fn as_cluster(&self) -> Option<&ShapedCluster> {
2994        match self {
2995            ShapedItem::Cluster(c) => Some(c),
2996            _ => None,
2997        }
2998    }
2999    /// Returns the bounding box of the item, relative to its own origin.
3000    ///
3001    /// The origin of the returned `Rect` is `(0,0)`, representing the top-left corner
3002    /// of the item's layout space before final positioning. The size represents the
3003    /// item's total advance (width in horizontal mode) and its line height (ascent + descent).
3004    pub fn bounds(&self) -> Rect {
3005        match self {
3006            ShapedItem::Cluster(cluster) => {
3007                // The width of a text cluster is its total advance.
3008                let width = cluster.advance;
3009
3010                // The height is the sum of its ascent and descent, which defines its line box.
3011                // We use the existing helper function which correctly calculates this from font
3012                // metrics.
3013                let (ascent, descent) = get_item_vertical_metrics(self);
3014                let height = ascent + descent;
3015
3016                Rect {
3017                    x: 0.0,
3018                    y: 0.0,
3019                    width,
3020                    height,
3021                }
3022            }
3023            // For atomic inline items like objects, combined blocks, and tabs,
3024            // their bounds have already been calculated during the shaping or measurement phase.
3025            ShapedItem::CombinedBlock { bounds, .. } => *bounds,
3026            ShapedItem::Object { bounds, .. } => *bounds,
3027            ShapedItem::Tab { bounds, .. } => *bounds,
3028
3029            // Breaks are control characters and have no visual geometry.
3030            ShapedItem::Break { .. } => Rect::default(), // A zero-sized rectangle.
3031        }
3032    }
3033}
3034
3035/// A group of glyphs that corresponds to one or more source characters (a cluster).
3036#[derive(Debug, Clone)]
3037pub struct ShapedCluster {
3038    /// The original text that this cluster was shaped from.
3039    /// This is crucial for correct hyphenation.
3040    pub text: String,
3041    /// The ID of the grapheme cluster this glyph cluster represents.
3042    pub source_cluster_id: GraphemeClusterId,
3043    /// The source `ContentIndex` for mapping back to logical items.
3044    pub source_content_index: ContentIndex,
3045    /// The DOM NodeId of the Text node this cluster originated from.
3046    /// None for generated content (list markers, ::before/::after, etc.)
3047    pub source_node_id: Option<NodeId>,
3048    /// The glyphs that make up this cluster.
3049    pub glyphs: Vec<ShapedGlyph>,
3050    /// The total advance width (horizontal) or height (vertical) of the cluster.
3051    pub advance: f32,
3052    /// The direction of this cluster, inherited from its `VisualItem`.
3053    pub direction: BidiDirection,
3054    /// Font style of this cluster
3055    pub style: Arc<StyleProperties>,
3056    /// If this cluster is a list marker: whether it should be positioned outside
3057    /// (in the padding gutter) or inside (inline with content).
3058    /// None for non-marker content.
3059    pub marker_position_outside: Option<bool>,
3060}
3061
3062/// A single, shaped glyph with its essential metrics.
3063#[derive(Debug, Clone)]
3064pub struct ShapedGlyph {
3065    /// The kind of glyph this is (character, hyphen, etc.).
3066    pub kind: GlyphKind,
3067    /// Glyph ID inside of the font
3068    pub glyph_id: u16,
3069    /// The byte offset of this glyph's source character(s) within its cluster text.
3070    pub cluster_offset: u32,
3071    /// The horizontal advance for this glyph (for horizontal text) - this is the BASE advance
3072    /// from the font metrics, WITHOUT kerning applied
3073    pub advance: f32,
3074    /// The kerning adjustment for this glyph (positive = more space, negative = less space)
3075    /// This is separate from advance so we can position glyphs absolutely
3076    pub kerning: f32,
3077    /// The horizontal offset/bearing for this glyph
3078    pub offset: Point,
3079    /// The vertical advance for this glyph (for vertical text).
3080    pub vertical_advance: f32,
3081    /// The vertical offset/bearing for this glyph.
3082    pub vertical_offset: Point,
3083    pub script: Script,
3084    pub style: Arc<StyleProperties>,
3085    /// Hash of the font - use LoadedFonts to look up the actual font when needed
3086    pub font_hash: u64,
3087    /// Cached font metrics to avoid font lookup for common operations
3088    pub font_metrics: LayoutFontMetrics,
3089}
3090
3091impl ShapedGlyph {
3092    pub fn into_glyph_instance<T: ParsedFontTrait>(
3093        &self,
3094        writing_mode: WritingMode,
3095        loaded_fonts: &LoadedFonts<T>,
3096    ) -> GlyphInstance {
3097        let size = loaded_fonts
3098            .get_by_hash(self.font_hash)
3099            .and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
3100            .unwrap_or_default();
3101
3102        let position = if writing_mode.is_advance_horizontal() {
3103            LogicalPosition {
3104                x: self.offset.x,
3105                y: self.offset.y,
3106            }
3107        } else {
3108            LogicalPosition {
3109                x: self.vertical_offset.x,
3110                y: self.vertical_offset.y,
3111            }
3112        };
3113
3114        GlyphInstance {
3115            index: self.glyph_id as u32,
3116            point: position,
3117            size,
3118        }
3119    }
3120
3121    /// Convert this ShapedGlyph into a GlyphInstance with an absolute position.
3122    /// This is used for display list generation where glyphs need their final page coordinates.
3123    pub fn into_glyph_instance_at<T: ParsedFontTrait>(
3124        &self,
3125        writing_mode: WritingMode,
3126        absolute_position: LogicalPosition,
3127        loaded_fonts: &LoadedFonts<T>,
3128    ) -> GlyphInstance {
3129        let size = loaded_fonts
3130            .get_by_hash(self.font_hash)
3131            .and_then(|font| font.get_glyph_size(self.glyph_id, self.style.font_size_px))
3132            .unwrap_or_default();
3133
3134        GlyphInstance {
3135            index: self.glyph_id as u32,
3136            point: absolute_position,
3137            size,
3138        }
3139    }
3140
3141    /// Convert this ShapedGlyph into a GlyphInstance with an absolute position.
3142    /// This version doesn't require fonts - it uses a default size.
3143    /// Use this when you don't need precise glyph bounds (e.g., display list generation).
3144    pub fn into_glyph_instance_at_simple(
3145        &self,
3146        _writing_mode: WritingMode,
3147        absolute_position: LogicalPosition,
3148    ) -> GlyphInstance {
3149        // Use font metrics to estimate size, or default to zero
3150        // The actual rendering will use the font directly
3151        GlyphInstance {
3152            index: self.glyph_id as u32,
3153            point: absolute_position,
3154            size: LogicalSize::default(),
3155        }
3156    }
3157}
3158
3159// --- Stage 4: Positioned Representation (Final Layout) ---
3160
3161#[derive(Debug, Clone)]
3162pub struct PositionedItem {
3163    pub item: ShapedItem,
3164    pub position: Point,
3165    pub line_index: usize,
3166}
3167
3168#[derive(Debug, Clone)]
3169pub struct UnifiedLayout {
3170    pub items: Vec<PositionedItem>,
3171    /// Information about content that did not fit.
3172    pub overflow: OverflowInfo,
3173}
3174
3175impl UnifiedLayout {
3176    /// Calculate the bounding box of all positioned items.
3177    /// This is computed on-demand rather than cached.
3178    pub fn bounds(&self) -> Rect {
3179        if self.items.is_empty() {
3180            return Rect::default();
3181        }
3182
3183        let mut min_x = f32::MAX;
3184        let mut min_y = f32::MAX;
3185        let mut max_x = f32::MIN;
3186        let mut max_y = f32::MIN;
3187
3188        for item in &self.items {
3189            let item_x = item.position.x;
3190            let item_y = item.position.y;
3191
3192            // Get item dimensions
3193            let item_bounds = item.item.bounds();
3194            let item_width = item_bounds.width;
3195            let item_height = item_bounds.height;
3196
3197            min_x = min_x.min(item_x);
3198            min_y = min_y.min(item_y);
3199            max_x = max_x.max(item_x + item_width);
3200            max_y = max_y.max(item_y + item_height);
3201        }
3202
3203        Rect {
3204            x: min_x,
3205            y: min_y,
3206            width: max_x - min_x,
3207            height: max_y - min_y,
3208        }
3209    }
3210
3211    pub fn is_empty(&self) -> bool {
3212        self.items.is_empty()
3213    }
3214    pub fn last_baseline(&self) -> Option<f32> {
3215        self.items
3216            .iter()
3217            .rev()
3218            .find_map(|item| get_baseline_for_item(&item.item))
3219    }
3220
3221    /// Takes a point relative to the layout's origin and returns the closest
3222    /// logical cursor position.
3223    ///
3224    /// This is the unified hit-testing implementation. The old `hit_test_to_cursor`
3225    /// method is deprecated in favor of this one.
3226    pub fn hittest_cursor(&self, point: LogicalPosition) -> Option<TextCursor> {
3227        if self.items.is_empty() {
3228            return None;
3229        }
3230
3231        // Find the closest cluster vertically and horizontally
3232        let mut closest_item_idx = 0;
3233        let mut closest_distance = f32::MAX;
3234
3235        for (idx, item) in self.items.iter().enumerate() {
3236            // Only consider cluster items for cursor placement
3237            if !matches!(item.item, ShapedItem::Cluster(_)) {
3238                continue;
3239            }
3240
3241            let item_bounds = item.item.bounds();
3242            let item_center_y = item.position.y + item_bounds.height / 2.0;
3243
3244            // Distance from click position to item center
3245            let vertical_distance = (point.y - item_center_y).abs();
3246
3247            // For horizontal distance, check if we're within the cluster bounds
3248            let horizontal_distance = if point.x < item.position.x {
3249                item.position.x - point.x
3250            } else if point.x > item.position.x + item_bounds.width {
3251                point.x - (item.position.x + item_bounds.width)
3252            } else {
3253                0.0 // Inside the cluster horizontally
3254            };
3255
3256            // Combined distance (prioritize vertical proximity)
3257            let distance = vertical_distance * 2.0 + horizontal_distance;
3258
3259            if distance < closest_distance {
3260                closest_distance = distance;
3261                closest_item_idx = idx;
3262            }
3263        }
3264
3265        // Get the closest cluster
3266        let closest_item = &self.items[closest_item_idx];
3267        let cluster = match &closest_item.item {
3268            ShapedItem::Cluster(c) => c,
3269            // Objects are treated as a single cluster for selection
3270            ShapedItem::Object { source, .. } | ShapedItem::CombinedBlock { source, .. } => {
3271                return Some(TextCursor {
3272                    cluster_id: GraphemeClusterId {
3273                        source_run: source.run_index,
3274                        start_byte_in_run: source.item_index,
3275                    },
3276                    affinity: if point.x
3277                        < closest_item.position.x + (closest_item.item.bounds().width / 2.0)
3278                    {
3279                        CursorAffinity::Leading
3280                    } else {
3281                        CursorAffinity::Trailing
3282                    },
3283                });
3284            }
3285            _ => return None,
3286        };
3287
3288        // Determine affinity based on which half of the cluster was clicked
3289        let cluster_mid_x = closest_item.position.x + cluster.advance / 2.0;
3290        let affinity = if point.x < cluster_mid_x {
3291            CursorAffinity::Leading
3292        } else {
3293            CursorAffinity::Trailing
3294        };
3295
3296        Some(TextCursor {
3297            cluster_id: cluster.source_cluster_id,
3298            affinity,
3299        })
3300    }
3301
3302    /// Given a logical selection range, returns a vector of visual rectangles
3303    /// that cover the selected text, in the layout's coordinate space.
3304    pub fn get_selection_rects(&self, range: &SelectionRange) -> Vec<LogicalRect> {
3305        // 1. Build a map from the logical cluster ID to the visual PositionedItem for fast lookups.
3306        let mut cluster_map: HashMap<GraphemeClusterId, &PositionedItem> = HashMap::new();
3307        for item in &self.items {
3308            if let Some(cluster) = item.item.as_cluster() {
3309                cluster_map.insert(cluster.source_cluster_id, item);
3310            }
3311        }
3312
3313        // 2. Normalize the range to ensure start always logically precedes end.
3314        let (start_cursor, end_cursor) = if range.start.cluster_id > range.end.cluster_id
3315            || (range.start.cluster_id == range.end.cluster_id
3316                && range.start.affinity > range.end.affinity)
3317        {
3318            (range.end, range.start)
3319        } else {
3320            (range.start, range.end)
3321        };
3322
3323        // 3. Find the positioned items corresponding to the start and end of the selection.
3324        let Some(start_item) = cluster_map.get(&start_cursor.cluster_id) else {
3325            return Vec::new();
3326        };
3327        let Some(end_item) = cluster_map.get(&end_cursor.cluster_id) else {
3328            return Vec::new();
3329        };
3330
3331        let mut rects = Vec::new();
3332
3333        // Helper to get the absolute visual X coordinate of a cursor.
3334        let get_cursor_x = |item: &PositionedItem, affinity: CursorAffinity| -> f32 {
3335            match affinity {
3336                CursorAffinity::Leading => item.position.x,
3337                CursorAffinity::Trailing => item.position.x + get_item_measure(&item.item, false),
3338            }
3339        };
3340
3341        // Helper to get the visual bounding box of all content on a specific line index.
3342        let get_line_bounds = |line_index: usize| -> Option<LogicalRect> {
3343            let items_on_line = self.items.iter().filter(|i| i.line_index == line_index);
3344
3345            let mut min_x: Option<f32> = None;
3346            let mut max_x: Option<f32> = None;
3347            let mut min_y: Option<f32> = None;
3348            let mut max_y: Option<f32> = None;
3349
3350            for item in items_on_line {
3351                // Skip items that don't take up space (like hard breaks)
3352                let item_bounds = item.item.bounds();
3353                if item_bounds.width <= 0.0 && item_bounds.height <= 0.0 {
3354                    continue;
3355                }
3356
3357                let item_x_end = item.position.x + item_bounds.width;
3358                let item_y_end = item.position.y + item_bounds.height;
3359
3360                min_x = Some(min_x.map_or(item.position.x, |mx| mx.min(item.position.x)));
3361                max_x = Some(max_x.map_or(item_x_end, |mx| mx.max(item_x_end)));
3362                min_y = Some(min_y.map_or(item.position.y, |my| my.min(item.position.y)));
3363                max_y = Some(max_y.map_or(item_y_end, |my| my.max(item_y_end)));
3364            }
3365
3366            if let (Some(min_x), Some(max_x), Some(min_y), Some(max_y)) =
3367                (min_x, max_x, min_y, max_y)
3368            {
3369                Some(LogicalRect {
3370                    origin: LogicalPosition { x: min_x, y: min_y },
3371                    size: LogicalSize {
3372                        width: max_x - min_x,
3373                        height: max_y - min_y,
3374                    },
3375                })
3376            } else {
3377                None
3378            }
3379        };
3380
3381        // 4. Handle single-line selection.
3382        if start_item.line_index == end_item.line_index {
3383            if let Some(line_bounds) = get_line_bounds(start_item.line_index) {
3384                let start_x = get_cursor_x(start_item, start_cursor.affinity);
3385                let end_x = get_cursor_x(end_item, end_cursor.affinity);
3386
3387                // Use min/max and abs to correctly handle selections made from right-to-left.
3388                rects.push(LogicalRect {
3389                    origin: LogicalPosition {
3390                        x: start_x.min(end_x),
3391                        y: line_bounds.origin.y,
3392                    },
3393                    size: LogicalSize {
3394                        width: (end_x - start_x).abs(),
3395                        height: line_bounds.size.height,
3396                    },
3397                });
3398            }
3399        }
3400        // 5. Handle multi-line selection.
3401        else {
3402            // Rectangle for the start line (from cursor to end of line).
3403            if let Some(start_line_bounds) = get_line_bounds(start_item.line_index) {
3404                let start_x = get_cursor_x(start_item, start_cursor.affinity);
3405                let line_end_x = start_line_bounds.origin.x + start_line_bounds.size.width;
3406                rects.push(LogicalRect {
3407                    origin: LogicalPosition {
3408                        x: start_x,
3409                        y: start_line_bounds.origin.y,
3410                    },
3411                    size: LogicalSize {
3412                        width: line_end_x - start_x,
3413                        height: start_line_bounds.size.height,
3414                    },
3415                });
3416            }
3417
3418            // Rectangles for all full lines in between.
3419            for line_idx in (start_item.line_index + 1)..end_item.line_index {
3420                if let Some(line_bounds) = get_line_bounds(line_idx) {
3421                    rects.push(line_bounds);
3422                }
3423            }
3424
3425            // Rectangle for the end line (from start of line to cursor).
3426            if let Some(end_line_bounds) = get_line_bounds(end_item.line_index) {
3427                let line_start_x = end_line_bounds.origin.x;
3428                let end_x = get_cursor_x(end_item, end_cursor.affinity);
3429                rects.push(LogicalRect {
3430                    origin: LogicalPosition {
3431                        x: line_start_x,
3432                        y: end_line_bounds.origin.y,
3433                    },
3434                    size: LogicalSize {
3435                        width: end_x - line_start_x,
3436                        height: end_line_bounds.size.height,
3437                    },
3438                });
3439            }
3440        }
3441
3442        rects
3443    }
3444
3445    /// Calculates the visual rectangle for a cursor at a given logical position.
3446    pub fn get_cursor_rect(&self, cursor: &TextCursor) -> Option<LogicalRect> {
3447        // Find the item and glyph corresponding to the cursor's cluster ID.
3448        for item in &self.items {
3449            if let ShapedItem::Cluster(cluster) = &item.item {
3450                if cluster.source_cluster_id == cursor.cluster_id {
3451                    // This is the correct cluster. Now find the position.
3452                    let line_height = item.item.bounds().height;
3453                    let cursor_x = match cursor.affinity {
3454                        CursorAffinity::Leading => item.position.x,
3455                        CursorAffinity::Trailing => item.position.x + cluster.advance,
3456                    };
3457                    return Some(LogicalRect {
3458                        origin: LogicalPosition {
3459                            x: cursor_x,
3460                            y: item.position.y,
3461                        },
3462                        size: LogicalSize {
3463                            width: 1.0,
3464                            height: line_height,
3465                        }, // 1px wide cursor
3466                    });
3467                }
3468            }
3469        }
3470        None
3471    }
3472
3473    /// Get a cursor at the first cluster (leading edge) in the layout.
3474    pub fn get_first_cluster_cursor(&self) -> Option<TextCursor> {
3475        for item in &self.items {
3476            if let ShapedItem::Cluster(cluster) = &item.item {
3477                return Some(TextCursor {
3478                    cluster_id: cluster.source_cluster_id,
3479                    affinity: CursorAffinity::Leading,
3480                });
3481            }
3482        }
3483        None
3484    }
3485
3486    /// Get a cursor at the last cluster (trailing edge) in the layout.
3487    pub fn get_last_cluster_cursor(&self) -> Option<TextCursor> {
3488        for item in self.items.iter().rev() {
3489            if let ShapedItem::Cluster(cluster) = &item.item {
3490                return Some(TextCursor {
3491                    cluster_id: cluster.source_cluster_id,
3492                    affinity: CursorAffinity::Trailing,
3493                });
3494            }
3495        }
3496        None
3497    }
3498
3499    /// Moves a cursor one visual unit to the left, handling line wrapping and Bidi text.
3500    pub fn move_cursor_left(
3501        &self,
3502        cursor: TextCursor,
3503        debug: &mut Option<Vec<String>>,
3504    ) -> TextCursor {
3505        if let Some(d) = debug {
3506            d.push(format!(
3507                "[Cursor] move_cursor_left: starting at byte {}, affinity {:?}",
3508                cursor.cluster_id.start_byte_in_run, cursor.affinity
3509            ));
3510        }
3511
3512        // Find current item
3513        let current_item_pos = self.items.iter().position(|i| {
3514            i.item
3515                .as_cluster()
3516                .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3517        });
3518
3519        let Some(current_pos) = current_item_pos else {
3520            if let Some(d) = debug {
3521                d.push(format!(
3522                    "[Cursor] move_cursor_left: cursor not found, staying at byte {}",
3523                    cursor.cluster_id.start_byte_in_run
3524                ));
3525            }
3526            return cursor;
3527        };
3528
3529        // If we're at trailing edge, move to leading edge of same cluster
3530        if cursor.affinity == CursorAffinity::Trailing {
3531            if let Some(d) = debug {
3532                d.push(format!(
3533                    "[Cursor] move_cursor_left: moving from trailing to leading edge of byte {}",
3534                    cursor.cluster_id.start_byte_in_run
3535                ));
3536            }
3537            return TextCursor {
3538                cluster_id: cursor.cluster_id,
3539                affinity: CursorAffinity::Leading,
3540            };
3541        }
3542
3543        // We're at leading edge, move to previous cluster's trailing edge
3544        // Search backwards for a cluster on the same line, or any cluster if at line start
3545        let current_line = self.items[current_pos].line_index;
3546
3547        if let Some(d) = debug {
3548            d.push(format!(
3549                "[Cursor] move_cursor_left: at leading edge, current line {}",
3550                current_line
3551            ));
3552        }
3553
3554        // First, try to find previous item on same line
3555        for i in (0..current_pos).rev() {
3556            if let Some(cluster) = self.items[i].item.as_cluster() {
3557                if self.items[i].line_index == current_line {
3558                    if let Some(d) = debug {
3559                        d.push(format!(
3560                            "[Cursor] move_cursor_left: found previous cluster on same line, byte \
3561                             {}",
3562                            cluster.source_cluster_id.start_byte_in_run
3563                        ));
3564                    }
3565                    return TextCursor {
3566                        cluster_id: cluster.source_cluster_id,
3567                        affinity: CursorAffinity::Trailing,
3568                    };
3569                }
3570            }
3571        }
3572
3573        // If no previous item on same line, try to move to end of previous line
3574        if current_line > 0 {
3575            let prev_line = current_line - 1;
3576            if let Some(d) = debug {
3577                d.push(format!(
3578                    "[Cursor] move_cursor_left: trying previous line {}",
3579                    prev_line
3580                ));
3581            }
3582            for i in (0..current_pos).rev() {
3583                if let Some(cluster) = self.items[i].item.as_cluster() {
3584                    if self.items[i].line_index == prev_line {
3585                        if let Some(d) = debug {
3586                            d.push(format!(
3587                                "[Cursor] move_cursor_left: found cluster on previous line, byte \
3588                                 {}",
3589                                cluster.source_cluster_id.start_byte_in_run
3590                            ));
3591                        }
3592                        return TextCursor {
3593                            cluster_id: cluster.source_cluster_id,
3594                            affinity: CursorAffinity::Trailing,
3595                        };
3596                    }
3597                }
3598            }
3599        }
3600
3601        // At start of text, can't move further
3602        if let Some(d) = debug {
3603            d.push(format!(
3604                "[Cursor] move_cursor_left: at start of text, staying at byte {}",
3605                cursor.cluster_id.start_byte_in_run
3606            ));
3607        }
3608        cursor
3609    }
3610
3611    /// Moves a cursor one visual unit to the right.
3612    pub fn move_cursor_right(
3613        &self,
3614        cursor: TextCursor,
3615        debug: &mut Option<Vec<String>>,
3616    ) -> TextCursor {
3617        if let Some(d) = debug {
3618            d.push(format!(
3619                "[Cursor] move_cursor_right: starting at byte {}, affinity {:?}",
3620                cursor.cluster_id.start_byte_in_run, cursor.affinity
3621            ));
3622        }
3623
3624        // Find current item
3625        let current_item_pos = self.items.iter().position(|i| {
3626            i.item
3627                .as_cluster()
3628                .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3629        });
3630
3631        let Some(current_pos) = current_item_pos else {
3632            if let Some(d) = debug {
3633                d.push(format!(
3634                    "[Cursor] move_cursor_right: cursor not found, staying at byte {}",
3635                    cursor.cluster_id.start_byte_in_run
3636                ));
3637            }
3638            return cursor;
3639        };
3640
3641        // If we're at leading edge, move to trailing edge of same cluster
3642        if cursor.affinity == CursorAffinity::Leading {
3643            if let Some(d) = debug {
3644                d.push(format!(
3645                    "[Cursor] move_cursor_right: moving from leading to trailing edge of byte {}",
3646                    cursor.cluster_id.start_byte_in_run
3647                ));
3648            }
3649            return TextCursor {
3650                cluster_id: cursor.cluster_id,
3651                affinity: CursorAffinity::Trailing,
3652            };
3653        }
3654
3655        // We're at trailing edge, move to next cluster's leading edge
3656        let current_line = self.items[current_pos].line_index;
3657
3658        if let Some(d) = debug {
3659            d.push(format!(
3660                "[Cursor] move_cursor_right: at trailing edge, current line {}",
3661                current_line
3662            ));
3663        }
3664
3665        // First, try to find next item on same line
3666        for i in (current_pos + 1)..self.items.len() {
3667            if let Some(cluster) = self.items[i].item.as_cluster() {
3668                if self.items[i].line_index == current_line {
3669                    if let Some(d) = debug {
3670                        d.push(format!(
3671                            "[Cursor] move_cursor_right: found next cluster on same line, byte {}",
3672                            cluster.source_cluster_id.start_byte_in_run
3673                        ));
3674                    }
3675                    return TextCursor {
3676                        cluster_id: cluster.source_cluster_id,
3677                        affinity: CursorAffinity::Leading,
3678                    };
3679                }
3680            }
3681        }
3682
3683        // If no next item on same line, try to move to start of next line
3684        let next_line = current_line + 1;
3685        if let Some(d) = debug {
3686            d.push(format!(
3687                "[Cursor] move_cursor_right: trying next line {}",
3688                next_line
3689            ));
3690        }
3691        for i in (current_pos + 1)..self.items.len() {
3692            if let Some(cluster) = self.items[i].item.as_cluster() {
3693                if self.items[i].line_index == next_line {
3694                    if let Some(d) = debug {
3695                        d.push(format!(
3696                            "[Cursor] move_cursor_right: found cluster on next line, byte {}",
3697                            cluster.source_cluster_id.start_byte_in_run
3698                        ));
3699                    }
3700                    return TextCursor {
3701                        cluster_id: cluster.source_cluster_id,
3702                        affinity: CursorAffinity::Leading,
3703                    };
3704                }
3705            }
3706        }
3707
3708        // At end of text, can't move further
3709        if let Some(d) = debug {
3710            d.push(format!(
3711                "[Cursor] move_cursor_right: at end of text, staying at byte {}",
3712                cursor.cluster_id.start_byte_in_run
3713            ));
3714        }
3715        cursor
3716    }
3717
3718    /// Moves a cursor up one line, attempting to preserve the horizontal column.
3719    pub fn move_cursor_up(
3720        &self,
3721        cursor: TextCursor,
3722        goal_x: &mut Option<f32>,
3723        debug: &mut Option<Vec<String>>,
3724    ) -> TextCursor {
3725        if let Some(d) = debug {
3726            d.push(format!(
3727                "[Cursor] move_cursor_up: from byte {} (affinity {:?})",
3728                cursor.cluster_id.start_byte_in_run, cursor.affinity
3729            ));
3730        }
3731
3732        let Some(current_item) = self.items.iter().find(|i| {
3733            i.item
3734                .as_cluster()
3735                .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3736        }) else {
3737            if let Some(d) = debug {
3738                d.push(format!(
3739                    "[Cursor] move_cursor_up: cursor not found in items, staying at byte {}",
3740                    cursor.cluster_id.start_byte_in_run
3741                ));
3742            }
3743            return cursor;
3744        };
3745
3746        if let Some(d) = debug {
3747            d.push(format!(
3748                "[Cursor] move_cursor_up: current line {}, position ({}, {})",
3749                current_item.line_index, current_item.position.x, current_item.position.y
3750            ));
3751        }
3752
3753        let target_line_idx = current_item.line_index.saturating_sub(1);
3754        if current_item.line_index == target_line_idx {
3755            if let Some(d) = debug {
3756                d.push(format!(
3757                    "[Cursor] move_cursor_up: already at top line {}, staying put",
3758                    current_item.line_index
3759                ));
3760            }
3761            return cursor;
3762        }
3763
3764        let current_x = goal_x.unwrap_or_else(|| {
3765            let x = match cursor.affinity {
3766                CursorAffinity::Leading => current_item.position.x,
3767                CursorAffinity::Trailing => {
3768                    current_item.position.x + get_item_measure(&current_item.item, false)
3769                }
3770            };
3771            *goal_x = Some(x);
3772            x
3773        });
3774
3775        // Find the Y coordinate of the middle of the target line
3776        let target_y = self
3777            .items
3778            .iter()
3779            .find(|i| i.line_index == target_line_idx)
3780            .map(|i| i.position.y + (i.item.bounds().height / 2.0))
3781            .unwrap_or(current_item.position.y);
3782
3783        if let Some(d) = debug {
3784            d.push(format!(
3785                "[Cursor] move_cursor_up: target line {}, hittesting at ({}, {})",
3786                target_line_idx, current_x, target_y
3787            ));
3788        }
3789
3790        let result = self
3791            .hittest_cursor(LogicalPosition {
3792                x: current_x,
3793                y: target_y,
3794            })
3795            .unwrap_or(cursor);
3796
3797        if let Some(d) = debug {
3798            d.push(format!(
3799                "[Cursor] move_cursor_up: result byte {} (affinity {:?})",
3800                result.cluster_id.start_byte_in_run, result.affinity
3801            ));
3802        }
3803
3804        result
3805    }
3806
3807    /// Moves a cursor down one line, attempting to preserve the horizontal column.
3808    pub fn move_cursor_down(
3809        &self,
3810        cursor: TextCursor,
3811        goal_x: &mut Option<f32>,
3812        debug: &mut Option<Vec<String>>,
3813    ) -> TextCursor {
3814        if let Some(d) = debug {
3815            d.push(format!(
3816                "[Cursor] move_cursor_down: from byte {} (affinity {:?})",
3817                cursor.cluster_id.start_byte_in_run, cursor.affinity
3818            ));
3819        }
3820
3821        let Some(current_item) = self.items.iter().find(|i| {
3822            i.item
3823                .as_cluster()
3824                .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3825        }) else {
3826            if let Some(d) = debug {
3827                d.push(format!(
3828                    "[Cursor] move_cursor_down: cursor not found in items, staying at byte {}",
3829                    cursor.cluster_id.start_byte_in_run
3830                ));
3831            }
3832            return cursor;
3833        };
3834
3835        if let Some(d) = debug {
3836            d.push(format!(
3837                "[Cursor] move_cursor_down: current line {}, position ({}, {})",
3838                current_item.line_index, current_item.position.x, current_item.position.y
3839            ));
3840        }
3841
3842        let max_line = self.items.iter().map(|i| i.line_index).max().unwrap_or(0);
3843        let target_line_idx = (current_item.line_index + 1).min(max_line);
3844        if current_item.line_index == target_line_idx {
3845            if let Some(d) = debug {
3846                d.push(format!(
3847                    "[Cursor] move_cursor_down: already at bottom line {}, staying put",
3848                    current_item.line_index
3849                ));
3850            }
3851            return cursor;
3852        }
3853
3854        let current_x = goal_x.unwrap_or_else(|| {
3855            let x = match cursor.affinity {
3856                CursorAffinity::Leading => current_item.position.x,
3857                CursorAffinity::Trailing => {
3858                    current_item.position.x + get_item_measure(&current_item.item, false)
3859                }
3860            };
3861            *goal_x = Some(x);
3862            x
3863        });
3864
3865        let target_y = self
3866            .items
3867            .iter()
3868            .find(|i| i.line_index == target_line_idx)
3869            .map(|i| i.position.y + (i.item.bounds().height / 2.0))
3870            .unwrap_or(current_item.position.y);
3871
3872        if let Some(d) = debug {
3873            d.push(format!(
3874                "[Cursor] move_cursor_down: hit testing at ({}, {})",
3875                current_x, target_y
3876            ));
3877        }
3878
3879        let result = self
3880            .hittest_cursor(LogicalPosition {
3881                x: current_x,
3882                y: target_y,
3883            })
3884            .unwrap_or(cursor);
3885
3886        if let Some(d) = debug {
3887            d.push(format!(
3888                "[Cursor] move_cursor_down: result byte {}, affinity {:?}",
3889                result.cluster_id.start_byte_in_run, result.affinity
3890            ));
3891        }
3892
3893        result
3894    }
3895
3896    /// Moves a cursor to the visual start of its current line.
3897    pub fn move_cursor_to_line_start(
3898        &self,
3899        cursor: TextCursor,
3900        debug: &mut Option<Vec<String>>,
3901    ) -> TextCursor {
3902        if let Some(d) = debug {
3903            d.push(format!(
3904                "[Cursor] move_cursor_to_line_start: starting at byte {}, affinity {:?}",
3905                cursor.cluster_id.start_byte_in_run, cursor.affinity
3906            ));
3907        }
3908
3909        let Some(current_item) = self.items.iter().find(|i| {
3910            i.item
3911                .as_cluster()
3912                .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3913        }) else {
3914            if let Some(d) = debug {
3915                d.push(format!(
3916                    "[Cursor] move_cursor_to_line_start: cursor not found, staying at byte {}",
3917                    cursor.cluster_id.start_byte_in_run
3918                ));
3919            }
3920            return cursor;
3921        };
3922
3923        if let Some(d) = debug {
3924            d.push(format!(
3925                "[Cursor] move_cursor_to_line_start: current line {}, position ({}, {})",
3926                current_item.line_index, current_item.position.x, current_item.position.y
3927            ));
3928        }
3929
3930        let first_item_on_line = self
3931            .items
3932            .iter()
3933            .filter(|i| i.line_index == current_item.line_index)
3934            .min_by(|a, b| {
3935                a.position
3936                    .x
3937                    .partial_cmp(&b.position.x)
3938                    .unwrap_or(Ordering::Equal)
3939            });
3940
3941        if let Some(item) = first_item_on_line {
3942            if let ShapedItem::Cluster(c) = &item.item {
3943                let result = TextCursor {
3944                    cluster_id: c.source_cluster_id,
3945                    affinity: CursorAffinity::Leading,
3946                };
3947                if let Some(d) = debug {
3948                    d.push(format!(
3949                        "[Cursor] move_cursor_to_line_start: result byte {}, affinity {:?}",
3950                        result.cluster_id.start_byte_in_run, result.affinity
3951                    ));
3952                }
3953                return result;
3954            }
3955        }
3956
3957        if let Some(d) = debug {
3958            d.push(format!(
3959                "[Cursor] move_cursor_to_line_start: no first item found, staying at byte {}",
3960                cursor.cluster_id.start_byte_in_run
3961            ));
3962        }
3963        cursor
3964    }
3965
3966    /// Moves a cursor to the visual end of its current line.
3967    pub fn move_cursor_to_line_end(
3968        &self,
3969        cursor: TextCursor,
3970        debug: &mut Option<Vec<String>>,
3971    ) -> TextCursor {
3972        if let Some(d) = debug {
3973            d.push(format!(
3974                "[Cursor] move_cursor_to_line_end: starting at byte {}, affinity {:?}",
3975                cursor.cluster_id.start_byte_in_run, cursor.affinity
3976            ));
3977        }
3978
3979        let Some(current_item) = self.items.iter().find(|i| {
3980            i.item
3981                .as_cluster()
3982                .map_or(false, |c| c.source_cluster_id == cursor.cluster_id)
3983        }) else {
3984            if let Some(d) = debug {
3985                d.push(format!(
3986                    "[Cursor] move_cursor_to_line_end: cursor not found, staying at byte {}",
3987                    cursor.cluster_id.start_byte_in_run
3988                ));
3989            }
3990            return cursor;
3991        };
3992
3993        if let Some(d) = debug {
3994            d.push(format!(
3995                "[Cursor] move_cursor_to_line_end: current line {}, position ({}, {})",
3996                current_item.line_index, current_item.position.x, current_item.position.y
3997            ));
3998        }
3999
4000        let last_item_on_line = self
4001            .items
4002            .iter()
4003            .filter(|i| i.line_index == current_item.line_index)
4004            .max_by(|a, b| {
4005                a.position
4006                    .x
4007                    .partial_cmp(&b.position.x)
4008                    .unwrap_or(Ordering::Equal)
4009            });
4010
4011        if let Some(item) = last_item_on_line {
4012            if let ShapedItem::Cluster(c) = &item.item {
4013                let result = TextCursor {
4014                    cluster_id: c.source_cluster_id,
4015                    affinity: CursorAffinity::Trailing,
4016                };
4017                if let Some(d) = debug {
4018                    d.push(format!(
4019                        "[Cursor] move_cursor_to_line_end: result byte {}, affinity {:?}",
4020                        result.cluster_id.start_byte_in_run, result.affinity
4021                    ));
4022                }
4023                return result;
4024            }
4025        }
4026
4027        if let Some(d) = debug {
4028            d.push(format!(
4029                "[Cursor] move_cursor_to_line_end: no last item found, staying at byte {}",
4030                cursor.cluster_id.start_byte_in_run
4031            ));
4032        }
4033        cursor
4034    }
4035}
4036
4037fn get_baseline_for_item(item: &ShapedItem) -> Option<f32> {
4038    match item {
4039        ShapedItem::CombinedBlock {
4040            baseline_offset, ..
4041        } => Some(*baseline_offset),
4042        ShapedItem::Object {
4043            baseline_offset, ..
4044        } => Some(*baseline_offset),
4045        // We have to get the clusters font from the last glyph
4046        ShapedItem::Cluster(ref cluster) => {
4047            if let Some(last_glyph) = cluster.glyphs.last() {
4048                Some(
4049                    last_glyph
4050                        .font_metrics
4051                        .baseline_scaled(last_glyph.style.font_size_px),
4052                )
4053            } else {
4054                None
4055            }
4056        }
4057        ShapedItem::Break { source, break_info } => {
4058            // Breaks do not contribute to baseline
4059            None
4060        }
4061        ShapedItem::Tab { source, bounds } => {
4062            // Tabs do not contribute to baseline
4063            None
4064        }
4065    }
4066}
4067
4068/// Stores information about content that exceeded the available layout space.
4069#[derive(Debug, Clone, Default)]
4070pub struct OverflowInfo {
4071    /// The items that did not fit within the constraints.
4072    pub overflow_items: Vec<ShapedItem>,
4073    /// The total bounds of all content, including overflowing items.
4074    /// This is useful for `OverflowBehavior::Visible` or `Scroll`.
4075    pub unclipped_bounds: Rect,
4076}
4077
4078impl OverflowInfo {
4079    pub fn has_overflow(&self) -> bool {
4080        !self.overflow_items.is_empty()
4081    }
4082}
4083
4084/// Intermediate structure carrying information from the line breaker to the positioner.
4085#[derive(Debug, Clone)]
4086pub struct UnifiedLine {
4087    pub items: Vec<ShapedItem>,
4088    /// The y-position (for horizontal) or x-position (for vertical) of the line's baseline.
4089    pub cross_axis_position: f32,
4090    /// The geometric segments this line must fit into.
4091    pub constraints: LineConstraints,
4092    pub is_last: bool,
4093}
4094
4095// --- Caching Infrastructure ---
4096
4097pub type CacheId = u64;
4098
4099/// Defines a single area for layout, with its own shape and properties.
4100#[derive(Debug, Clone)]
4101pub struct LayoutFragment {
4102    /// A unique identifier for this fragment (e.g., "main-content", "sidebar").
4103    pub id: String,
4104    /// The geometric and style constraints for this specific fragment.
4105    pub constraints: UnifiedConstraints,
4106}
4107
4108/// Represents the final layout distributed across multiple fragments.
4109#[derive(Debug, Clone)]
4110pub struct FlowLayout {
4111    /// A map from a fragment's unique ID to the layout it contains.
4112    pub fragment_layouts: HashMap<String, Arc<UnifiedLayout>>,
4113    /// Any items that did not fit into the last fragment in the flow chain.
4114    /// This is useful for pagination or determining if more layout space is needed.
4115    pub remaining_items: Vec<ShapedItem>,
4116}
4117
4118pub struct LayoutCache {
4119    // Stage 1 Cache: InlineContent -> LogicalItems
4120    logical_items: HashMap<CacheId, Arc<Vec<LogicalItem>>>,
4121    // Stage 2 Cache: LogicalItems -> VisualItems
4122    visual_items: HashMap<CacheId, Arc<Vec<VisualItem>>>,
4123    // Stage 3 Cache: VisualItems -> ShapedItems (now strongly typed)
4124    shaped_items: HashMap<CacheId, Arc<Vec<ShapedItem>>>,
4125    // Stage 4 Cache: ShapedItems + Constraints -> Final Layout (now strongly typed)
4126    layouts: HashMap<CacheId, Arc<UnifiedLayout>>,
4127}
4128
4129impl LayoutCache {
4130    pub fn new() -> Self {
4131        Self {
4132            logical_items: HashMap::new(),
4133            visual_items: HashMap::new(),
4134            shaped_items: HashMap::new(),
4135            layouts: HashMap::new(),
4136        }
4137    }
4138
4139    /// Get a layout from the cache by its ID
4140    pub fn get_layout(&self, cache_id: &CacheId) -> Option<&Arc<UnifiedLayout>> {
4141        self.layouts.get(cache_id)
4142    }
4143
4144    /// Get all layout cache IDs (for iteration/debugging)
4145    pub fn get_all_layout_ids(&self) -> Vec<CacheId> {
4146        self.layouts.keys().copied().collect()
4147    }
4148    
4149    /// Check if we can reuse an old layout based on layout-affecting parameters.
4150    /// 
4151    /// This function compares only the parameters that affect glyph positions,
4152    /// not rendering-only parameters like color or text-decoration.
4153    /// 
4154    /// # Parameters
4155    /// - `old_constraints`: The constraints used for the cached layout
4156    /// - `new_constraints`: The constraints for the new layout request
4157    /// - `old_content`: The content used for the cached layout
4158    /// - `new_content`: The new content to layout
4159    /// 
4160    /// # Returns
4161    /// - `true` if the old layout can be reused (only rendering changed)
4162    /// - `false` if a new layout is needed (layout-affecting params changed)
4163    pub fn use_old_layout(
4164        old_constraints: &UnifiedConstraints,
4165        new_constraints: &UnifiedConstraints,
4166        old_content: &[InlineContent],
4167        new_content: &[InlineContent],
4168    ) -> bool {
4169        // First check: constraints must match exactly for layout purposes
4170        if old_constraints != new_constraints {
4171            return false;
4172        }
4173        
4174        // Second check: content length must match
4175        if old_content.len() != new_content.len() {
4176            return false;
4177        }
4178        
4179        // Third check: each content item must have same layout properties
4180        for (old, new) in old_content.iter().zip(new_content.iter()) {
4181            if !Self::inline_content_layout_eq(old, new) {
4182                return false;
4183            }
4184        }
4185        
4186        true
4187    }
4188    
4189    /// Compare two InlineContent items for layout equality.
4190    /// 
4191    /// Returns true if the layouts would be identical (only rendering differs).
4192    fn inline_content_layout_eq(old: &InlineContent, new: &InlineContent) -> bool {
4193        use InlineContent::*;
4194        match (old, new) {
4195            (Text(old_run), Text(new_run)) => {
4196                // Text must match exactly, but style only needs layout_eq
4197                old_run.text == new_run.text 
4198                    && old_run.style.layout_eq(&new_run.style)
4199            }
4200            (Image(old_img), Image(new_img)) => {
4201                // Images: size affects layout, but not visual properties
4202                old_img.intrinsic_size == new_img.intrinsic_size
4203                    && old_img.display_size == new_img.display_size
4204                    && old_img.baseline_offset == new_img.baseline_offset
4205                    && old_img.alignment == new_img.alignment
4206            }
4207            (Space(old_sp), Space(new_sp)) => old_sp == new_sp,
4208            (LineBreak(old_br), LineBreak(new_br)) => old_br == new_br,
4209            (Tab, Tab) => true,
4210            (Marker { run: old_run, position_outside: old_pos },
4211             Marker { run: new_run, position_outside: new_pos }) => {
4212                old_pos == new_pos
4213                    && old_run.text == new_run.text
4214                    && old_run.style.layout_eq(&new_run.style)
4215            }
4216            (Shape(old_shape), Shape(new_shape)) => {
4217                // Shapes: shape_def affects layout, not fill/stroke
4218                old_shape.shape_def == new_shape.shape_def
4219                    && old_shape.baseline_offset == new_shape.baseline_offset
4220            }
4221            (Ruby { base: old_base, text: old_text, style: old_style },
4222             Ruby { base: new_base, text: new_text, style: new_style }) => {
4223                old_style.layout_eq(new_style)
4224                    && old_base.len() == new_base.len()
4225                    && old_text.len() == new_text.len()
4226                    && old_base.iter().zip(new_base.iter())
4227                        .all(|(o, n)| Self::inline_content_layout_eq(o, n))
4228                    && old_text.iter().zip(new_text.iter())
4229                        .all(|(o, n)| Self::inline_content_layout_eq(o, n))
4230            }
4231            // Different variants cannot have same layout
4232            _ => false,
4233        }
4234    }
4235}
4236
4237impl Default for LayoutCache {
4238    fn default() -> Self {
4239        Self::new()
4240    }
4241}
4242
4243/// Key for caching the conversion from `InlineContent` to `LogicalItem`s.
4244#[derive(Debug, Clone, Eq, PartialEq, Hash)]
4245pub struct LogicalItemsKey<'a> {
4246    pub inline_content_hash: u64, // Pre-hash the content for efficiency
4247    pub default_font_size: u32,   // Affects space widths
4248    // Add other relevant properties from constraints if they affect this stage
4249    pub _marker: std::marker::PhantomData<&'a ()>,
4250}
4251
4252/// Key for caching the Bidi reordering stage.
4253#[derive(Debug, Clone, Eq, PartialEq, Hash)]
4254pub struct VisualItemsKey {
4255    pub logical_items_id: CacheId,
4256    pub base_direction: BidiDirection,
4257}
4258
4259/// Key for caching the shaping stage.
4260#[derive(Debug, Clone, Eq, PartialEq, Hash)]
4261pub struct ShapedItemsKey {
4262    pub visual_items_id: CacheId,
4263    pub style_hash: u64, // Represents a hash of all font/style properties
4264}
4265
4266impl ShapedItemsKey {
4267    pub fn new(visual_items_id: CacheId, visual_items: &[VisualItem]) -> Self {
4268        let style_hash = {
4269            let mut hasher = DefaultHasher::new();
4270            for item in visual_items.iter() {
4271                // Hash the style from the logical source, as this is what determines the font.
4272                match &item.logical_source {
4273                    LogicalItem::Text { style, .. } | LogicalItem::CombinedText { style, .. } => {
4274                        style.as_ref().hash(&mut hasher);
4275                    }
4276                    _ => {}
4277                }
4278            }
4279            hasher.finish()
4280        };
4281
4282        Self {
4283            visual_items_id,
4284            style_hash,
4285        }
4286    }
4287}
4288
4289/// Key for the final layout stage.
4290#[derive(Debug, Clone, Eq, PartialEq, Hash)]
4291pub struct LayoutKey {
4292    pub shaped_items_id: CacheId,
4293    pub constraints: UnifiedConstraints,
4294}
4295
4296/// Helper to create a `CacheId` from any `Hash`able type.
4297fn calculate_id<T: Hash>(item: &T) -> CacheId {
4298    let mut hasher = DefaultHasher::new();
4299    item.hash(&mut hasher);
4300    hasher.finish()
4301}
4302
4303// --- Main Layout Pipeline Implementation ---
4304
4305impl LayoutCache {
4306    /// New top-level entry point for flowing layout across multiple regions.
4307    ///
4308    /// This function orchestrates the entire layout pipeline, but instead of fitting
4309    /// content into a single set of constraints, it flows the content through an
4310    /// ordered sequence of `LayoutFragment`s.
4311    ///
4312    /// # CSS Inline Layout Module Level 3: Pipeline Implementation
4313    ///
4314    /// This implements the inline formatting context with 5 stages:
4315    ///
4316    /// ## Stage 1: Logical Analysis (InlineContent -> LogicalItem)
4317    /// \u2705 IMPLEMENTED: Parses raw content into logical units
4318    /// - Handles text runs, inline-blocks, replaced elements
4319    /// - Applies style overrides at character level
4320    /// - Implements \u00a7 2.2: Content size contribution calculation
4321    ///
4322    /// ## Stage 2: BiDi Reordering (LogicalItem -> VisualItem)
4323    /// \u2705 IMPLEMENTED: Uses CSS 'direction' property per CSS Writing Modes
4324    /// - Reorders items for right-to-left text (Arabic, Hebrew)
4325    /// - Respects containing block direction (not auto-detection)
4326    /// - Conforms to Unicode BiDi Algorithm (UAX #9)
4327    ///
4328    /// ## Stage 3: Shaping (VisualItem -> ShapedItem)
4329    /// \u2705 IMPLEMENTED: Converts text to glyphs
4330    /// - Uses HarfBuzz for OpenType shaping
4331    /// - Handles ligatures, kerning, contextual forms
4332    /// - Caches shaped results for performance
4333    ///
4334    /// ## Stage 4: Text Orientation Transformations
4335    /// \u26a0\ufe0f PARTIAL: Applies text-orientation for vertical text
4336    /// - Uses constraints from *first* fragment only
4337    /// - \u274c TODO: Should re-orient if fragments have different writing modes
4338    ///
4339    /// ## Stage 5: Flow Loop (ShapedItem -> PositionedItem)
4340    /// \u2705 IMPLEMENTED: Breaks lines and positions content
4341    /// - Calls perform_fragment_layout for each fragment
4342    /// - Uses BreakCursor to flow content across fragments
4343    /// - Implements \u00a7 5: Line breaking and hyphenation
4344    ///
4345    /// # Missing Features from CSS Inline-3:
4346    /// - \u00a7 3.3: initial-letter (drop caps)
4347    /// - \u00a7 4: vertical-align (only baseline supported)
4348    /// - \u00a7 6: text-box-trim (leading trim)
4349    /// - \u00a7 7: inline-sizing (aspect-ratio for inline-blocks)
4350    ///
4351    /// # Arguments
4352    /// * `content` - The raw `InlineContent` to be laid out.
4353    /// * `style_overrides` - Character-level style changes.
4354    /// * `flow_chain` - An ordered slice of `LayoutFragment` defining the regions (e.g., columns,
4355    ///   pages) that the content should flow through.
4356    /// * `font_chain_cache` - Pre-resolved font chains (from FontManager.font_chain_cache)
4357    /// * `fc_cache` - The fontconfig cache for font lookups
4358    /// * `loaded_fonts` - Pre-loaded fonts, keyed by FontId
4359    ///
4360    /// # Returns
4361    /// A `FlowLayout` struct containing the positioned items for each fragment that
4362    /// was filled, and any content that did not fit in the final fragment.
4363    pub fn layout_flow<T: ParsedFontTrait>(
4364        &mut self,
4365        content: &[InlineContent],
4366        style_overrides: &[StyleOverride],
4367        flow_chain: &[LayoutFragment],
4368        font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
4369        fc_cache: &FcFontCache,
4370        loaded_fonts: &LoadedFonts<T>,
4371        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
4372    ) -> Result<FlowLayout, LayoutError> {
4373        // --- Stages 1-3: Preparation ---
4374        // These stages are independent of the final geometry. We perform them once
4375        // on the entire content block before flowing. Caching is used at each stage.
4376
4377        // Stage 1: Logical Analysis (InlineContent -> LogicalItem)
4378        let logical_items_id = calculate_id(&content);
4379        let logical_items = self
4380            .logical_items
4381            .entry(logical_items_id)
4382            .or_insert_with(|| {
4383                Arc::new(create_logical_items(
4384                    content,
4385                    style_overrides,
4386                    debug_messages,
4387                ))
4388            })
4389            .clone();
4390
4391        // Get the first fragment's constraints to extract the CSS direction property.
4392        // This is used for BiDi reordering in Stage 2.
4393        let default_constraints = UnifiedConstraints::default();
4394        let first_constraints = flow_chain
4395            .first()
4396            .map(|f| &f.constraints)
4397            .unwrap_or(&default_constraints);
4398
4399        // Stage 2: Bidi Reordering (LogicalItem -> VisualItem)
4400        // Use CSS direction property from constraints instead of auto-detecting from text content.
4401        // This fixes issues with mixed-direction text (e.g., "Arabic - Latin") where auto-detection
4402        // would treat the entire paragraph as RTL if the first strong character is Arabic.
4403        // Per HTML/CSS spec, base direction should come from the 'direction' CSS property,
4404        // defaulting to LTR if not specified.
4405        let base_direction = first_constraints.direction.unwrap_or(BidiDirection::Ltr);
4406        let visual_key = VisualItemsKey {
4407            logical_items_id,
4408            base_direction,
4409        };
4410        let visual_items_id = calculate_id(&visual_key);
4411        let visual_items = self
4412            .visual_items
4413            .entry(visual_items_id)
4414            .or_insert_with(|| {
4415                Arc::new(
4416                    reorder_logical_items(&logical_items, base_direction, debug_messages).unwrap(),
4417                )
4418            })
4419            .clone();
4420
4421        // Stage 3: Shaping (VisualItem -> ShapedItem)
4422        let shaped_key = ShapedItemsKey::new(visual_items_id, &visual_items);
4423        let shaped_items_id = calculate_id(&shaped_key);
4424        let shaped_items = match self.shaped_items.get(&shaped_items_id) {
4425            Some(cached) => cached.clone(),
4426            None => {
4427                let items = Arc::new(shape_visual_items(
4428                    &visual_items,
4429                    font_chain_cache,
4430                    fc_cache,
4431                    loaded_fonts,
4432                    debug_messages,
4433                )?);
4434                self.shaped_items.insert(shaped_items_id, items.clone());
4435                items
4436            }
4437        };
4438
4439        // --- Stage 4: Apply Vertical Text Transformations ---
4440
4441        // Note: first_constraints was already extracted above for BiDi reordering (Stage 2).
4442        // This orients all text based on the constraints of the *first* fragment.
4443        // A more advanced system could defer orientation until inside the loop if
4444        // fragments can have different writing modes.
4445        let oriented_items = apply_text_orientation(shaped_items, first_constraints)?;
4446
4447        // --- Stage 5: The Flow Loop ---
4448
4449        let mut fragment_layouts = HashMap::new();
4450        // The cursor now manages the stream of items for the entire flow.
4451        let mut cursor = BreakCursor::new(&oriented_items);
4452
4453        for fragment in flow_chain {
4454            // Perform layout for this single fragment, consuming items from the cursor.
4455            let fragment_layout = perform_fragment_layout(
4456                &mut cursor,
4457                &logical_items,
4458                &fragment.constraints,
4459                debug_messages,
4460                loaded_fonts,
4461            )?;
4462
4463            fragment_layouts.insert(fragment.id.clone(), Arc::new(fragment_layout));
4464            if cursor.is_done() {
4465                break; // All content has been laid out.
4466            }
4467        }
4468
4469        Ok(FlowLayout {
4470            fragment_layouts,
4471            remaining_items: cursor.drain_remaining(),
4472        })
4473    }
4474}
4475
4476// --- Stage 1 Implementation ---
4477pub fn create_logical_items(
4478    content: &[InlineContent],
4479    style_overrides: &[StyleOverride],
4480    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
4481) -> Vec<LogicalItem> {
4482    if let Some(msgs) = debug_messages {
4483        msgs.push(LayoutDebugMessage::info(
4484            "\n--- Entering create_logical_items (Refactored) ---".to_string(),
4485        ));
4486        msgs.push(LayoutDebugMessage::info(format!(
4487            "Input content length: {}",
4488            content.len()
4489        )));
4490        msgs.push(LayoutDebugMessage::info(format!(
4491            "Input overrides length: {}",
4492            style_overrides.len()
4493        )));
4494    }
4495
4496    let mut items = Vec::new();
4497    let mut style_cache: HashMap<u64, Arc<StyleProperties>> = HashMap::new();
4498
4499    // 1. Organize overrides for fast lookup per run.
4500    let mut run_overrides: HashMap<u32, HashMap<u32, &PartialStyleProperties>> = HashMap::new();
4501    for override_item in style_overrides {
4502        run_overrides
4503            .entry(override_item.target.run_index)
4504            .or_default()
4505            .insert(override_item.target.item_index, &override_item.style);
4506    }
4507
4508    for (run_idx, inline_item) in content.iter().enumerate() {
4509        if let Some(msgs) = debug_messages {
4510            msgs.push(LayoutDebugMessage::info(format!(
4511                "Processing content run #{}",
4512                run_idx
4513            )));
4514        }
4515
4516        // Extract marker information if this is a marker
4517        let marker_position_outside = match inline_item {
4518            InlineContent::Marker {
4519                position_outside, ..
4520            } => Some(*position_outside),
4521            _ => None,
4522        };
4523
4524        match inline_item {
4525            InlineContent::Text(run) | InlineContent::Marker { run, .. } => {
4526                let text = &run.text;
4527                if text.is_empty() {
4528                    if let Some(msgs) = debug_messages {
4529                        msgs.push(LayoutDebugMessage::info(
4530                            "  Run is empty, skipping.".to_string(),
4531                        ));
4532                    }
4533                    continue;
4534                }
4535                if let Some(msgs) = debug_messages {
4536                    msgs.push(LayoutDebugMessage::info(format!("  Run text: '{}'", text)));
4537                }
4538
4539                let current_run_overrides = run_overrides.get(&(run_idx as u32));
4540                let mut boundaries = BTreeSet::new();
4541                boundaries.insert(0);
4542                boundaries.insert(text.len());
4543
4544                // --- Stateful Boundary Generation ---
4545                let mut scan_cursor = 0;
4546                while scan_cursor < text.len() {
4547                    let style_at_cursor = if let Some(partial) =
4548                        current_run_overrides.and_then(|o| o.get(&(scan_cursor as u32)))
4549                    {
4550                        // Create a temporary, full style to check its properties
4551                        run.style.apply_override(partial)
4552                    } else {
4553                        (*run.style).clone()
4554                    };
4555
4556                    let current_char = text[scan_cursor..].chars().next().unwrap();
4557
4558                    // Rule 1: Multi-character features take precedence.
4559                    if let Some(TextCombineUpright::Digits(max_digits)) =
4560                        style_at_cursor.text_combine_upright
4561                    {
4562                        if max_digits > 0 && current_char.is_ascii_digit() {
4563                            let digit_chunk: String = text[scan_cursor..]
4564                                .chars()
4565                                .take(max_digits as usize)
4566                                .take_while(|c| c.is_ascii_digit())
4567                                .collect();
4568
4569                            let end_of_chunk = scan_cursor + digit_chunk.len();
4570                            boundaries.insert(scan_cursor);
4571                            boundaries.insert(end_of_chunk);
4572                            scan_cursor = end_of_chunk; // Jump past the entire sequence
4573                            continue;
4574                        }
4575                    }
4576
4577                    // Rule 2: If no multi-char feature, check for a normal single-grapheme
4578                    // override.
4579                    if current_run_overrides
4580                        .and_then(|o| o.get(&(scan_cursor as u32)))
4581                        .is_some()
4582                    {
4583                        let grapheme_len = text[scan_cursor..]
4584                            .graphemes(true)
4585                            .next()
4586                            .unwrap_or("")
4587                            .len();
4588                        boundaries.insert(scan_cursor);
4589                        boundaries.insert(scan_cursor + grapheme_len);
4590                        scan_cursor += grapheme_len;
4591                        continue;
4592                    }
4593
4594                    // Rule 3: No special features or overrides at this point, just advance one
4595                    // char.
4596                    scan_cursor += current_char.len_utf8();
4597                }
4598
4599                if let Some(msgs) = debug_messages {
4600                    msgs.push(LayoutDebugMessage::info(format!(
4601                        "  Boundaries: {:?}",
4602                        boundaries
4603                    )));
4604                }
4605
4606                // --- Chunk Processing ---
4607                for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
4608                    let (start, end) = (*start, *end);
4609                    if start >= end {
4610                        continue;
4611                    }
4612
4613                    let text_slice = &text[start..end];
4614                    if let Some(msgs) = debug_messages {
4615                        msgs.push(LayoutDebugMessage::info(format!(
4616                            "  Processing chunk from {} to {}: '{}'",
4617                            start, end, text_slice
4618                        )));
4619                    }
4620
4621                    let style_to_use = if let Some(partial_style) =
4622                        current_run_overrides.and_then(|o| o.get(&(start as u32)))
4623                    {
4624                        if let Some(msgs) = debug_messages {
4625                            msgs.push(LayoutDebugMessage::info(format!(
4626                                "  -> Applying override at byte {}",
4627                                start
4628                            )));
4629                        }
4630                        let mut hasher = DefaultHasher::new();
4631                        Arc::as_ptr(&run.style).hash(&mut hasher);
4632                        partial_style.hash(&mut hasher);
4633                        style_cache
4634                            .entry(hasher.finish())
4635                            .or_insert_with(|| Arc::new(run.style.apply_override(partial_style)))
4636                            .clone()
4637                    } else {
4638                        run.style.clone()
4639                    };
4640
4641                    let is_combinable_chunk = if let Some(TextCombineUpright::Digits(max_digits)) =
4642                        &style_to_use.text_combine_upright
4643                    {
4644                        *max_digits > 0
4645                            && !text_slice.is_empty()
4646                            && text_slice.chars().all(|c| c.is_ascii_digit())
4647                            && text_slice.chars().count() <= *max_digits as usize
4648                    } else {
4649                        false
4650                    };
4651
4652                    if is_combinable_chunk {
4653                        items.push(LogicalItem::CombinedText {
4654                            source: ContentIndex {
4655                                run_index: run_idx as u32,
4656                                item_index: start as u32,
4657                            },
4658                            text: text_slice.to_string(),
4659                            style: style_to_use,
4660                        });
4661                    } else {
4662                        items.push(LogicalItem::Text {
4663                            source: ContentIndex {
4664                                run_index: run_idx as u32,
4665                                item_index: start as u32,
4666                            },
4667                            text: text_slice.to_string(),
4668                            style: style_to_use,
4669                            marker_position_outside,
4670                            source_node_id: run.source_node_id,
4671                        });
4672                    }
4673                }
4674            }
4675            // Handle explicit line breaks (from white-space: pre or <br>)
4676            InlineContent::LineBreak(break_info) => {
4677                if let Some(msgs) = debug_messages {
4678                    msgs.push(LayoutDebugMessage::info(format!(
4679                        "  LineBreak: {:?}",
4680                        break_info
4681                    )));
4682                }
4683                items.push(LogicalItem::Break {
4684                    source: ContentIndex {
4685                        run_index: run_idx as u32,
4686                        item_index: 0,
4687                    },
4688                    break_info: break_info.clone(),
4689                });
4690            }
4691            // Other cases (Image, Shape, Space, Tab, Ruby)
4692            _ => {
4693                if let Some(msgs) = debug_messages {
4694                    msgs.push(LayoutDebugMessage::info(
4695                        "  Run is not text, creating generic LogicalItem.".to_string(),
4696                    ));
4697                }
4698                items.push(LogicalItem::Object {
4699                    source: ContentIndex {
4700                        run_index: run_idx as u32,
4701                        item_index: 0,
4702                    },
4703                    content: inline_item.clone(),
4704                });
4705            }
4706        }
4707    }
4708    if let Some(msgs) = debug_messages {
4709        msgs.push(LayoutDebugMessage::info(format!(
4710            "--- Exiting create_logical_items, created {} items ---",
4711            items.len()
4712        )));
4713    }
4714    items
4715}
4716
4717// --- Stage 2 Implementation ---
4718
4719pub fn get_base_direction_from_logical(logical_items: &[LogicalItem]) -> BidiDirection {
4720    let first_strong = logical_items.iter().find_map(|item| {
4721        if let LogicalItem::Text { text, .. } = item {
4722            Some(unicode_bidi::get_base_direction(text.as_str()))
4723        } else {
4724            None
4725        }
4726    });
4727
4728    match first_strong {
4729        Some(unicode_bidi::Direction::Rtl) => BidiDirection::Rtl,
4730        _ => BidiDirection::Ltr,
4731    }
4732}
4733
4734pub fn reorder_logical_items(
4735    logical_items: &[LogicalItem],
4736    base_direction: BidiDirection,
4737    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
4738) -> Result<Vec<VisualItem>, LayoutError> {
4739    if let Some(msgs) = debug_messages {
4740        msgs.push(LayoutDebugMessage::info(
4741            "\n--- Entering reorder_logical_items ---".to_string(),
4742        ));
4743        msgs.push(LayoutDebugMessage::info(format!(
4744            "Input logical items count: {}",
4745            logical_items.len()
4746        )));
4747        msgs.push(LayoutDebugMessage::info(format!(
4748            "Base direction: {:?}",
4749            base_direction
4750        )));
4751    }
4752
4753    let mut bidi_str = String::new();
4754    let mut item_map = Vec::new();
4755    for (idx, item) in logical_items.iter().enumerate() {
4756        let text = match item {
4757            LogicalItem::Text { text, .. } => text.as_str(),
4758            LogicalItem::CombinedText { text, .. } => text.as_str(),
4759            _ => "\u{FFFC}",
4760        };
4761        let start_byte = bidi_str.len();
4762        bidi_str.push_str(text);
4763        for _ in start_byte..bidi_str.len() {
4764            item_map.push(idx);
4765        }
4766    }
4767
4768    if bidi_str.is_empty() {
4769        if let Some(msgs) = debug_messages {
4770            msgs.push(LayoutDebugMessage::info(
4771                "Bidi string is empty, returning.".to_string(),
4772            ));
4773        }
4774        return Ok(Vec::new());
4775    }
4776    if let Some(msgs) = debug_messages {
4777        msgs.push(LayoutDebugMessage::info(format!(
4778            "Constructed bidi string: '{}'",
4779            bidi_str
4780        )));
4781    }
4782
4783    let bidi_level = if base_direction == BidiDirection::Rtl {
4784        Some(Level::rtl())
4785    } else {
4786        Some(Level::ltr())
4787    };
4788    let bidi_info = BidiInfo::new(&bidi_str, bidi_level);
4789    let para = &bidi_info.paragraphs[0];
4790    let (levels, visual_runs) = bidi_info.visual_runs(para, para.range.clone());
4791
4792    if let Some(msgs) = debug_messages {
4793        msgs.push(LayoutDebugMessage::info(
4794            "Bidi visual runs generated:".to_string(),
4795        ));
4796        for (i, run_range) in visual_runs.iter().enumerate() {
4797            let level = levels[run_range.start].number();
4798            let slice = &bidi_str[run_range.start..run_range.end];
4799            msgs.push(LayoutDebugMessage::info(format!(
4800                "  Run {}: range={:?}, level={}, text='{}'",
4801                i, run_range, level, slice
4802            )));
4803        }
4804    }
4805
4806    let mut visual_items = Vec::new();
4807    for run_range in visual_runs {
4808        let bidi_level = BidiLevel::new(levels[run_range.start].number());
4809        let mut sub_run_start = run_range.start;
4810
4811        for i in (run_range.start + 1)..run_range.end {
4812            if item_map[i] != item_map[sub_run_start] {
4813                let logical_idx = item_map[sub_run_start];
4814                let logical_item = &logical_items[logical_idx];
4815                let text_slice = &bidi_str[sub_run_start..i];
4816                visual_items.push(VisualItem {
4817                    logical_source: logical_item.clone(),
4818                    bidi_level,
4819                    script: crate::text3::script::detect_script(text_slice)
4820                        .unwrap_or(Script::Latin),
4821                    text: text_slice.to_string(),
4822                });
4823                sub_run_start = i;
4824            }
4825        }
4826
4827        let logical_idx = item_map[sub_run_start];
4828        let logical_item = &logical_items[logical_idx];
4829        let text_slice = &bidi_str[sub_run_start..run_range.end];
4830        visual_items.push(VisualItem {
4831            logical_source: logical_item.clone(),
4832            bidi_level,
4833            script: crate::text3::script::detect_script(text_slice).unwrap_or(Script::Latin),
4834            text: text_slice.to_string(),
4835        });
4836    }
4837
4838    if let Some(msgs) = debug_messages {
4839        msgs.push(LayoutDebugMessage::info(
4840            "Final visual items produced:".to_string(),
4841        ));
4842        for (i, item) in visual_items.iter().enumerate() {
4843            msgs.push(LayoutDebugMessage::info(format!(
4844                "  Item {}: level={}, text='{}'",
4845                i,
4846                item.bidi_level.level(),
4847                item.text
4848            )));
4849        }
4850        msgs.push(LayoutDebugMessage::info(
4851            "--- Exiting reorder_logical_items ---".to_string(),
4852        ));
4853    }
4854    Ok(visual_items)
4855}
4856
4857// --- Stage 3 Implementation ---
4858
4859/// Shape visual items into ShapedItems using pre-loaded fonts.
4860///
4861/// This function does NOT load any fonts - all fonts must be pre-loaded and passed in.
4862/// If a required font is not in `loaded_fonts`, the text will be skipped with a warning.
4863pub fn shape_visual_items<T: ParsedFontTrait>(
4864    visual_items: &[VisualItem],
4865    font_chain_cache: &HashMap<FontChainKey, rust_fontconfig::FontFallbackChain>,
4866    fc_cache: &FcFontCache,
4867    loaded_fonts: &LoadedFonts<T>,
4868    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
4869) -> Result<Vec<ShapedItem>, LayoutError> {
4870    let mut shaped = Vec::new();
4871
4872    for item in visual_items {
4873        match &item.logical_source {
4874            LogicalItem::Text {
4875                style,
4876                source,
4877                marker_position_outside,
4878                source_node_id,
4879                ..
4880            } => {
4881                let direction = if item.bidi_level.is_rtl() {
4882                    BidiDirection::Rtl
4883                } else {
4884                    BidiDirection::Ltr
4885                };
4886
4887                let language = script_to_language(item.script, &item.text);
4888
4889                // Shape text using either FontRef directly or fontconfig-resolved font
4890                let shaped_clusters_result: Result<Vec<ShapedCluster>, LayoutError> = match &style.font_stack {
4891                    FontStack::Ref(font_ref) => {
4892                        // For FontRef, use the font directly without fontconfig
4893                        if let Some(msgs) = debug_messages {
4894                            msgs.push(LayoutDebugMessage::info(format!(
4895                                "[TextLayout] Using direct FontRef for text: '{}'",
4896                                item.text.chars().take(30).collect::<String>()
4897                            )));
4898                        }
4899                        shape_text_correctly(
4900                            &item.text,
4901                            item.script,
4902                            language,
4903                            direction,
4904                            font_ref,
4905                            style,
4906                            *source,
4907                            *source_node_id,
4908                        )
4909                    }
4910                    FontStack::Stack(selectors) => {
4911                        // Build FontChainKey and resolve through fontconfig
4912                        let cache_key = FontChainKey::from_selectors(selectors);
4913
4914                        // Look up pre-resolved font chain
4915                        let font_chain = match font_chain_cache.get(&cache_key) {
4916                            Some(chain) => chain,
4917                            None => {
4918                                if let Some(msgs) = debug_messages {
4919                                    msgs.push(LayoutDebugMessage::warning(format!(
4920                                        "[TextLayout] Font chain not pre-resolved for {:?} - text will \
4921                                         not be rendered",
4922                                        cache_key.font_families
4923                                    )));
4924                                }
4925                                continue;
4926                            }
4927                        };
4928
4929                        // Use the font chain to resolve which font to use for the first character
4930                        let first_char = item.text.chars().next().unwrap_or('A');
4931                        let font_id = match font_chain.resolve_char(fc_cache, first_char) {
4932                            Some((id, _css_source)) => id,
4933                            None => {
4934                                if let Some(msgs) = debug_messages {
4935                                    msgs.push(LayoutDebugMessage::warning(format!(
4936                                        "[TextLayout] No font in chain can render character '{}' \
4937                                         (U+{:04X})",
4938                                        first_char, first_char as u32
4939                                    )));
4940                                }
4941                                continue;
4942                            }
4943                        };
4944
4945                        // Look up the pre-loaded font
4946                        match loaded_fonts.get(&font_id) {
4947                            Some(font) => {
4948                                shape_text_correctly(
4949                                    &item.text,
4950                                    item.script,
4951                                    language,
4952                                    direction,
4953                                    font,
4954                                    style,
4955                                    *source,
4956                                    *source_node_id,
4957                                )
4958                            }
4959                            None => {
4960                                if let Some(msgs) = debug_messages {
4961                                    let truncated_text = item.text.chars().take(50).collect::<String>();
4962                                    let display_text = if item.text.chars().count() > 50 {
4963                                        format!("{}...", truncated_text)
4964                                    } else {
4965                                        truncated_text
4966                                    };
4967
4968                                    msgs.push(LayoutDebugMessage::warning(format!(
4969                                        "[TextLayout] Font {:?} not pre-loaded for text: '{}'",
4970                                        font_id, display_text
4971                                    )));
4972                                }
4973                                continue;
4974                            }
4975                        }
4976                    }
4977                };
4978
4979                let mut shaped_clusters = shaped_clusters_result?;
4980
4981                // Set marker flag on all clusters if this is a marker
4982                if let Some(is_outside) = marker_position_outside {
4983                    for cluster in &mut shaped_clusters {
4984                        cluster.marker_position_outside = Some(*is_outside);
4985                    }
4986                }
4987
4988                shaped.extend(shaped_clusters.into_iter().map(ShapedItem::Cluster));
4989            }
4990            LogicalItem::Tab { source, style } => {
4991                // TODO: To get the space width accurately, we would need to shape
4992                // a space character with the current font.
4993                // For now, we approximate it as a fraction of the font size.
4994                let space_advance = style.font_size_px * 0.33;
4995                let tab_width = style.tab_size * space_advance;
4996                shaped.push(ShapedItem::Tab {
4997                    source: *source,
4998                    bounds: Rect {
4999                        x: 0.0,
5000                        y: 0.0,
5001                        width: tab_width,
5002                        height: 0.0,
5003                    },
5004                });
5005            }
5006            LogicalItem::Ruby {
5007                source,
5008                base_text,
5009                ruby_text,
5010                style,
5011            } => {
5012                // TODO: Implement Ruby layout. This is a major feature.
5013                // 1. Recursively call layout for the `base_text` to get its size.
5014                // 2. Recursively call layout for the `ruby_text` (with a smaller font from
5015                //    `style`).
5016                // 3. Position the ruby text bounds above/beside the base text bounds.
5017                // 4. Create a single `ShapedItem::Object` or `ShapedItem::CombinedBlock` that
5018                //    represents the combined metric bounds of the group, which will be used for
5019                //    line breaking and positioning on the main line.
5020                // For now, create a placeholder object.
5021                let placeholder_width = base_text.chars().count() as f32 * style.font_size_px * 0.6;
5022                shaped.push(ShapedItem::Object {
5023                    source: *source,
5024                    bounds: Rect {
5025                        x: 0.0,
5026                        y: 0.0,
5027                        width: placeholder_width,
5028                        height: style.line_height * 1.5,
5029                    },
5030                    baseline_offset: 0.0,
5031                    content: InlineContent::Text(StyledRun {
5032                        text: base_text.clone(),
5033                        style: style.clone(),
5034                        logical_start_byte: 0,
5035                        source_node_id: None, // Ruby text is generated, not from DOM
5036                    }),
5037                });
5038            }
5039            LogicalItem::CombinedText {
5040                style,
5041                source,
5042                text,
5043            } => {
5044                let language = script_to_language(item.script, &item.text);
5045
5046                // Shape CombinedText using either FontRef directly or fontconfig-resolved font
5047                let glyphs: Vec<Glyph> = match &style.font_stack {
5048                    FontStack::Ref(font_ref) => {
5049                        // For FontRef, use the font directly without fontconfig
5050                        if let Some(msgs) = debug_messages {
5051                            msgs.push(LayoutDebugMessage::info(format!(
5052                                "[TextLayout] Using direct FontRef for CombinedText: '{}'",
5053                                text.chars().take(30).collect::<String>()
5054                            )));
5055                        }
5056                        font_ref.shape_text(
5057                            text,
5058                            item.script,
5059                            language,
5060                            BidiDirection::Ltr,
5061                            style.as_ref(),
5062                        )?
5063                    }
5064                    FontStack::Stack(selectors) => {
5065                        // Build FontChainKey and resolve through fontconfig
5066                        let cache_key = FontChainKey::from_selectors(selectors);
5067
5068                        let font_chain = match font_chain_cache.get(&cache_key) {
5069                            Some(chain) => chain,
5070                            None => {
5071                                if let Some(msgs) = debug_messages {
5072                                    msgs.push(LayoutDebugMessage::warning(format!(
5073                                        "[TextLayout] Font chain not pre-resolved for CombinedText {:?}",
5074                                        cache_key.font_families
5075                                    )));
5076                                }
5077                                continue;
5078                            }
5079                        };
5080
5081                        let first_char = text.chars().next().unwrap_or('A');
5082                        let font_id = match font_chain.resolve_char(fc_cache, first_char) {
5083                            Some((id, _)) => id,
5084                            None => {
5085                                if let Some(msgs) = debug_messages {
5086                                    msgs.push(LayoutDebugMessage::warning(format!(
5087                                        "[TextLayout] No font for CombinedText char '{}'",
5088                                        first_char
5089                                    )));
5090                                }
5091                                continue;
5092                            }
5093                        };
5094
5095                        match loaded_fonts.get(&font_id) {
5096                            Some(font) => {
5097                                font.shape_text(
5098                                    text,
5099                                    item.script,
5100                                    language,
5101                                    BidiDirection::Ltr,
5102                                    style.as_ref(),
5103                                )?
5104                            }
5105                            None => {
5106                                if let Some(msgs) = debug_messages {
5107                                    msgs.push(LayoutDebugMessage::warning(format!(
5108                                        "[TextLayout] Font {:?} not pre-loaded for CombinedText",
5109                                        font_id
5110                                    )));
5111                                }
5112                                continue;
5113                            }
5114                        }
5115                    }
5116                };
5117
5118                let shaped_glyphs = glyphs
5119                    .into_iter()
5120                    .map(|g| ShapedGlyph {
5121                        kind: GlyphKind::Character,
5122                        glyph_id: g.glyph_id,
5123                        script: g.script,
5124                        font_hash: g.font_hash,
5125                        font_metrics: g.font_metrics,
5126                        style: g.style,
5127                        cluster_offset: 0,
5128                        advance: g.advance,
5129                        kerning: g.kerning,
5130                        offset: g.offset,
5131                        vertical_advance: g.vertical_advance,
5132                        vertical_offset: g.vertical_bearing,
5133                    })
5134                    .collect::<Vec<_>>();
5135
5136                let total_width: f32 = shaped_glyphs.iter().map(|g| g.advance + g.kerning).sum();
5137                let bounds = Rect {
5138                    x: 0.0,
5139                    y: 0.0,
5140                    width: total_width,
5141                    height: style.line_height,
5142                };
5143
5144                shaped.push(ShapedItem::CombinedBlock {
5145                    source: *source,
5146                    glyphs: shaped_glyphs,
5147                    bounds,
5148                    baseline_offset: 0.0,
5149                });
5150            }
5151            LogicalItem::Object {
5152                content, source, ..
5153            } => {
5154                let (bounds, baseline) = measure_inline_object(content)?;
5155                shaped.push(ShapedItem::Object {
5156                    source: *source,
5157                    bounds,
5158                    baseline_offset: baseline,
5159                    content: content.clone(),
5160                });
5161            }
5162            LogicalItem::Break { source, break_info } => {
5163                shaped.push(ShapedItem::Break {
5164                    source: *source,
5165                    break_info: break_info.clone(),
5166                });
5167            }
5168        }
5169    }
5170    Ok(shaped)
5171}
5172
5173/// Helper to check if a cluster contains only hanging punctuation.
5174fn is_hanging_punctuation(item: &ShapedItem) -> bool {
5175    if let ShapedItem::Cluster(c) = item {
5176        if c.glyphs.len() == 1 {
5177            match c.text.as_str() {
5178                "." | "," | ":" | ";" => true,
5179                _ => false,
5180            }
5181        } else {
5182            false
5183        }
5184    } else {
5185        false
5186    }
5187}
5188
5189fn shape_text_correctly<T: ParsedFontTrait>(
5190    text: &str,
5191    script: Script,
5192    language: crate::text3::script::Language,
5193    direction: BidiDirection,
5194    font: &T, // Changed from &Arc<T>
5195    style: &Arc<StyleProperties>,
5196    source_index: ContentIndex,
5197    source_node_id: Option<NodeId>,
5198) -> Result<Vec<ShapedCluster>, LayoutError> {
5199    let glyphs = font.shape_text(text, script, language, direction, style.as_ref())?;
5200
5201    if glyphs.is_empty() {
5202        return Ok(Vec::new());
5203    }
5204
5205    let mut clusters = Vec::new();
5206
5207    // Group glyphs by cluster ID from the shaper.
5208    let mut current_cluster_glyphs = Vec::new();
5209    let mut cluster_id = glyphs[0].cluster;
5210    let mut cluster_start_byte_in_text = glyphs[0].logical_byte_index;
5211
5212    for glyph in glyphs {
5213        if glyph.cluster != cluster_id {
5214            // Finalize previous cluster
5215            let advance = current_cluster_glyphs
5216                .iter()
5217                .map(|g: &Glyph| g.advance)
5218                .sum();
5219
5220            // Safely extract cluster text - handle cases where byte indices may be out of order
5221            // (can happen with RTL text or complex GSUB reordering)
5222            let (start, end) = if cluster_start_byte_in_text <= glyph.logical_byte_index {
5223                (cluster_start_byte_in_text, glyph.logical_byte_index)
5224            } else {
5225                (glyph.logical_byte_index, cluster_start_byte_in_text)
5226            };
5227            let cluster_text = text.get(start..end).unwrap_or("");
5228
5229            clusters.push(ShapedCluster {
5230                text: cluster_text.to_string(), // Store original text for hyphenation
5231                source_cluster_id: GraphemeClusterId {
5232                    source_run: source_index.run_index,
5233                    start_byte_in_run: cluster_id,
5234                },
5235                source_content_index: source_index,
5236                source_node_id,
5237                glyphs: current_cluster_glyphs
5238                    .iter()
5239                    .map(|g| {
5240                        let source_char = text
5241                            .get(g.logical_byte_index..)
5242                            .and_then(|s| s.chars().next())
5243                            .unwrap_or('\u{FFFD}');
5244                        // Calculate cluster_offset safely
5245                        let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
5246                            (g.logical_byte_index - cluster_start_byte_in_text) as u32
5247                        } else {
5248                            0
5249                        };
5250                        ShapedGlyph {
5251                            kind: if g.glyph_id == 0 {
5252                                GlyphKind::NotDef
5253                            } else {
5254                                GlyphKind::Character
5255                            },
5256                            glyph_id: g.glyph_id,
5257                            script: g.script,
5258                            font_hash: g.font_hash,
5259                            font_metrics: g.font_metrics.clone(),
5260                            style: g.style.clone(),
5261                            cluster_offset,
5262                            advance: g.advance,
5263                            kerning: g.kerning,
5264                            vertical_advance: g.vertical_advance,
5265                            vertical_offset: g.vertical_bearing,
5266                            offset: g.offset,
5267                        }
5268                    })
5269                    .collect(),
5270                advance,
5271                direction,
5272                style: style.clone(),
5273                marker_position_outside: None,
5274            });
5275            current_cluster_glyphs.clear();
5276            cluster_id = glyph.cluster;
5277            cluster_start_byte_in_text = glyph.logical_byte_index;
5278        }
5279        current_cluster_glyphs.push(glyph);
5280    }
5281
5282    // Finalize the last cluster
5283    if !current_cluster_glyphs.is_empty() {
5284        let advance = current_cluster_glyphs
5285            .iter()
5286            .map(|g: &Glyph| g.advance)
5287            .sum();
5288        let cluster_text = text.get(cluster_start_byte_in_text..).unwrap_or("");
5289        clusters.push(ShapedCluster {
5290            text: cluster_text.to_string(), // Store original text
5291            source_cluster_id: GraphemeClusterId {
5292                source_run: source_index.run_index,
5293                start_byte_in_run: cluster_id,
5294            },
5295            source_content_index: source_index,
5296            source_node_id,
5297            glyphs: current_cluster_glyphs
5298                .iter()
5299                .map(|g| {
5300                    let source_char = text
5301                        .get(g.logical_byte_index..)
5302                        .and_then(|s| s.chars().next())
5303                        .unwrap_or('\u{FFFD}');
5304                    // Calculate cluster_offset safely
5305                    let cluster_offset = if g.logical_byte_index >= cluster_start_byte_in_text {
5306                        (g.logical_byte_index - cluster_start_byte_in_text) as u32
5307                    } else {
5308                        0
5309                    };
5310                    ShapedGlyph {
5311                        kind: if g.glyph_id == 0 {
5312                            GlyphKind::NotDef
5313                        } else {
5314                            GlyphKind::Character
5315                        },
5316                        glyph_id: g.glyph_id,
5317                        font_hash: g.font_hash,
5318                        font_metrics: g.font_metrics.clone(),
5319                        style: g.style.clone(),
5320                        script: g.script,
5321                        vertical_advance: g.vertical_advance,
5322                        vertical_offset: g.vertical_bearing,
5323                        cluster_offset,
5324                        advance: g.advance,
5325                        kerning: g.kerning,
5326                        offset: g.offset,
5327                    }
5328                })
5329                .collect(),
5330            advance,
5331            direction,
5332            style: style.clone(),
5333            marker_position_outside: None,
5334        });
5335    }
5336
5337    Ok(clusters)
5338}
5339
5340/// Measures a non-text object, returning its bounds and baseline offset.
5341fn measure_inline_object(item: &InlineContent) -> Result<(Rect, f32), LayoutError> {
5342    match item {
5343        InlineContent::Image(img) => {
5344            let size = img.display_size.unwrap_or(img.intrinsic_size);
5345            Ok((
5346                Rect {
5347                    x: 0.0,
5348                    y: 0.0,
5349                    width: size.width,
5350                    height: size.height,
5351                },
5352                img.baseline_offset,
5353            ))
5354        }
5355        InlineContent::Shape(shape) => Ok({
5356            let size = shape.shape_def.get_size();
5357            (
5358                Rect {
5359                    x: 0.0,
5360                    y: 0.0,
5361                    width: size.width,
5362                    height: size.height,
5363                },
5364                shape.baseline_offset,
5365            )
5366        }),
5367        InlineContent::Space(space) => Ok((
5368            Rect {
5369                x: 0.0,
5370                y: 0.0,
5371                width: space.width,
5372                height: 0.0,
5373            },
5374            0.0,
5375        )),
5376        InlineContent::Marker { .. } => {
5377            // Markers are treated as text content, not measurable objects
5378            Err(LayoutError::InvalidText(
5379                "Marker is text content, not a measurable object".into(),
5380            ))
5381        }
5382        _ => Err(LayoutError::InvalidText("Not a measurable object".into())),
5383    }
5384}
5385
5386// --- Stage 4 Implementation: Vertical Text ---
5387
5388/// Applies orientation and vertical metrics to glyphs if the writing mode is vertical.
5389fn apply_text_orientation(
5390    items: Arc<Vec<ShapedItem>>,
5391    constraints: &UnifiedConstraints,
5392) -> Result<Arc<Vec<ShapedItem>>, LayoutError> {
5393    if !constraints.is_vertical() {
5394        return Ok(items);
5395    }
5396
5397    let mut oriented_items = Vec::with_capacity(items.len());
5398    let writing_mode = constraints.writing_mode.unwrap_or_default();
5399
5400    for item in items.iter() {
5401        match item {
5402            ShapedItem::Cluster(cluster) => {
5403                let mut new_cluster = cluster.clone();
5404                let mut total_vertical_advance = 0.0;
5405
5406                for glyph in &mut new_cluster.glyphs {
5407                    // Use the vertical metrics already computed during shaping
5408                    // If they're zero, use fallback values
5409                    if glyph.vertical_advance > 0.0 {
5410                        total_vertical_advance += glyph.vertical_advance;
5411                    } else {
5412                        // Fallback: use line height for vertical advance
5413                        let fallback_advance = cluster.style.line_height;
5414                        glyph.vertical_advance = fallback_advance;
5415                        // Center the glyph horizontally as a fallback
5416                        glyph.vertical_offset = Point {
5417                            x: -glyph.advance / 2.0,
5418                            y: 0.0,
5419                        };
5420                        total_vertical_advance += fallback_advance;
5421                    }
5422                }
5423                // The cluster's `advance` now represents vertical advance.
5424                new_cluster.advance = total_vertical_advance;
5425                oriented_items.push(ShapedItem::Cluster(new_cluster));
5426            }
5427            // Non-text objects also need their advance axis swapped.
5428            ShapedItem::Object {
5429                source,
5430                bounds,
5431                baseline_offset,
5432                content,
5433            } => {
5434                let mut new_bounds = *bounds;
5435                std::mem::swap(&mut new_bounds.width, &mut new_bounds.height);
5436                oriented_items.push(ShapedItem::Object {
5437                    source: *source,
5438                    bounds: new_bounds,
5439                    baseline_offset: *baseline_offset,
5440                    content: content.clone(),
5441                });
5442            }
5443            _ => oriented_items.push(item.clone()),
5444        }
5445    }
5446
5447    Ok(Arc::new(oriented_items))
5448}
5449
5450// --- Stage 5 & 6 Implementation: Combined Layout Pass ---
5451// This section replaces the previous simple line breaking and positioning logic.
5452
5453/// Gets the ascent (distance from baseline to top) and descent (distance from baseline to bottom)
5454/// for a single item.
5455pub fn get_item_vertical_metrics(item: &ShapedItem) -> (f32, f32) {
5456    // (ascent, descent)
5457    match item {
5458        ShapedItem::Cluster(c) => {
5459            if c.glyphs.is_empty() {
5460                // For an empty text cluster, use the line height from its style as a fallback.
5461                return (c.style.line_height, 0.0);
5462            }
5463            // CORRECTED: Iterate through ALL glyphs in the cluster to find the true max
5464            // ascent/descent.
5465            c.glyphs
5466                .iter()
5467                .fold((0.0f32, 0.0f32), |(max_asc, max_desc), glyph| {
5468                    let metrics = &glyph.font_metrics;
5469                    if metrics.units_per_em == 0 {
5470                        return (max_asc, max_desc);
5471                    }
5472                    let scale = glyph.style.font_size_px / metrics.units_per_em as f32;
5473                    let item_asc = metrics.ascent * scale;
5474                    // Descent in OpenType is typically negative, so we negate it to get a positive
5475                    // distance.
5476                    let item_desc = (-metrics.descent * scale).max(0.0);
5477                    (max_asc.max(item_asc), max_desc.max(item_desc))
5478                })
5479        }
5480        ShapedItem::Object {
5481            bounds,
5482            baseline_offset,
5483            ..
5484        } => {
5485            // Per analysis, `baseline_offset` is the distance from the bottom.
5486            let ascent = bounds.height - *baseline_offset;
5487            let descent = *baseline_offset;
5488            (ascent.max(0.0), descent.max(0.0))
5489        }
5490        ShapedItem::CombinedBlock {
5491            bounds,
5492            baseline_offset,
5493            ..
5494        } => {
5495            // CORRECTED: Treat baseline_offset consistently as distance from the bottom (descent).
5496            let ascent = bounds.height - *baseline_offset;
5497            let descent = *baseline_offset;
5498            (ascent.max(0.0), descent.max(0.0))
5499        }
5500        _ => (0.0, 0.0), // Breaks and other non-visible items don't affect line height.
5501    }
5502}
5503
5504/// Calculates the maximum ascent and descent for an entire line of items.
5505/// This determines the "line box" used for vertical alignment.
5506fn calculate_line_metrics(items: &[ShapedItem]) -> (f32, f32) {
5507    // (max_ascent, max_descent)
5508    items
5509        .iter()
5510        .fold((0.0f32, 0.0f32), |(max_asc, max_desc), item| {
5511            let (item_asc, item_desc) = get_item_vertical_metrics(item);
5512            (max_asc.max(item_asc), max_desc.max(item_desc))
5513        })
5514}
5515
5516/// Performs layout for a single fragment, consuming items from a `BreakCursor`.
5517///
5518/// This function contains the core line-breaking and positioning logic, but is
5519/// designed to operate on a portion of a larger content stream and within the
5520/// constraints of a single geometric area (a fragment).
5521///
5522/// The loop terminates when either the fragment is filled (e.g., runs out of
5523/// vertical space) or the content stream managed by the `cursor` is exhausted.
5524///
5525/// # CSS Inline Layout Module Level 3 Implementation
5526///
5527/// This function implements the inline formatting context as described in:
5528/// https://www.w3.org/TR/css-inline-3/#inline-formatting-context
5529///
5530/// ## § 2.1 Layout of Line Boxes
5531/// "In general, the line-left edge of a line box touches the line-left edge of its
5532/// containing block and the line-right edge touches the line-right edge of its
5533/// containing block, and thus the logical width of a line box is equal to the inner
5534/// logical width of its containing block."
5535///
5536/// [ISSUE] available_width should be set to the containing block's inner width,
5537/// but is currently defaulting to 0.0 in UnifiedConstraints::default().
5538/// This causes premature line breaking.
5539///
5540/// ## § 2.2 Layout Within Line Boxes
5541/// The layout process follows these steps:
5542/// 1. Baseline Alignment: All inline-level boxes are aligned by their baselines
5543/// 2. Content Size Contribution: Calculate layout bounds for each box
5544/// 3. Line Box Sizing: Size line box to fit aligned layout bounds
5545/// 4. Content Positioning: Position boxes within the line box
5546///
5547/// ## Missing Features:
5548/// - § 3 Baselines and Alignment Metrics: Only basic baseline alignment implemented
5549/// - § 4 Baseline Alignment: vertical-align property not fully supported
5550/// - § 5 Line Spacing: line-height implemented, but line-fit-edge missing
5551/// - § 6 Trimming Leading: text-box-trim not implemented
5552pub fn perform_fragment_layout<T: ParsedFontTrait>(
5553    cursor: &mut BreakCursor,
5554    logical_items: &[LogicalItem],
5555    fragment_constraints: &UnifiedConstraints,
5556    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
5557    fonts: &LoadedFonts<T>,
5558) -> Result<UnifiedLayout, LayoutError> {
5559    if let Some(msgs) = debug_messages {
5560        msgs.push(LayoutDebugMessage::info(
5561            "\n--- Entering perform_fragment_layout ---".to_string(),
5562        ));
5563        msgs.push(LayoutDebugMessage::info(format!(
5564            "Constraints: available_width={:?}, available_height={:?}, columns={}, text_wrap={:?}",
5565            fragment_constraints.available_width,
5566            fragment_constraints.available_height,
5567            fragment_constraints.columns,
5568            fragment_constraints.text_wrap
5569        )));
5570    }
5571
5572    // For TextWrap::Balance, use Knuth-Plass algorithm for optimal line breaking
5573    // This produces more visually balanced lines at the cost of more computation
5574    if fragment_constraints.text_wrap == TextWrap::Balance {
5575        if let Some(msgs) = debug_messages {
5576            msgs.push(LayoutDebugMessage::info(
5577                "Using Knuth-Plass algorithm for text-wrap: balance".to_string(),
5578            ));
5579        }
5580
5581        // Get the shaped items from the cursor
5582        let shaped_items: Vec<ShapedItem> = cursor.drain_remaining();
5583
5584        let hyphenator = if fragment_constraints.hyphenation {
5585            fragment_constraints
5586                .hyphenation_language
5587                .and_then(|lang| get_hyphenator(lang).ok())
5588        } else {
5589            None
5590        };
5591
5592        // Use the Knuth-Plass algorithm for optimal line breaking
5593        return crate::text3::knuth_plass::kp_layout(
5594            &shaped_items,
5595            logical_items,
5596            fragment_constraints,
5597            hyphenator.as_ref(),
5598            fonts,
5599        );
5600    }
5601
5602    let hyphenator = if fragment_constraints.hyphenation {
5603        fragment_constraints
5604            .hyphenation_language
5605            .and_then(|lang| get_hyphenator(lang).ok())
5606    } else {
5607        None
5608    };
5609
5610    let mut positioned_items = Vec::new();
5611    let mut layout_bounds = Rect::default();
5612
5613    let num_columns = fragment_constraints.columns.max(1);
5614    let total_column_gap = fragment_constraints.column_gap * (num_columns - 1) as f32;
5615
5616    // CSS Inline Layout § 2.1: "the logical width of a line box is equal to the inner
5617    // logical width of its containing block"
5618    //
5619    // Handle the different available space modes:
5620    // - Definite(width): Use the specified width for column calculation
5621    // - MinContent: Use 0.0 to force line breaks at every opportunity
5622    // - MaxContent: Use a large value to allow content to expand naturally
5623    let column_width = match fragment_constraints.available_width {
5624        AvailableSpace::Definite(width) => (width - total_column_gap) / num_columns as f32,
5625        AvailableSpace::MinContent => {
5626            // Min-content: effectively 0 width forces immediate line breaks
5627            0.0
5628        }
5629        AvailableSpace::MaxContent => {
5630            // Max-content: very large width allows content to expand
5631            // Using f32::MAX / 2.0 to avoid overflow issues
5632            f32::MAX / 2.0
5633        }
5634    };
5635    let mut current_column = 0;
5636    if let Some(msgs) = debug_messages {
5637        msgs.push(LayoutDebugMessage::info(format!(
5638            "Column width calculated: {}",
5639            column_width
5640        )));
5641    }
5642
5643    // Use the CSS direction from constraints instead of auto-detecting from text
5644    // This ensures that mixed-direction text (e.g., "مرحبا - Hello") uses the
5645    // correct paragraph-level direction for alignment purposes
5646    let base_direction = fragment_constraints.direction.unwrap_or(BidiDirection::Ltr);
5647
5648    if let Some(msgs) = debug_messages {
5649        msgs.push(LayoutDebugMessage::info(format!(
5650            "[PFLayout] Base direction: {:?} (from CSS), Text align: {:?}",
5651            base_direction, fragment_constraints.text_align
5652        )));
5653    }
5654
5655    'column_loop: while current_column < num_columns {
5656        if let Some(msgs) = debug_messages {
5657            msgs.push(LayoutDebugMessage::info(format!(
5658                "\n-- Starting Column {} --",
5659                current_column
5660            )));
5661        }
5662        let column_start_x =
5663            (column_width + fragment_constraints.column_gap) * current_column as f32;
5664        let mut line_top_y = 0.0;
5665        let mut line_index = 0;
5666        let mut empty_segment_count = 0; // Failsafe counter for infinite loops
5667        const MAX_EMPTY_SEGMENTS: usize = 1000; // Maximum allowed consecutive empty segments
5668
5669        while !cursor.is_done() {
5670            if let Some(max_height) = fragment_constraints.available_height {
5671                if line_top_y >= max_height {
5672                    if let Some(msgs) = debug_messages {
5673                        msgs.push(LayoutDebugMessage::info(format!(
5674                            "  Column full (pen {} >= height {}), breaking to next column.",
5675                            line_top_y, max_height
5676                        )));
5677                    }
5678                    break;
5679                }
5680            }
5681
5682            if let Some(clamp) = fragment_constraints.line_clamp {
5683                if line_index >= clamp.get() {
5684                    break;
5685                }
5686            }
5687
5688            // Create constraints specific to the current column for the line breaker.
5689            let mut column_constraints = fragment_constraints.clone();
5690            column_constraints.available_width = AvailableSpace::Definite(column_width);
5691            let line_constraints = get_line_constraints(
5692                line_top_y,
5693                fragment_constraints.line_height,
5694                &column_constraints,
5695                debug_messages,
5696            );
5697
5698            if line_constraints.segments.is_empty() {
5699                empty_segment_count += 1;
5700                if let Some(msgs) = debug_messages {
5701                    msgs.push(LayoutDebugMessage::info(format!(
5702                        "  No available segments at y={}, skipping to next line. (empty count: \
5703                         {}/{})",
5704                        line_top_y, empty_segment_count, MAX_EMPTY_SEGMENTS
5705                    )));
5706                }
5707
5708                // Failsafe: If we've skipped too many lines without content, break out
5709                if empty_segment_count >= MAX_EMPTY_SEGMENTS {
5710                    if let Some(msgs) = debug_messages {
5711                        msgs.push(LayoutDebugMessage::warning(format!(
5712                            "  [WARN] Reached maximum empty segment count ({}). Breaking to \
5713                             prevent infinite loop.",
5714                            MAX_EMPTY_SEGMENTS
5715                        )));
5716                        msgs.push(LayoutDebugMessage::warning(
5717                            "  This likely means the shape constraints are too restrictive or \
5718                             positioned incorrectly."
5719                                .to_string(),
5720                        ));
5721                        msgs.push(LayoutDebugMessage::warning(format!(
5722                            "  Current y={}, shape boundaries might be outside this range.",
5723                            line_top_y
5724                        )));
5725                    }
5726                    break;
5727                }
5728
5729                // Additional check: If we have shapes and are far beyond the expected height,
5730                // also break to avoid infinite loops
5731                if !fragment_constraints.shape_boundaries.is_empty() && empty_segment_count > 50 {
5732                    // Calculate maximum shape height
5733                    let max_shape_y: f32 = fragment_constraints
5734                        .shape_boundaries
5735                        .iter()
5736                        .map(|shape| {
5737                            match shape {
5738                                ShapeBoundary::Circle { center, radius } => center.y + radius,
5739                                ShapeBoundary::Ellipse { center, radii } => center.y + radii.height,
5740                                ShapeBoundary::Polygon { points } => {
5741                                    points.iter().map(|p| p.y).fold(0.0, f32::max)
5742                                }
5743                                ShapeBoundary::Rectangle(rect) => rect.y + rect.height,
5744                                ShapeBoundary::Path { .. } => f32::MAX, // Can't determine for path
5745                            }
5746                        })
5747                        .fold(0.0, f32::max);
5748
5749                    if line_top_y > max_shape_y + 100.0 {
5750                        if let Some(msgs) = debug_messages {
5751                            msgs.push(LayoutDebugMessage::info(format!(
5752                                "  [INFO] Current y={} is far beyond maximum shape extent y={}. \
5753                                 Breaking layout.",
5754                                line_top_y, max_shape_y
5755                            )));
5756                            msgs.push(LayoutDebugMessage::info(
5757                                "  Shape boundaries exist but no segments available - text cannot \
5758                                 fit in shape."
5759                                    .to_string(),
5760                            ));
5761                        }
5762                        break;
5763                    }
5764                }
5765
5766                line_top_y += fragment_constraints.line_height;
5767                continue;
5768            }
5769
5770            // Reset counter when we find valid segments
5771            empty_segment_count = 0;
5772
5773            // CSS Text Module Level 3 § 5 Line Breaking and Word Boundaries
5774            // https://www.w3.org/TR/css-text-3/#line-breaking
5775            // "When an inline box exceeds the logical width of a line box, it is split
5776            // into several fragments, which are partitioned across multiple line boxes."
5777            let (mut line_items, was_hyphenated) =
5778                break_one_line(cursor, &line_constraints, false, hyphenator.as_ref(), fonts);
5779            if line_items.is_empty() {
5780                if let Some(msgs) = debug_messages {
5781                    msgs.push(LayoutDebugMessage::info(
5782                        "  Break returned no items. Ending column.".to_string(),
5783                    ));
5784                }
5785                break;
5786            }
5787
5788            let line_text_before_rev: String = line_items
5789                .iter()
5790                .filter_map(|i| i.as_cluster())
5791                .map(|c| c.text.as_str())
5792                .collect();
5793            if let Some(msgs) = debug_messages {
5794                msgs.push(LayoutDebugMessage::info(format!(
5795                    // FIX: The log message was misleading. Items are in visual order.
5796                    "[PFLayout] Line items from breaker (visual order): [{}]",
5797                    line_text_before_rev
5798                )));
5799            }
5800
5801            let (mut line_pos_items, line_height) = position_one_line(
5802                line_items,
5803                &line_constraints,
5804                line_top_y,
5805                line_index,
5806                fragment_constraints.text_align,
5807                base_direction,
5808                cursor.is_done() && !was_hyphenated,
5809                fragment_constraints,
5810                debug_messages,
5811                fonts,
5812            );
5813
5814            for item in &mut line_pos_items {
5815                item.position.x += column_start_x;
5816            }
5817
5818            line_top_y += line_height.max(fragment_constraints.line_height);
5819            line_index += 1;
5820            positioned_items.extend(line_pos_items);
5821        }
5822        current_column += 1;
5823    }
5824
5825    if let Some(msgs) = debug_messages {
5826        msgs.push(LayoutDebugMessage::info(format!(
5827            "--- Exiting perform_fragment_layout, positioned {} items ---",
5828            positioned_items.len()
5829        )));
5830    }
5831
5832    let layout = UnifiedLayout {
5833        items: positioned_items,
5834        overflow: OverflowInfo::default(),
5835    };
5836
5837    // Calculate bounds on demand via the bounds() method
5838    let calculated_bounds = layout.bounds();
5839    
5840    if let Some(msgs) = debug_messages {
5841        msgs.push(LayoutDebugMessage::info(format!(
5842            "--- Calculated bounds: width={}, height={} ---",
5843            calculated_bounds.width, calculated_bounds.height
5844        )));
5845    }
5846
5847    Ok(layout)
5848}
5849
5850/// Breaks a single line of items to fit within the given geometric constraints,
5851/// handling multi-segment lines and hyphenation.
5852/// Break a single line from the current cursor position.
5853///
5854/// # CSS Text Module Level 3 \u00a7 5 Line Breaking and Word Boundaries
5855/// https://www.w3.org/TR/css-text-3/#line-breaking
5856///
5857/// Implements the line breaking algorithm:
5858/// 1. "When an inline box exceeds the logical width of a line box, it is split into several
5859///    fragments, which are partitioned across multiple line boxes."
5860///
5861/// ## \u2705 Implemented Features:
5862/// - **Break Opportunities**: Identifies word boundaries and break points
5863/// - **Soft Wraps**: Wraps at spaces between words
5864/// - **Hard Breaks**: Handles explicit line breaks (\\n)
5865/// - **Overflow**: If a word is too long, places it anyway to avoid infinite loop
5866/// - **Hyphenation**: Tries to break long words at hyphenation points (\u00a7 5.4)
5867///
5868/// ## \u26a0\ufe0f Known Issues:
5869/// - If `line_constraints.total_available` is 0.0 (from `available_width: 0.0` bug), every word
5870///   will overflow, causing single-word lines
5871/// - This is the symptom visible in the PDF: "List items break extremely early"
5872///
5873/// ## \u00a7 5.2 Breaking Rules for Letters
5874/// \u2705 IMPLEMENTED: Uses Unicode line breaking algorithm
5875/// - Relies on UAX #14 for break opportunities
5876/// - Respects non-breaking spaces and zero-width joiners
5877///
5878/// ## \u00a7 5.3 Breaking Rules for Punctuation
5879/// \u26a0\ufe0f PARTIAL: Basic punctuation handling
5880/// - \u274c TODO: hanging-punctuation is declared in UnifiedConstraints but not used here
5881/// - \u274c TODO: Should implement punctuation trimming at line edges
5882///
5883/// ## \u00a7 5.4 Hyphenation
5884/// \u2705 IMPLEMENTED: Automatic hyphenation with hyphenator library
5885/// - Tries to hyphenate words that overflow
5886/// - Inserts hyphen glyph at break point
5887/// - Carries remainder to next line
5888///
5889/// ## \u00a7 5.5 Overflow Wrapping
5890/// \u2705 IMPLEMENTED: Emergency breaking
5891/// - If line is empty and word doesn't fit, forces at least one item
5892/// - Prevents infinite loop
5893/// - This is "overflow-wrap: break-word" behavior
5894///
5895/// # Missing Features:
5896/// - \u274c word-break property (normal, break-all, keep-all)
5897/// - \u274c line-break property (auto, loose, normal, strict, anywhere)
5898/// - \u274c overflow-wrap: anywhere vs break-word distinction
5899/// - \u274c white-space: break-spaces handling
5900pub fn break_one_line<T: ParsedFontTrait>(
5901    cursor: &mut BreakCursor,
5902    line_constraints: &LineConstraints,
5903    is_vertical: bool,
5904    hyphenator: Option<&Standard>,
5905    fonts: &LoadedFonts<T>,
5906) -> (Vec<ShapedItem>, bool) {
5907    let mut line_items = Vec::new();
5908    let mut current_width = 0.0;
5909
5910    if cursor.is_done() {
5911        return (Vec::new(), false);
5912    }
5913
5914    // CSS Text Module Level 3 § 4.1.1: At the beginning of a line, white space
5915    // is collapsed away. Skip leading whitespace at line start.
5916    // https://www.w3.org/TR/css-text-3/#white-space-phase-2
5917    while !cursor.is_done() {
5918        let next_unit = cursor.peek_next_unit();
5919        if next_unit.is_empty() {
5920            break;
5921        }
5922        // Check if the first item is whitespace-only
5923        if next_unit.len() == 1 && is_word_separator(&next_unit[0]) {
5924            // Skip this whitespace at line start
5925            cursor.consume(1);
5926        } else {
5927            break;
5928        }
5929    }
5930
5931    loop {
5932        // 1. Identify the next unbreakable unit (word) or break opportunity.
5933        let next_unit = cursor.peek_next_unit();
5934        if next_unit.is_empty() {
5935            break; // End of content
5936        }
5937
5938        // Handle hard breaks immediately.
5939        if let Some(ShapedItem::Break { .. }) = next_unit.first() {
5940            line_items.push(next_unit[0].clone());
5941            cursor.consume(1);
5942            return (line_items, false);
5943        }
5944
5945        let unit_width: f32 = next_unit
5946            .iter()
5947            .map(|item| get_item_measure(item, is_vertical))
5948            .sum();
5949        let available_width = line_constraints.total_available - current_width;
5950
5951        // 2. Can the whole unit fit on the current line?
5952        if unit_width <= available_width {
5953            line_items.extend_from_slice(&next_unit);
5954            current_width += unit_width;
5955            cursor.consume(next_unit.len());
5956        } else {
5957            // 3. The unit overflows. Can we hyphenate it?
5958            if let Some(hyphenator) = hyphenator {
5959                // We only try to hyphenate if the unit is a word (not a space).
5960                if !is_break_opportunity(next_unit.last().unwrap()) {
5961                    if let Some(hyphenation_result) = try_hyphenate_word_cluster(
5962                        &next_unit,
5963                        available_width,
5964                        is_vertical,
5965                        hyphenator,
5966                        fonts,
5967                    ) {
5968                        line_items.extend(hyphenation_result.line_part);
5969                        // Consume the original full word from the cursor.
5970                        cursor.consume(next_unit.len());
5971                        // Put the remainder back for the next line.
5972                        cursor.partial_remainder = hyphenation_result.remainder_part;
5973                        return (line_items, true);
5974                    }
5975                }
5976            }
5977
5978            // 4. Cannot hyphenate or fit. The line is finished.
5979            // If the line is empty, we must force at least one item to avoid an infinite loop.
5980            if line_items.is_empty() {
5981                line_items.push(next_unit[0].clone());
5982                cursor.consume(1);
5983            }
5984            break;
5985        }
5986    }
5987
5988    (line_items, false)
5989}
5990
5991/// Represents a single valid hyphenation point within a word.
5992#[derive(Clone)]
5993pub struct HyphenationBreak {
5994    /// The number of characters from the original word string included on the line.
5995    pub char_len_on_line: usize,
5996    /// The total advance width of the line part + the hyphen.
5997    pub width_on_line: f32,
5998    /// The cluster(s) that will remain on the current line.
5999    pub line_part: Vec<ShapedItem>,
6000    /// The cluster that represents the hyphen character itself.
6001    pub hyphen_item: ShapedItem,
6002    /// The cluster(s) that will be carried over to the next line.
6003    /// CRITICAL FIX: Changed from ShapedItem to Vec<ShapedItem>
6004    pub remainder_part: Vec<ShapedItem>,
6005}
6006
6007/// A "word" is defined as a sequence of one or more adjacent ShapedClusters.
6008pub fn find_all_hyphenation_breaks<T: ParsedFontTrait>(
6009    word_clusters: &[ShapedCluster],
6010    hyphenator: &Standard,
6011    is_vertical: bool, // Pass this in to use correct metrics
6012    fonts: &LoadedFonts<T>,
6013) -> Option<Vec<HyphenationBreak>> {
6014    if word_clusters.is_empty() {
6015        return None;
6016    }
6017
6018    // --- 1. Concatenate the TRUE text and build a robust map ---
6019    let mut word_string = String::new();
6020    let mut char_map = Vec::new();
6021    let mut current_width = 0.0;
6022
6023    for (cluster_idx, cluster) in word_clusters.iter().enumerate() {
6024        for (char_byte_offset, _ch) in cluster.text.char_indices() {
6025            let glyph_idx = cluster
6026                .glyphs
6027                .iter()
6028                .rposition(|g| g.cluster_offset as usize <= char_byte_offset)
6029                .unwrap_or(0);
6030            let glyph = &cluster.glyphs[glyph_idx];
6031
6032            let num_chars_in_glyph = cluster.text[glyph.cluster_offset as usize..]
6033                .chars()
6034                .count();
6035            let advance_per_char = if is_vertical {
6036                glyph.vertical_advance
6037            } else {
6038                glyph.advance
6039            } / (num_chars_in_glyph as f32).max(1.0);
6040
6041            current_width += advance_per_char;
6042            char_map.push((cluster_idx, glyph_idx, current_width));
6043        }
6044        word_string.push_str(&cluster.text);
6045    }
6046
6047    // --- 2. Get hyphenation opportunities ---
6048    let opportunities = hyphenator.hyphenate(&word_string);
6049    if opportunities.breaks.is_empty() {
6050        return None;
6051    }
6052
6053    let last_cluster = word_clusters.last().unwrap();
6054    let last_glyph = last_cluster.glyphs.last().unwrap();
6055    let style = last_cluster.style.clone();
6056
6057    // Look up font from hash
6058    let font = fonts.get_by_hash(last_glyph.font_hash)?;
6059    let (hyphen_glyph_id, hyphen_advance) =
6060        font.get_hyphen_glyph_and_advance(style.font_size_px)?;
6061
6062    let mut possible_breaks = Vec::new();
6063
6064    // --- 3. Generate a HyphenationBreak for each valid opportunity ---
6065    for &break_char_idx in &opportunities.breaks {
6066        // The break is *before* the character at this index.
6067        // So the last character on the line is at `break_char_idx - 1`.
6068        if break_char_idx == 0 || break_char_idx > char_map.len() {
6069            continue;
6070        }
6071
6072        let (_, _, width_at_break) = char_map[break_char_idx - 1];
6073
6074        // The line part is all clusters *before* the break index.
6075        let line_part: Vec<ShapedItem> = word_clusters[..break_char_idx]
6076            .iter()
6077            .map(|c| ShapedItem::Cluster(c.clone()))
6078            .collect();
6079
6080        // The remainder is all clusters *from* the break index onward.
6081        let remainder_part: Vec<ShapedItem> = word_clusters[break_char_idx..]
6082            .iter()
6083            .map(|c| ShapedItem::Cluster(c.clone()))
6084            .collect();
6085
6086        let hyphen_item = ShapedItem::Cluster(ShapedCluster {
6087            text: "-".to_string(),
6088            source_cluster_id: GraphemeClusterId {
6089                source_run: u32::MAX,
6090                start_byte_in_run: u32::MAX,
6091            },
6092            source_content_index: ContentIndex {
6093                run_index: u32::MAX,
6094                item_index: u32::MAX,
6095            },
6096            source_node_id: None, // Hyphen is generated, not from DOM
6097            glyphs: vec![ShapedGlyph {
6098                kind: GlyphKind::Hyphen,
6099                glyph_id: hyphen_glyph_id,
6100                font_hash: last_glyph.font_hash,
6101                font_metrics: last_glyph.font_metrics.clone(),
6102                cluster_offset: 0,
6103                script: Script::Latin,
6104                advance: hyphen_advance,
6105                kerning: 0.0,
6106                offset: Point::default(),
6107                style: style.clone(),
6108                vertical_advance: hyphen_advance,
6109                vertical_offset: Point::default(),
6110            }],
6111            advance: hyphen_advance,
6112            direction: BidiDirection::Ltr,
6113            style: style.clone(),
6114            marker_position_outside: None,
6115        });
6116
6117        possible_breaks.push(HyphenationBreak {
6118            char_len_on_line: break_char_idx,
6119            width_on_line: width_at_break + hyphen_advance,
6120            line_part,
6121            hyphen_item,
6122            remainder_part,
6123        });
6124    }
6125
6126    Some(possible_breaks)
6127}
6128
6129/// Tries to find a hyphenation point within a word, returning the line part and remainder.
6130fn try_hyphenate_word_cluster<T: ParsedFontTrait>(
6131    word_items: &[ShapedItem],
6132    remaining_width: f32,
6133    is_vertical: bool,
6134    hyphenator: &Standard,
6135    fonts: &LoadedFonts<T>,
6136) -> Option<HyphenationResult> {
6137    let word_clusters: Vec<ShapedCluster> = word_items
6138        .iter()
6139        .filter_map(|item| item.as_cluster().cloned())
6140        .collect();
6141
6142    if word_clusters.is_empty() {
6143        return None;
6144    }
6145
6146    let all_breaks = find_all_hyphenation_breaks(&word_clusters, hyphenator, is_vertical, fonts)?;
6147
6148    if let Some(best_break) = all_breaks
6149        .into_iter()
6150        .rfind(|b| b.width_on_line <= remaining_width)
6151    {
6152        let mut line_part = best_break.line_part;
6153        line_part.push(best_break.hyphen_item);
6154
6155        return Some(HyphenationResult {
6156            line_part,
6157            remainder_part: best_break.remainder_part,
6158        });
6159    }
6160
6161    None
6162}
6163
6164/// Positions a single line of items, handling alignment and justification within segments.
6165///
6166/// This function is architecturally critical for cache safety. It does not mutate the
6167/// `advance` or `bounds` of the input `ShapedItem`s. Instead, it applies justification
6168/// spacing by adjusting the drawing pen's position (`main_axis_pen`).
6169///
6170/// # Returns
6171/// A tuple containing the `Vec` of positioned items and the calculated height of the line box.
6172/// Position items on a single line after breaking.
6173///
6174/// # CSS Inline Layout Module Level 3 \u00a7 2.2 Layout Within Line Boxes
6175/// https://www.w3.org/TR/css-inline-3/#layout-within-line-boxes
6176///
6177/// Implements the positioning algorithm:
6178/// 1. "All inline-level boxes are aligned by their baselines"
6179/// 2. "Calculate layout bounds for each inline box"
6180/// 3. "Size the line box to fit the aligned layout bounds"
6181/// 4. "Position all inline boxes within the line box"
6182///
6183/// ## \u2705 Implemented Features:
6184///
6185/// ### \u00a7 4 Baseline Alignment (vertical-align)
6186/// \u26a0\ufe0f PARTIAL IMPLEMENTATION:
6187/// - \u2705 `baseline`: Aligns box baseline with parent baseline (default)
6188/// - \u2705 `top`: Aligns top of box with top of line box
6189/// - \u2705 `middle`: Centers box within line box
6190/// - \u2705 `bottom`: Aligns bottom of box with bottom of line box
6191/// - \u274c MISSING: `text-top`, `text-bottom`, `sub`, `super`
6192/// - \u274c MISSING: `<length>`, `<percentage>` values for custom offset
6193///
6194/// ### \u00a7 2.2.1 Text Alignment (text-align)
6195/// \u2705 IMPLEMENTED:
6196/// - `left`, `right`, `center`: Physical alignment
6197/// - `start`, `end`: Logical alignment (respects direction: ltr/rtl)
6198/// - `justify`: Distributes space between words/characters
6199/// - `justify-all`: Justifies last line too
6200///
6201/// ### \u00a7 7.3 Text Justification (text-justify)
6202/// \u2705 IMPLEMENTED:
6203/// - `inter-word`: Adds space between words
6204/// - `inter-character`: Adds space between characters
6205/// - `kashida`: Arabic kashida elongation
6206/// - \u274c MISSING: `distribute` (CJK justification)
6207///
6208/// ### CSS Text \u00a7 8.1 Text Indentation (text-indent)
6209/// \u2705 IMPLEMENTED: First line indentation
6210///
6211/// ### CSS Text \u00a7 4.1 Word Spacing (word-spacing)
6212/// \u2705 IMPLEMENTED: Additional space between words
6213///
6214/// ### CSS Text \u00a7 4.2 Letter Spacing (letter-spacing)
6215/// \u2705 IMPLEMENTED: Additional space between characters
6216///
6217/// ## Segment-Aware Layout:
6218/// \u2705 Handles CSS Shapes and multi-column layouts
6219/// - Breaks line into segments (for shape boundaries)
6220/// - Calculates justification per segment
6221/// - Applies alignment within each segment's bounds
6222///
6223/// ## Known Issues:
6224/// - \u26a0\ufe0f If segment.width is infinite (from intrinsic sizing), sets alignment_offset=0 to
6225///   avoid infinite positioning. This is correct for measurement but documented for clarity.
6226/// - The function assumes `line_index == 0` means first line for text-indent. A more robust system
6227///   would track paragraph boundaries.
6228///
6229/// # Missing Features:
6230/// - \u274c \u00a7 6 Trimming Leading (text-box-trim, text-box-edge)
6231/// - \u274c \u00a7 3.3 Initial Letters (drop caps)
6232/// - \u274c Full vertical-align support (sub, super, lengths, percentages)
6233/// - \u274c white-space: break-spaces alignment behavior
6234pub fn position_one_line<T: ParsedFontTrait>(
6235    line_items: Vec<ShapedItem>,
6236    line_constraints: &LineConstraints,
6237    line_top_y: f32,
6238    line_index: usize,
6239    text_align: TextAlign,
6240    base_direction: BidiDirection,
6241    is_last_line: bool,
6242    constraints: &UnifiedConstraints,
6243    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6244    fonts: &LoadedFonts<T>,
6245) -> (Vec<PositionedItem>, f32) {
6246    let line_text: String = line_items
6247        .iter()
6248        .filter_map(|i| i.as_cluster())
6249        .map(|c| c.text.as_str())
6250        .collect();
6251    if let Some(msgs) = debug_messages {
6252        msgs.push(LayoutDebugMessage::info(format!(
6253            "\n--- Entering position_one_line for line: [{}] ---",
6254            line_text
6255        )));
6256    }
6257    // NEW: Resolve the final physical alignment here, inside the function.
6258    let physical_align = match (text_align, base_direction) {
6259        (TextAlign::Start, BidiDirection::Ltr) => TextAlign::Left,
6260        (TextAlign::Start, BidiDirection::Rtl) => TextAlign::Right,
6261        (TextAlign::End, BidiDirection::Ltr) => TextAlign::Right,
6262        (TextAlign::End, BidiDirection::Rtl) => TextAlign::Left,
6263        // Physical alignments are returned as-is, regardless of direction.
6264        (other, _) => other,
6265    };
6266    if let Some(msgs) = debug_messages {
6267        msgs.push(LayoutDebugMessage::info(format!(
6268            "[Pos1Line] Physical align: {:?}",
6269            physical_align
6270        )));
6271    }
6272
6273    if line_items.is_empty() {
6274        return (Vec::new(), 0.0);
6275    }
6276    let mut positioned = Vec::new();
6277    let is_vertical = constraints.is_vertical();
6278
6279    // The line box is calculated once for all items on the line, regardless of segment.
6280    let (line_ascent, line_descent) = calculate_line_metrics(&line_items);
6281    let line_box_height = line_ascent + line_descent;
6282
6283    // The baseline for the entire line is determined by its tallest item.
6284    let line_baseline_y = line_top_y + line_ascent;
6285
6286    // --- Segment-Aware Positioning ---
6287    let mut item_cursor = 0;
6288    let is_first_line_of_para = line_index == 0; // Simplified assumption
6289
6290    for (segment_idx, segment) in line_constraints.segments.iter().enumerate() {
6291        if item_cursor >= line_items.len() {
6292            break;
6293        }
6294
6295        // 1. Collect all items that fit into the current segment.
6296        let mut segment_items = Vec::new();
6297        let mut current_segment_width = 0.0;
6298        while item_cursor < line_items.len() {
6299            let item = &line_items[item_cursor];
6300            let item_measure = get_item_measure(item, is_vertical);
6301            // Put at least one item in the segment to avoid getting stuck.
6302            if current_segment_width + item_measure > segment.width && !segment_items.is_empty() {
6303                break;
6304            }
6305            segment_items.push(item.clone());
6306            current_segment_width += item_measure;
6307            item_cursor += 1;
6308        }
6309
6310        if segment_items.is_empty() {
6311            continue;
6312        }
6313
6314        // 2. Calculate justification spacing *for this segment only*.
6315        let (extra_word_spacing, extra_char_spacing) = if constraints.text_justify
6316            != JustifyContent::None
6317            && (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
6318            && constraints.text_justify != JustifyContent::Kashida
6319        {
6320            let segment_line_constraints = LineConstraints {
6321                segments: vec![segment.clone()],
6322                total_available: segment.width,
6323            };
6324            calculate_justification_spacing(
6325                &segment_items,
6326                &segment_line_constraints,
6327                constraints.text_justify,
6328                is_vertical,
6329            )
6330        } else {
6331            (0.0, 0.0)
6332        };
6333
6334        // Kashida justification needs to be segment-aware if used.
6335        let justified_segment_items = if constraints.text_justify == JustifyContent::Kashida
6336            && (!is_last_line || constraints.text_align == TextAlign::JustifyAll)
6337        {
6338            let segment_line_constraints = LineConstraints {
6339                segments: vec![segment.clone()],
6340                total_available: segment.width,
6341            };
6342            justify_kashida_and_rebuild(
6343                segment_items,
6344                &segment_line_constraints,
6345                is_vertical,
6346                debug_messages,
6347                fonts,
6348            )
6349        } else {
6350            segment_items
6351        };
6352
6353        // Recalculate width in case kashida changed the item list
6354        let final_segment_width: f32 = justified_segment_items
6355            .iter()
6356            .map(|item| get_item_measure(item, is_vertical))
6357            .sum();
6358
6359        // 3. Calculate alignment offset *within this segment*.
6360        let remaining_space = segment.width - final_segment_width;
6361
6362        // Handle MaxContent/indefinite width: when available_width is MaxContent (for intrinsic
6363        // sizing), segment.width will be f32::MAX / 2.0. Alignment calculations would
6364        // produce huge offsets. In this case, treat as left-aligned (offset = 0) since
6365        // we're measuring natural content width. We check for both infinite AND very large
6366        // values (> 1e30) to catch the MaxContent case.
6367        let is_indefinite_width = segment.width.is_infinite() || segment.width > 1e30;
6368        let alignment_offset = if is_indefinite_width {
6369            0.0 // No alignment offset for indefinite width
6370        } else {
6371            match physical_align {
6372                TextAlign::Center => remaining_space / 2.0,
6373                TextAlign::Right => remaining_space,
6374                _ => 0.0, // Left, Justify
6375            }
6376        };
6377
6378        let mut main_axis_pen = segment.start_x + alignment_offset;
6379        if let Some(msgs) = debug_messages {
6380            msgs.push(LayoutDebugMessage::info(format!(
6381                "[Pos1Line] Segment width: {}, Item width: {}, Remaining space: {}, Initial pen: \
6382                 {}",
6383                segment.width, final_segment_width, remaining_space, main_axis_pen
6384            )));
6385        }
6386
6387        // Apply text-indent only to the very first segment of the first line.
6388        if is_first_line_of_para && segment_idx == 0 {
6389            main_axis_pen += constraints.text_indent;
6390        }
6391
6392        // Calculate total marker width for proper outside marker positioning
6393        // We need to position all marker clusters together in the padding gutter
6394        let total_marker_width: f32 = justified_segment_items
6395            .iter()
6396            .filter_map(|item| {
6397                if let ShapedItem::Cluster(c) = item {
6398                    if c.marker_position_outside == Some(true) {
6399                        return Some(get_item_measure(item, is_vertical));
6400                    }
6401                }
6402                None
6403            })
6404            .sum();
6405
6406        // Track marker pen separately - starts at negative position for outside markers
6407        let marker_spacing = 4.0; // Small gap between marker and content
6408        let mut marker_pen = if total_marker_width > 0.0 {
6409            -(total_marker_width + marker_spacing)
6410        } else {
6411            0.0
6412        };
6413
6414        // 4. Position the items belonging to this segment.
6415        //
6416        // Vertical alignment positioning (CSS vertical-align)
6417        //
6418        // Currently, we use `constraints.vertical_align` for ALL items on the line.
6419        // This is the GLOBAL vertical alignment set on the containing block.
6420        //
6421        // KNOWN LIMITATION / TODO:
6422        //
6423        // Per-item vertical-align (stored in `InlineImage.alignment`) is NOT used here.
6424        // According to CSS, each inline element can have its own vertical-align value:
6425        //   <img style="vertical-align: top"> would align to line top
6426        //   <img style="vertical-align: middle"> would center in line box
6427        //   <img style="vertical-align: bottom"> would align to line bottom
6428        //
6429        // To fix this, we would need dir_to:
6430        // 1. Add a helper function `get_item_vertical_align(&item)` that extracts the alignment
6431        //    from ShapedItem::Object -> InlineContent::Image -> alignment
6432        // 2. Use that alignment instead of `constraints.vertical_align` for Objects
6433        //
6434        // For now, all items use the global alignment which works correctly for
6435        // text-only content or when all images have the same alignment.
6436        //
6437        // Reference: CSS Inline Layout Level 3 § 4 Baseline Alignment
6438        // https://www.w3.org/TR/css-inline-3/#baseline-alignment
6439        for item in justified_segment_items {
6440            let (item_ascent, item_descent) = get_item_vertical_metrics(&item);
6441            let item_baseline_pos = match constraints.vertical_align {
6442                VerticalAlign::Top => line_top_y + item_ascent,
6443                VerticalAlign::Middle => {
6444                    line_top_y + (line_box_height / 2.0) - ((item_ascent + item_descent) / 2.0)
6445                        + item_ascent
6446                }
6447                VerticalAlign::Bottom => line_top_y + line_box_height - item_descent,
6448                _ => line_baseline_y, // Baseline
6449            };
6450
6451            // Calculate item measure (needed for both positioning and pen advance)
6452            let item_measure = get_item_measure(&item, is_vertical);
6453
6454            let position = if is_vertical {
6455                Point {
6456                    x: item_baseline_pos - item_ascent,
6457                    y: main_axis_pen,
6458                }
6459            } else {
6460                if let Some(msgs) = debug_messages {
6461                    msgs.push(LayoutDebugMessage::info(format!(
6462                        "[Pos1Line] is_vertical=false, main_axis_pen={}, item_baseline_pos={}, \
6463                         item_ascent={}",
6464                        main_axis_pen, item_baseline_pos, item_ascent
6465                    )));
6466                }
6467
6468                // Check if this is an outside marker - if so, position it in the padding gutter
6469                let x_position = if let ShapedItem::Cluster(cluster) = &item {
6470                    if cluster.marker_position_outside == Some(true) {
6471                        // Use marker_pen for sequential marker positioning
6472                        let marker_width = item_measure;
6473                        if let Some(msgs) = debug_messages {
6474                            msgs.push(LayoutDebugMessage::info(format!(
6475                                "[Pos1Line] Outside marker detected! width={}, positioning at \
6476                                 marker_pen={}",
6477                                marker_width, marker_pen
6478                            )));
6479                        }
6480                        let pos = marker_pen;
6481                        marker_pen += marker_width; // Advance marker pen for next marker cluster
6482                        pos
6483                    } else {
6484                        main_axis_pen
6485                    }
6486                } else {
6487                    main_axis_pen
6488                };
6489
6490                Point {
6491                    y: item_baseline_pos - item_ascent,
6492                    x: x_position,
6493                }
6494            };
6495
6496            // item_measure is calculated above for marker positioning
6497            let item_text = item
6498                .as_cluster()
6499                .map(|c| c.text.as_str())
6500                .unwrap_or("[OBJ]");
6501            if let Some(msgs) = debug_messages {
6502                msgs.push(LayoutDebugMessage::info(format!(
6503                    "[Pos1Line] Positioning item '{}' at pen_x={}",
6504                    item_text, main_axis_pen
6505                )));
6506            }
6507            positioned.push(PositionedItem {
6508                item: item.clone(),
6509                position,
6510                line_index,
6511            });
6512
6513            // Outside markers don't advance the pen - they're positioned in the padding gutter
6514            let is_outside_marker = if let ShapedItem::Cluster(c) = &item {
6515                c.marker_position_outside == Some(true)
6516            } else {
6517                false
6518            };
6519
6520            if !is_outside_marker {
6521                main_axis_pen += item_measure;
6522            }
6523
6524            // Apply calculated spacing to the pen (skip for outside markers)
6525            if !is_outside_marker && extra_char_spacing > 0.0 && can_justify_after(&item) {
6526                main_axis_pen += extra_char_spacing;
6527            }
6528            if let ShapedItem::Cluster(c) = &item {
6529                if !is_outside_marker {
6530                    let letter_spacing_px = match c.style.letter_spacing {
6531                        Spacing::Px(px) => px as f32,
6532                        Spacing::Em(em) => em * c.style.font_size_px,
6533                    };
6534                    main_axis_pen += letter_spacing_px;
6535                    if is_word_separator(&item) {
6536                        let word_spacing_px = match c.style.word_spacing {
6537                            Spacing::Px(px) => px as f32,
6538                            Spacing::Em(em) => em * c.style.font_size_px,
6539                        };
6540                        main_axis_pen += word_spacing_px;
6541                        main_axis_pen += extra_word_spacing;
6542                    }
6543                }
6544            }
6545        }
6546    }
6547
6548    (positioned, line_box_height)
6549}
6550
6551/// Calculates the starting pen offset to achieve the desired text alignment.
6552fn calculate_alignment_offset(
6553    items: &[ShapedItem],
6554    line_constraints: &LineConstraints,
6555    align: TextAlign,
6556    is_vertical: bool,
6557    constraints: &UnifiedConstraints,
6558) -> f32 {
6559    // Simplified to use the first segment for alignment.
6560    if let Some(segment) = line_constraints.segments.first() {
6561        let total_width: f32 = items
6562            .iter()
6563            .map(|item| get_item_measure(item, is_vertical))
6564            .sum();
6565
6566        let available_width = if constraints.segment_alignment == SegmentAlignment::Total {
6567            line_constraints.total_available
6568        } else {
6569            segment.width
6570        };
6571
6572        if total_width >= available_width {
6573            return 0.0; // No alignment needed if line is full or overflows
6574        }
6575
6576        let remaining_space = available_width - total_width;
6577
6578        match align {
6579            TextAlign::Center => remaining_space / 2.0,
6580            TextAlign::Right => remaining_space,
6581            _ => 0.0, // Left, Justify, Start, End
6582        }
6583    } else {
6584        0.0
6585    }
6586}
6587
6588/// Calculates the extra spacing needed for justification without modifying the items.
6589///
6590/// This function is pure and does not mutate any state, making it safe to use
6591/// with cached `ShapedItem` data.
6592///
6593/// # Arguments
6594/// * `items` - A slice of items on the line.
6595/// * `line_constraints` - The geometric constraints for the line.
6596/// * `text_justify` - The type of justification to calculate.
6597/// * `is_vertical` - Whether the layout is vertical.
6598///
6599/// # Returns
6600/// A tuple `(extra_per_word, extra_per_char)` containing the extra space in pixels
6601/// to add at each word or character justification opportunity.
6602fn calculate_justification_spacing(
6603    items: &[ShapedItem],
6604    line_constraints: &LineConstraints,
6605    text_justify: JustifyContent,
6606    is_vertical: bool,
6607) -> (f32, f32) {
6608    // (extra_per_word, extra_per_char)
6609    let total_width: f32 = items
6610        .iter()
6611        .map(|item| get_item_measure(item, is_vertical))
6612        .sum();
6613    let available_width = line_constraints.total_available;
6614
6615    if total_width >= available_width || available_width <= 0.0 {
6616        return (0.0, 0.0);
6617    }
6618
6619    let extra_space = available_width - total_width;
6620
6621    match text_justify {
6622        JustifyContent::InterWord => {
6623            // Count justification opportunities (spaces).
6624            let space_count = items.iter().filter(|item| is_word_separator(item)).count();
6625            if space_count > 0 {
6626                (extra_space / space_count as f32, 0.0)
6627            } else {
6628                (0.0, 0.0) // No spaces to expand, do nothing.
6629            }
6630        }
6631        JustifyContent::InterCharacter | JustifyContent::Distribute => {
6632            // Count justification opportunities (between non-combining characters).
6633            let gap_count = items
6634                .iter()
6635                .enumerate()
6636                .filter(|(i, item)| *i < items.len() - 1 && can_justify_after(item))
6637                .count();
6638            if gap_count > 0 {
6639                (0.0, extra_space / gap_count as f32)
6640            } else {
6641                (0.0, 0.0) // No gaps to expand, do nothing.
6642            }
6643        }
6644        // Kashida justification modifies the item list and is handled by a separate function.
6645        _ => (0.0, 0.0),
6646    }
6647}
6648
6649/// Rebuilds a line of items, inserting Kashida glyphs for justification.
6650///
6651/// This function is non-mutating with respect to its inputs. It takes ownership of the
6652/// original items and returns a completely new `Vec`. This is necessary because Kashida
6653/// justification changes the number of items on the line, and must not modify cached data.
6654pub fn justify_kashida_and_rebuild<T: ParsedFontTrait>(
6655    items: Vec<ShapedItem>,
6656    line_constraints: &LineConstraints,
6657    is_vertical: bool,
6658    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6659    fonts: &LoadedFonts<T>,
6660) -> Vec<ShapedItem> {
6661    if let Some(msgs) = debug_messages {
6662        msgs.push(LayoutDebugMessage::info(
6663            "\n--- Entering justify_kashida_and_rebuild ---".to_string(),
6664        ));
6665    }
6666    let total_width: f32 = items
6667        .iter()
6668        .map(|item| get_item_measure(item, is_vertical))
6669        .sum();
6670    let available_width = line_constraints.total_available;
6671    if let Some(msgs) = debug_messages {
6672        msgs.push(LayoutDebugMessage::info(format!(
6673            "Total item width: {}, Available width: {}",
6674            total_width, available_width
6675        )));
6676    }
6677
6678    if total_width >= available_width || available_width <= 0.0 {
6679        if let Some(msgs) = debug_messages {
6680            msgs.push(LayoutDebugMessage::info(
6681                "No justification needed (line is full or invalid).".to_string(),
6682            ));
6683        }
6684        return items;
6685    }
6686
6687    let extra_space = available_width - total_width;
6688    if let Some(msgs) = debug_messages {
6689        msgs.push(LayoutDebugMessage::info(format!(
6690            "Extra space to fill: {}",
6691            extra_space
6692        )));
6693    }
6694
6695    let font_info = items.iter().find_map(|item| {
6696        if let ShapedItem::Cluster(c) = item {
6697            if let Some(glyph) = c.glyphs.first() {
6698                if glyph.script == Script::Arabic {
6699                    // Look up font from hash
6700                    if let Some(font) = fonts.get_by_hash(glyph.font_hash) {
6701                        return Some((
6702                            font.clone(),
6703                            glyph.font_hash,
6704                            glyph.font_metrics.clone(),
6705                            glyph.style.clone(),
6706                        ));
6707                    }
6708                }
6709            }
6710        }
6711        None
6712    });
6713
6714    let (font, font_hash, font_metrics, style) = match font_info {
6715        Some(info) => {
6716            if let Some(msgs) = debug_messages {
6717                msgs.push(LayoutDebugMessage::info(
6718                    "Found Arabic font for kashida.".to_string(),
6719                ));
6720            }
6721            info
6722        }
6723        None => {
6724            if let Some(msgs) = debug_messages {
6725                msgs.push(LayoutDebugMessage::info(
6726                    "No Arabic font found on line. Cannot insert kashidas.".to_string(),
6727                ));
6728            }
6729            return items;
6730        }
6731    };
6732
6733    let (kashida_glyph_id, kashida_advance) =
6734        match font.get_kashida_glyph_and_advance(style.font_size_px) {
6735            Some((id, adv)) if adv > 0.0 => {
6736                if let Some(msgs) = debug_messages {
6737                    msgs.push(LayoutDebugMessage::info(format!(
6738                        "Font provides kashida glyph with advance {}",
6739                        adv
6740                    )));
6741                }
6742                (id, adv)
6743            }
6744            _ => {
6745                if let Some(msgs) = debug_messages {
6746                    msgs.push(LayoutDebugMessage::info(
6747                        "Font does not support kashida justification.".to_string(),
6748                    ));
6749                }
6750                return items;
6751            }
6752        };
6753
6754    let opportunity_indices: Vec<usize> = items
6755        .windows(2)
6756        .enumerate()
6757        .filter_map(|(i, window)| {
6758            if let (ShapedItem::Cluster(cur), ShapedItem::Cluster(next)) = (&window[0], &window[1])
6759            {
6760                if is_arabic_cluster(cur)
6761                    && is_arabic_cluster(next)
6762                    && !is_word_separator(&window[1])
6763                {
6764                    return Some(i + 1);
6765                }
6766            }
6767            None
6768        })
6769        .collect();
6770
6771    if let Some(msgs) = debug_messages {
6772        msgs.push(LayoutDebugMessage::info(format!(
6773            "Found {} kashida insertion opportunities at indices: {:?}",
6774            opportunity_indices.len(),
6775            opportunity_indices
6776        )));
6777    }
6778
6779    if opportunity_indices.is_empty() {
6780        if let Some(msgs) = debug_messages {
6781            msgs.push(LayoutDebugMessage::info(
6782                "No opportunities found. Exiting.".to_string(),
6783            ));
6784        }
6785        return items;
6786    }
6787
6788    let num_kashidas_to_insert = (extra_space / kashida_advance).floor() as usize;
6789    if let Some(msgs) = debug_messages {
6790        msgs.push(LayoutDebugMessage::info(format!(
6791            "Calculated number of kashidas to insert: {}",
6792            num_kashidas_to_insert
6793        )));
6794    }
6795
6796    if num_kashidas_to_insert == 0 {
6797        return items;
6798    }
6799
6800    let kashidas_per_point = num_kashidas_to_insert / opportunity_indices.len();
6801    let mut remainder = num_kashidas_to_insert % opportunity_indices.len();
6802    if let Some(msgs) = debug_messages {
6803        msgs.push(LayoutDebugMessage::info(format!(
6804            "Distributing kashidas: {} per point, with {} remainder.",
6805            kashidas_per_point, remainder
6806        )));
6807    }
6808
6809    let kashida_item = {
6810        /* ... as before ... */
6811        let kashida_glyph = ShapedGlyph {
6812            kind: GlyphKind::Kashida {
6813                width: kashida_advance,
6814            },
6815            glyph_id: kashida_glyph_id,
6816            font_hash,
6817            font_metrics: font_metrics.clone(),
6818            style: style.clone(),
6819            script: Script::Arabic,
6820            advance: kashida_advance,
6821            kerning: 0.0,
6822            cluster_offset: 0,
6823            offset: Point::default(),
6824            vertical_advance: 0.0,
6825            vertical_offset: Point::default(),
6826        };
6827        ShapedItem::Cluster(ShapedCluster {
6828            text: "\u{0640}".to_string(),
6829            source_cluster_id: GraphemeClusterId {
6830                source_run: u32::MAX,
6831                start_byte_in_run: u32::MAX,
6832            },
6833            source_content_index: ContentIndex {
6834                run_index: u32::MAX,
6835                item_index: u32::MAX,
6836            },
6837            source_node_id: None, // Kashida is generated, not from DOM
6838            glyphs: vec![kashida_glyph],
6839            advance: kashida_advance,
6840            direction: BidiDirection::Ltr,
6841            style,
6842            marker_position_outside: None,
6843        })
6844    };
6845
6846    let mut new_items = Vec::with_capacity(items.len() + num_kashidas_to_insert);
6847    let mut last_copy_idx = 0;
6848    for &point in &opportunity_indices {
6849        new_items.extend_from_slice(&items[last_copy_idx..point]);
6850        let mut num_to_insert = kashidas_per_point;
6851        if remainder > 0 {
6852            num_to_insert += 1;
6853            remainder -= 1;
6854        }
6855        for _ in 0..num_to_insert {
6856            new_items.push(kashida_item.clone());
6857        }
6858        last_copy_idx = point;
6859    }
6860    new_items.extend_from_slice(&items[last_copy_idx..]);
6861
6862    if let Some(msgs) = debug_messages {
6863        msgs.push(LayoutDebugMessage::info(format!(
6864            "--- Exiting justify_kashida_and_rebuild, new item count: {} ---",
6865            new_items.len()
6866        )));
6867    }
6868    new_items
6869}
6870
6871/// Helper to determine if a cluster belongs to the Arabic script.
6872fn is_arabic_cluster(cluster: &ShapedCluster) -> bool {
6873    // A cluster is considered Arabic if its first non-NotDef glyph is from the Arabic script.
6874    // This is a robust heuristic for mixed-script lines.
6875    cluster.glyphs.iter().any(|g| g.script == Script::Arabic)
6876}
6877
6878/// Helper to identify if an item is a word separator (like a space).
6879pub fn is_word_separator(item: &ShapedItem) -> bool {
6880    if let ShapedItem::Cluster(c) = item {
6881        // A cluster is a word separator if its text is whitespace.
6882        // This is a simplification; a single glyph might be whitespace.
6883        c.text.chars().any(|g| g.is_whitespace())
6884    } else {
6885        false
6886    }
6887}
6888
6889/// Helper to identify if space can be added after an item.
6890fn can_justify_after(item: &ShapedItem) -> bool {
6891    if let ShapedItem::Cluster(c) = item {
6892        c.text.chars().last().map_or(false, |g| {
6893            !g.is_whitespace() && classify_character(g as u32) != CharacterClass::Combining
6894        })
6895    } else {
6896        // Can generally justify after inline objects unless they are followed by a break.
6897        !matches!(item, ShapedItem::Break { .. })
6898    }
6899}
6900
6901/// Classifies a character for layout purposes (e.g., justification behavior).
6902/// Copied from `mod.rs`.
6903fn classify_character(codepoint: u32) -> CharacterClass {
6904    match codepoint {
6905        0x0020 | 0x00A0 | 0x3000 => CharacterClass::Space,
6906        0x0021..=0x002F | 0x003A..=0x0040 | 0x005B..=0x0060 | 0x007B..=0x007E => {
6907            CharacterClass::Punctuation
6908        }
6909        0x4E00..=0x9FFF | 0x3400..=0x4DBF => CharacterClass::Ideograph,
6910        0x0300..=0x036F | 0x1AB0..=0x1AFF => CharacterClass::Combining,
6911        // Mongolian script range
6912        0x1800..=0x18AF => CharacterClass::Letter,
6913        _ => CharacterClass::Letter,
6914    }
6915}
6916
6917/// Helper to get the primary measure (width or height) of a shaped item.
6918pub fn get_item_measure(item: &ShapedItem, is_vertical: bool) -> f32 {
6919    match item {
6920        ShapedItem::Cluster(c) => {
6921            // Total width = base advance + kerning adjustments
6922            // Kerning is stored separately in glyphs for inspection, but the total
6923            // cluster width must include it for correct layout positioning
6924            let total_kerning: f32 = c.glyphs.iter().map(|g| g.kerning).sum();
6925            c.advance + total_kerning
6926        }
6927        ShapedItem::Object { bounds, .. }
6928        | ShapedItem::CombinedBlock { bounds, .. }
6929        | ShapedItem::Tab { bounds, .. } => {
6930            if is_vertical {
6931                bounds.height
6932            } else {
6933                bounds.width
6934            }
6935        }
6936        ShapedItem::Break { .. } => 0.0,
6937    }
6938}
6939
6940/// Helper to get the final positioned bounds of an item.
6941fn get_item_bounds(item: &PositionedItem) -> Rect {
6942    let measure = get_item_measure(&item.item, false); // for simplicity, use horizontal
6943    let cross_measure = match &item.item {
6944        ShapedItem::Object { bounds, .. } => bounds.height,
6945        _ => 20.0, // placeholder line height
6946    };
6947    Rect {
6948        x: item.position.x,
6949        y: item.position.y,
6950        width: measure,
6951        height: cross_measure,
6952    }
6953}
6954
6955/// Calculates the available horizontal segments for a line at a given vertical position,
6956/// considering both shape boundaries and exclusions.
6957fn get_line_constraints(
6958    line_y: f32,
6959    line_height: f32,
6960    constraints: &UnifiedConstraints,
6961    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6962) -> LineConstraints {
6963    if let Some(msgs) = debug_messages {
6964        msgs.push(LayoutDebugMessage::info(format!(
6965            "\n--- Entering get_line_constraints for y={} ---",
6966            line_y
6967        )));
6968    }
6969
6970    let mut available_segments = Vec::new();
6971    if constraints.shape_boundaries.is_empty() {
6972        // The segment_width is determined by available_width, NOT by TextWrap.
6973        // TextWrap::NoWrap only affects whether the LineBreaker can insert soft breaks,
6974        // it should NOT override a definite width constraint from CSS.
6975        // CSS Text Level 3: For 'white-space: pre/nowrap', text overflows horizontally
6976        // if it doesn't fit, rather than expanding the container.
6977        let segment_width = match constraints.available_width {
6978            AvailableSpace::Definite(w) => w, // Respect definite width from CSS
6979            AvailableSpace::MaxContent => f32::MAX / 2.0, // For intrinsic max-content sizing
6980            AvailableSpace::MinContent => 0.0, // For intrinsic min-content sizing
6981        };
6982        // Note: TextWrap::NoWrap is handled by the LineBreaker in break_one_line()
6983        // to prevent soft wraps. The text will simply overflow if it exceeds segment_width.
6984        available_segments.push(LineSegment {
6985            start_x: 0.0,
6986            width: segment_width,
6987            priority: 0,
6988        });
6989    } else {
6990        // ... complex boundary logic ...
6991    }
6992
6993    if let Some(msgs) = debug_messages {
6994        msgs.push(LayoutDebugMessage::info(format!(
6995            "Initial available segments: {:?}",
6996            available_segments
6997        )));
6998    }
6999
7000    for (idx, exclusion) in constraints.shape_exclusions.iter().enumerate() {
7001        if let Some(msgs) = debug_messages {
7002            msgs.push(LayoutDebugMessage::info(format!(
7003                "Applying exclusion #{}: {:?}",
7004                idx, exclusion
7005            )));
7006        }
7007        let exclusion_spans =
7008            get_shape_horizontal_spans(exclusion, line_y, line_height).unwrap_or_default();
7009        if let Some(msgs) = debug_messages {
7010            msgs.push(LayoutDebugMessage::info(format!(
7011                "  Exclusion spans at y={}: {:?}",
7012                line_y, exclusion_spans
7013            )));
7014        }
7015
7016        if exclusion_spans.is_empty() {
7017            continue;
7018        }
7019
7020        let mut next_segments = Vec::new();
7021        for (excl_start, excl_end) in exclusion_spans {
7022            for segment in &available_segments {
7023                let seg_start = segment.start_x;
7024                let seg_end = segment.start_x + segment.width;
7025
7026                // Create new segments by subtracting the exclusion
7027                if seg_end > excl_start && seg_start < excl_end {
7028                    if seg_start < excl_start {
7029                        // Left part
7030                        next_segments.push(LineSegment {
7031                            start_x: seg_start,
7032                            width: excl_start - seg_start,
7033                            priority: segment.priority,
7034                        });
7035                    }
7036                    if seg_end > excl_end {
7037                        // Right part
7038                        next_segments.push(LineSegment {
7039                            start_x: excl_end,
7040                            width: seg_end - excl_end,
7041                            priority: segment.priority,
7042                        });
7043                    }
7044                } else {
7045                    next_segments.push(segment.clone()); // No overlap
7046                }
7047            }
7048            available_segments = merge_segments(next_segments);
7049            next_segments = Vec::new();
7050        }
7051        if let Some(msgs) = debug_messages {
7052            msgs.push(LayoutDebugMessage::info(format!(
7053                "  Segments after exclusion #{}: {:?}",
7054                idx, available_segments
7055            )));
7056        }
7057    }
7058
7059    let total_width = available_segments.iter().map(|s| s.width).sum();
7060    if let Some(msgs) = debug_messages {
7061        msgs.push(LayoutDebugMessage::info(format!(
7062            "Final segments: {:?}, total available width: {}",
7063            available_segments, total_width
7064        )));
7065        msgs.push(LayoutDebugMessage::info(
7066            "--- Exiting get_line_constraints ---".to_string(),
7067        ));
7068    }
7069
7070    LineConstraints {
7071        segments: available_segments,
7072        total_available: total_width,
7073    }
7074}
7075
7076/// Helper function to get the horizontal spans of any shape at a given y-coordinate.
7077/// Returns a list of (start_x, end_x) tuples.
7078fn get_shape_horizontal_spans(
7079    shape: &ShapeBoundary,
7080    y: f32,
7081    line_height: f32,
7082) -> Result<Vec<(f32, f32)>, LayoutError> {
7083    match shape {
7084        ShapeBoundary::Rectangle(rect) => {
7085            // Check for any overlap between the line box [y, y + line_height]
7086            // and the rectangle's vertical span [rect.y, rect.y + rect.height].
7087            let line_start = y;
7088            let line_end = y + line_height;
7089            let rect_start = rect.y;
7090            let rect_end = rect.y + rect.height;
7091
7092            if line_start < rect_end && line_end > rect_start {
7093                Ok(vec![(rect.x, rect.x + rect.width)])
7094            } else {
7095                Ok(vec![])
7096            }
7097        }
7098        ShapeBoundary::Circle { center, radius } => {
7099            let line_center_y = y + line_height / 2.0;
7100            let dy = (line_center_y - center.y).abs();
7101            if dy <= *radius {
7102                let dx = (radius.powi(2) - dy.powi(2)).sqrt();
7103                Ok(vec![(center.x - dx, center.x + dx)])
7104            } else {
7105                Ok(vec![])
7106            }
7107        }
7108        ShapeBoundary::Ellipse { center, radii } => {
7109            let line_center_y = y + line_height / 2.0;
7110            let dy = line_center_y - center.y;
7111            if dy.abs() <= radii.height {
7112                // Formula: (x-h)^2/a^2 + (y-k)^2/b^2 = 1
7113                let y_term = dy / radii.height;
7114                let x_term_squared = 1.0 - y_term.powi(2);
7115                if x_term_squared >= 0.0 {
7116                    let dx = radii.width * x_term_squared.sqrt();
7117                    Ok(vec![(center.x - dx, center.x + dx)])
7118                } else {
7119                    Ok(vec![])
7120                }
7121            } else {
7122                Ok(vec![])
7123            }
7124        }
7125        ShapeBoundary::Polygon { points } => {
7126            let segments = polygon_line_intersection(points, y, line_height)?;
7127            Ok(segments
7128                .iter()
7129                .map(|s| (s.start_x, s.start_x + s.width))
7130                .collect())
7131        }
7132        ShapeBoundary::Path { .. } => Ok(vec![]), // TODO!
7133    }
7134}
7135
7136/// Merges overlapping or adjacent line segments into larger ones.
7137fn merge_segments(mut segments: Vec<LineSegment>) -> Vec<LineSegment> {
7138    if segments.len() <= 1 {
7139        return segments;
7140    }
7141    segments.sort_by(|a, b| a.start_x.partial_cmp(&b.start_x).unwrap());
7142    let mut merged = vec![segments[0].clone()];
7143    for next_seg in segments.iter().skip(1) {
7144        let last = merged.last_mut().unwrap();
7145        if next_seg.start_x <= last.start_x + last.width {
7146            let new_width = (next_seg.start_x + next_seg.width) - last.start_x;
7147            last.width = last.width.max(new_width);
7148        } else {
7149            merged.push(next_seg.clone());
7150        }
7151    }
7152    merged
7153}
7154
7155// TODO: Dummy polygon function to make it compile
7156fn polygon_line_intersection(
7157    points: &[Point],
7158    y: f32,
7159    line_height: f32,
7160) -> Result<Vec<LineSegment>, LayoutError> {
7161    if points.len() < 3 {
7162        return Ok(vec![]);
7163    }
7164
7165    let line_center_y = y + line_height / 2.0;
7166    let mut intersections = Vec::new();
7167
7168    // Use winding number algorithm for robustness with complex polygons.
7169    for i in 0..points.len() {
7170        let p1 = points[i];
7171        let p2 = points[(i + 1) % points.len()];
7172
7173        // Skip horizontal edges as they don't intersect a horizontal scanline in a meaningful way.
7174        if (p2.y - p1.y).abs() < f32::EPSILON {
7175            continue;
7176        }
7177
7178        // Check if our horizontal scanline at `line_center_y` crosses this polygon edge.
7179        let crosses = (p1.y <= line_center_y && p2.y > line_center_y)
7180            || (p1.y > line_center_y && p2.y <= line_center_y);
7181
7182        if crosses {
7183            // Calculate intersection x-coordinate using linear interpolation.
7184            let t = (line_center_y - p1.y) / (p2.y - p1.y);
7185            let x = p1.x + t * (p2.x - p1.x);
7186            intersections.push(x);
7187        }
7188    }
7189
7190    // Sort intersections by x-coordinate to form spans.
7191    intersections.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
7192
7193    // Build segments from paired intersection points.
7194    let mut segments = Vec::new();
7195    for chunk in intersections.chunks_exact(2) {
7196        let start_x = chunk[0];
7197        let end_x = chunk[1];
7198        if end_x > start_x {
7199            segments.push(LineSegment {
7200                start_x,
7201                width: end_x - start_x,
7202                priority: 0,
7203            });
7204        }
7205    }
7206
7207    Ok(segments)
7208}
7209
7210// ADDITION: A helper function to get a hyphenator.
7211/// Helper to get a hyphenator for a given language.
7212/// TODO: In a real app, this would be cached.
7213#[cfg(feature = "text_layout_hyphenation")]
7214fn get_hyphenator(language: HyphenationLanguage) -> Result<Standard, LayoutError> {
7215    Standard::from_embedded(language).map_err(|e| LayoutError::HyphenationError(e.to_string()))
7216}
7217
7218/// Stub when hyphenation is disabled - always returns an error
7219#[cfg(not(feature = "text_layout_hyphenation"))]
7220fn get_hyphenator(_language: Language) -> Result<Standard, LayoutError> {
7221    Err(LayoutError::HyphenationError("Hyphenation feature not enabled".to_string()))
7222}
7223
7224fn is_break_opportunity(item: &ShapedItem) -> bool {
7225    // Break after spaces or explicit break items.
7226    if is_word_separator(item) {
7227        return true;
7228    }
7229    if let ShapedItem::Break { .. } = item {
7230        return true;
7231    }
7232    // Also consider soft hyphens as opportunities.
7233    if let ShapedItem::Cluster(c) = item {
7234        if c.text.starts_with('\u{00AD}') {
7235            return true;
7236        }
7237    }
7238    false
7239}
7240
7241// A cursor to manage the state of the line breaking process.
7242// This allows us to handle items that are partially consumed by hyphenation.
7243pub struct BreakCursor<'a> {
7244    /// A reference to the complete list of shaped items.
7245    pub items: &'a [ShapedItem],
7246    /// The index of the next *full* item to be processed from the `items` slice.
7247    pub next_item_index: usize,
7248    /// The remainder of an item that was split by hyphenation on the previous line.
7249    /// This will be the very first piece of content considered for the next line.
7250    pub partial_remainder: Vec<ShapedItem>,
7251}
7252
7253impl<'a> BreakCursor<'a> {
7254    pub fn new(items: &'a [ShapedItem]) -> Self {
7255        Self {
7256            items,
7257            next_item_index: 0,
7258            partial_remainder: Vec::new(),
7259        }
7260    }
7261
7262    /// Checks if the cursor is at the very beginning of the content stream.
7263    pub fn is_at_start(&self) -> bool {
7264        self.next_item_index == 0 && self.partial_remainder.is_empty()
7265    }
7266
7267    /// Consumes the cursor and returns all remaining items as a `Vec`.
7268    pub fn drain_remaining(&mut self) -> Vec<ShapedItem> {
7269        let mut remaining = std::mem::take(&mut self.partial_remainder);
7270        if self.next_item_index < self.items.len() {
7271            remaining.extend_from_slice(&self.items[self.next_item_index..]);
7272        }
7273        self.next_item_index = self.items.len();
7274        remaining
7275    }
7276
7277    /// Checks if all content, including any partial remainders, has been processed.
7278    pub fn is_done(&self) -> bool {
7279        self.next_item_index >= self.items.len() && self.partial_remainder.is_empty()
7280    }
7281
7282    /// Consumes a number of items from the cursor's stream.
7283    pub fn consume(&mut self, count: usize) {
7284        if count == 0 {
7285            return;
7286        }
7287
7288        let remainder_len = self.partial_remainder.len();
7289        if count <= remainder_len {
7290            // Consuming only from the remainder.
7291            self.partial_remainder.drain(..count);
7292        } else {
7293            // Consuming all of the remainder and some from the main list.
7294            let from_main_list = count - remainder_len;
7295            self.partial_remainder.clear();
7296            self.next_item_index += from_main_list;
7297        }
7298    }
7299
7300    /// Looks ahead and returns the next "unbreakable" unit of content.
7301    /// This is typically a word (a series of non-space clusters) followed by a
7302    /// space, or just a single space if that's next.
7303    pub fn peek_next_unit(&self) -> Vec<ShapedItem> {
7304        let mut unit = Vec::new();
7305        let mut source_items = self.partial_remainder.clone();
7306        source_items.extend_from_slice(&self.items[self.next_item_index..]);
7307
7308        if source_items.is_empty() {
7309            return unit;
7310        }
7311
7312        // If the first item is a break opportunity (like a space), it's a unit on its own.
7313        if is_break_opportunity(&source_items[0]) {
7314            unit.push(source_items[0].clone());
7315            return unit;
7316        }
7317
7318        // Otherwise, collect all items until the next break opportunity.
7319        for item in source_items {
7320            if is_break_opportunity(&item) {
7321                break;
7322            }
7323            unit.push(item.clone());
7324        }
7325        unit
7326    }
7327}
7328
7329// A structured result from a hyphenation attempt.
7330struct HyphenationResult {
7331    /// The items that fit on the current line, including the new hyphen.
7332    line_part: Vec<ShapedItem>,
7333    /// The remainder of the split item to be carried over to the next line.
7334    remainder_part: Vec<ShapedItem>,
7335}
7336
7337fn perform_bidi_analysis<'a, 'b: 'a>(
7338    styled_runs: &'a [TextRunInfo],
7339    full_text: &'b str,
7340    force_lang: Option<Language>,
7341) -> Result<(Vec<VisualRun<'a>>, BidiDirection), LayoutError> {
7342    if full_text.is_empty() {
7343        return Ok((Vec::new(), BidiDirection::Ltr));
7344    }
7345
7346    let bidi_info = BidiInfo::new(full_text, None);
7347    let para = &bidi_info.paragraphs[0];
7348    let base_direction = if para.level.is_rtl() {
7349        BidiDirection::Rtl
7350    } else {
7351        BidiDirection::Ltr
7352    };
7353
7354    // Create a map from each byte index to its original styled run.
7355    let mut byte_to_run_index: Vec<usize> = vec![0; full_text.len()];
7356    for (run_idx, run) in styled_runs.iter().enumerate() {
7357        let start = run.logical_start;
7358        let end = start + run.text.len();
7359        for i in start..end {
7360            byte_to_run_index[i] = run_idx;
7361        }
7362    }
7363
7364    let mut final_visual_runs = Vec::new();
7365    let (levels, visual_run_ranges) = bidi_info.visual_runs(para, para.range.clone());
7366
7367    for range in visual_run_ranges {
7368        let bidi_level = levels[range.start];
7369        let mut sub_run_start = range.start;
7370
7371        // Iterate through the bytes of the visual run to detect style changes.
7372        for i in (range.start + 1)..range.end {
7373            if byte_to_run_index[i] != byte_to_run_index[sub_run_start] {
7374                // Style boundary found. Finalize the previous sub-run.
7375                let original_run_idx = byte_to_run_index[sub_run_start];
7376                let script = crate::text3::script::detect_script(&full_text[sub_run_start..i])
7377                    .unwrap_or(Script::Latin);
7378                final_visual_runs.push(VisualRun {
7379                    text_slice: &full_text[sub_run_start..i],
7380                    style: styled_runs[original_run_idx].style.clone(),
7381                    logical_start_byte: sub_run_start,
7382                    bidi_level: BidiLevel::new(bidi_level.number()),
7383                    language: force_lang.unwrap_or_else(|| {
7384                        crate::text3::script::script_to_language(
7385                            script,
7386                            &full_text[sub_run_start..i],
7387                        )
7388                    }),
7389                    script,
7390                });
7391                // Start a new sub-run.
7392                sub_run_start = i;
7393            }
7394        }
7395
7396        // Add the last sub-run (or the only one if no style change occurred).
7397        let original_run_idx = byte_to_run_index[sub_run_start];
7398        let script = crate::text3::script::detect_script(&full_text[sub_run_start..range.end])
7399            .unwrap_or(Script::Latin);
7400
7401        final_visual_runs.push(VisualRun {
7402            text_slice: &full_text[sub_run_start..range.end],
7403            style: styled_runs[original_run_idx].style.clone(),
7404            logical_start_byte: sub_run_start,
7405            bidi_level: BidiLevel::new(bidi_level.number()),
7406            script,
7407            language: force_lang.unwrap_or_else(|| {
7408                crate::text3::script::script_to_language(
7409                    script,
7410                    &full_text[sub_run_start..range.end],
7411                )
7412            }),
7413        });
7414    }
7415
7416    Ok((final_visual_runs, base_direction))
7417}
7418
7419fn get_justification_priority(class: CharacterClass) -> u8 {
7420    match class {
7421        CharacterClass::Space => 0,
7422        CharacterClass::Punctuation => 64,
7423        CharacterClass::Ideograph => 128,
7424        CharacterClass::Letter => 192,
7425        CharacterClass::Symbol => 224,
7426        CharacterClass::Combining => 255,
7427    }
7428}