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