Skip to main content

ply_engine/
engine.rs

1//! Pure Rust implementation of the Ply layout engine.
2//! A UI layout engine inspired by Clay.
3
4use rustc_hash::FxHashMap;
5
6use crate::align::{AlignX, AlignY};
7use crate::color::Color;
8use crate::elements::BorderPosition;
9use crate::renderer::ImageSource;
10use crate::shaders::ShaderConfig;
11use crate::elements::{
12    FloatingAttachToElement, FloatingClipToElement, PointerCaptureMode,
13};
14use crate::layout::{LayoutDirection, CornerRadius};
15use crate::math::{BoundingBox, Dimensions, Vector2};
16use crate::text::{TextConfig, WrapMode};
17
18const DEFAULT_MAX_ELEMENT_COUNT: i32 = 8192;
19const DEFAULT_MAX_MEASURE_TEXT_WORD_CACHE_COUNT: i32 = 16384;
20const MAXFLOAT: f32 = 3.40282346638528859812e+38;
21const EPSILON: f32 = 0.01;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24#[repr(u8)]
25pub enum SizingType {
26    #[default]
27    Fit,
28    Grow,
29    Percent,
30    Fixed,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34#[repr(u8)]
35pub enum RenderCommandType {
36    #[default]
37    None,
38    Rectangle,
39    Border,
40    Text,
41    Image,
42    ScissorStart,
43    ScissorEnd,
44    Custom,
45    GroupBegin,
46    GroupEnd,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
50#[repr(u8)]
51pub enum PointerDataInteractionState {
52    PressedThisFrame,
53    Pressed,
54    ReleasedThisFrame,
55    #[default]
56    Released,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum ArrowDirection {
61    Left,
62    Right,
63    Up,
64    Down,
65}
66
67/// Actions that can be performed on a focused text input.
68#[derive(Debug, Clone)]
69pub enum TextInputAction {
70    MoveLeft { shift: bool },
71    MoveRight { shift: bool },
72    MoveWordLeft { shift: bool },
73    MoveWordRight { shift: bool },
74    MoveHome { shift: bool },
75    MoveEnd { shift: bool },
76    MoveUp { shift: bool },
77    MoveDown { shift: bool },
78    Backspace,
79    Delete,
80    BackspaceWord,
81    DeleteWord,
82    SelectAll,
83    Copy,
84    Cut,
85    Paste { text: String },
86    Submit,
87    Undo,
88    Redo,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92#[repr(u8)]
93pub enum ElementConfigType {
94    Shared,
95    Text,
96    Image,
97    Floating,
98    Custom,
99    Clip,
100    Border,
101    Aspect,
102    TextInput,
103}
104
105#[derive(Debug, Clone, Copy, Default)]
106pub struct SizingMinMax {
107    pub min: f32,
108    pub max: f32,
109}
110
111#[derive(Debug, Clone, Copy)]
112pub struct SizingAxis {
113    pub type_: SizingType,
114    pub min_max: SizingMinMax,
115    pub percent: f32,
116    pub grow_weight: f32,
117}
118
119impl Default for SizingAxis {
120    fn default() -> Self {
121        Self {
122            type_: SizingType::Fit,
123            min_max: SizingMinMax::default(),
124            percent: 0.0,
125            grow_weight: 1.0,
126        }
127    }
128}
129
130#[derive(Debug, Clone, Copy, Default)]
131pub struct SizingConfig {
132    pub width: SizingAxis,
133    pub height: SizingAxis,
134}
135
136#[derive(Debug, Clone, Copy, Default)]
137pub struct PaddingConfig {
138    pub left: u16,
139    pub right: u16,
140    pub top: u16,
141    pub bottom: u16,
142}
143
144#[derive(Debug, Clone, Copy, Default)]
145pub struct ChildAlignmentConfig {
146    pub x: AlignX,
147    pub y: AlignY,
148}
149
150#[derive(Debug, Clone, Copy, Default)]
151pub struct LayoutConfig {
152    pub sizing: SizingConfig,
153    pub padding: PaddingConfig,
154    pub child_gap: u16,
155    pub wrap: bool,
156    pub wrap_gap: u16,
157    pub child_alignment: ChildAlignmentConfig,
158    pub layout_direction: LayoutDirection,
159}
160
161
162#[derive(Debug, Clone, Copy)]
163pub struct VisualRotationConfig {
164    /// Rotation angle in radians.
165    pub rotation_radians: f32,
166    /// Normalized pivot X (0.0 = left, 0.5 = center, 1.0 = right). Default 0.5.
167    pub pivot_x: f32,
168    /// Normalized pivot Y (0.0 = top, 0.5 = center, 1.0 = bottom). Default 0.5.
169    pub pivot_y: f32,
170    /// Mirror horizontally.
171    pub flip_x: bool,
172    /// Mirror vertically.
173    pub flip_y: bool,
174}
175
176impl Default for VisualRotationConfig {
177    fn default() -> Self {
178        Self {
179            rotation_radians: 0.0,
180            pivot_x: 0.5,
181            pivot_y: 0.5,
182            flip_x: false,
183            flip_y: false,
184        }
185    }
186}
187
188impl VisualRotationConfig {
189    /// Returns `true` when the config is effectively a no-op.
190    pub fn is_noop(&self) -> bool {
191        self.rotation_radians == 0.0 && !self.flip_x && !self.flip_y
192    }
193}
194
195#[derive(Debug, Clone, Copy)]
196pub struct ShapeRotationConfig {
197    /// Rotation angle in radians.
198    pub rotation_radians: f32,
199    /// Mirror horizontally (applied before rotation).
200    pub flip_x: bool,
201    /// Mirror vertically (applied before rotation).
202    pub flip_y: bool,
203}
204
205impl Default for ShapeRotationConfig {
206    fn default() -> Self {
207        Self {
208            rotation_radians: 0.0,
209            flip_x: false,
210            flip_y: false,
211        }
212    }
213}
214
215impl ShapeRotationConfig {
216    /// Returns `true` when the config is effectively a no-op.
217    pub fn is_noop(&self) -> bool {
218        self.rotation_radians == 0.0 && !self.flip_x && !self.flip_y
219    }
220}
221
222#[derive(Debug, Clone, Copy, Default)]
223pub struct FloatingAttachPoints {
224    pub element_x: AlignX,
225    pub element_y: AlignY,
226    pub parent_x: AlignX,
227    pub parent_y: AlignY,
228}
229
230#[derive(Debug, Clone, Copy, Default)]
231pub struct FloatingConfig {
232    pub offset: Vector2,
233    pub parent_id: u32,
234    pub z_index: i16,
235    pub attach_points: FloatingAttachPoints,
236    pub pointer_capture_mode: PointerCaptureMode,
237    pub attach_to: FloatingAttachToElement,
238    pub clip_to: FloatingClipToElement,
239}
240
241#[derive(Debug, Clone, Copy, Default)]
242pub struct ClipConfig {
243    pub horizontal: bool,
244    pub vertical: bool,
245    pub scroll_x: bool,
246    pub scroll_y: bool,
247    pub no_drag_scroll: bool,
248    pub child_offset: Vector2,
249    pub scrollbar: Option<ScrollbarConfig>,
250}
251
252#[derive(Debug, Clone, Copy)]
253pub struct ScrollbarConfig {
254    pub width: f32,
255    pub corner_radius: f32,
256    pub thumb_color: Color,
257    pub track_color: Option<Color>,
258    pub min_thumb_size: f32,
259    pub hide_after_frames: Option<u32>,
260}
261
262impl Default for ScrollbarConfig {
263    fn default() -> Self {
264        Self {
265            width: 6.0,
266            corner_radius: 3.0,
267            thumb_color: Color::rgba(128.0, 128.0, 128.0, 128.0),
268            track_color: None,
269            min_thumb_size: 20.0,
270            hide_after_frames: None,
271        }
272    }
273}
274
275#[derive(Debug, Clone, Copy, Default)]
276pub struct BorderWidth {
277    pub left: u16,
278    pub right: u16,
279    pub top: u16,
280    pub bottom: u16,
281    pub between_children: u16,
282}
283
284impl BorderWidth {
285    pub fn is_zero(&self) -> bool {
286        self.left == 0
287            && self.right == 0
288            && self.top == 0
289            && self.bottom == 0
290            && self.between_children == 0
291    }
292}
293
294#[derive(Debug, Clone, Copy, Default)]
295pub struct BorderConfig {
296    pub color: Color,
297    pub width: BorderWidth,
298    pub position: BorderPosition,
299}
300
301/// The top-level element declaration.
302#[derive(Debug, Clone)]
303pub struct ElementDeclaration<CustomElementData: Clone + Default + std::fmt::Debug = ()> {
304    pub layout: LayoutConfig,
305    pub background_color: Color,
306    pub corner_radius: CornerRadius,
307    pub aspect_ratio: f32,
308    pub cover_aspect_ratio: bool,
309    pub image_data: Option<ImageSource>,
310    pub floating: FloatingConfig,
311    pub custom_data: Option<CustomElementData>,
312    pub clip: ClipConfig,
313    pub border: BorderConfig,
314    pub user_data: usize,
315    pub effects: Vec<ShaderConfig>,
316    pub shaders: Vec<ShaderConfig>,
317    pub visual_rotation: Option<VisualRotationConfig>,
318    pub shape_rotation: Option<ShapeRotationConfig>,
319    pub accessibility: Option<crate::accessibility::AccessibilityConfig>,
320    pub text_input: Option<crate::text_input::TextInputConfig>,
321    pub preserve_focus: bool,
322}
323
324impl<CustomElementData: Clone + Default + std::fmt::Debug> Default for ElementDeclaration<CustomElementData> {
325    fn default() -> Self {
326        Self {
327            layout: LayoutConfig::default(),
328            background_color: Color::rgba(0.0, 0.0, 0.0, 0.0),
329            corner_radius: CornerRadius::default(),
330            aspect_ratio: 0.0,
331            cover_aspect_ratio: false,
332            image_data: None,
333            floating: FloatingConfig::default(),
334            custom_data: None,
335            clip: ClipConfig::default(),
336            border: BorderConfig::default(),
337            user_data: 0,
338            effects: Vec::new(),
339            shaders: Vec::new(),
340            visual_rotation: None,
341            shape_rotation: None,
342            accessibility: None,
343            text_input: None,
344            preserve_focus: false,
345        }
346    }
347}
348
349use crate::id::{Id, StringId};
350
351#[derive(Debug, Clone, Copy, Default)]
352struct SharedElementConfig {
353    background_color: Color,
354    corner_radius: CornerRadius,
355    user_data: usize,
356}
357
358#[derive(Debug, Clone, Copy)]
359struct ElementConfig {
360    config_type: ElementConfigType,
361    config_index: usize,
362}
363
364#[derive(Debug, Clone, Copy, Default)]
365struct ElementConfigSlice {
366    start: usize,
367    length: i32,
368}
369
370#[derive(Debug, Clone, Copy, Default)]
371struct WrappedTextLine {
372    dimensions: Dimensions,
373    start: usize,
374    length: usize,
375}
376
377#[derive(Debug, Clone)]
378struct TextElementData {
379    text: String,
380    preferred_dimensions: Dimensions,
381    element_index: i32,
382    wrapped_lines_start: usize,
383    wrapped_lines_length: i32,
384}
385
386#[derive(Debug, Clone, Copy, Default)]
387struct LayoutElement {
388    // Children data (for non-text elements)
389    children_start: usize,
390    children_length: u16,
391    // Text data (for text elements)
392    text_data_index: i32, // -1 means no text, >= 0 is index
393    dimensions: Dimensions,
394    min_dimensions: Dimensions,
395    layout_config_index: usize,
396    element_configs: ElementConfigSlice,
397    id: u32,
398    floating_children_count: u16,
399}
400
401#[derive(Default)]
402struct LayoutElementHashMapItem {
403    bounding_box: BoundingBox,
404    element_id: Id,
405    layout_element_index: i32,
406    on_hover_fn: Option<Box<dyn FnMut(Id, PointerData)>>,
407    on_press_fn: Option<Box<dyn FnMut(Id, PointerData)>>,
408    on_release_fn: Option<Box<dyn FnMut(Id, PointerData)>>,
409    on_focus_fn: Option<Box<dyn FnMut(Id)>>,
410    on_unfocus_fn: Option<Box<dyn FnMut(Id)>>,
411    on_text_changed_fn: Option<Box<dyn FnMut(&str)>>,
412    on_text_submit_fn: Option<Box<dyn FnMut(&str)>>,
413    is_text_input: bool,
414    preserve_focus: bool,
415    generation: u32,
416    collision: bool,
417    collapsed: bool,
418}
419
420impl Clone for LayoutElementHashMapItem {
421    fn clone(&self) -> Self {
422        Self {
423            bounding_box: self.bounding_box,
424            element_id: self.element_id.clone(),
425            layout_element_index: self.layout_element_index,
426            on_hover_fn: None, // Callbacks are not cloneable
427            on_press_fn: None,
428            on_release_fn: None,
429            on_focus_fn: None,
430            on_unfocus_fn: None,
431            on_text_changed_fn: None,
432            on_text_submit_fn: None,
433            is_text_input: self.is_text_input,
434            preserve_focus: self.preserve_focus,
435            generation: self.generation,
436            collision: self.collision,
437            collapsed: self.collapsed,
438        }
439    }
440}
441
442#[derive(Debug, Clone, Copy, Default)]
443struct MeasuredWord {
444    start_offset: i32,
445    length: i32,
446    width: f32,
447    next: i32,
448}
449
450#[derive(Debug, Clone, Copy, Default)]
451#[allow(dead_code)]
452struct MeasureTextCacheItem {
453    unwrapped_dimensions: Dimensions,
454    measured_words_start_index: i32,
455    min_width: f32,
456    contains_newlines: bool,
457    id: u32,
458    generation: u32,
459}
460
461#[derive(Debug, Clone, Copy, Default)]
462#[allow(dead_code)]
463struct ScrollContainerDataInternal {
464    bounding_box: BoundingBox,
465    content_size: Dimensions,
466    scroll_origin: Vector2,
467    pointer_origin: Vector2,
468    scroll_momentum: Vector2,
469    scroll_position: Vector2,
470    previous_delta: Vector2,
471    scrollbar: Option<ScrollbarConfig>,
472    scroll_x_enabled: bool,
473    scroll_y_enabled: bool,
474    no_drag_scroll: bool,
475    scrollbar_idle_frames: u32,
476    scrollbar_activity_this_frame: bool,
477    scrollbar_thumb_drag_active_x: bool,
478    scrollbar_thumb_drag_active_y: bool,
479    scrollbar_drag_origin: Vector2,
480    scrollbar_drag_scroll_origin: Vector2,
481    element_id: u32,
482    layout_element_index: i32,
483    open_this_frame: bool,
484    pointer_scroll_active: bool,
485}
486
487#[derive(Debug, Clone, Copy, Default)]
488struct LayoutElementTreeNode {
489    layout_element_index: i32,
490    position: Vector2,
491    next_child_offset: Vector2,
492}
493
494#[derive(Debug, Clone, Copy, Default)]
495struct LayoutElementTreeRoot {
496    layout_element_index: i32,
497    parent_id: u32,
498    clip_element_id: u32,
499    z_index: i16,
500    pointer_offset: Vector2,
501}
502
503#[derive(Debug, Clone, Copy)]
504struct FocusableEntry {
505    element_id: u32,
506    tab_index: Option<i32>,
507    insertion_order: u32,
508}
509
510#[derive(Debug, Clone, Copy, Default)]
511pub struct PointerData {
512    pub position: Vector2,
513    pub state: PointerDataInteractionState,
514}
515
516#[derive(Debug, Clone, Copy, Default)]
517#[allow(dead_code)]
518struct BooleanWarnings {
519    max_elements_exceeded: bool,
520    text_measurement_fn_not_set: bool,
521    max_text_measure_cache_exceeded: bool,
522    max_render_commands_exceeded: bool,
523}
524
525#[derive(Debug, Clone)]
526pub struct InternalRenderCommand<CustomElementData: Clone + Default + std::fmt::Debug = ()> {
527    pub bounding_box: BoundingBox,
528    pub command_type: RenderCommandType,
529    pub render_data: InternalRenderData<CustomElementData>,
530    pub user_data: usize,
531    pub id: u32,
532    pub z_index: i16,
533    pub effects: Vec<ShaderConfig>,
534    pub visual_rotation: Option<VisualRotationConfig>,
535    pub shape_rotation: Option<ShapeRotationConfig>,
536}
537
538#[derive(Debug, Clone)]
539pub enum InternalRenderData<CustomElementData: Clone + Default + std::fmt::Debug = ()> {
540    None,
541    Rectangle {
542        background_color: Color,
543        corner_radius: CornerRadius,
544    },
545    Text {
546        text: String,
547        text_color: Color,
548        font_size: u16,
549        letter_spacing: u16,
550        line_height: u16,
551        font_asset: Option<&'static crate::renderer::FontAsset>,
552    },
553    Image {
554        background_color: Color,
555        corner_radius: CornerRadius,
556        image_data: ImageSource,
557    },
558    Custom {
559        background_color: Color,
560        corner_radius: CornerRadius,
561        custom_data: CustomElementData,
562    },
563    Border {
564        color: Color,
565        corner_radius: CornerRadius,
566        width: BorderWidth,
567        position: BorderPosition,
568    },
569    Clip {
570        horizontal: bool,
571        vertical: bool,
572    },
573}
574
575impl<CustomElementData: Clone + Default + std::fmt::Debug> Default for InternalRenderData<CustomElementData> {
576    fn default() -> Self {
577        Self::None
578    }
579}
580
581impl<CustomElementData: Clone + Default + std::fmt::Debug> Default for InternalRenderCommand<CustomElementData> {
582    fn default() -> Self {
583        Self {
584            bounding_box: BoundingBox::default(),
585            command_type: RenderCommandType::None,
586            render_data: InternalRenderData::None,
587            user_data: 0,
588            id: 0,
589            z_index: 0,
590            effects: Vec::new(),
591            visual_rotation: None,
592            shape_rotation: None,
593        }
594    }
595}
596
597#[derive(Debug, Clone, Copy)]
598pub struct ScrollContainerData {
599    pub scroll_position: Vector2,
600    pub scroll_container_dimensions: Dimensions,
601    pub content_dimensions: Dimensions,
602    pub horizontal: bool,
603    pub vertical: bool,
604    pub found: bool,
605}
606
607impl Default for ScrollContainerData {
608    fn default() -> Self {
609        Self {
610            scroll_position: Vector2::default(),
611            scroll_container_dimensions: Dimensions::default(),
612            content_dimensions: Dimensions::default(),
613            horizontal: false,
614            vertical: false,
615            found: false,
616        }
617    }
618}
619
620pub struct PlyContext<CustomElementData: Clone + Default + std::fmt::Debug = ()> {
621    // Settings
622    pub max_element_count: i32,
623    pub max_measure_text_cache_word_count: i32,
624    pub debug_mode_enabled: bool,
625    debug_view_width: f32,
626    pub culling_disabled: bool,
627    pub external_scroll_handling_enabled: bool,
628    pub debug_selected_element_id: u32,
629    pub generation: u32,
630
631    // Warnings
632    boolean_warnings: BooleanWarnings,
633
634    // Pointer info
635    pointer_info: PointerData,
636    pub layout_dimensions: Dimensions,
637
638    // Dynamic element tracking
639    dynamic_element_index: u32,
640
641    // Measure text callback
642    measure_text_fn: Option<Box<dyn Fn(&str, &TextConfig) -> Dimensions>>,
643
644    // Layout elements
645    layout_elements: Vec<LayoutElement>,
646    render_commands: Vec<InternalRenderCommand<CustomElementData>>,
647    open_layout_element_stack: Vec<i32>,
648    layout_element_children: Vec<i32>,
649    layout_element_children_buffer: Vec<i32>,
650    text_element_data: Vec<TextElementData>,
651    aspect_ratio_element_indexes: Vec<i32>,
652    reusable_element_index_buffer: Vec<i32>,
653    layout_element_clip_element_ids: Vec<i32>,
654
655    // Configs
656    layout_configs: Vec<LayoutConfig>,
657    element_configs: Vec<ElementConfig>,
658    text_element_configs: Vec<TextConfig>,
659    aspect_ratio_configs: Vec<f32>,
660    aspect_ratio_cover_configs: Vec<bool>,
661    image_element_configs: Vec<ImageSource>,
662    floating_element_configs: Vec<FloatingConfig>,
663    clip_element_configs: Vec<ClipConfig>,
664    custom_element_configs: Vec<CustomElementData>,
665    border_element_configs: Vec<BorderConfig>,
666    shared_element_configs: Vec<SharedElementConfig>,
667
668    // Per-element shader effects (indexed by layout element index)
669    element_effects: Vec<Vec<ShaderConfig>>,
670    // Per-element group shaders (indexed by layout element index)
671    element_shaders: Vec<Vec<ShaderConfig>>,
672
673    // Per-element visual rotation (indexed by layout element index)
674    element_visual_rotations: Vec<Option<VisualRotationConfig>>,
675
676    // Per-element shape rotation (indexed by layout element index)
677    element_shape_rotations: Vec<Option<ShapeRotationConfig>>,
678    // Original dimensions before AABB expansion (only set when shape_rotation is active)
679    element_pre_rotation_dimensions: Vec<Option<Dimensions>>,
680
681    // String IDs for debug
682    layout_element_id_strings: Vec<StringId>,
683
684    // Text wrapping
685    wrapped_text_lines: Vec<WrappedTextLine>,
686
687    // Tree traversal
688    tree_node_array: Vec<LayoutElementTreeNode>,
689    layout_element_tree_roots: Vec<LayoutElementTreeRoot>,
690
691    // Layout element map: element id -> element data (bounding box, hover callback, etc.)
692    layout_element_map: FxHashMap<u32, LayoutElementHashMapItem>,
693
694    // Text measurement cache: content hash -> measured dimensions and words
695    measure_text_cache: FxHashMap<u32, MeasureTextCacheItem>,
696    measured_words: Vec<MeasuredWord>,
697    measured_words_free_list: Vec<i32>,
698
699    // Clip/scroll
700    open_clip_element_stack: Vec<i32>,
701    pointer_over_ids: Vec<Id>,
702    pressed_element_ids: Vec<Id>,
703    released_this_frame_ids: Vec<Id>,
704    released_this_frame_generation: u32,
705    keyboard_press_this_frame_generation: u32,
706    scroll_container_datas: Vec<ScrollContainerDataInternal>,
707
708    // Accessibility / focus
709    pub focused_element_id: u32, // 0 = no focus
710    /// True when focus was set via keyboard (Tab/arrow keys), false when via mouse click.
711    pub(crate) focus_from_keyboard: bool,
712    focusable_elements: Vec<FocusableEntry>,
713    pub(crate) accessibility_configs: FxHashMap<u32, crate::accessibility::AccessibilityConfig>,
714    pub(crate) accessibility_element_order: Vec<u32>,
715
716    // Text input
717    pub(crate) text_edit_states: FxHashMap<u32, crate::text_input::TextEditState>,
718    text_input_configs: Vec<crate::text_input::TextInputConfig>,
719    /// Set of element IDs that are text inputs this frame.
720    pub(crate) text_input_element_ids: Vec<u32>,
721    /// Pending click on a text input: (element_id, click_x_relative, click_y_relative, shift_held)
722    pub(crate) pending_text_click: Option<(u32, f32, f32, bool)>,
723    /// Text input scrollbar auto-hide counters (frames since last activity) by element id.
724    pub(crate) text_input_scrollbar_idle_frames: FxHashMap<u32, u32>,
725    /// Text input drag-scroll state (mobile-first: drag scrolls, doesn't select).
726    pub(crate) text_input_drag_active: bool,
727    pub(crate) text_input_drag_origin: crate::math::Vector2,
728    pub(crate) text_input_drag_scroll_origin: crate::math::Vector2,
729    pub(crate) text_input_drag_element_id: u32,
730    pub(crate) text_input_drag_from_touch: bool,
731    pub(crate) text_input_scrollbar_drag_active: bool,
732    pub(crate) text_input_scrollbar_drag_vertical: bool,
733    pub(crate) text_input_scrollbar_drag_origin: f32,
734    pub(crate) text_input_scrollbar_drag_scroll_origin: f32,
735    /// Current absolute time in seconds (set by lib.rs each frame).
736    pub(crate) current_time: f64,
737    /// Delta time for the current frame in seconds (set by lib.rs each frame).
738    pub(crate) frame_delta_time: f32,
739
740    // Visited flags for DFS
741    tree_node_visited: Vec<bool>,
742
743    // Dynamic string data (for int-to-string etc.)
744    dynamic_string_data: Vec<u8>,
745
746    // Font height cache: (font_key, font_size) -> height in pixels.
747    // Avoids repeated calls to measure_fn("Mg", ...) which are expensive.
748    font_height_cache: FxHashMap<(&'static str, u16), f32>,
749
750    // The key of the default font (set by Ply::new, used in debug view)
751    pub(crate) default_font_key: &'static str,
752
753    // Debug view: heap-allocated strings that survive the frame
754}
755
756fn hash_data_scalar(data: &[u8]) -> u64 {
757    let mut hash: u64 = 0;
758    for &b in data {
759        hash = hash.wrapping_add(b as u64);
760        hash = hash.wrapping_add(hash << 10);
761        hash ^= hash >> 6;
762    }
763    hash
764}
765
766pub fn hash_string(key: &str, seed: u32) -> Id {
767    let mut hash: u32 = seed;
768    for b in key.bytes() {
769        hash = hash.wrapping_add(b as u32);
770        hash = hash.wrapping_add(hash << 10);
771        hash ^= hash >> 6;
772    }
773    hash = hash.wrapping_add(hash << 3);
774    hash ^= hash >> 11;
775    hash = hash.wrapping_add(hash << 15);
776    Id {
777        id: hash.wrapping_add(1),
778        offset: 0,
779        base_id: hash.wrapping_add(1),
780        string_id: StringId::from_str(key),
781    }
782}
783
784pub fn hash_string_with_offset(key: &str, offset: u32, seed: u32) -> Id {
785    let mut base: u32 = seed;
786    for b in key.bytes() {
787        base = base.wrapping_add(b as u32);
788        base = base.wrapping_add(base << 10);
789        base ^= base >> 6;
790    }
791    let mut hash = base;
792    hash = hash.wrapping_add(offset);
793    hash = hash.wrapping_add(hash << 10);
794    hash ^= hash >> 6;
795
796    hash = hash.wrapping_add(hash << 3);
797    base = base.wrapping_add(base << 3);
798    hash ^= hash >> 11;
799    base ^= base >> 11;
800    hash = hash.wrapping_add(hash << 15);
801    base = base.wrapping_add(base << 15);
802    Id {
803        id: hash.wrapping_add(1),
804        offset,
805        base_id: base.wrapping_add(1),
806        string_id: StringId::from_str(key),
807    }
808}
809
810fn hash_number(offset: u32, seed: u32) -> Id {
811    let mut hash = seed;
812    hash = hash.wrapping_add(offset.wrapping_add(48));
813    hash = hash.wrapping_add(hash << 10);
814    hash ^= hash >> 6;
815    hash = hash.wrapping_add(hash << 3);
816    hash ^= hash >> 11;
817    hash = hash.wrapping_add(hash << 15);
818    Id {
819        id: hash.wrapping_add(1),
820        offset,
821        base_id: seed,
822        string_id: StringId::empty(),
823    }
824}
825
826fn hash_string_contents_with_config(
827    text: &str,
828    config: &TextConfig,
829) -> u32 {
830    let mut hash: u32 = (hash_data_scalar(text.as_bytes()) % u32::MAX as u64) as u32;
831    // Fold in font key bytes
832    for &b in config.font_asset.map(|a| a.key()).unwrap_or("").as_bytes() {
833        hash = hash.wrapping_add(b as u32);
834        hash = hash.wrapping_add(hash << 10);
835        hash ^= hash >> 6;
836    }
837    hash = hash.wrapping_add(config.font_size as u32);
838    hash = hash.wrapping_add(hash << 10);
839    hash ^= hash >> 6;
840    hash = hash.wrapping_add(config.letter_spacing as u32);
841    hash = hash.wrapping_add(hash << 10);
842    hash ^= hash >> 6;
843    hash = hash.wrapping_add(hash << 3);
844    hash ^= hash >> 11;
845    hash = hash.wrapping_add(hash << 15);
846    hash.wrapping_add(1)
847}
848
849fn float_equal(left: f32, right: f32) -> bool {
850    let diff = left - right;
851    diff < EPSILON && diff > -EPSILON
852}
853
854fn point_is_inside_rect(point: Vector2, rect: BoundingBox) -> bool {
855    point.x >= rect.x
856        && point.x <= rect.x + rect.width
857        && point.y >= rect.y
858        && point.y <= rect.y + rect.height
859}
860
861#[derive(Debug, Clone, Copy)]
862struct ScrollbarAxisGeometry {
863    track_bbox: BoundingBox,
864    thumb_bbox: BoundingBox,
865    max_scroll: f32,
866    thumb_travel: f32,
867}
868
869#[derive(Debug, Clone, Copy, Default)]
870struct WrappedLayoutLine {
871    start_child_offset: usize,
872    end_child_offset: usize,
873    main_size: f32,
874    cross_size: f32,
875}
876
877fn scrollbar_visibility_alpha(config: ScrollbarConfig, idle_frames: u32) -> f32 {
878    match config.hide_after_frames {
879        None => 1.0,
880        Some(hide) => {
881            if hide == 0 {
882                return 0.0;
883            }
884            if idle_frames <= hide {
885                return 1.0;
886            }
887            let fade_frames = ((hide as f32) * 0.25).ceil().max(1.0) as u32;
888            let fade_progress = (idle_frames - hide) as f32 / fade_frames as f32;
889            (1.0 - fade_progress).clamp(0.0, 1.0)
890        }
891    }
892}
893
894fn apply_alpha(color: Color, alpha_mul: f32) -> Color {
895    Color::rgba(
896        color.r,
897        color.g,
898        color.b,
899        (color.a * alpha_mul).clamp(0.0, 255.0),
900    )
901}
902
903fn compute_vertical_scrollbar_geometry(
904    bbox: BoundingBox,
905    content_height: f32,
906    scroll_position_positive: f32,
907    config: ScrollbarConfig,
908) -> Option<ScrollbarAxisGeometry> {
909    let viewport = bbox.height;
910    let max_scroll = (content_height - viewport).max(0.0);
911    if viewport <= 0.0 || max_scroll <= 0.0 {
912        return None;
913    }
914
915    let thickness = config.width.max(1.0);
916    let track_len = viewport;
917    let thumb_len = (track_len * (viewport / content_height.max(viewport)))
918        .max(config.min_thumb_size.max(1.0))
919        .min(track_len);
920    let thumb_travel = (track_len - thumb_len).max(0.0);
921    let thumb_offset = if thumb_travel <= 0.0 {
922        0.0
923    } else {
924        (scroll_position_positive.clamp(0.0, max_scroll) / max_scroll) * thumb_travel
925    };
926
927    Some(ScrollbarAxisGeometry {
928        track_bbox: BoundingBox::new(
929            bbox.x + bbox.width - thickness,
930            bbox.y,
931            thickness,
932            track_len,
933        ),
934        thumb_bbox: BoundingBox::new(
935            bbox.x + bbox.width - thickness,
936            bbox.y + thumb_offset,
937            thickness,
938            thumb_len,
939        ),
940        max_scroll,
941        thumb_travel,
942    })
943}
944
945fn compute_horizontal_scrollbar_geometry(
946    bbox: BoundingBox,
947    content_width: f32,
948    scroll_position_positive: f32,
949    config: ScrollbarConfig,
950) -> Option<ScrollbarAxisGeometry> {
951    let viewport = bbox.width;
952    let max_scroll = (content_width - viewport).max(0.0);
953    if viewport <= 0.0 || max_scroll <= 0.0 {
954        return None;
955    }
956
957    let thickness = config.width.max(1.0);
958    let track_len = viewport;
959    let thumb_len = (track_len * (viewport / content_width.max(viewport)))
960        .max(config.min_thumb_size.max(1.0))
961        .min(track_len);
962    let thumb_travel = (track_len - thumb_len).max(0.0);
963    let thumb_offset = if thumb_travel <= 0.0 {
964        0.0
965    } else {
966        (scroll_position_positive.clamp(0.0, max_scroll) / max_scroll) * thumb_travel
967    };
968
969    Some(ScrollbarAxisGeometry {
970        track_bbox: BoundingBox::new(
971            bbox.x,
972            bbox.y + bbox.height - thickness,
973            track_len,
974            thickness,
975        ),
976        thumb_bbox: BoundingBox::new(
977            bbox.x + thumb_offset,
978            bbox.y + bbox.height - thickness,
979            thumb_len,
980            thickness,
981        ),
982        max_scroll,
983        thumb_travel,
984    })
985}
986
987impl<CustomElementData: Clone + Default + std::fmt::Debug> PlyContext<CustomElementData> {
988    pub fn new(dimensions: Dimensions) -> Self {
989        let max_element_count = DEFAULT_MAX_ELEMENT_COUNT;
990        let max_measure_text_cache_word_count = DEFAULT_MAX_MEASURE_TEXT_WORD_CACHE_COUNT;
991
992        let ctx = Self {
993            max_element_count,
994            max_measure_text_cache_word_count,
995            debug_mode_enabled: false,
996            debug_view_width: Self::DEBUG_VIEW_DEFAULT_WIDTH,
997            culling_disabled: false,
998            external_scroll_handling_enabled: false,
999            debug_selected_element_id: 0,
1000            generation: 0,
1001            boolean_warnings: BooleanWarnings::default(),
1002            pointer_info: PointerData::default(),
1003            layout_dimensions: dimensions,
1004            dynamic_element_index: 0,
1005            measure_text_fn: None,
1006            layout_elements: Vec::new(),
1007            render_commands: Vec::new(),
1008            open_layout_element_stack: Vec::new(),
1009            layout_element_children: Vec::new(),
1010            layout_element_children_buffer: Vec::new(),
1011            text_element_data: Vec::new(),
1012            aspect_ratio_element_indexes: Vec::new(),
1013            reusable_element_index_buffer: Vec::new(),
1014            layout_element_clip_element_ids: Vec::new(),
1015            layout_configs: Vec::new(),
1016            element_configs: Vec::new(),
1017            text_element_configs: Vec::new(),
1018            aspect_ratio_configs: Vec::new(),
1019            aspect_ratio_cover_configs: Vec::new(),
1020            image_element_configs: Vec::new(),
1021            floating_element_configs: Vec::new(),
1022            clip_element_configs: Vec::new(),
1023            custom_element_configs: Vec::new(),
1024            border_element_configs: Vec::new(),
1025            shared_element_configs: Vec::new(),
1026            element_effects: Vec::new(),
1027            element_shaders: Vec::new(),
1028            element_visual_rotations: Vec::new(),
1029            element_shape_rotations: Vec::new(),
1030            element_pre_rotation_dimensions: Vec::new(),
1031            layout_element_id_strings: Vec::new(),
1032            wrapped_text_lines: Vec::new(),
1033            tree_node_array: Vec::new(),
1034            layout_element_tree_roots: Vec::new(),
1035            layout_element_map: FxHashMap::default(),
1036            measure_text_cache: FxHashMap::default(),
1037            measured_words: Vec::new(),
1038            measured_words_free_list: Vec::new(),
1039            open_clip_element_stack: Vec::new(),
1040            pointer_over_ids: Vec::new(),
1041            pressed_element_ids: Vec::new(),
1042            released_this_frame_ids: Vec::new(),
1043            released_this_frame_generation: 0,
1044            keyboard_press_this_frame_generation: 0,
1045            scroll_container_datas: Vec::new(),
1046            focused_element_id: 0,
1047            focus_from_keyboard: false,
1048            focusable_elements: Vec::new(),
1049            accessibility_configs: FxHashMap::default(),
1050            accessibility_element_order: Vec::new(),
1051            text_edit_states: FxHashMap::default(),
1052            text_input_configs: Vec::new(),
1053            text_input_element_ids: Vec::new(),
1054            pending_text_click: None,
1055            text_input_scrollbar_idle_frames: FxHashMap::default(),
1056            text_input_drag_active: false,
1057            text_input_drag_origin: Vector2::default(),
1058            text_input_drag_scroll_origin: Vector2::default(),
1059            text_input_drag_element_id: 0,
1060            text_input_drag_from_touch: false,
1061            text_input_scrollbar_drag_active: false,
1062            text_input_scrollbar_drag_vertical: false,
1063            text_input_scrollbar_drag_origin: 0.0,
1064            text_input_scrollbar_drag_scroll_origin: 0.0,
1065            current_time: 0.0,
1066            frame_delta_time: 0.0,
1067            tree_node_visited: Vec::new(),
1068            dynamic_string_data: Vec::new(),
1069            font_height_cache: FxHashMap::default(),
1070            default_font_key: "",
1071        };
1072        ctx
1073    }
1074
1075    fn get_open_layout_element(&self) -> usize {
1076        let idx = *self.open_layout_element_stack.last().unwrap();
1077        idx as usize
1078    }
1079
1080    /// Returns the internal u32 id of the currently open element.
1081    pub fn get_open_element_id(&self) -> u32 {
1082        let open_idx = self.get_open_layout_element();
1083        self.layout_elements[open_idx].id
1084    }
1085
1086    pub fn get_parent_element_id(&self) -> u32 {
1087        let stack_len = self.open_layout_element_stack.len();
1088        let parent_idx = self.open_layout_element_stack[stack_len - 2] as usize;
1089        self.layout_elements[parent_idx].id
1090    }
1091
1092    fn add_hash_map_item(
1093        &mut self,
1094        element_id: &Id,
1095        layout_element_index: i32,
1096    ) {
1097        let gen = self.generation;
1098        match self.layout_element_map.entry(element_id.id) {
1099            std::collections::hash_map::Entry::Occupied(mut entry) => {
1100                let item = entry.get_mut();
1101                if item.generation <= gen {
1102                    item.element_id = element_id.clone();
1103                    item.generation = gen + 1;
1104                    item.layout_element_index = layout_element_index;
1105                    item.collision = false;
1106                    item.on_hover_fn = None;
1107                    item.on_press_fn = None;
1108                    item.on_release_fn = None;
1109                    item.on_focus_fn = None;
1110                    item.on_unfocus_fn = None;
1111                    item.on_text_changed_fn = None;
1112                    item.on_text_submit_fn = None;
1113                    item.is_text_input = false;
1114                    item.preserve_focus = false;
1115                } else {
1116                    // Duplicate ID
1117                    item.collision = true;
1118                }
1119            }
1120            std::collections::hash_map::Entry::Vacant(entry) => {
1121                entry.insert(LayoutElementHashMapItem {
1122                    element_id: element_id.clone(),
1123                    layout_element_index,
1124                    generation: gen + 1,
1125                    bounding_box: BoundingBox::default(),
1126                    on_hover_fn: None,
1127                    on_press_fn: None,
1128                    on_release_fn: None,
1129                    on_focus_fn: None,
1130                    on_unfocus_fn: None,
1131                    on_text_changed_fn: None,
1132                    on_text_submit_fn: None,
1133                    is_text_input: false,
1134                    preserve_focus: false,
1135                    collision: false,
1136                    collapsed: false,
1137                });
1138            }
1139        }
1140    }
1141
1142    fn generate_id_for_anonymous_element(&mut self, open_element_index: usize) -> Id {
1143        let stack_len = self.open_layout_element_stack.len();
1144        let parent_idx = self.open_layout_element_stack[stack_len - 2] as usize;
1145        let parent = &self.layout_elements[parent_idx];
1146        let offset =
1147            parent.children_length as u32 + parent.floating_children_count as u32;
1148        let parent_id = parent.id;
1149        let element_id = hash_number(offset, parent_id);
1150        self.layout_elements[open_element_index].id = element_id.id;
1151        self.add_hash_map_item(&element_id, open_element_index as i32);
1152        if self.debug_mode_enabled {
1153            self.layout_element_id_strings.push(element_id.string_id.clone());
1154        }
1155        element_id
1156    }
1157
1158    fn element_has_config(
1159        &self,
1160        element_index: usize,
1161        config_type: ElementConfigType,
1162    ) -> bool {
1163        let element = &self.layout_elements[element_index];
1164        let start = element.element_configs.start;
1165        let length = element.element_configs.length;
1166        for i in 0..length {
1167            let config = &self.element_configs[start + i as usize];
1168            if config.config_type == config_type {
1169                return true;
1170            }
1171        }
1172        false
1173    }
1174
1175    fn find_element_config_index(
1176        &self,
1177        element_index: usize,
1178        config_type: ElementConfigType,
1179    ) -> Option<usize> {
1180        let element = &self.layout_elements[element_index];
1181        let start = element.element_configs.start;
1182        let length = element.element_configs.length;
1183        for i in 0..length {
1184            let config = &self.element_configs[start + i as usize];
1185            if config.config_type == config_type {
1186                return Some(config.config_index);
1187            }
1188        }
1189        None
1190    }
1191
1192    fn update_aspect_ratio_box(&mut self, element_index: usize) {
1193        if let Some(config_idx) =
1194            self.find_element_config_index(element_index, ElementConfigType::Aspect)
1195        {
1196            let aspect_ratio = self.aspect_ratio_configs[config_idx];
1197            if aspect_ratio == 0.0 {
1198                return;
1199            }
1200            let elem = &mut self.layout_elements[element_index];
1201            if elem.dimensions.width == 0.0 && elem.dimensions.height != 0.0 {
1202                elem.dimensions.width = elem.dimensions.height * aspect_ratio;
1203            } else if elem.dimensions.width != 0.0 && elem.dimensions.height == 0.0 {
1204                elem.dimensions.height = elem.dimensions.width * (1.0 / aspect_ratio);
1205            }
1206        }
1207    }
1208
1209    pub fn store_text_element_config(
1210        &mut self,
1211        config: TextConfig,
1212    ) -> usize {
1213        self.text_element_configs.push(config);
1214        self.text_element_configs.len() - 1
1215    }
1216
1217    fn store_layout_config(&mut self, config: LayoutConfig) -> usize {
1218        self.layout_configs.push(config);
1219        self.layout_configs.len() - 1
1220    }
1221
1222    fn store_shared_config(&mut self, config: SharedElementConfig) -> usize {
1223        self.shared_element_configs.push(config);
1224        self.shared_element_configs.len() - 1
1225    }
1226
1227    fn attach_element_config(&mut self, config_type: ElementConfigType, config_index: usize) {
1228        if self.boolean_warnings.max_elements_exceeded {
1229            return;
1230        }
1231        let open_idx = self.get_open_layout_element();
1232        self.layout_elements[open_idx].element_configs.length += 1;
1233        self.element_configs.push(ElementConfig {
1234            config_type,
1235            config_index,
1236        });
1237    }
1238
1239    pub fn open_element(&mut self) {
1240        if self.boolean_warnings.max_elements_exceeded {
1241            return;
1242        }
1243        let elem = LayoutElement {
1244            text_data_index: -1,
1245            ..Default::default()
1246        };
1247        self.layout_elements.push(elem);
1248        let idx = (self.layout_elements.len() - 1) as i32;
1249        self.open_layout_element_stack.push(idx);
1250
1251        // Ensure clip IDs array is large enough
1252        while self.layout_element_clip_element_ids.len() < self.layout_elements.len() {
1253            self.layout_element_clip_element_ids.push(0);
1254        }
1255
1256        self.generate_id_for_anonymous_element(idx as usize);
1257
1258        if !self.open_clip_element_stack.is_empty() {
1259            let clip_id = *self.open_clip_element_stack.last().unwrap();
1260            self.layout_element_clip_element_ids[idx as usize] = clip_id;
1261        } else {
1262            self.layout_element_clip_element_ids[idx as usize] = 0;
1263        }
1264    }
1265
1266    pub fn open_element_with_id(&mut self, element_id: &Id) {
1267        if self.boolean_warnings.max_elements_exceeded {
1268            return;
1269        }
1270        let mut elem = LayoutElement {
1271            text_data_index: -1,
1272            ..Default::default()
1273        };
1274        elem.id = element_id.id;
1275        self.layout_elements.push(elem);
1276        let idx = (self.layout_elements.len() - 1) as i32;
1277        self.open_layout_element_stack.push(idx);
1278
1279        while self.layout_element_clip_element_ids.len() < self.layout_elements.len() {
1280            self.layout_element_clip_element_ids.push(0);
1281        }
1282
1283        self.add_hash_map_item(element_id, idx);
1284        if self.debug_mode_enabled {
1285            self.layout_element_id_strings.push(element_id.string_id.clone());
1286        }
1287
1288        if !self.open_clip_element_stack.is_empty() {
1289            let clip_id = *self.open_clip_element_stack.last().unwrap();
1290            self.layout_element_clip_element_ids[idx as usize] = clip_id;
1291        } else {
1292            self.layout_element_clip_element_ids[idx as usize] = 0;
1293        }
1294    }
1295
1296    pub fn configure_open_element(&mut self, declaration: &ElementDeclaration<CustomElementData>) {
1297        if self.boolean_warnings.max_elements_exceeded {
1298            return;
1299        }
1300        let open_idx = self.get_open_layout_element();
1301        let layout_config_index = self.store_layout_config(declaration.layout);
1302        self.layout_elements[open_idx].layout_config_index = layout_config_index;
1303
1304        // Record the start of element configs for this element
1305        self.layout_elements[open_idx].element_configs.start = self.element_configs.len();
1306
1307        // Shared config (background color, corner radius, user data)
1308        let mut shared_config_index: Option<usize> = None;
1309        if declaration.background_color.a > 0.0 {
1310            let idx = self.store_shared_config(SharedElementConfig {
1311                background_color: declaration.background_color,
1312                corner_radius: CornerRadius::default(),
1313                user_data: 0,
1314            });
1315            shared_config_index = Some(idx);
1316            self.attach_element_config(ElementConfigType::Shared, idx);
1317        }
1318        if !declaration.corner_radius.is_zero() {
1319            if let Some(idx) = shared_config_index {
1320                self.shared_element_configs[idx].corner_radius = declaration.corner_radius;
1321            } else {
1322                let idx = self.store_shared_config(SharedElementConfig {
1323                    background_color: Color::rgba(0.0, 0.0, 0.0, 0.0),
1324                    corner_radius: declaration.corner_radius,
1325                    user_data: 0,
1326                });
1327                shared_config_index = Some(idx);
1328                self.attach_element_config(ElementConfigType::Shared, idx);
1329            }
1330        }
1331        if declaration.user_data != 0 {
1332            if let Some(idx) = shared_config_index {
1333                self.shared_element_configs[idx].user_data = declaration.user_data;
1334            } else {
1335                let idx = self.store_shared_config(SharedElementConfig {
1336                    background_color: Color::rgba(0.0, 0.0, 0.0, 0.0),
1337                    corner_radius: CornerRadius::default(),
1338                    user_data: declaration.user_data,
1339                });
1340                self.attach_element_config(ElementConfigType::Shared, idx);
1341            }
1342        }
1343
1344        // Image config
1345        if let Some(image_data) = declaration.image_data.clone() {
1346            self.image_element_configs.push(image_data);
1347            let idx = self.image_element_configs.len() - 1;
1348            self.attach_element_config(ElementConfigType::Image, idx);
1349        }
1350
1351        // Aspect ratio config
1352        if declaration.aspect_ratio > 0.0 {
1353            self.aspect_ratio_configs.push(declaration.aspect_ratio);
1354            self.aspect_ratio_cover_configs
1355                .push(declaration.cover_aspect_ratio);
1356            let idx = self.aspect_ratio_configs.len() - 1;
1357            self.attach_element_config(ElementConfigType::Aspect, idx);
1358            self.aspect_ratio_element_indexes
1359                .push((self.layout_elements.len() - 1) as i32);
1360        }
1361
1362        // Floating config
1363        if declaration.floating.attach_to != FloatingAttachToElement::None {
1364            let mut floating_config = declaration.floating;
1365            let stack_len = self.open_layout_element_stack.len();
1366
1367            if stack_len >= 2 {
1368                let hierarchical_parent_idx =
1369                    self.open_layout_element_stack[stack_len - 2] as usize;
1370                let hierarchical_parent_id = self.layout_elements[hierarchical_parent_idx].id;
1371
1372                let mut clip_element_id: u32 = 0;
1373
1374                if declaration.floating.attach_to == FloatingAttachToElement::Parent {
1375                    floating_config.parent_id = hierarchical_parent_id;
1376                    if !self.open_clip_element_stack.is_empty() {
1377                        clip_element_id =
1378                            *self.open_clip_element_stack.last().unwrap() as u32;
1379                    }
1380                } else if declaration.floating.attach_to
1381                    == FloatingAttachToElement::ElementWithId
1382                {
1383                    if let Some(parent_item) =
1384                        self.layout_element_map.get(&floating_config.parent_id)
1385                    {
1386                        let parent_elem_idx = parent_item.layout_element_index as usize;
1387                        clip_element_id =
1388                            self.layout_element_clip_element_ids[parent_elem_idx] as u32;
1389                    }
1390                } else if declaration.floating.attach_to
1391                    == FloatingAttachToElement::Root
1392                {
1393                    floating_config.parent_id =
1394                        hash_string("Ply__RootContainer", 0).id;
1395                }
1396
1397                if declaration.floating.clip_to == FloatingClipToElement::None {
1398                    clip_element_id = 0;
1399                }
1400
1401                let current_element_index =
1402                    *self.open_layout_element_stack.last().unwrap();
1403                self.layout_element_clip_element_ids[current_element_index as usize] =
1404                    clip_element_id as i32;
1405                self.open_clip_element_stack.push(clip_element_id as i32);
1406
1407                self.layout_element_tree_roots
1408                    .push(LayoutElementTreeRoot {
1409                        layout_element_index: current_element_index,
1410                        parent_id: floating_config.parent_id,
1411                        clip_element_id,
1412                        z_index: floating_config.z_index,
1413                        pointer_offset: Vector2::default(),
1414                    });
1415
1416                self.floating_element_configs.push(floating_config);
1417                let idx = self.floating_element_configs.len() - 1;
1418                self.attach_element_config(ElementConfigType::Floating, idx);
1419            }
1420        }
1421
1422        // Custom config
1423        if let Some(ref custom_data) = declaration.custom_data {
1424            self.custom_element_configs.push(custom_data.clone());
1425            let idx = self.custom_element_configs.len() - 1;
1426            self.attach_element_config(ElementConfigType::Custom, idx);
1427        }
1428
1429        // Clip config
1430        if declaration.clip.horizontal || declaration.clip.vertical {
1431            let mut clip = declaration.clip;
1432
1433            let elem_id = self.layout_elements[open_idx].id;
1434
1435            // Auto-apply stored scroll position as child_offset
1436            if clip.scroll_x || clip.scroll_y {
1437                for scd in &self.scroll_container_datas {
1438                    if scd.element_id == elem_id {
1439                        clip.child_offset = scd.scroll_position;
1440                        break;
1441                    }
1442                }
1443            }
1444
1445            self.clip_element_configs.push(clip);
1446            let idx = self.clip_element_configs.len() - 1;
1447            self.attach_element_config(ElementConfigType::Clip, idx);
1448
1449            self.open_clip_element_stack.push(elem_id as i32);
1450
1451            // Track scroll container
1452            if clip.scroll_x || clip.scroll_y {
1453                let mut found_existing = false;
1454                for scd in &mut self.scroll_container_datas {
1455                    if elem_id == scd.element_id {
1456                        scd.layout_element_index = open_idx as i32;
1457                        scd.open_this_frame = true;
1458                        scd.scrollbar = clip.scrollbar;
1459                        scd.scroll_x_enabled = clip.scroll_x;
1460                        scd.scroll_y_enabled = clip.scroll_y;
1461                        scd.no_drag_scroll = clip.no_drag_scroll;
1462                        found_existing = true;
1463                        break;
1464                    }
1465                }
1466                if !found_existing {
1467                    self.scroll_container_datas.push(ScrollContainerDataInternal {
1468                        layout_element_index: open_idx as i32,
1469                        scroll_origin: Vector2::new(-1.0, -1.0),
1470                        scrollbar: clip.scrollbar,
1471                        scroll_x_enabled: clip.scroll_x,
1472                        scroll_y_enabled: clip.scroll_y,
1473                        no_drag_scroll: clip.no_drag_scroll,
1474                        element_id: elem_id,
1475                        open_this_frame: true,
1476                        ..Default::default()
1477                    });
1478                }
1479            }
1480        }
1481
1482        // Border config
1483        if !declaration.border.width.is_zero() {
1484            self.border_element_configs.push(declaration.border);
1485            let idx = self.border_element_configs.len() - 1;
1486            self.attach_element_config(ElementConfigType::Border, idx);
1487        }
1488
1489        // Store per-element shader effects
1490        // Ensure element_effects is large enough for open_idx
1491        while self.element_effects.len() <= open_idx {
1492            self.element_effects.push(Vec::new());
1493        }
1494        self.element_effects[open_idx] = declaration.effects.clone();
1495
1496        // Store per-element group shaders
1497        while self.element_shaders.len() <= open_idx {
1498            self.element_shaders.push(Vec::new());
1499        }
1500        self.element_shaders[open_idx] = declaration.shaders.clone();
1501
1502        // Store per-element visual rotation
1503        while self.element_visual_rotations.len() <= open_idx {
1504            self.element_visual_rotations.push(None);
1505        }
1506        self.element_visual_rotations[open_idx] = declaration.visual_rotation;
1507
1508        // Store per-element shape rotation
1509        while self.element_shape_rotations.len() <= open_idx {
1510            self.element_shape_rotations.push(None);
1511        }
1512        self.element_shape_rotations[open_idx] = declaration.shape_rotation;
1513
1514        // Accessibility config
1515        if let Some(ref a11y) = declaration.accessibility {
1516            let elem_id = self.layout_elements[open_idx].id;
1517            if a11y.focusable {
1518                self.focusable_elements.push(FocusableEntry {
1519                    element_id: elem_id,
1520                    tab_index: a11y.tab_index,
1521                    insertion_order: self.focusable_elements.len() as u32,
1522                });
1523            }
1524            self.accessibility_configs.insert(elem_id, a11y.clone());
1525            self.accessibility_element_order.push(elem_id);
1526        }
1527
1528        // Text input config
1529        if let Some(ref ti_config) = declaration.text_input {
1530            let elem_id = self.layout_elements[open_idx].id;
1531            self.text_input_configs.push(ti_config.clone());
1532            let idx = self.text_input_configs.len() - 1;
1533            self.attach_element_config(ElementConfigType::TextInput, idx);
1534            self.text_input_element_ids.push(elem_id);
1535
1536            // Mark the element as a text input in the layout map
1537            if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
1538                item.is_text_input = true;
1539            }
1540
1541            // Ensure a TextEditState exists for this element
1542            self.text_edit_states.entry(elem_id)
1543                .or_insert_with(crate::text_input::TextEditState::default);
1544
1545            self.text_input_scrollbar_idle_frames
1546                .entry(elem_id)
1547                .or_insert(0);
1548
1549            // Sync config flags to persistent state
1550            if let Some(state) = self.text_edit_states.get_mut(&elem_id) {
1551                state.no_styles_movement = ti_config.no_styles_movement;
1552            }
1553
1554            // Process any pending click on this text input
1555            if let Some((click_elem, click_x, click_y, click_shift)) = self.pending_text_click.take() {
1556                if click_elem == elem_id {
1557                    if let Some(ref measure_fn) = self.measure_text_fn {
1558                        let state = self.text_edit_states.get(&elem_id).cloned()
1559                            .unwrap_or_default();
1560                        let disp_text = crate::text_input::display_text(
1561                            &state.text,
1562                            &ti_config.placeholder,
1563                            ti_config.is_password,
1564                        );
1565                        // Only position cursor in actual text, not placeholder
1566                        if !state.text.is_empty() {
1567                            // Double-click detection
1568                            let is_double_click = state.last_click_element == elem_id
1569                                && (self.current_time - state.last_click_time) < 0.4;
1570
1571                            if ti_config.is_multiline {
1572                                // Multiline: determine which visual line was clicked
1573                                let elem_width = self.layout_element_map.get(&elem_id)
1574                                    .map(|item| item.bounding_box.width)
1575                                    .unwrap_or(200.0);
1576                                let visual_lines = crate::text_input::wrap_lines(
1577                                    &disp_text,
1578                                    elem_width,
1579                                    ti_config.font_asset,
1580                                    ti_config.font_size,
1581                                    measure_fn.as_ref(),
1582                                );
1583                                let font_height = if ti_config.line_height > 0 {
1584                                    ti_config.line_height as f32
1585                                } else {
1586                                    let config = crate::text::TextConfig {
1587                                        font_asset: ti_config.font_asset,
1588                                        font_size: ti_config.font_size,
1589                                        ..Default::default()
1590                                    };
1591                                    measure_fn(&"Mg", &config).height
1592                                };
1593                                let adjusted_y = click_y + state.scroll_offset_y;
1594                                let clicked_line = (adjusted_y / font_height).floor().max(0.0) as usize;
1595                                let clicked_line = clicked_line.min(visual_lines.len().saturating_sub(1));
1596
1597                                let vl = &visual_lines[clicked_line];
1598                                let line_char_x_positions = crate::text_input::compute_char_x_positions(
1599                                    &vl.text,
1600                                    ti_config.font_asset,
1601                                    ti_config.font_size,
1602                                    measure_fn.as_ref(),
1603                                );
1604                                let col = crate::text_input::find_nearest_char_boundary(
1605                                    click_x, &line_char_x_positions,
1606                                );
1607                                let global_pos = vl.global_char_start + col;
1608
1609                                if let Some(state) = self.text_edit_states.get_mut(&elem_id) {
1610                                    #[cfg(feature = "text-styling")]
1611                                    {
1612                                        let visual_pos = crate::text_input::styling::raw_to_cursor(&state.text, global_pos);
1613                                        if is_double_click {
1614                                            state.select_word_at_styled(visual_pos);
1615                                        } else {
1616                                            state.click_to_cursor_styled(visual_pos, click_shift);
1617                                        }
1618                                    }
1619                                    #[cfg(not(feature = "text-styling"))]
1620                                    {
1621                                        if is_double_click {
1622                                            state.select_word_at(global_pos);
1623                                        } else {
1624                                            if click_shift {
1625                                                if state.selection_anchor.is_none() {
1626                                                    state.selection_anchor = Some(state.cursor_pos);
1627                                                }
1628                                            } else {
1629                                                state.selection_anchor = None;
1630                                            }
1631                                            state.cursor_pos = global_pos;
1632                                            state.reset_blink();
1633                                        }
1634                                    }
1635                                    state.last_click_time = self.current_time;
1636                                    state.last_click_element = elem_id;
1637                                }
1638                            } else {
1639                                // Single-line: existing behavior
1640                                let char_x_positions = crate::text_input::compute_char_x_positions(
1641                                    &disp_text,
1642                                    ti_config.font_asset,
1643                                    ti_config.font_size,
1644                                    measure_fn.as_ref(),
1645                                );
1646                                let adjusted_x = click_x + state.scroll_offset;
1647
1648                                if let Some(state) = self.text_edit_states.get_mut(&elem_id) {
1649                                    let raw_click_pos = crate::text_input::find_nearest_char_boundary(
1650                                        adjusted_x, &char_x_positions,
1651                                    );
1652                                    #[cfg(feature = "text-styling")]
1653                                    {
1654                                        let visual_pos = crate::text_input::styling::raw_to_cursor(&state.text, raw_click_pos);
1655                                        if is_double_click {
1656                                            state.select_word_at_styled(visual_pos);
1657                                        } else {
1658                                            state.click_to_cursor_styled(visual_pos, click_shift);
1659                                        }
1660                                    }
1661                                    #[cfg(not(feature = "text-styling"))]
1662                                    {
1663                                        if is_double_click {
1664                                            state.select_word_at(raw_click_pos);
1665                                        } else {
1666                                            state.click_to_cursor(adjusted_x, &char_x_positions, click_shift);
1667                                        }
1668                                    }
1669                                    state.last_click_time = self.current_time;
1670                                    state.last_click_element = elem_id;
1671                                }
1672                            }
1673                        } else if let Some(state) = self.text_edit_states.get_mut(&elem_id) {
1674                            state.cursor_pos = 0;
1675                            state.selection_anchor = None;
1676                            state.last_click_time = self.current_time;
1677                            state.last_click_element = elem_id;
1678                            state.reset_blink();
1679                        }
1680                    }
1681                } else {
1682                    // Wasn't for this element, put it back
1683                    self.pending_text_click = Some((click_elem, click_x, click_y, click_shift));
1684                }
1685            }
1686
1687            // Auto-register as focusable if not already done via accessibility
1688            if declaration.accessibility.is_none() || !declaration.accessibility.as_ref().unwrap().focusable {
1689                // Check it's not already registered
1690                let already = self.focusable_elements.iter().any(|e| e.element_id == elem_id);
1691                if !already {
1692                    self.focusable_elements.push(FocusableEntry {
1693                        element_id: elem_id,
1694                        tab_index: None,
1695                        insertion_order: self.focusable_elements.len() as u32,
1696                    });
1697                }
1698            }
1699        }
1700
1701        // Preserve-focus flag
1702        if declaration.preserve_focus {
1703            let elem_id = self.layout_elements[open_idx].id;
1704            if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
1705                item.preserve_focus = true;
1706            }
1707        }
1708    }
1709
1710    pub fn close_element(&mut self) {
1711        if self.boolean_warnings.max_elements_exceeded {
1712            return;
1713        }
1714
1715        let open_idx = self.get_open_layout_element();
1716        let layout_config_index = self.layout_elements[open_idx].layout_config_index;
1717        let layout_config = self.layout_configs[layout_config_index];
1718
1719        // Check for clip and floating configs
1720        let mut element_has_clip_horizontal = false;
1721        let mut element_has_clip_vertical = false;
1722        let element_configs_start = self.layout_elements[open_idx].element_configs.start;
1723        let element_configs_length = self.layout_elements[open_idx].element_configs.length;
1724
1725        for i in 0..element_configs_length {
1726            let config = &self.element_configs[element_configs_start + i as usize];
1727            if config.config_type == ElementConfigType::Clip {
1728                let clip = &self.clip_element_configs[config.config_index];
1729                element_has_clip_horizontal = clip.horizontal;
1730                element_has_clip_vertical = clip.vertical;
1731                self.open_clip_element_stack.pop();
1732                break;
1733            } else if config.config_type == ElementConfigType::Floating {
1734                self.open_clip_element_stack.pop();
1735            }
1736        }
1737
1738        let left_right_padding =
1739            (layout_config.padding.left + layout_config.padding.right) as f32;
1740        let top_bottom_padding =
1741            (layout_config.padding.top + layout_config.padding.bottom) as f32;
1742
1743        let children_length = self.layout_elements[open_idx].children_length;
1744
1745        // Attach children to the current open element
1746        let children_start = self.layout_element_children.len();
1747        self.layout_elements[open_idx].children_start = children_start;
1748
1749        if layout_config.layout_direction == LayoutDirection::LeftToRight {
1750            self.layout_elements[open_idx].dimensions.width = left_right_padding;
1751            self.layout_elements[open_idx].min_dimensions.width = left_right_padding;
1752
1753            for i in 0..children_length {
1754                let buf_idx = self.layout_element_children_buffer.len()
1755                    - children_length as usize
1756                    + i as usize;
1757                let child_index = self.layout_element_children_buffer[buf_idx];
1758                let child = &self.layout_elements[child_index as usize];
1759                let child_width = child.dimensions.width;
1760                let child_height = child.dimensions.height;
1761                let child_min_width = child.min_dimensions.width;
1762                let child_min_height = child.min_dimensions.height;
1763
1764                self.layout_elements[open_idx].dimensions.width += child_width;
1765                let current_height = self.layout_elements[open_idx].dimensions.height;
1766                self.layout_elements[open_idx].dimensions.height =
1767                    f32::max(current_height, child_height + top_bottom_padding);
1768
1769                if !element_has_clip_horizontal {
1770                    self.layout_elements[open_idx].min_dimensions.width += child_min_width;
1771                }
1772                if !element_has_clip_vertical {
1773                    let current_min_h = self.layout_elements[open_idx].min_dimensions.height;
1774                    self.layout_elements[open_idx].min_dimensions.height =
1775                        f32::max(current_min_h, child_min_height + top_bottom_padding);
1776                }
1777                self.layout_element_children.push(child_index);
1778            }
1779            let child_gap =
1780                (children_length.saturating_sub(1) as u32 * layout_config.child_gap as u32) as f32;
1781            self.layout_elements[open_idx].dimensions.width += child_gap;
1782            if !element_has_clip_horizontal {
1783                self.layout_elements[open_idx].min_dimensions.width += child_gap;
1784            }
1785        } else {
1786            // TopToBottom
1787            self.layout_elements[open_idx].dimensions.height = top_bottom_padding;
1788            self.layout_elements[open_idx].min_dimensions.height = top_bottom_padding;
1789
1790            for i in 0..children_length {
1791                let buf_idx = self.layout_element_children_buffer.len()
1792                    - children_length as usize
1793                    + i as usize;
1794                let child_index = self.layout_element_children_buffer[buf_idx];
1795                let child = &self.layout_elements[child_index as usize];
1796                let child_width = child.dimensions.width;
1797                let child_height = child.dimensions.height;
1798                let child_min_width = child.min_dimensions.width;
1799                let child_min_height = child.min_dimensions.height;
1800
1801                self.layout_elements[open_idx].dimensions.height += child_height;
1802                let current_width = self.layout_elements[open_idx].dimensions.width;
1803                self.layout_elements[open_idx].dimensions.width =
1804                    f32::max(current_width, child_width + left_right_padding);
1805
1806                if !element_has_clip_vertical {
1807                    self.layout_elements[open_idx].min_dimensions.height += child_min_height;
1808                }
1809                if !element_has_clip_horizontal {
1810                    let current_min_w = self.layout_elements[open_idx].min_dimensions.width;
1811                    self.layout_elements[open_idx].min_dimensions.width =
1812                        f32::max(current_min_w, child_min_width + left_right_padding);
1813                }
1814                self.layout_element_children.push(child_index);
1815            }
1816            let child_gap =
1817                (children_length.saturating_sub(1) as u32 * layout_config.child_gap as u32) as f32;
1818            self.layout_elements[open_idx].dimensions.height += child_gap;
1819            if !element_has_clip_vertical {
1820                self.layout_elements[open_idx].min_dimensions.height += child_gap;
1821            }
1822        }
1823
1824        // Remove children from buffer
1825        let remove_count = children_length as usize;
1826        let new_len = self.layout_element_children_buffer.len().saturating_sub(remove_count);
1827        self.layout_element_children_buffer.truncate(new_len);
1828
1829        // Clamp width
1830        {
1831            let sizing_type = self.layout_configs[layout_config_index].sizing.width.type_;
1832            if sizing_type != SizingType::Percent {
1833                let mut max_w = self.layout_configs[layout_config_index].sizing.width.min_max.max;
1834                if max_w <= 0.0 {
1835                    max_w = MAXFLOAT;
1836                    self.layout_configs[layout_config_index].sizing.width.min_max.max = max_w;
1837                }
1838                let min_w = self.layout_configs[layout_config_index].sizing.width.min_max.min;
1839                self.layout_elements[open_idx].dimensions.width = f32::min(
1840                    f32::max(self.layout_elements[open_idx].dimensions.width, min_w),
1841                    max_w,
1842                );
1843                self.layout_elements[open_idx].min_dimensions.width = f32::min(
1844                    f32::max(self.layout_elements[open_idx].min_dimensions.width, min_w),
1845                    max_w,
1846                );
1847            } else {
1848                self.layout_elements[open_idx].dimensions.width = 0.0;
1849            }
1850        }
1851
1852        // Clamp height
1853        {
1854            let sizing_type = self.layout_configs[layout_config_index].sizing.height.type_;
1855            if sizing_type != SizingType::Percent {
1856                let mut max_h = self.layout_configs[layout_config_index].sizing.height.min_max.max;
1857                if max_h <= 0.0 {
1858                    max_h = MAXFLOAT;
1859                    self.layout_configs[layout_config_index].sizing.height.min_max.max = max_h;
1860                }
1861                let min_h = self.layout_configs[layout_config_index].sizing.height.min_max.min;
1862                self.layout_elements[open_idx].dimensions.height = f32::min(
1863                    f32::max(self.layout_elements[open_idx].dimensions.height, min_h),
1864                    max_h,
1865                );
1866                self.layout_elements[open_idx].min_dimensions.height = f32::min(
1867                    f32::max(self.layout_elements[open_idx].min_dimensions.height, min_h),
1868                    max_h,
1869                );
1870            } else {
1871                self.layout_elements[open_idx].dimensions.height = 0.0;
1872            }
1873        }
1874
1875        self.update_aspect_ratio_box(open_idx);
1876
1877        // Apply shape rotation AABB expansion
1878        if let Some(shape_rot) = self.element_shape_rotations.get(open_idx).copied().flatten() {
1879            if !shape_rot.is_noop() {
1880                let orig_w = self.layout_elements[open_idx].dimensions.width;
1881                let orig_h = self.layout_elements[open_idx].dimensions.height;
1882
1883                // Find corner radius for this element
1884                let cr = self
1885                    .find_element_config_index(open_idx, ElementConfigType::Shared)
1886                    .map(|idx| self.shared_element_configs[idx].corner_radius)
1887                    .unwrap_or_default();
1888
1889                let (eff_w, eff_h) = crate::math::compute_rotated_aabb(
1890                    orig_w,
1891                    orig_h,
1892                    &cr,
1893                    shape_rot.rotation_radians,
1894                );
1895
1896                // Store original dimensions for renderer
1897                while self.element_pre_rotation_dimensions.len() <= open_idx {
1898                    self.element_pre_rotation_dimensions.push(None);
1899                }
1900                self.element_pre_rotation_dimensions[open_idx] =
1901                    Some(Dimensions::new(orig_w, orig_h));
1902
1903                // Replace layout dimensions with AABB
1904                self.layout_elements[open_idx].dimensions.width = eff_w;
1905                self.layout_elements[open_idx].dimensions.height = eff_h;
1906                self.layout_elements[open_idx].min_dimensions.width = eff_w;
1907                self.layout_elements[open_idx].min_dimensions.height = eff_h;
1908            }
1909        }
1910
1911        let element_is_floating =
1912            self.element_has_config(open_idx, ElementConfigType::Floating);
1913
1914        // Pop from open stack
1915        self.open_layout_element_stack.pop();
1916
1917        // Add to parent's children
1918        if self.open_layout_element_stack.len() > 1 {
1919            if element_is_floating {
1920                let parent_idx = self.get_open_layout_element();
1921                self.layout_elements[parent_idx].floating_children_count += 1;
1922                return;
1923            }
1924            let parent_idx = self.get_open_layout_element();
1925            self.layout_elements[parent_idx].children_length += 1;
1926            self.layout_element_children_buffer.push(open_idx as i32);
1927        }
1928    }
1929
1930    pub fn open_text_element(
1931        &mut self,
1932        text: &str,
1933        text_config_index: usize,
1934    ) {
1935        if self.boolean_warnings.max_elements_exceeded {
1936            return;
1937        }
1938
1939        let parent_idx = self.get_open_layout_element();
1940        let parent_id = self.layout_elements[parent_idx].id;
1941        let parent_children_count = self.layout_elements[parent_idx].children_length;
1942
1943        // Create text layout element
1944        let text_element = LayoutElement {
1945            text_data_index: -1,
1946            ..Default::default()
1947        };
1948        self.layout_elements.push(text_element);
1949        let text_elem_idx = (self.layout_elements.len() - 1) as i32;
1950
1951        while self.layout_element_clip_element_ids.len() < self.layout_elements.len() {
1952            self.layout_element_clip_element_ids.push(0);
1953        }
1954        if !self.open_clip_element_stack.is_empty() {
1955            let clip_id = *self.open_clip_element_stack.last().unwrap();
1956            self.layout_element_clip_element_ids[text_elem_idx as usize] = clip_id;
1957        } else {
1958            self.layout_element_clip_element_ids[text_elem_idx as usize] = 0;
1959        }
1960
1961        self.layout_element_children_buffer.push(text_elem_idx);
1962
1963        // Measure text
1964        let text_config = self.text_element_configs[text_config_index].clone();
1965        let text_measured =
1966            self.measure_text_cached(text, &text_config);
1967
1968        let element_id = hash_number(parent_children_count as u32, parent_id);
1969        self.layout_elements[text_elem_idx as usize].id = element_id.id;
1970        self.add_hash_map_item(&element_id, text_elem_idx);
1971        if self.debug_mode_enabled {
1972            self.layout_element_id_strings.push(element_id.string_id);
1973        }
1974
1975        // If the text element is marked accessible, register it in the
1976        // accessibility tree with a StaticText role and the text content
1977        // as the label.
1978        if text_config.accessible {
1979            let a11y = crate::accessibility::AccessibilityConfig {
1980                role: crate::accessibility::AccessibilityRole::StaticText,
1981                label: text.to_string(),
1982                ..Default::default()
1983            };
1984            self.accessibility_configs.insert(element_id.id, a11y);
1985            self.accessibility_element_order.push(element_id.id);
1986        }
1987
1988        let text_width = text_measured.unwrapped_dimensions.width;
1989        let text_height = if text_config.line_height > 0 {
1990            text_config.line_height as f32
1991        } else {
1992            text_measured.unwrapped_dimensions.height
1993        };
1994        let min_width = text_measured.min_width;
1995
1996        self.layout_elements[text_elem_idx as usize].dimensions =
1997            Dimensions::new(text_width, text_height);
1998        self.layout_elements[text_elem_idx as usize].min_dimensions =
1999            Dimensions::new(min_width, text_height);
2000
2001        // Store text element data
2002        let text_data = TextElementData {
2003            text: text.to_string(),
2004            preferred_dimensions: text_measured.unwrapped_dimensions,
2005            element_index: text_elem_idx,
2006            wrapped_lines_start: 0,
2007            wrapped_lines_length: 0,
2008        };
2009        self.text_element_data.push(text_data);
2010        let text_data_idx = (self.text_element_data.len() - 1) as i32;
2011        self.layout_elements[text_elem_idx as usize].text_data_index = text_data_idx;
2012
2013        // Attach text config
2014        self.layout_elements[text_elem_idx as usize].element_configs.start =
2015            self.element_configs.len();
2016        self.element_configs.push(ElementConfig {
2017            config_type: ElementConfigType::Text,
2018            config_index: text_config_index,
2019        });
2020        self.layout_elements[text_elem_idx as usize].element_configs.length = 1;
2021
2022        // Set default layout config
2023        let default_layout_idx = self.store_layout_config(LayoutConfig::default());
2024        self.layout_elements[text_elem_idx as usize].layout_config_index = default_layout_idx;
2025
2026        // Add to parent's children count
2027        self.layout_elements[parent_idx].children_length += 1;
2028    }
2029
2030    /// Returns the cached font height for the given (font_asset, font_size) pair.
2031    /// Measures `"Mg"` on the first call for each pair and caches the result.
2032    fn font_height(&mut self, font_asset: Option<&'static crate::renderer::FontAsset>, font_size: u16) -> f32 {
2033        let font_key = font_asset.map(|a| a.key()).unwrap_or("");
2034        let key = (font_key, font_size);
2035        if let Some(&h) = self.font_height_cache.get(&key) {
2036            return h;
2037        }
2038        let h = if let Some(ref measure_fn) = self.measure_text_fn {
2039            let config = TextConfig {
2040                font_asset,
2041                font_size,
2042                ..Default::default()
2043            };
2044            measure_fn("Mg", &config).height
2045        } else {
2046            font_size as f32
2047        };
2048        let font_loaded = font_asset
2049            .map_or(true, |a| crate::renderer::FontManager::is_loaded(a));
2050        if font_loaded {
2051            self.font_height_cache.insert(key, h);
2052        }
2053        h
2054    }
2055
2056    fn measure_text_cached(
2057        &mut self,
2058        text: &str,
2059        config: &TextConfig,
2060    ) -> MeasureTextCacheItem {
2061        match &self.measure_text_fn {
2062            Some(_) => {},
2063            None => {
2064                if !self.boolean_warnings.text_measurement_fn_not_set {
2065                    self.boolean_warnings.text_measurement_fn_not_set = true;
2066                }
2067                return MeasureTextCacheItem::default();
2068            }
2069        };
2070
2071        let id = hash_string_contents_with_config(text, config);
2072
2073        // Check cache
2074        if let Some(item) = self.measure_text_cache.get_mut(&id) {
2075            item.generation = self.generation;
2076            return *item;
2077        }
2078
2079        // Not cached - measure now
2080        let text_data = text.as_bytes();
2081        let text_length = text_data.len() as i32;
2082
2083        let space_str = " ";
2084        let space_width = (self.measure_text_fn.as_ref().unwrap())(space_str, config).width;
2085
2086        let mut start: i32 = 0;
2087        let mut end: i32 = 0;
2088        let mut line_width: f32 = 0.0;
2089        let mut measured_width: f32 = 0.0;
2090        let mut measured_height: f32 = 0.0;
2091        let mut min_width: f32 = 0.0;
2092        let mut contains_newlines = false;
2093
2094        let mut temp_word_next: i32 = -1;
2095        let mut previous_word_index: i32 = -1;
2096
2097        while end < text_length {
2098            let current = text_data[end as usize];
2099            if current == b' ' || current == b'\n' {
2100                let length = end - start;
2101                let mut dimensions = Dimensions::default();
2102                if length > 0 {
2103                    let substr =
2104                        core::str::from_utf8(&text_data[start as usize..end as usize]).unwrap();
2105                    dimensions = (self.measure_text_fn.as_ref().unwrap())(substr, config);
2106                }
2107                min_width = f32::max(dimensions.width, min_width);
2108                measured_height = f32::max(measured_height, dimensions.height);
2109
2110                if current == b' ' {
2111                    dimensions.width += space_width;
2112                    let word = MeasuredWord {
2113                        start_offset: start,
2114                        length: length + 1,
2115                        width: dimensions.width,
2116                        next: -1,
2117                    };
2118                    let word_idx = self.add_measured_word(word, previous_word_index);
2119                    if previous_word_index == -1 {
2120                        temp_word_next = word_idx;
2121                    }
2122                    previous_word_index = word_idx;
2123                    line_width += dimensions.width;
2124                }
2125                if current == b'\n' {
2126                    if length > 0 {
2127                        let word = MeasuredWord {
2128                            start_offset: start,
2129                            length,
2130                            width: dimensions.width,
2131                            next: -1,
2132                        };
2133                        let word_idx = self.add_measured_word(word, previous_word_index);
2134                        if previous_word_index == -1 {
2135                            temp_word_next = word_idx;
2136                        }
2137                        previous_word_index = word_idx;
2138                    }
2139                    let newline_word = MeasuredWord {
2140                        start_offset: end + 1,
2141                        length: 0,
2142                        width: 0.0,
2143                        next: -1,
2144                    };
2145                    let word_idx = self.add_measured_word(newline_word, previous_word_index);
2146                    if previous_word_index == -1 {
2147                        temp_word_next = word_idx;
2148                    }
2149                    previous_word_index = word_idx;
2150                    line_width += dimensions.width;
2151                    measured_width = f32::max(line_width, measured_width);
2152                    contains_newlines = true;
2153                    line_width = 0.0;
2154                }
2155                start = end + 1;
2156            }
2157            end += 1;
2158        }
2159
2160        if end - start > 0 {
2161            let substr =
2162                core::str::from_utf8(&text_data[start as usize..end as usize]).unwrap();
2163            let dimensions = (self.measure_text_fn.as_ref().unwrap())(substr, config);
2164            let word = MeasuredWord {
2165                start_offset: start,
2166                length: end - start,
2167                width: dimensions.width,
2168                next: -1,
2169            };
2170            let word_idx = self.add_measured_word(word, previous_word_index);
2171            if previous_word_index == -1 {
2172                temp_word_next = word_idx;
2173            }
2174            line_width += dimensions.width;
2175            measured_height = f32::max(measured_height, dimensions.height);
2176            min_width = f32::max(dimensions.width, min_width);
2177        }
2178
2179        measured_width =
2180            f32::max(line_width, measured_width) - config.letter_spacing as f32;
2181
2182        let result = MeasureTextCacheItem {
2183            id,
2184            generation: self.generation,
2185            measured_words_start_index: temp_word_next,
2186            unwrapped_dimensions: Dimensions::new(measured_width, measured_height),
2187            min_width,
2188            contains_newlines,
2189        };
2190        self.measure_text_cache.insert(id, result);
2191        result
2192    }
2193
2194    fn add_measured_word(&mut self, word: MeasuredWord, previous_word_index: i32) -> i32 {
2195        let new_index: i32;
2196        if let Some(&free_idx) = self.measured_words_free_list.last() {
2197            self.measured_words_free_list.pop();
2198            new_index = free_idx;
2199            self.measured_words[free_idx as usize] = word;
2200        } else {
2201            self.measured_words.push(word);
2202            new_index = (self.measured_words.len() - 1) as i32;
2203        }
2204        if previous_word_index >= 0 {
2205            self.measured_words[previous_word_index as usize].next = new_index;
2206        }
2207        new_index
2208    }
2209
2210    pub fn begin_layout(&mut self) {
2211        self.initialize_ephemeral_memory();
2212        self.generation += 1;
2213        if self.released_this_frame_generation != self.generation {
2214            self.released_this_frame_ids.clear();
2215        }
2216        self.dynamic_element_index = 0;
2217
2218        // Evict stale text measurement cache entries
2219        self.evict_stale_text_cache();
2220
2221        let root_width = self.layout_dimensions.width;
2222        let root_height = self.layout_dimensions.height;
2223
2224        self.boolean_warnings = BooleanWarnings::default();
2225
2226        let root_id = hash_string("Ply__RootContainer", 0);
2227        self.open_element_with_id(&root_id);
2228
2229        let root_decl = ElementDeclaration {
2230            layout: LayoutConfig {
2231                sizing: SizingConfig {
2232                    width: SizingAxis {
2233                        type_: SizingType::Fixed,
2234                        min_max: SizingMinMax {
2235                            min: root_width,
2236                            max: root_width,
2237                        },
2238                        percent: 0.0,
2239                        grow_weight: 1.0,
2240                    },
2241                    height: SizingAxis {
2242                        type_: SizingType::Fixed,
2243                        min_max: SizingMinMax {
2244                            min: root_height,
2245                            max: root_height,
2246                        },
2247                        percent: 0.0,
2248                        grow_weight: 1.0,
2249                    },
2250                },
2251                ..Default::default()
2252            },
2253            ..Default::default()
2254        };
2255        self.configure_open_element(&root_decl);
2256        self.open_layout_element_stack.push(0);
2257        self.layout_element_tree_roots.push(LayoutElementTreeRoot {
2258            layout_element_index: 0,
2259            ..Default::default()
2260        });
2261    }
2262
2263    pub fn end_layout(&mut self) -> &[InternalRenderCommand<CustomElementData>] {
2264        self.close_element();
2265
2266        if self.open_layout_element_stack.len() > 1 {
2267            // Unbalanced open/close warning
2268        }
2269
2270        if self.debug_mode_enabled {
2271            self.render_debug_view();
2272        }
2273
2274        self.calculate_final_layout();
2275        &self.render_commands
2276    }
2277
2278    /// Evicts stale entries from the text measurement cache.
2279    /// Entries that haven't been used for more than 2 generations are removed.
2280    fn evict_stale_text_cache(&mut self) {
2281        let gen = self.generation;
2282        let measured_words = &mut self.measured_words;
2283        let free_list = &mut self.measured_words_free_list;
2284        self.measure_text_cache.retain(|_, item| {
2285            if gen.wrapping_sub(item.generation) <= 2 {
2286                true
2287            } else {
2288                // Clean up measured words for this evicted entry
2289                let mut idx = item.measured_words_start_index;
2290                while idx != -1 {
2291                    let word = measured_words[idx as usize];
2292                    free_list.push(idx);
2293                    idx = word.next;
2294                }
2295                false
2296            }
2297        });
2298    }
2299
2300    fn initialize_ephemeral_memory(&mut self) {
2301        self.layout_element_children_buffer.clear();
2302        self.layout_elements.clear();
2303        self.layout_configs.clear();
2304        self.element_configs.clear();
2305        self.text_element_configs.clear();
2306        self.aspect_ratio_configs.clear();
2307        self.aspect_ratio_cover_configs.clear();
2308        self.image_element_configs.clear();
2309        self.floating_element_configs.clear();
2310        self.clip_element_configs.clear();
2311        self.custom_element_configs.clear();
2312        self.border_element_configs.clear();
2313        self.shared_element_configs.clear();
2314        self.element_effects.clear();
2315        self.element_shaders.clear();
2316        self.element_visual_rotations.clear();
2317        self.element_shape_rotations.clear();
2318        self.element_pre_rotation_dimensions.clear();
2319        self.layout_element_id_strings.clear();
2320        self.wrapped_text_lines.clear();
2321        self.tree_node_array.clear();
2322        self.layout_element_tree_roots.clear();
2323        self.layout_element_children.clear();
2324        self.open_layout_element_stack.clear();
2325        self.text_element_data.clear();
2326        self.aspect_ratio_element_indexes.clear();
2327        self.render_commands.clear();
2328        self.tree_node_visited.clear();
2329        self.open_clip_element_stack.clear();
2330        self.reusable_element_index_buffer.clear();
2331        self.layout_element_clip_element_ids.clear();
2332        self.dynamic_string_data.clear();
2333        self.focusable_elements.clear();
2334        self.accessibility_configs.clear();
2335        self.accessibility_element_order.clear();
2336        self.text_input_configs.clear();
2337        self.text_input_element_ids.clear();
2338    }
2339
2340    fn child_size_on_axis(&self, child_index: usize, x_axis: bool) -> f32 {
2341        if x_axis {
2342            self.layout_elements[child_index].dimensions.width
2343        } else {
2344            self.layout_elements[child_index].dimensions.height
2345        }
2346    }
2347
2348    fn child_sizing_on_axis(&self, child_index: usize, x_axis: bool) -> SizingAxis {
2349        let child_layout_idx = self.layout_elements[child_index].layout_config_index;
2350        if x_axis {
2351            self.layout_configs[child_layout_idx].sizing.width
2352        } else {
2353            self.layout_configs[child_layout_idx].sizing.height
2354        }
2355    }
2356
2357    fn child_wrap_break_main_size(&self, child_index: usize, main_axis_x: bool) -> f32 {
2358        let child_sizing = self.child_sizing_on_axis(child_index, main_axis_x);
2359        match child_sizing.type_ {
2360            SizingType::Grow => child_sizing.min_max.min,
2361            SizingType::Percent | SizingType::Fixed | SizingType::Fit => {
2362                self.child_size_on_axis(child_index, main_axis_x)
2363            }
2364        }
2365    }
2366
2367    fn build_wrapped_line(
2368        &self,
2369        parent_index: usize,
2370        start_child_offset: usize,
2371        end_child_offset: usize,
2372        main_axis_x: bool,
2373    ) -> WrappedLayoutLine {
2374        let layout_idx = self.layout_elements[parent_index].layout_config_index;
2375        let layout = self.layout_configs[layout_idx];
2376        let children_start = self.layout_elements[parent_index].children_start;
2377
2378        let mut main_size = 0.0;
2379        let mut cross_size = 0.0;
2380
2381        for child_offset in start_child_offset..end_child_offset {
2382            let child_index = self.layout_element_children[children_start + child_offset] as usize;
2383            if child_offset > start_child_offset {
2384                main_size += layout.child_gap as f32;
2385            }
2386
2387            let child_main = self.child_size_on_axis(child_index, main_axis_x);
2388            let child_cross = self.child_size_on_axis(child_index, !main_axis_x);
2389            main_size += child_main;
2390            cross_size = f32::max(cross_size, child_cross);
2391        }
2392
2393        WrappedLayoutLine {
2394            start_child_offset,
2395            end_child_offset,
2396            main_size,
2397            cross_size,
2398        }
2399    }
2400
2401    fn compute_wrapped_lines(
2402        &self,
2403        parent_index: usize,
2404        main_axis_x: bool,
2405    ) -> Vec<WrappedLayoutLine> {
2406        let layout_idx = self.layout_elements[parent_index].layout_config_index;
2407        let layout = self.layout_configs[layout_idx];
2408
2409        let children_length = self.layout_elements[parent_index].children_length as usize;
2410        if children_length == 0 {
2411            return Vec::new();
2412        }
2413
2414        let parent_main_size = if main_axis_x {
2415            self.layout_elements[parent_index].dimensions.width
2416        } else {
2417            self.layout_elements[parent_index].dimensions.height
2418        };
2419        let main_padding = if main_axis_x {
2420            (layout.padding.left + layout.padding.right) as f32
2421        } else {
2422            (layout.padding.top + layout.padding.bottom) as f32
2423        };
2424        let available_main = (parent_main_size - main_padding).max(0.0);
2425
2426        if !layout.wrap {
2427            return vec![self.build_wrapped_line(
2428                parent_index,
2429                0,
2430                children_length,
2431                main_axis_x,
2432            )];
2433        }
2434
2435        let children_start = self.layout_elements[parent_index].children_start;
2436        let mut lines = Vec::new();
2437        let mut line_start = 0usize;
2438        let mut line_break_main = 0.0;
2439
2440        for child_offset in 0..children_length {
2441            let child_index = self.layout_element_children[children_start + child_offset] as usize;
2442            let break_size = self.child_wrap_break_main_size(child_index, main_axis_x);
2443
2444            let additional = if child_offset == line_start {
2445                break_size
2446            } else {
2447                layout.child_gap as f32 + break_size
2448            };
2449
2450            if child_offset > line_start && line_break_main + additional > available_main + EPSILON {
2451                lines.push(self.build_wrapped_line(
2452                    parent_index,
2453                    line_start,
2454                    child_offset,
2455                    main_axis_x,
2456                ));
2457                line_start = child_offset;
2458                line_break_main = break_size;
2459            } else {
2460                line_break_main += additional;
2461            }
2462        }
2463
2464        lines.push(self.build_wrapped_line(
2465            parent_index,
2466            line_start,
2467            children_length,
2468            main_axis_x,
2469        ));
2470
2471        lines
2472    }
2473
2474    fn wrapped_content_dimensions(
2475        &self,
2476        parent_index: usize,
2477        main_axis_x: bool,
2478        lines: &[WrappedLayoutLine],
2479    ) -> Dimensions {
2480        let layout_idx = self.layout_elements[parent_index].layout_config_index;
2481        let layout = self.layout_configs[layout_idx];
2482
2483        let lr_padding = (layout.padding.left + layout.padding.right) as f32;
2484        let tb_padding = (layout.padding.top + layout.padding.bottom) as f32;
2485
2486        if lines.is_empty() {
2487            return Dimensions::new(lr_padding, tb_padding);
2488        }
2489
2490        let wrap_gap_total = lines.len().saturating_sub(1) as f32 * layout.wrap_gap as f32;
2491        let max_main = lines.iter().fold(0.0_f32, |acc, line| acc.max(line.main_size));
2492        let total_cross = lines.iter().map(|line| line.cross_size).sum::<f32>() + wrap_gap_total;
2493
2494        if main_axis_x {
2495            Dimensions::new(max_main + lr_padding, total_cross + tb_padding)
2496        } else {
2497            Dimensions::new(total_cross + lr_padding, max_main + tb_padding)
2498        }
2499    }
2500
2501    fn size_containers_along_axis(&mut self, x_axis: bool) {
2502        let mut bfs_buffer: Vec<i32> = Vec::new();
2503        let mut resizable_container_buffer: Vec<i32> = Vec::new();
2504
2505        for root_index in 0..self.layout_element_tree_roots.len() {
2506            bfs_buffer.clear();
2507            let root = self.layout_element_tree_roots[root_index];
2508            let root_elem_idx = root.layout_element_index as usize;
2509            bfs_buffer.push(root.layout_element_index);
2510
2511            // Size floating containers to their parents
2512            if self.element_has_config(root_elem_idx, ElementConfigType::Floating) {
2513                if let Some(float_cfg_idx) =
2514                    self.find_element_config_index(root_elem_idx, ElementConfigType::Floating)
2515                {
2516                    let parent_id = self.floating_element_configs[float_cfg_idx].parent_id;
2517                    if let Some(parent_item) = self.layout_element_map.get(&parent_id) {
2518                        let parent_elem_idx = parent_item.layout_element_index as usize;
2519                        let parent_dims = self.layout_elements[parent_elem_idx].dimensions;
2520                        let root_layout_idx =
2521                            self.layout_elements[root_elem_idx].layout_config_index;
2522
2523                        let w_type = self.layout_configs[root_layout_idx].sizing.width.type_;
2524                        match w_type {
2525                            SizingType::Grow => {
2526                                self.layout_elements[root_elem_idx].dimensions.width =
2527                                    parent_dims.width;
2528                            }
2529                            SizingType::Percent => {
2530                                self.layout_elements[root_elem_idx].dimensions.width =
2531                                    parent_dims.width
2532                                        * self.layout_configs[root_layout_idx]
2533                                            .sizing
2534                                            .width
2535                                            .percent;
2536                            }
2537                            _ => {}
2538                        }
2539                        let h_type = self.layout_configs[root_layout_idx].sizing.height.type_;
2540                        match h_type {
2541                            SizingType::Grow => {
2542                                self.layout_elements[root_elem_idx].dimensions.height =
2543                                    parent_dims.height;
2544                            }
2545                            SizingType::Percent => {
2546                                self.layout_elements[root_elem_idx].dimensions.height =
2547                                    parent_dims.height
2548                                        * self.layout_configs[root_layout_idx]
2549                                            .sizing
2550                                            .height
2551                                            .percent;
2552                            }
2553                            _ => {}
2554                        }
2555                    }
2556                }
2557            }
2558
2559            // Clamp root element
2560            let root_layout_idx = self.layout_elements[root_elem_idx].layout_config_index;
2561            if self.layout_configs[root_layout_idx].sizing.width.type_ != SizingType::Percent {
2562                let min = self.layout_configs[root_layout_idx].sizing.width.min_max.min;
2563                let max = self.layout_configs[root_layout_idx].sizing.width.min_max.max;
2564                self.layout_elements[root_elem_idx].dimensions.width = f32::min(
2565                    f32::max(self.layout_elements[root_elem_idx].dimensions.width, min),
2566                    max,
2567                );
2568            }
2569            if self.layout_configs[root_layout_idx].sizing.height.type_ != SizingType::Percent {
2570                let min = self.layout_configs[root_layout_idx].sizing.height.min_max.min;
2571                let max = self.layout_configs[root_layout_idx].sizing.height.min_max.max;
2572                self.layout_elements[root_elem_idx].dimensions.height = f32::min(
2573                    f32::max(self.layout_elements[root_elem_idx].dimensions.height, min),
2574                    max,
2575                );
2576            }
2577
2578            let mut i = 0;
2579            while i < bfs_buffer.len() {
2580                let parent_index = bfs_buffer[i] as usize;
2581                i += 1;
2582
2583                let parent_layout_idx = self.layout_elements[parent_index].layout_config_index;
2584                let parent_config = self.layout_configs[parent_layout_idx];
2585                let parent_size = if x_axis {
2586                    self.layout_elements[parent_index].dimensions.width
2587                } else {
2588                    self.layout_elements[parent_index].dimensions.height
2589                };
2590                let parent_padding = if x_axis {
2591                    (parent_config.padding.left + parent_config.padding.right) as f32
2592                } else {
2593                    (parent_config.padding.top + parent_config.padding.bottom) as f32
2594                };
2595                let sizing_along_axis = (x_axis
2596                    && parent_config.layout_direction == LayoutDirection::LeftToRight)
2597                    || (!x_axis
2598                        && parent_config.layout_direction == LayoutDirection::TopToBottom);
2599
2600                let mut inner_content_size: f32 = 0.0;
2601                let mut total_padding_and_child_gaps = parent_padding;
2602                let parent_child_gap = parent_config.child_gap as f32;
2603                // None = no grow seen, Some(Some(idx)) = exactly one grow, Some(None) = 2+ grows.
2604                let mut single_along_axis_grow_candidate: Option<Option<usize>> = None;
2605
2606                resizable_container_buffer.clear();
2607
2608                let children_start = self.layout_elements[parent_index].children_start;
2609                let children_length = self.layout_elements[parent_index].children_length as usize;
2610
2611                for child_offset in 0..children_length {
2612                    let child_element_index =
2613                        self.layout_element_children[children_start + child_offset] as usize;
2614                    let child_layout_idx =
2615                        self.layout_elements[child_element_index].layout_config_index;
2616                    let child_sizing = if x_axis {
2617                        self.layout_configs[child_layout_idx].sizing.width
2618                    } else {
2619                        self.layout_configs[child_layout_idx].sizing.height
2620                    };
2621                    let child_size = if x_axis {
2622                        self.layout_elements[child_element_index].dimensions.width
2623                    } else {
2624                        self.layout_elements[child_element_index].dimensions.height
2625                    };
2626
2627                    let is_text_element =
2628                        self.element_has_config(child_element_index, ElementConfigType::Text);
2629                    let has_children = self.layout_elements[child_element_index].children_length > 0;
2630
2631                    if !is_text_element && has_children {
2632                        bfs_buffer.push(child_element_index as i32);
2633                    }
2634
2635                    let is_wrapping_text = if is_text_element {
2636                        if let Some(text_cfg_idx) = self.find_element_config_index(
2637                            child_element_index,
2638                            ElementConfigType::Text,
2639                        ) {
2640                            self.text_element_configs[text_cfg_idx].wrap_mode
2641                                == WrapMode::Words
2642                        } else {
2643                            false
2644                        }
2645                    } else {
2646                        false
2647                    };
2648
2649                    if child_sizing.type_ != SizingType::Percent
2650                        && child_sizing.type_ != SizingType::Fixed
2651                        && (!is_text_element || is_wrapping_text)
2652                    {
2653                        resizable_container_buffer.push(child_element_index as i32);
2654                        if sizing_along_axis
2655                            && child_sizing.type_ == SizingType::Grow
2656                            && child_sizing.grow_weight > 0.0
2657                        {
2658                            single_along_axis_grow_candidate = match single_along_axis_grow_candidate {
2659                                None => Some(Some(child_element_index)),
2660                                Some(Some(_)) | Some(None) => Some(None),
2661                            };
2662                        }
2663                    }
2664
2665                    if sizing_along_axis {
2666                        inner_content_size += if child_sizing.type_ == SizingType::Percent {
2667                            0.0
2668                        } else {
2669                            child_size
2670                        };
2671                        if child_offset > 0 {
2672                            inner_content_size += parent_child_gap;
2673                            total_padding_and_child_gaps += parent_child_gap;
2674                        }
2675                    } else {
2676                        inner_content_size = f32::max(child_size, inner_content_size);
2677                    }
2678                }
2679
2680                // Expand percentage containers
2681                for child_offset in 0..children_length {
2682                    let child_element_index =
2683                        self.layout_element_children[children_start + child_offset] as usize;
2684                    let child_layout_idx =
2685                        self.layout_elements[child_element_index].layout_config_index;
2686                    let child_sizing = if x_axis {
2687                        self.layout_configs[child_layout_idx].sizing.width
2688                    } else {
2689                        self.layout_configs[child_layout_idx].sizing.height
2690                    };
2691                    if child_sizing.type_ == SizingType::Percent {
2692                        let new_size =
2693                            (parent_size - total_padding_and_child_gaps) * child_sizing.percent;
2694                        if x_axis {
2695                            self.layout_elements[child_element_index].dimensions.width = new_size;
2696                        } else {
2697                            self.layout_elements[child_element_index].dimensions.height = new_size;
2698                        }
2699                        if sizing_along_axis {
2700                            inner_content_size += new_size;
2701                        }
2702                        self.update_aspect_ratio_box(child_element_index);
2703                    }
2704                }
2705
2706                if sizing_along_axis && parent_config.wrap {
2707                    let parent_clips = if let Some(clip_idx) = self
2708                        .find_element_config_index(parent_index, ElementConfigType::Clip)
2709                    {
2710                        let clip = &self.clip_element_configs[clip_idx];
2711                        (x_axis && clip.horizontal) || (!x_axis && clip.vertical)
2712                    } else {
2713                        false
2714                    };
2715
2716                    let wrapped_lines = self.compute_wrapped_lines(parent_index, x_axis);
2717
2718                    for line in wrapped_lines {
2719                        if line.end_child_offset <= line.start_child_offset {
2720                            continue;
2721                        }
2722
2723                        let mut line_children: Vec<usize> = Vec::new();
2724                        for child_offset in line.start_child_offset..line.end_child_offset {
2725                            let child_element_index =
2726                                self.layout_element_children[children_start + child_offset] as usize;
2727                            line_children.push(child_element_index);
2728                        }
2729
2730                        let mut line_inner_content_size: f32 = 0.0;
2731                        let mut line_resizable_buffer: Vec<i32> = Vec::new();
2732                        let mut single_line_grow_candidate: Option<Option<usize>> = None;
2733
2734                        for line_child_offset in 0..line_children.len() {
2735                            let child_idx = line_children[line_child_offset];
2736                            let child_layout_idx = self.layout_elements[child_idx].layout_config_index;
2737                            let child_sizing = if x_axis {
2738                                self.layout_configs[child_layout_idx].sizing.width
2739                            } else {
2740                                self.layout_configs[child_layout_idx].sizing.height
2741                            };
2742                            let child_size = if x_axis {
2743                                self.layout_elements[child_idx].dimensions.width
2744                            } else {
2745                                self.layout_elements[child_idx].dimensions.height
2746                            };
2747
2748                            if line_child_offset > 0 {
2749                                line_inner_content_size += parent_child_gap;
2750                            }
2751                            line_inner_content_size += child_size;
2752
2753                            let is_text_element =
2754                                self.element_has_config(child_idx, ElementConfigType::Text);
2755                            let is_wrapping_text = if is_text_element {
2756                                if let Some(text_cfg_idx) = self
2757                                    .find_element_config_index(child_idx, ElementConfigType::Text)
2758                                {
2759                                    self.text_element_configs[text_cfg_idx].wrap_mode
2760                                        == WrapMode::Words
2761                                } else {
2762                                    false
2763                                }
2764                            } else {
2765                                false
2766                            };
2767
2768                            if child_sizing.type_ != SizingType::Percent
2769                                && child_sizing.type_ != SizingType::Fixed
2770                                && (!is_text_element || is_wrapping_text)
2771                            {
2772                                line_resizable_buffer.push(child_idx as i32);
2773
2774                                if child_sizing.type_ == SizingType::Grow
2775                                    && child_sizing.grow_weight > 0.0
2776                                {
2777                                    single_line_grow_candidate = match single_line_grow_candidate {
2778                                        None => Some(Some(child_idx)),
2779                                        Some(Some(_)) | Some(None) => Some(None),
2780                                    };
2781                                }
2782                            }
2783                        }
2784
2785                        let size_to_distribute =
2786                            parent_size - parent_padding - line_inner_content_size;
2787
2788                        if size_to_distribute < 0.0 {
2789                            if parent_clips {
2790                                continue;
2791                            }
2792
2793                            let mut distribute = size_to_distribute;
2794                            while distribute < -EPSILON && !line_resizable_buffer.is_empty() {
2795                                let mut largest: f32 = 0.0;
2796                                let mut second_largest: f32 = 0.0;
2797                                let mut width_to_add = distribute;
2798
2799                                for &child_idx in &line_resizable_buffer {
2800                                    let cs = if x_axis {
2801                                        self.layout_elements[child_idx as usize].dimensions.width
2802                                    } else {
2803                                        self.layout_elements[child_idx as usize].dimensions.height
2804                                    };
2805                                    if float_equal(cs, largest) {
2806                                        continue;
2807                                    }
2808                                    if cs > largest {
2809                                        second_largest = largest;
2810                                        largest = cs;
2811                                    }
2812                                    if cs < largest {
2813                                        second_largest = f32::max(second_largest, cs);
2814                                        width_to_add = second_largest - largest;
2815                                    }
2816                                }
2817
2818                                width_to_add = f32::max(
2819                                    width_to_add,
2820                                    distribute / line_resizable_buffer.len() as f32,
2821                                );
2822
2823                                let mut j = 0;
2824                                while j < line_resizable_buffer.len() {
2825                                    let child_idx = line_resizable_buffer[j] as usize;
2826                                    let current_size = if x_axis {
2827                                        self.layout_elements[child_idx].dimensions.width
2828                                    } else {
2829                                        self.layout_elements[child_idx].dimensions.height
2830                                    };
2831                                    let min_size = if x_axis {
2832                                        self.layout_elements[child_idx].min_dimensions.width
2833                                    } else {
2834                                        self.layout_elements[child_idx].min_dimensions.height
2835                                    };
2836
2837                                    if float_equal(current_size, largest) {
2838                                        let new_size = current_size + width_to_add;
2839                                        if new_size <= min_size {
2840                                            if x_axis {
2841                                                self.layout_elements[child_idx].dimensions.width =
2842                                                    min_size;
2843                                            } else {
2844                                                self.layout_elements[child_idx].dimensions.height =
2845                                                    min_size;
2846                                            }
2847                                            distribute -= min_size - current_size;
2848                                            line_resizable_buffer.swap_remove(j);
2849                                            continue;
2850                                        }
2851
2852                                        if x_axis {
2853                                            self.layout_elements[child_idx].dimensions.width =
2854                                                new_size;
2855                                        } else {
2856                                            self.layout_elements[child_idx].dimensions.height =
2857                                                new_size;
2858                                        }
2859                                        distribute -= new_size - current_size;
2860                                    }
2861
2862                                    j += 1;
2863                                }
2864                            }
2865                        } else if size_to_distribute > 0.0 {
2866                            if let Some(Some(single_line_grow_child_idx)) =
2867                                single_line_grow_candidate
2868                            {
2869                                let child_layout_idx =
2870                                    self.layout_elements[single_line_grow_child_idx]
2871                                        .layout_config_index;
2872                                let child_max_size = if x_axis {
2873                                    self.layout_configs[child_layout_idx].sizing.width.min_max.max
2874                                } else {
2875                                    self.layout_configs[child_layout_idx]
2876                                        .sizing
2877                                        .height
2878                                        .min_max
2879                                        .max
2880                                };
2881
2882                                let child_size_ref = if x_axis {
2883                                    &mut self.layout_elements[single_line_grow_child_idx]
2884                                        .dimensions
2885                                        .width
2886                                } else {
2887                                    &mut self.layout_elements[single_line_grow_child_idx]
2888                                        .dimensions
2889                                        .height
2890                                };
2891
2892                                *child_size_ref =
2893                                    f32::min(*child_size_ref + size_to_distribute, child_max_size);
2894                            } else {
2895                                let mut j = 0;
2896                                while j < line_resizable_buffer.len() {
2897                                    let child_idx = line_resizable_buffer[j] as usize;
2898                                    let child_layout_idx =
2899                                        self.layout_elements[child_idx].layout_config_index;
2900                                    let child_sizing = if x_axis {
2901                                        self.layout_configs[child_layout_idx].sizing.width
2902                                    } else {
2903                                        self.layout_configs[child_layout_idx].sizing.height
2904                                    };
2905                                    if child_sizing.type_ != SizingType::Grow
2906                                        || child_sizing.grow_weight <= 0.0
2907                                    {
2908                                        line_resizable_buffer.swap_remove(j);
2909                                    } else {
2910                                        j += 1;
2911                                    }
2912                                }
2913
2914                                let mut distribute = size_to_distribute;
2915                                while distribute > EPSILON && !line_resizable_buffer.is_empty() {
2916                                    let mut total_weight = 0.0;
2917                                    let mut smallest_ratio = MAXFLOAT;
2918                                    let mut second_smallest_ratio = MAXFLOAT;
2919
2920                                    for &child_idx in &line_resizable_buffer {
2921                                        let child_layout_idx =
2922                                            self.layout_elements[child_idx as usize]
2923                                                .layout_config_index;
2924                                        let child_sizing = if x_axis {
2925                                            self.layout_configs[child_layout_idx].sizing.width
2926                                        } else {
2927                                            self.layout_configs[child_layout_idx].sizing.height
2928                                        };
2929
2930                                        total_weight += child_sizing.grow_weight;
2931
2932                                        let child_size = if x_axis {
2933                                            self.layout_elements[child_idx as usize].dimensions.width
2934                                        } else {
2935                                            self.layout_elements[child_idx as usize].dimensions.height
2936                                        };
2937                                        let child_ratio = child_size / child_sizing.grow_weight;
2938
2939                                        if float_equal(child_ratio, smallest_ratio) {
2940                                            continue;
2941                                        }
2942                                        if child_ratio < smallest_ratio {
2943                                            second_smallest_ratio = smallest_ratio;
2944                                            smallest_ratio = child_ratio;
2945                                        } else if child_ratio > smallest_ratio {
2946                                            second_smallest_ratio =
2947                                                f32::min(second_smallest_ratio, child_ratio);
2948                                        }
2949                                    }
2950
2951                                    if total_weight <= 0.0 {
2952                                        break;
2953                                    }
2954
2955                                    let per_weight_growth = distribute / total_weight;
2956                                    let ratio_step_cap = if second_smallest_ratio == MAXFLOAT {
2957                                        MAXFLOAT
2958                                    } else {
2959                                        second_smallest_ratio - smallest_ratio
2960                                    };
2961
2962                                    let mut resized_any = false;
2963
2964                                    let mut j = 0;
2965                                    while j < line_resizable_buffer.len() {
2966                                        let child_idx = line_resizable_buffer[j] as usize;
2967                                        let child_layout_idx =
2968                                            self.layout_elements[child_idx].layout_config_index;
2969                                        let child_sizing = if x_axis {
2970                                            self.layout_configs[child_layout_idx].sizing.width
2971                                        } else {
2972                                            self.layout_configs[child_layout_idx].sizing.height
2973                                        };
2974
2975                                        let child_size_ref = if x_axis {
2976                                            &mut self.layout_elements[child_idx].dimensions.width
2977                                        } else {
2978                                            &mut self.layout_elements[child_idx].dimensions.height
2979                                        };
2980
2981                                        let child_ratio =
2982                                            *child_size_ref / child_sizing.grow_weight;
2983                                        if !float_equal(child_ratio, smallest_ratio) {
2984                                            j += 1;
2985                                            continue;
2986                                        }
2987
2988                                        let max_size = if x_axis {
2989                                            self.layout_configs[child_layout_idx]
2990                                                .sizing
2991                                                .width
2992                                                .min_max
2993                                                .max
2994                                        } else {
2995                                            self.layout_configs[child_layout_idx]
2996                                                .sizing
2997                                                .height
2998                                                .min_max
2999                                                .max
3000                                        };
3001
3002                                        let mut growth_share =
3003                                            per_weight_growth * child_sizing.grow_weight;
3004                                        if ratio_step_cap != MAXFLOAT {
3005                                            growth_share = f32::min(
3006                                                growth_share,
3007                                                ratio_step_cap * child_sizing.grow_weight,
3008                                            );
3009                                        }
3010
3011                                        let previous = *child_size_ref;
3012                                        let proposed = previous + growth_share;
3013                                        if proposed >= max_size {
3014                                            *child_size_ref = max_size;
3015                                            resized_any = true;
3016                                            line_resizable_buffer.swap_remove(j);
3017                                            continue;
3018                                        }
3019
3020                                        *child_size_ref = proposed;
3021                                        distribute -= *child_size_ref - previous;
3022                                        resized_any = true;
3023                                        j += 1;
3024                                    }
3025
3026                                    if !resized_any {
3027                                        break;
3028                                    }
3029                                }
3030                            }
3031                        }
3032                    }
3033                } else if sizing_along_axis {
3034                    let size_to_distribute = parent_size - parent_padding - inner_content_size;
3035
3036                    if size_to_distribute < 0.0 {
3037                        // Check if parent clips
3038                        let parent_clips = if let Some(clip_idx) = self
3039                            .find_element_config_index(parent_index, ElementConfigType::Clip)
3040                        {
3041                            let clip = &self.clip_element_configs[clip_idx];
3042                            (x_axis && clip.horizontal) || (!x_axis && clip.vertical)
3043                        } else {
3044                            false
3045                        };
3046                        if parent_clips {
3047                            continue;
3048                        }
3049
3050                        // Compress children
3051                        let mut distribute = size_to_distribute;
3052                        while distribute < -EPSILON && !resizable_container_buffer.is_empty() {
3053                            let mut largest: f32 = 0.0;
3054                            let mut second_largest: f32 = 0.0;
3055                            let mut width_to_add = distribute;
3056
3057                            for &child_idx in &resizable_container_buffer {
3058                                let cs = if x_axis {
3059                                    self.layout_elements[child_idx as usize].dimensions.width
3060                                } else {
3061                                    self.layout_elements[child_idx as usize].dimensions.height
3062                                };
3063                                if float_equal(cs, largest) {
3064                                    continue;
3065                                }
3066                                if cs > largest {
3067                                    second_largest = largest;
3068                                    largest = cs;
3069                                }
3070                                if cs < largest {
3071                                    second_largest = f32::max(second_largest, cs);
3072                                    width_to_add = second_largest - largest;
3073                                }
3074                            }
3075                            width_to_add = f32::max(
3076                                width_to_add,
3077                                distribute / resizable_container_buffer.len() as f32,
3078                            );
3079
3080                            let mut j = 0;
3081                            while j < resizable_container_buffer.len() {
3082                                let child_idx = resizable_container_buffer[j] as usize;
3083                                let current_size = if x_axis {
3084                                    self.layout_elements[child_idx].dimensions.width
3085                                } else {
3086                                    self.layout_elements[child_idx].dimensions.height
3087                                };
3088                                let min_size = if x_axis {
3089                                    self.layout_elements[child_idx].min_dimensions.width
3090                                } else {
3091                                    self.layout_elements[child_idx].min_dimensions.height
3092                                };
3093                                if float_equal(current_size, largest) {
3094                                    let new_size = current_size + width_to_add;
3095                                    if new_size <= min_size {
3096                                        if x_axis {
3097                                            self.layout_elements[child_idx].dimensions.width = min_size;
3098                                        } else {
3099                                            self.layout_elements[child_idx].dimensions.height = min_size;
3100                                        }
3101                                        distribute -= min_size - current_size;
3102                                        resizable_container_buffer.swap_remove(j);
3103                                        continue;
3104                                    }
3105                                    if x_axis {
3106                                        self.layout_elements[child_idx].dimensions.width = new_size;
3107                                    } else {
3108                                        self.layout_elements[child_idx].dimensions.height = new_size;
3109                                    }
3110                                    distribute -= new_size - current_size;
3111                                }
3112                                j += 1;
3113                            }
3114                        }
3115                    } else if size_to_distribute > 0.0 {
3116                        if let Some(Some(single_along_axis_grow_child_idx)) =
3117                            single_along_axis_grow_candidate
3118                        {
3119                            let child_layout_idx = self
3120                                .layout_elements[single_along_axis_grow_child_idx]
3121                                .layout_config_index;
3122                            let child_max_size = if x_axis {
3123                                self.layout_configs[child_layout_idx].sizing.width.min_max.max
3124                            } else {
3125                                self.layout_configs[child_layout_idx].sizing.height.min_max.max
3126                            };
3127                            let child_size_ref = if x_axis {
3128                                &mut self.layout_elements[single_along_axis_grow_child_idx]
3129                                    .dimensions
3130                                    .width
3131                            } else {
3132                                &mut self.layout_elements[single_along_axis_grow_child_idx]
3133                                    .dimensions
3134                                    .height
3135                            };
3136                            *child_size_ref = f32::min(*child_size_ref + size_to_distribute, child_max_size);
3137                        } else {
3138                            // Remove non-grow from resizable buffer
3139                            let mut j = 0;
3140                            while j < resizable_container_buffer.len() {
3141                                let child_idx = resizable_container_buffer[j] as usize;
3142                                let child_layout_idx =
3143                                    self.layout_elements[child_idx].layout_config_index;
3144                                let child_sizing = if x_axis {
3145                                    self.layout_configs[child_layout_idx].sizing.width
3146                                } else {
3147                                    self.layout_configs[child_layout_idx].sizing.height
3148                                };
3149                                if child_sizing.type_ != SizingType::Grow
3150                                    || child_sizing.grow_weight <= 0.0
3151                                {
3152                                    resizable_container_buffer.swap_remove(j);
3153                                } else {
3154                                    j += 1;
3155                                }
3156                            }
3157
3158                            let mut distribute = size_to_distribute;
3159                            while distribute > EPSILON && !resizable_container_buffer.is_empty() {
3160                                let mut total_weight = 0.0;
3161                                let mut smallest_ratio = MAXFLOAT;
3162                                let mut second_smallest_ratio = MAXFLOAT;
3163
3164                                for &child_idx in &resizable_container_buffer {
3165                                    let child_layout_idx =
3166                                        self.layout_elements[child_idx as usize].layout_config_index;
3167                                    let child_sizing = if x_axis {
3168                                        self.layout_configs[child_layout_idx].sizing.width
3169                                    } else {
3170                                        self.layout_configs[child_layout_idx].sizing.height
3171                                    };
3172
3173                                    total_weight += child_sizing.grow_weight;
3174
3175                                    let child_size = if x_axis {
3176                                        self.layout_elements[child_idx as usize].dimensions.width
3177                                    } else {
3178                                        self.layout_elements[child_idx as usize].dimensions.height
3179                                    };
3180                                    let child_ratio = child_size / child_sizing.grow_weight;
3181
3182                                    if float_equal(child_ratio, smallest_ratio) {
3183                                        continue;
3184                                    }
3185                                    if child_ratio < smallest_ratio {
3186                                        second_smallest_ratio = smallest_ratio;
3187                                        smallest_ratio = child_ratio;
3188                                    } else if child_ratio > smallest_ratio {
3189                                        second_smallest_ratio = f32::min(second_smallest_ratio, child_ratio);
3190                                    }
3191                                }
3192
3193                                if total_weight <= 0.0 {
3194                                    break;
3195                                }
3196
3197                                let per_weight_growth = distribute / total_weight;
3198
3199                                let ratio_step_cap = if second_smallest_ratio == MAXFLOAT {
3200                                    MAXFLOAT
3201                                } else {
3202                                    second_smallest_ratio - smallest_ratio
3203                                };
3204
3205                                let mut resized_any = false;
3206
3207                                let mut j = 0;
3208                                while j < resizable_container_buffer.len() {
3209                                    let child_idx = resizable_container_buffer[j] as usize;
3210                                    let child_layout_idx =
3211                                        self.layout_elements[child_idx].layout_config_index;
3212                                    let child_sizing = if x_axis {
3213                                        self.layout_configs[child_layout_idx].sizing.width
3214                                    } else {
3215                                        self.layout_configs[child_layout_idx].sizing.height
3216                                    };
3217
3218                                    let child_size_ref = if x_axis {
3219                                        &mut self.layout_elements[child_idx].dimensions.width
3220                                    } else {
3221                                        &mut self.layout_elements[child_idx].dimensions.height
3222                                    };
3223
3224                                    let child_ratio = *child_size_ref / child_sizing.grow_weight;
3225                                    if !float_equal(child_ratio, smallest_ratio) {
3226                                        j += 1;
3227                                        continue;
3228                                    }
3229
3230                                    let max_size = if x_axis {
3231                                        self.layout_configs[child_layout_idx]
3232                                            .sizing
3233                                            .width
3234                                            .min_max
3235                                            .max
3236                                    } else {
3237                                        self.layout_configs[child_layout_idx]
3238                                            .sizing
3239                                            .height
3240                                            .min_max
3241                                            .max
3242                                    };
3243
3244                                    let mut growth_share = per_weight_growth * child_sizing.grow_weight;
3245                                    if ratio_step_cap != MAXFLOAT {
3246                                        growth_share =
3247                                            f32::min(growth_share, ratio_step_cap * child_sizing.grow_weight);
3248                                    }
3249
3250                                    let previous = *child_size_ref;
3251                                    let proposed = previous + growth_share;
3252                                    if proposed >= max_size {
3253                                        *child_size_ref = max_size;
3254                                        resized_any = true;
3255                                        resizable_container_buffer.swap_remove(j);
3256                                        continue;
3257                                    }
3258
3259                                    *child_size_ref = proposed;
3260                                    distribute -= *child_size_ref - previous;
3261                                    resized_any = true;
3262                                    j += 1;
3263                                }
3264
3265                                if !resized_any {
3266                                    break;
3267                                }
3268                            }
3269                        }
3270                    }
3271                } else {
3272                    // Off-axis sizing
3273                    for &child_idx in &resizable_container_buffer {
3274                        let child_idx = child_idx as usize;
3275                        let child_layout_idx =
3276                            self.layout_elements[child_idx].layout_config_index;
3277                        let child_sizing = if x_axis {
3278                            self.layout_configs[child_layout_idx].sizing.width
3279                        } else {
3280                            self.layout_configs[child_layout_idx].sizing.height
3281                        };
3282                        let min_size = if x_axis {
3283                            self.layout_elements[child_idx].min_dimensions.width
3284                        } else {
3285                            self.layout_elements[child_idx].min_dimensions.height
3286                        };
3287
3288                        let mut max_size = parent_size - parent_padding;
3289                        if let Some(clip_idx) =
3290                            self.find_element_config_index(parent_index, ElementConfigType::Clip)
3291                        {
3292                            let clip = &self.clip_element_configs[clip_idx];
3293                            if (x_axis && clip.horizontal) || (!x_axis && clip.vertical) {
3294                                max_size = f32::max(max_size, inner_content_size);
3295                            }
3296                        }
3297
3298                        let is_cover_aspect = self
3299                            .find_element_config_index(child_idx, ElementConfigType::Aspect)
3300                            .map(|cfg_idx| self.aspect_ratio_cover_configs[cfg_idx])
3301                            .unwrap_or(false);
3302
3303                        let child_size_ref = if x_axis {
3304                            &mut self.layout_elements[child_idx].dimensions.width
3305                        } else {
3306                            &mut self.layout_elements[child_idx].dimensions.height
3307                        };
3308
3309                        if child_sizing.type_ == SizingType::Grow && child_sizing.grow_weight > 0.0 {
3310                            if is_cover_aspect {
3311                                *child_size_ref = f32::max(*child_size_ref, max_size);
3312                            } else {
3313                                *child_size_ref = f32::min(max_size, child_sizing.min_max.max);
3314                            }
3315                        }
3316
3317                        if is_cover_aspect {
3318                            *child_size_ref = f32::max(min_size, *child_size_ref);
3319                        } else {
3320                            *child_size_ref = f32::max(min_size, f32::min(*child_size_ref, max_size));
3321                        }
3322                    }
3323                }
3324            }
3325        }
3326    }
3327
3328    fn calculate_final_layout(&mut self) {
3329        // Size along X axis
3330        self.size_containers_along_axis(true);
3331
3332        // Wrap text
3333        self.wrap_text();
3334
3335        // Scale vertical heights by aspect ratio
3336        for i in 0..self.aspect_ratio_element_indexes.len() {
3337            let elem_idx = self.aspect_ratio_element_indexes[i] as usize;
3338            if let Some(cfg_idx) =
3339                self.find_element_config_index(elem_idx, ElementConfigType::Aspect)
3340            {
3341                let aspect_ratio = self.aspect_ratio_configs[cfg_idx];
3342                let is_cover = self.aspect_ratio_cover_configs[cfg_idx];
3343                let new_height =
3344                    (1.0 / aspect_ratio) * self.layout_elements[elem_idx].dimensions.width;
3345                self.layout_elements[elem_idx].dimensions.height = new_height;
3346                let layout_idx = self.layout_elements[elem_idx].layout_config_index;
3347                self.layout_configs[layout_idx].sizing.height.min_max.min = new_height;
3348                self.layout_configs[layout_idx].sizing.height.min_max.max = if is_cover {
3349                    MAXFLOAT
3350                } else {
3351                    new_height
3352                };
3353            }
3354        }
3355
3356        // Propagate height changes up tree (DFS)
3357        self.propagate_sizes_up_tree();
3358
3359        // Size along Y axis
3360        self.size_containers_along_axis(false);
3361
3362        // Scale horizontal widths by aspect ratio
3363        for i in 0..self.aspect_ratio_element_indexes.len() {
3364            let elem_idx = self.aspect_ratio_element_indexes[i] as usize;
3365            if let Some(cfg_idx) =
3366                self.find_element_config_index(elem_idx, ElementConfigType::Aspect)
3367            {
3368                let aspect_ratio = self.aspect_ratio_configs[cfg_idx];
3369                let is_cover = self.aspect_ratio_cover_configs[cfg_idx];
3370                let new_width =
3371                    aspect_ratio * self.layout_elements[elem_idx].dimensions.height;
3372                self.layout_elements[elem_idx].dimensions.width = new_width;
3373                let layout_idx = self.layout_elements[elem_idx].layout_config_index;
3374                self.layout_configs[layout_idx].sizing.width.min_max.min = new_width;
3375                self.layout_configs[layout_idx].sizing.width.min_max.max = if is_cover {
3376                    MAXFLOAT
3377                } else {
3378                    new_width
3379                };
3380            }
3381        }
3382
3383        // Sort tree roots by z-index (bubble sort)
3384        let mut sort_max = self.layout_element_tree_roots.len().saturating_sub(1);
3385        while sort_max > 0 {
3386            for i in 0..sort_max {
3387                if self.layout_element_tree_roots[i + 1].z_index
3388                    < self.layout_element_tree_roots[i].z_index
3389                {
3390                    self.layout_element_tree_roots.swap(i, i + 1);
3391                }
3392            }
3393            sort_max -= 1;
3394        }
3395
3396        // Generate render commands
3397        self.generate_render_commands();
3398    }
3399
3400    fn wrap_text(&mut self) {
3401        for text_idx in 0..self.text_element_data.len() {
3402            let elem_index = self.text_element_data[text_idx].element_index as usize;
3403            let text = self.text_element_data[text_idx].text.clone();
3404            let preferred_dims = self.text_element_data[text_idx].preferred_dimensions;
3405
3406            self.text_element_data[text_idx].wrapped_lines_start = self.wrapped_text_lines.len();
3407            self.text_element_data[text_idx].wrapped_lines_length = 0;
3408
3409            let container_width = self.layout_elements[elem_index].dimensions.width;
3410
3411            // Find text config
3412            let text_config_idx = self
3413                .find_element_config_index(elem_index, ElementConfigType::Text)
3414                .unwrap_or(0);
3415            let text_config = self.text_element_configs[text_config_idx].clone();
3416
3417            let measured = self.measure_text_cached(&text, &text_config);
3418
3419            let line_height = if text_config.line_height > 0 {
3420                text_config.line_height as f32
3421            } else {
3422                preferred_dims.height
3423            };
3424
3425            if !measured.contains_newlines && preferred_dims.width <= container_width {
3426                // Single line
3427                self.wrapped_text_lines.push(WrappedTextLine {
3428                    dimensions: self.layout_elements[elem_index].dimensions,
3429                    start: 0,
3430                    length: text.len(),
3431                });
3432                self.text_element_data[text_idx].wrapped_lines_length = 1;
3433                continue;
3434            }
3435
3436            // Multi-line wrapping
3437            let measure_fn = self.measure_text_fn.as_ref().unwrap();
3438            let space_width = {
3439                let space_config = text_config.clone();
3440                measure_fn(" ", &space_config).width
3441            };
3442
3443            let mut word_index = measured.measured_words_start_index;
3444            let mut line_width: f32 = 0.0;
3445            let mut line_length_chars: i32 = 0;
3446            let mut line_start_offset: i32 = 0;
3447
3448            while word_index != -1 {
3449                let measured_word = self.measured_words[word_index as usize];
3450
3451                // Word doesn't fit but it's the only word on the line
3452                if line_length_chars == 0 && line_width + measured_word.width > container_width {
3453                    self.wrapped_text_lines.push(WrappedTextLine {
3454                        dimensions: Dimensions::new(measured_word.width, line_height),
3455                        start: measured_word.start_offset as usize,
3456                        length: measured_word.length as usize,
3457                    });
3458                    self.text_element_data[text_idx].wrapped_lines_length += 1;
3459                    word_index = measured_word.next;
3460                    line_start_offset = measured_word.start_offset + measured_word.length;
3461                }
3462                // Newline or overflow
3463                else if measured_word.length == 0
3464                    || line_width + measured_word.width > container_width
3465                {
3466                    let text_bytes = text.as_bytes();
3467                    let final_char_idx = (line_start_offset + line_length_chars - 1).max(0) as usize;
3468                    let final_char_is_space =
3469                        final_char_idx < text_bytes.len() && text_bytes[final_char_idx] == b' ';
3470                    let adj_width = line_width
3471                        + if final_char_is_space {
3472                            -space_width
3473                        } else {
3474                            0.0
3475                        };
3476                    let adj_length = line_length_chars
3477                        + if final_char_is_space { -1 } else { 0 };
3478
3479                    self.wrapped_text_lines.push(WrappedTextLine {
3480                        dimensions: Dimensions::new(adj_width, line_height),
3481                        start: line_start_offset as usize,
3482                        length: adj_length as usize,
3483                    });
3484                    self.text_element_data[text_idx].wrapped_lines_length += 1;
3485
3486                    if line_length_chars == 0 || measured_word.length == 0 {
3487                        word_index = measured_word.next;
3488                    }
3489                    line_width = 0.0;
3490                    line_length_chars = 0;
3491                    line_start_offset = measured_word.start_offset;
3492                } else {
3493                    line_width += measured_word.width + text_config.letter_spacing as f32;
3494                    line_length_chars += measured_word.length;
3495                    word_index = measured_word.next;
3496                }
3497            }
3498
3499            if line_length_chars > 0 {
3500                self.wrapped_text_lines.push(WrappedTextLine {
3501                    dimensions: Dimensions::new(
3502                        line_width - text_config.letter_spacing as f32,
3503                        line_height,
3504                    ),
3505                    start: line_start_offset as usize,
3506                    length: line_length_chars as usize,
3507                });
3508                self.text_element_data[text_idx].wrapped_lines_length += 1;
3509            }
3510
3511            let num_lines = self.text_element_data[text_idx].wrapped_lines_length;
3512            self.layout_elements[elem_index].dimensions.height =
3513                line_height * num_lines as f32;
3514        }
3515    }
3516
3517    fn propagate_sizes_up_tree(&mut self) {
3518        let mut dfs_buffer: Vec<i32> = Vec::new();
3519        let mut visited: Vec<bool> = Vec::new();
3520
3521        for i in 0..self.layout_element_tree_roots.len() {
3522            let root = self.layout_element_tree_roots[i];
3523            dfs_buffer.push(root.layout_element_index);
3524            visited.push(false);
3525        }
3526
3527        while !dfs_buffer.is_empty() {
3528            let buf_idx = dfs_buffer.len() - 1;
3529            let current_elem_idx = dfs_buffer[buf_idx] as usize;
3530
3531            if !visited[buf_idx] {
3532                visited[buf_idx] = true;
3533                let is_text =
3534                    self.element_has_config(current_elem_idx, ElementConfigType::Text);
3535                let children_length = self.layout_elements[current_elem_idx].children_length;
3536                if is_text || children_length == 0 {
3537                    dfs_buffer.pop();
3538                    visited.pop();
3539                    continue;
3540                }
3541                let children_start = self.layout_elements[current_elem_idx].children_start;
3542                for j in 0..children_length as usize {
3543                    let child_idx = self.layout_element_children[children_start + j];
3544                    dfs_buffer.push(child_idx);
3545                    visited.push(false);
3546                }
3547                continue;
3548            }
3549
3550            dfs_buffer.pop();
3551            visited.pop();
3552
3553            let layout_idx = self.layout_elements[current_elem_idx].layout_config_index;
3554            let layout_config = self.layout_configs[layout_idx];
3555            let children_start = self.layout_elements[current_elem_idx].children_start;
3556            let children_length = self.layout_elements[current_elem_idx].children_length;
3557
3558            if layout_config.layout_direction == LayoutDirection::LeftToRight {
3559                if layout_config.wrap {
3560                    let lines = self.compute_wrapped_lines(current_elem_idx, true);
3561                    let mut content_height =
3562                        layout_config.padding.top as f32 + layout_config.padding.bottom as f32;
3563                    if !lines.is_empty() {
3564                        content_height +=
3565                            lines.iter().map(|line| line.cross_size).sum::<f32>();
3566                        content_height +=
3567                            lines.len().saturating_sub(1) as f32 * layout_config.wrap_gap as f32;
3568                    }
3569                    self.layout_elements[current_elem_idx].dimensions.height = f32::min(
3570                        f32::max(content_height, layout_config.sizing.height.min_max.min),
3571                        layout_config.sizing.height.min_max.max,
3572                    );
3573                } else {
3574                    for j in 0..children_length as usize {
3575                        let child_idx =
3576                            self.layout_element_children[children_start + j] as usize;
3577                        let child_height_with_padding = f32::max(
3578                            self.layout_elements[child_idx].dimensions.height
3579                                + layout_config.padding.top as f32
3580                                + layout_config.padding.bottom as f32,
3581                            self.layout_elements[current_elem_idx].dimensions.height,
3582                        );
3583                        self.layout_elements[current_elem_idx].dimensions.height = f32::min(
3584                            f32::max(
3585                                child_height_with_padding,
3586                                layout_config.sizing.height.min_max.min,
3587                            ),
3588                            layout_config.sizing.height.min_max.max,
3589                        );
3590                    }
3591                }
3592            } else if layout_config.wrap {
3593                let lines = self.compute_wrapped_lines(current_elem_idx, false);
3594
3595                let max_column_height = lines
3596                    .iter()
3597                    .fold(0.0_f32, |acc, line| acc.max(line.main_size));
3598                let content_height = layout_config.padding.top as f32
3599                    + layout_config.padding.bottom as f32
3600                    + max_column_height;
3601
3602                self.layout_elements[current_elem_idx].dimensions.height = f32::min(
3603                    f32::max(content_height, layout_config.sizing.height.min_max.min),
3604                    layout_config.sizing.height.min_max.max,
3605                );
3606
3607                if layout_config.sizing.width.type_ != SizingType::Percent {
3608                    let mut content_width =
3609                        layout_config.padding.left as f32 + layout_config.padding.right as f32;
3610                    if !lines.is_empty() {
3611                        content_width += lines.iter().map(|line| line.cross_size).sum::<f32>();
3612                        content_width +=
3613                            lines.len().saturating_sub(1) as f32 * layout_config.wrap_gap as f32;
3614                    }
3615
3616                    self.layout_elements[current_elem_idx].dimensions.width = f32::min(
3617                        f32::max(content_width, layout_config.sizing.width.min_max.min),
3618                        layout_config.sizing.width.min_max.max,
3619                    );
3620                }
3621            } else {
3622                let mut content_height = layout_config.padding.top as f32
3623                    + layout_config.padding.bottom as f32;
3624                for j in 0..children_length as usize {
3625                    let child_idx =
3626                        self.layout_element_children[children_start + j] as usize;
3627                    content_height += self.layout_elements[child_idx].dimensions.height;
3628                }
3629                content_height += children_length.saturating_sub(1) as f32
3630                    * layout_config.child_gap as f32;
3631                self.layout_elements[current_elem_idx].dimensions.height = f32::min(
3632                    f32::max(content_height, layout_config.sizing.height.min_max.min),
3633                    layout_config.sizing.height.min_max.max,
3634                );
3635            }
3636        }
3637    }
3638
3639    fn element_is_offscreen(&self, bbox: &BoundingBox) -> bool {
3640        if self.culling_disabled {
3641            return false;
3642        }
3643        bbox.x > self.layout_dimensions.width
3644            || bbox.y > self.layout_dimensions.height
3645            || bbox.x + bbox.width < 0.0
3646            || bbox.y + bbox.height < 0.0
3647    }
3648
3649    fn add_render_command(&mut self, cmd: InternalRenderCommand<CustomElementData>) {
3650        self.render_commands.push(cmd);
3651    }
3652
3653    fn generate_render_commands(&mut self) {
3654        self.render_commands.clear();
3655        let mut dfs_buffer: Vec<LayoutElementTreeNode> = Vec::new();
3656        let mut visited: Vec<bool> = Vec::new();
3657
3658        for root_index in 0..self.layout_element_tree_roots.len() {
3659            dfs_buffer.clear();
3660            visited.clear();
3661            let root = self.layout_element_tree_roots[root_index];
3662            let root_elem_idx = root.layout_element_index as usize;
3663            let root_element = &self.layout_elements[root_elem_idx];
3664            let mut root_position = Vector2::default();
3665
3666            // Position floating containers
3667            if self.element_has_config(root_elem_idx, ElementConfigType::Floating) {
3668                if let Some(parent_item) = self.layout_element_map.get(&root.parent_id) {
3669                    let parent_bbox = parent_item.bounding_box;
3670                    if let Some(float_cfg_idx) = self
3671                        .find_element_config_index(root_elem_idx, ElementConfigType::Floating)
3672                    {
3673                        let config = &self.floating_element_configs[float_cfg_idx];
3674                        let root_dims = root_element.dimensions;
3675                        let mut target = Vector2::default();
3676
3677                        // X position - parent attach point
3678                        match config.attach_points.parent_x {
3679                            AlignX::Left => {
3680                                target.x = parent_bbox.x;
3681                            }
3682                            AlignX::CenterX => {
3683                                target.x = parent_bbox.x + parent_bbox.width / 2.0;
3684                            }
3685                            AlignX::Right => {
3686                                target.x = parent_bbox.x + parent_bbox.width;
3687                            }
3688                        }
3689                        // X position - element attach point
3690                        match config.attach_points.element_x {
3691                            AlignX::Left => {}
3692                            AlignX::CenterX => {
3693                                target.x -= root_dims.width / 2.0;
3694                            }
3695                            AlignX::Right => {
3696                                target.x -= root_dims.width;
3697                            }
3698                        }
3699                        // Y position - parent attach point
3700                        match config.attach_points.parent_y {
3701                            AlignY::Top => {
3702                                target.y = parent_bbox.y;
3703                            }
3704                            AlignY::CenterY => {
3705                                target.y = parent_bbox.y + parent_bbox.height / 2.0;
3706                            }
3707                            AlignY::Bottom => {
3708                                target.y = parent_bbox.y + parent_bbox.height;
3709                            }
3710                        }
3711                        // Y position - element attach point
3712                        match config.attach_points.element_y {
3713                            AlignY::Top => {}
3714                            AlignY::CenterY => {
3715                                target.y -= root_dims.height / 2.0;
3716                            }
3717                            AlignY::Bottom => {
3718                                target.y -= root_dims.height;
3719                            }
3720                        }
3721                        target.x += config.offset.x;
3722                        target.y += config.offset.y;
3723                        root_position = target;
3724                    }
3725                }
3726            }
3727
3728            // Clip scissor start
3729            if root.clip_element_id != 0 {
3730                if let Some(clip_item) = self.layout_element_map.get(&root.clip_element_id) {
3731                    let clip_bbox = clip_item.bounding_box;
3732                    self.add_render_command(InternalRenderCommand {
3733                        bounding_box: clip_bbox,
3734                        command_type: RenderCommandType::ScissorStart,
3735                        id: hash_number(
3736                            root_element.id,
3737                            root_element.children_length as u32 + 10,
3738                        )
3739                        .id,
3740                        z_index: root.z_index,
3741                        ..Default::default()
3742                    });
3743                }
3744            }
3745
3746            let root_layout_idx = self.layout_elements[root_elem_idx].layout_config_index;
3747            let root_padding_left = self.layout_configs[root_layout_idx].padding.left as f32;
3748            let root_padding_top = self.layout_configs[root_layout_idx].padding.top as f32;
3749
3750            dfs_buffer.push(LayoutElementTreeNode {
3751                layout_element_index: root.layout_element_index,
3752                position: root_position,
3753                next_child_offset: Vector2::new(root_padding_left, root_padding_top),
3754            });
3755            visited.push(false);
3756
3757            while !dfs_buffer.is_empty() {
3758                let buf_idx = dfs_buffer.len() - 1;
3759                let current_node = dfs_buffer[buf_idx];
3760                let current_elem_idx = current_node.layout_element_index as usize;
3761                let layout_idx = self.layout_elements[current_elem_idx].layout_config_index;
3762                let layout_config = self.layout_configs[layout_idx];
3763                let mut scroll_offset = Vector2::default();
3764
3765                if !visited[buf_idx] {
3766                    visited[buf_idx] = true;
3767
3768                    let current_bbox = BoundingBox::new(
3769                        current_node.position.x,
3770                        current_node.position.y,
3771                        self.layout_elements[current_elem_idx].dimensions.width,
3772                        self.layout_elements[current_elem_idx].dimensions.height,
3773                    );
3774
3775                    // Apply scroll offset
3776                    let mut _scroll_container_data_idx: Option<usize> = None;
3777                    if self.element_has_config(current_elem_idx, ElementConfigType::Clip) {
3778                        if let Some(clip_cfg_idx) = self
3779                            .find_element_config_index(current_elem_idx, ElementConfigType::Clip)
3780                        {
3781                            let clip_config = self.clip_element_configs[clip_cfg_idx];
3782                            for si in 0..self.scroll_container_datas.len() {
3783                                if self.scroll_container_datas[si].layout_element_index
3784                                    == current_elem_idx as i32
3785                                {
3786                                    _scroll_container_data_idx = Some(si);
3787                                    self.scroll_container_datas[si].bounding_box = current_bbox;
3788                                    scroll_offset = clip_config.child_offset;
3789                                    break;
3790                                }
3791                            }
3792                        }
3793                    }
3794
3795                    // Update hash map bounding box
3796                    let elem_id = self.layout_elements[current_elem_idx].id;
3797                    if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
3798                        item.bounding_box = current_bbox;
3799                    }
3800
3801                    // Generate render commands for this element
3802                    let shared_config = self
3803                        .find_element_config_index(current_elem_idx, ElementConfigType::Shared)
3804                        .map(|idx| self.shared_element_configs[idx]);
3805                    let shared = shared_config.unwrap_or_default();
3806                    let mut emit_rectangle = shared.background_color.a > 0.0;
3807                    let offscreen = self.element_is_offscreen(&current_bbox);
3808                    let should_render_base = !offscreen;
3809
3810                    // Get per-element shader effects
3811                    let elem_effects = self.element_effects.get(current_elem_idx).cloned().unwrap_or_default();
3812
3813                    // Get per-element visual rotation
3814                    let elem_visual_rotation = self.element_visual_rotations.get(current_elem_idx).cloned().flatten();
3815                    // Filter out no-op rotations
3816                    let elem_visual_rotation = elem_visual_rotation.filter(|vr| !vr.is_noop());
3817
3818                    // Get per-element shape rotation and compute original bbox
3819                    let elem_shape_rotation = self.element_shape_rotations.get(current_elem_idx).cloned().flatten()
3820                        .filter(|sr| !sr.is_noop());
3821                    // If shape rotation is active, current_bbox has AABB dims.
3822                    // Compute the original-dimension bbox centered within the AABB.
3823                    let shape_draw_bbox = if let Some(ref _sr) = elem_shape_rotation {
3824                        if let Some(orig_dims) = self.element_pre_rotation_dimensions.get(current_elem_idx).copied().flatten() {
3825                            let offset_x = (current_bbox.width - orig_dims.width) / 2.0;
3826                            let offset_y = (current_bbox.height - orig_dims.height) / 2.0;
3827                            BoundingBox::new(
3828                                current_bbox.x + offset_x,
3829                                current_bbox.y + offset_y,
3830                                orig_dims.width,
3831                                orig_dims.height,
3832                            )
3833                        } else {
3834                            current_bbox
3835                        }
3836                    } else {
3837                        current_bbox
3838                    };
3839
3840                    // Emit GroupBegin commands for group shaders BEFORE element drawing
3841                    // so that the element's background, children, and border are all captured.
3842                    // If visual_rotation is present, it is attached to the outermost group.
3843                    let elem_shaders = self.element_shaders.get(current_elem_idx).cloned().unwrap_or_default();
3844
3845                    if !elem_shaders.is_empty() {
3846                        // Emit GroupBegin for each shader (outermost first = reversed order)
3847                        for (i, shader) in elem_shaders.iter().rev().enumerate() {
3848                            // Attach visual_rotation to the outermost GroupBegin (i == 0)
3849                            let vr = if i == 0 { elem_visual_rotation } else { None };
3850                            self.add_render_command(InternalRenderCommand {
3851                                bounding_box: current_bbox,
3852                                command_type: RenderCommandType::GroupBegin,
3853                                effects: vec![shader.clone()],
3854                                id: elem_id,
3855                                z_index: root.z_index,
3856                                visual_rotation: vr,
3857                                ..Default::default()
3858                            });
3859                        }
3860                    } else if let Some(vr) = elem_visual_rotation {
3861                        // No shaders but visual rotation: emit standalone GroupBegin/End
3862                        self.add_render_command(InternalRenderCommand {
3863                            bounding_box: current_bbox,
3864                            command_type: RenderCommandType::GroupBegin,
3865                            effects: vec![],
3866                            id: elem_id,
3867                            z_index: root.z_index,
3868                            visual_rotation: Some(vr),
3869                            ..Default::default()
3870                        });
3871                    }
3872
3873                    // Process each config
3874                    let configs_start = self.layout_elements[current_elem_idx].element_configs.start;
3875                    let configs_length =
3876                        self.layout_elements[current_elem_idx].element_configs.length;
3877
3878                    for cfg_i in 0..configs_length {
3879                        let config = self.element_configs[configs_start + cfg_i as usize];
3880                        let should_render = should_render_base;
3881
3882                        match config.config_type {
3883                            ElementConfigType::Shared
3884                            | ElementConfigType::Aspect
3885                            | ElementConfigType::Floating
3886                            | ElementConfigType::Border => {}
3887                            ElementConfigType::Clip => {
3888                                let clip = &self.clip_element_configs[config.config_index];
3889                                self.add_render_command(InternalRenderCommand {
3890                                    bounding_box: current_bbox,
3891                                    command_type: RenderCommandType::ScissorStart,
3892                                    render_data: InternalRenderData::Clip {
3893                                        horizontal: clip.horizontal,
3894                                        vertical: clip.vertical,
3895                                    },
3896                                    user_data: 0,
3897                                    id: elem_id,
3898                                    z_index: root.z_index,
3899                                    visual_rotation: None,
3900                                    shape_rotation: None,
3901                                    effects: Vec::new(),
3902                                });
3903                            }
3904                            ElementConfigType::Image => {
3905                                if should_render {
3906                                    let image_data =
3907                                        self.image_element_configs[config.config_index].clone();
3908                                    self.add_render_command(InternalRenderCommand {
3909                                        bounding_box: shape_draw_bbox,
3910                                        command_type: RenderCommandType::Image,
3911                                        render_data: InternalRenderData::Image {
3912                                            background_color: shared.background_color,
3913                                            corner_radius: shared.corner_radius,
3914                                            image_data,
3915                                        },
3916                                        user_data: shared.user_data,
3917                                        id: elem_id,
3918                                        z_index: root.z_index,
3919                                        visual_rotation: None,
3920                                        shape_rotation: elem_shape_rotation,
3921                                        effects: elem_effects.clone(),
3922                                    });
3923                                }
3924                                emit_rectangle = false;
3925                            }
3926                            ElementConfigType::Text => {
3927                                if !should_render {
3928                                    continue;
3929                                }
3930                                let text_config =
3931                                    self.text_element_configs[config.config_index].clone();
3932                                let text_data_idx =
3933                                    self.layout_elements[current_elem_idx].text_data_index;
3934                                if text_data_idx < 0 {
3935                                    continue;
3936                                }
3937                                let text_data = &self.text_element_data[text_data_idx as usize];
3938                                let natural_line_height = text_data.preferred_dimensions.height;
3939                                let final_line_height = if text_config.line_height > 0 {
3940                                    text_config.line_height as f32
3941                                } else {
3942                                    natural_line_height
3943                                };
3944                                let line_height_offset =
3945                                    (final_line_height - natural_line_height) / 2.0;
3946                                let mut y_position = line_height_offset;
3947
3948                                let lines_start = text_data.wrapped_lines_start;
3949                                let lines_length = text_data.wrapped_lines_length;
3950                                let parent_text = text_data.text.clone();
3951
3952                                // Collect line data first to avoid borrow issues
3953                                let lines_data: Vec<_> = (0..lines_length)
3954                                    .map(|li| {
3955                                        let line = &self.wrapped_text_lines[lines_start + li as usize];
3956                                        (line.start, line.length, line.dimensions)
3957                                    })
3958                                    .collect();
3959
3960                                for (line_index, &(start, length, line_dims)) in lines_data.iter().enumerate() {
3961                                    if length == 0 {
3962                                        y_position += final_line_height;
3963                                        continue;
3964                                    }
3965
3966                                    let line_text = parent_text[start..start + length].to_string();
3967
3968                                    let align_width = if buf_idx > 0 {
3969                                        let parent_node = dfs_buffer[buf_idx - 1];
3970                                        let parent_elem_idx =
3971                                            parent_node.layout_element_index as usize;
3972                                        let parent_layout_idx = self.layout_elements
3973                                            [parent_elem_idx]
3974                                            .layout_config_index;
3975                                        let pp = self.layout_configs[parent_layout_idx].padding;
3976                                        self.layout_elements[parent_elem_idx].dimensions.width
3977                                            - pp.left as f32
3978                                            - pp.right as f32
3979                                    } else {
3980                                        current_bbox.width
3981                                    };
3982
3983                                    let mut offset = align_width - line_dims.width;
3984                                    if text_config.alignment == AlignX::Left {
3985                                        offset = 0.0;
3986                                    }
3987                                    if text_config.alignment == AlignX::CenterX {
3988                                        offset /= 2.0;
3989                                    }
3990
3991                                    self.add_render_command(InternalRenderCommand {
3992                                        bounding_box: BoundingBox::new(
3993                                            current_bbox.x + offset,
3994                                            current_bbox.y + y_position,
3995                                            line_dims.width,
3996                                            line_dims.height,
3997                                        ),
3998                                        command_type: RenderCommandType::Text,
3999                                        render_data: InternalRenderData::Text {
4000                                            text: line_text,
4001                                            text_color: text_config.color,
4002                                            font_size: text_config.font_size,
4003                                            letter_spacing: text_config.letter_spacing,
4004                                            line_height: text_config.line_height,
4005                                            font_asset: text_config.font_asset,
4006                                        },
4007                                        user_data: text_config.user_data,
4008                                        id: hash_number(line_index as u32, elem_id).id,
4009                                        z_index: root.z_index,
4010                                        visual_rotation: None,
4011                                        shape_rotation: None,
4012                                        effects: text_config.effects.clone(),
4013                                    });
4014                                    y_position += final_line_height;
4015                                }
4016                            }
4017                            ElementConfigType::Custom => {
4018                                if should_render {
4019                                    let custom_data =
4020                                        self.custom_element_configs[config.config_index].clone();
4021                                    self.add_render_command(InternalRenderCommand {
4022                                        bounding_box: shape_draw_bbox,
4023                                        command_type: RenderCommandType::Custom,
4024                                        render_data: InternalRenderData::Custom {
4025                                            background_color: shared.background_color,
4026                                            corner_radius: shared.corner_radius,
4027                                            custom_data,
4028                                        },
4029                                        user_data: shared.user_data,
4030                                        id: elem_id,
4031                                        z_index: root.z_index,
4032                                        visual_rotation: None,
4033                                        shape_rotation: elem_shape_rotation,
4034                                        effects: elem_effects.clone(),
4035                                    });
4036                                }
4037                                emit_rectangle = false;
4038                            }
4039                            ElementConfigType::TextInput => {
4040                                if should_render {
4041                                    let ti_config = self.text_input_configs[config.config_index].clone();
4042                                    let is_focused = self.focused_element_id == elem_id;
4043
4044                                    // Emit background rectangle FIRST so text renders on top
4045                                    if shared.background_color.a > 0.0 || !shared.corner_radius.is_zero() {
4046                                        self.add_render_command(InternalRenderCommand {
4047                                            bounding_box: shape_draw_bbox,
4048                                            command_type: RenderCommandType::Rectangle,
4049                                            render_data: InternalRenderData::Rectangle {
4050                                                background_color: shared.background_color,
4051                                                corner_radius: shared.corner_radius,
4052                                            },
4053                                            user_data: shared.user_data,
4054                                            id: elem_id,
4055                                            z_index: root.z_index,
4056                                            visual_rotation: None,
4057                                            shape_rotation: elem_shape_rotation,
4058                                            effects: elem_effects.clone(),
4059                                        });
4060                                    }
4061
4062                                    // Get or create edit state
4063                                    let state = self.text_edit_states
4064                                        .entry(elem_id)
4065                                        .or_insert_with(crate::text_input::TextEditState::default)
4066                                        .clone();
4067
4068                                    let disp_text = crate::text_input::display_text(
4069                                        &state.text,
4070                                        &ti_config.placeholder,
4071                                        ti_config.is_password && !ti_config.is_multiline,
4072                                    );
4073
4074                                    let is_placeholder = state.text.is_empty();
4075                                    let text_color = if is_placeholder {
4076                                        ti_config.placeholder_color
4077                                    } else {
4078                                        ti_config.text_color
4079                                    };
4080                                    let mut content_width = 0.0_f32;
4081                                    let mut content_height = 0.0_f32;
4082                                    let mut scroll_pos_x = state.scroll_offset;
4083                                    let mut scroll_pos_y = state.scroll_offset_y;
4084
4085                                    // Measure font height for cursor
4086                                    let natural_font_height = self.font_height(ti_config.font_asset, ti_config.font_size);
4087                                    let line_step = if ti_config.line_height > 0 {
4088                                        ti_config.line_height as f32
4089                                    } else {
4090                                        natural_font_height
4091                                    };
4092                                    // Offset to vertically center text/cursor within each line slot
4093                                    let line_y_offset = (line_step - natural_font_height) / 2.0;
4094
4095                                    // Clip text content to the element's bounding box
4096                                    self.add_render_command(InternalRenderCommand {
4097                                        bounding_box: current_bbox,
4098                                        command_type: RenderCommandType::ScissorStart,
4099                                        render_data: InternalRenderData::Clip {
4100                                            horizontal: true,
4101                                            vertical: true,
4102                                        },
4103                                        user_data: 0,
4104                                        id: hash_number(1000, elem_id).id,
4105                                        z_index: root.z_index,
4106                                        visual_rotation: None,
4107                                        shape_rotation: None,
4108                                        effects: Vec::new(),
4109                                    });
4110
4111                                    if ti_config.is_multiline {
4112                                        // ── Multiline rendering (with word wrapping) ──
4113                                        let scroll_offset_x = state.scroll_offset;
4114                                        let scroll_offset_y = state.scroll_offset_y;
4115
4116                                        let visual_lines = if let Some(ref measure_fn) = self.measure_text_fn {
4117                                            crate::text_input::wrap_lines(
4118                                                &disp_text,
4119                                                current_bbox.width,
4120                                                ti_config.font_asset,
4121                                                ti_config.font_size,
4122                                                measure_fn.as_ref(),
4123                                            )
4124                                        } else {
4125                                            vec![crate::text_input::VisualLine {
4126                                                text: disp_text.clone(),
4127                                                global_char_start: 0,
4128                                                char_count: disp_text.chars().count(),
4129                                            }]
4130                                        };
4131
4132                                        let (cursor_line, cursor_col) = if is_placeholder {
4133                                            (0, 0)
4134                                        } else {
4135                                            #[cfg(feature = "text-styling")]
4136                                            let raw_cursor = state.cursor_pos_raw();
4137                                            #[cfg(not(feature = "text-styling"))]
4138                                            let raw_cursor = state.cursor_pos;
4139                                            crate::text_input::cursor_to_visual_pos(&visual_lines, raw_cursor)
4140                                        };
4141
4142                                        // Compute per-line char positions
4143                                        let line_positions: Vec<Vec<f32>> = if let Some(ref measure_fn) = self.measure_text_fn {
4144                                            visual_lines.iter().map(|vl| {
4145                                                crate::text_input::compute_char_x_positions(
4146                                                    &vl.text,
4147                                                    ti_config.font_asset,
4148                                                    ti_config.font_size,
4149                                                    measure_fn.as_ref(),
4150                                                )
4151                                            }).collect()
4152                                        } else {
4153                                            visual_lines.iter().map(|_| vec![0.0]).collect()
4154                                        };
4155                                        content_width = line_positions.iter()
4156                                            .filter_map(|p| p.last().copied())
4157                                            .fold(0.0_f32, |a, b| a.max(b));
4158                                        content_height = visual_lines.len() as f32 * line_step;
4159                                        scroll_pos_x = scroll_offset_x;
4160                                        scroll_pos_y = scroll_offset_y;
4161
4162                                        // Selection rendering (multiline)
4163                                        if is_focused {
4164                                            #[cfg(feature = "text-styling")]
4165                                            let sel_range = state.selection_range_raw();
4166                                            #[cfg(not(feature = "text-styling"))]
4167                                            let sel_range = state.selection_range();
4168                                            if let Some((sel_start, sel_end)) = sel_range {
4169                                                let (sel_start_line, sel_start_col) = crate::text_input::cursor_to_visual_pos(&visual_lines, sel_start);
4170                                                let (sel_end_line, sel_end_col) = crate::text_input::cursor_to_visual_pos(&visual_lines, sel_end);
4171                                                for (line_idx, vl) in visual_lines.iter().enumerate() {
4172                                                    if line_idx < sel_start_line || line_idx > sel_end_line {
4173                                                        continue;
4174                                                    }
4175                                                    let positions = &line_positions[line_idx];
4176                                                    let col_start = if line_idx == sel_start_line { sel_start_col } else { 0 };
4177                                                    let col_end = if line_idx == sel_end_line { sel_end_col } else { vl.char_count };
4178                                                    let x_start = positions.get(col_start).copied().unwrap_or(0.0);
4179                                                    let x_end = positions.get(col_end).copied().unwrap_or(
4180                                                        positions.last().copied().unwrap_or(0.0)
4181                                                    );
4182                                                    let sel_width = x_end - x_start;
4183                                                    if sel_width > 0.0 {
4184                                                        let sel_y = current_bbox.y + line_idx as f32 * line_step - scroll_offset_y;
4185                                                        self.add_render_command(InternalRenderCommand {
4186                                                            bounding_box: BoundingBox::new(
4187                                                                current_bbox.x - scroll_offset_x + x_start,
4188                                                                sel_y,
4189                                                                sel_width,
4190                                                                line_step,
4191                                                            ),
4192                                                            command_type: RenderCommandType::Rectangle,
4193                                                            render_data: InternalRenderData::Rectangle {
4194                                                                background_color: ti_config.selection_color,
4195                                                                corner_radius: CornerRadius::default(),
4196                                                            },
4197                                                            user_data: 0,
4198                                                            id: hash_number(1001 + line_idx as u32, elem_id).id,
4199                                                            z_index: root.z_index,
4200                                                            visual_rotation: None,
4201                                                            shape_rotation: None,
4202                                                            effects: Vec::new(),
4203                                                        });
4204                                                    }
4205                                                }
4206                                            }
4207                                        }
4208
4209                                        // Render each visual line of text
4210                                        for (line_idx, vl) in visual_lines.iter().enumerate() {
4211                                            if !vl.text.is_empty() {
4212                                                let positions = &line_positions[line_idx];
4213                                                let text_width = positions.last().copied().unwrap_or(0.0);
4214                                                let line_y = current_bbox.y + line_idx as f32 * line_step + line_y_offset - scroll_offset_y;
4215                                                self.add_render_command(InternalRenderCommand {
4216                                                    bounding_box: BoundingBox::new(
4217                                                        current_bbox.x - scroll_offset_x,
4218                                                        line_y,
4219                                                        text_width,
4220                                                        natural_font_height,
4221                                                    ),
4222                                                    command_type: RenderCommandType::Text,
4223                                                    render_data: InternalRenderData::Text {
4224                                                        text: vl.text.clone(),
4225                                                        text_color,
4226                                                        font_size: ti_config.font_size,
4227                                                        letter_spacing: 0,
4228                                                        line_height: 0,
4229                                                        font_asset: ti_config.font_asset,
4230                                                    },
4231                                                    user_data: 0,
4232                                                    id: hash_number(2000 + line_idx as u32, elem_id).id,
4233                                                    z_index: root.z_index,
4234                                                    visual_rotation: None,
4235                                                    shape_rotation: None,
4236                                                    effects: Vec::new(),
4237                                                });
4238                                            }
4239                                        }
4240
4241                                        // Cursor (multiline)
4242                                        if is_focused && state.cursor_visible() {
4243                                            let cursor_positions = &line_positions[cursor_line.min(line_positions.len() - 1)];
4244                                            let cursor_x_pos = cursor_positions.get(cursor_col).copied().unwrap_or(0.0);
4245                                            let cursor_y = current_bbox.y + cursor_line as f32 * line_step - scroll_offset_y;
4246                                            self.add_render_command(InternalRenderCommand {
4247                                                bounding_box: BoundingBox::new(
4248                                                    current_bbox.x - scroll_offset_x + cursor_x_pos,
4249                                                    cursor_y,
4250                                                    2.0,
4251                                                    line_step,
4252                                                ),
4253                                                command_type: RenderCommandType::Rectangle,
4254                                                render_data: InternalRenderData::Rectangle {
4255                                                    background_color: ti_config.cursor_color,
4256                                                    corner_radius: CornerRadius::default(),
4257                                                },
4258                                                user_data: 0,
4259                                                id: hash_number(1003, elem_id).id,
4260                                                z_index: root.z_index,
4261                                                visual_rotation: None,
4262                                                shape_rotation: None,
4263                                                effects: Vec::new(),
4264                                            });
4265                                        }
4266                                    } else {
4267                                        // ── Single-line rendering ──
4268                                        let char_x_positions = if let Some(ref measure_fn) = self.measure_text_fn {
4269                                            crate::text_input::compute_char_x_positions(
4270                                                &disp_text,
4271                                                ti_config.font_asset,
4272                                                ti_config.font_size,
4273                                                measure_fn.as_ref(),
4274                                            )
4275                                        } else {
4276                                            vec![0.0]
4277                                        };
4278
4279                                        let scroll_offset = state.scroll_offset;
4280                                        let text_x = current_bbox.x - scroll_offset;
4281                                        let font_height = natural_font_height;
4282
4283                                        // Convert cursor/selection to raw positions for char_x_positions indexing
4284                                        #[cfg(feature = "text-styling")]
4285                                        let render_cursor_pos = if is_placeholder { 0 } else { state.cursor_pos_raw() };
4286                                        #[cfg(not(feature = "text-styling"))]
4287                                        let render_cursor_pos = if is_placeholder { 0 } else { state.cursor_pos };
4288
4289                                        #[cfg(feature = "text-styling")]
4290                                        let render_selection = if !is_placeholder { state.selection_range_raw() } else { None };
4291                                        #[cfg(not(feature = "text-styling"))]
4292                                        let render_selection = if !is_placeholder { state.selection_range() } else { None };
4293
4294                                        // Selection highlight
4295                                        if is_focused {
4296                                            if let Some((sel_start, sel_end)) = render_selection {
4297                                                let sel_start_x = char_x_positions.get(sel_start).copied().unwrap_or(0.0);
4298                                                let sel_end_x = char_x_positions.get(sel_end).copied().unwrap_or(0.0);
4299                                                let sel_width = sel_end_x - sel_start_x;
4300                                                if sel_width > 0.0 {
4301                                                    let sel_y = current_bbox.y + (current_bbox.height - font_height) / 2.0;
4302                                                    self.add_render_command(InternalRenderCommand {
4303                                                        bounding_box: BoundingBox::new(
4304                                                            text_x + sel_start_x,
4305                                                            sel_y,
4306                                                            sel_width,
4307                                                            font_height,
4308                                                        ),
4309                                                        command_type: RenderCommandType::Rectangle,
4310                                                        render_data: InternalRenderData::Rectangle {
4311                                                            background_color: ti_config.selection_color,
4312                                                            corner_radius: CornerRadius::default(),
4313                                                        },
4314                                                        user_data: 0,
4315                                                        id: hash_number(1001, elem_id).id,
4316                                                        z_index: root.z_index,
4317                                                        visual_rotation: None,
4318                                                        shape_rotation: None,
4319                                                        effects: Vec::new(),
4320                                                    });
4321                                                }
4322                                            }
4323                                        }
4324
4325                                        // Text
4326                                        if !disp_text.is_empty() {
4327                                            let text_width = char_x_positions.last().copied().unwrap_or(0.0);
4328                                            content_width = text_width;
4329                                            content_height = font_height;
4330                                            scroll_pos_x = scroll_offset;
4331                                            scroll_pos_y = 0.0;
4332                                            let text_y = current_bbox.y + (current_bbox.height - font_height) / 2.0;
4333                                            self.add_render_command(InternalRenderCommand {
4334                                                bounding_box: BoundingBox::new(
4335                                                    text_x,
4336                                                    text_y,
4337                                                    text_width,
4338                                                    font_height,
4339                                                ),
4340                                                command_type: RenderCommandType::Text,
4341                                                render_data: InternalRenderData::Text {
4342                                                    text: disp_text,
4343                                                    text_color,
4344                                                    font_size: ti_config.font_size,
4345                                                    letter_spacing: 0,
4346                                                    line_height: 0,
4347                                                    font_asset: ti_config.font_asset,
4348                                                },
4349                                                user_data: 0,
4350                                                id: hash_number(1002, elem_id).id,
4351                                                z_index: root.z_index,
4352                                                visual_rotation: None,
4353                                                shape_rotation: None,
4354                                                effects: Vec::new(),
4355                                            });
4356                                        }
4357
4358                                        // Cursor
4359                                        if is_focused && state.cursor_visible() {
4360                                            let cursor_x_pos = char_x_positions
4361                                                .get(render_cursor_pos)
4362                                                .copied()
4363                                                .unwrap_or(0.0);
4364                                            let cursor_y = current_bbox.y + (current_bbox.height - font_height) / 2.0;
4365                                            self.add_render_command(InternalRenderCommand {
4366                                                bounding_box: BoundingBox::new(
4367                                                    text_x + cursor_x_pos,
4368                                                    cursor_y,
4369                                                    2.0,
4370                                                    font_height,
4371                                                ),
4372                                                command_type: RenderCommandType::Rectangle,
4373                                                render_data: InternalRenderData::Rectangle {
4374                                                    background_color: ti_config.cursor_color,
4375                                                    corner_radius: CornerRadius::default(),
4376                                                },
4377                                                user_data: 0,
4378                                                id: hash_number(1003, elem_id).id,
4379                                                z_index: root.z_index,
4380                                                visual_rotation: None,
4381                                                shape_rotation: None,
4382                                                effects: Vec::new(),
4383                                            });
4384                                        }
4385                                    }
4386
4387                                    if let Some(scrollbar_cfg) = ti_config.scrollbar {
4388                                        let idle_frames = self
4389                                            .text_input_scrollbar_idle_frames
4390                                            .get(&elem_id)
4391                                            .copied()
4392                                            .unwrap_or(0);
4393                                        let alpha = scrollbar_visibility_alpha(scrollbar_cfg, idle_frames);
4394                                        if alpha > 0.0 {
4395                                            let vertical = if ti_config.is_multiline {
4396                                                compute_vertical_scrollbar_geometry(
4397                                                    current_bbox,
4398                                                    content_height,
4399                                                    scroll_pos_y,
4400                                                    scrollbar_cfg,
4401                                                )
4402                                            } else {
4403                                                None
4404                                            };
4405
4406                                            let horizontal = compute_horizontal_scrollbar_geometry(
4407                                                current_bbox,
4408                                                content_width,
4409                                                scroll_pos_x,
4410                                                scrollbar_cfg,
4411                                            );
4412
4413                                            if vertical.is_some() || horizontal.is_some() {
4414                                                self.render_scrollbar_geometry(
4415                                                    hash_number(elem_id, 8010).id,
4416                                                    root.z_index,
4417                                                    scrollbar_cfg,
4418                                                    alpha,
4419                                                    vertical,
4420                                                    horizontal,
4421                                                );
4422                                            }
4423                                        }
4424                                    }
4425
4426                                    // End clipping
4427                                    self.add_render_command(InternalRenderCommand {
4428                                        bounding_box: current_bbox,
4429                                        command_type: RenderCommandType::ScissorEnd,
4430                                        render_data: InternalRenderData::None,
4431                                        user_data: 0,
4432                                        id: hash_number(1004, elem_id).id,
4433                                        z_index: root.z_index,
4434                                        visual_rotation: None,
4435                                        shape_rotation: None,
4436                                        effects: Vec::new(),
4437                                    });
4438                                }
4439                                // Background already emitted above; skip the default rectangle
4440                                emit_rectangle = false;
4441                            }
4442                        }
4443                    }
4444
4445                    if emit_rectangle {
4446                        self.add_render_command(InternalRenderCommand {
4447                            bounding_box: shape_draw_bbox,
4448                            command_type: RenderCommandType::Rectangle,
4449                            render_data: InternalRenderData::Rectangle {
4450                                background_color: shared.background_color,
4451                                corner_radius: shared.corner_radius,
4452                            },
4453                            user_data: shared.user_data,
4454                            id: elem_id,
4455                            z_index: root.z_index,
4456                            visual_rotation: None,
4457                            shape_rotation: elem_shape_rotation,
4458                            effects: elem_effects.clone(),
4459                        });
4460                    }
4461
4462                    // Setup child alignment
4463                    let is_text =
4464                        self.element_has_config(current_elem_idx, ElementConfigType::Text);
4465                    if !is_text {
4466                        let children_start =
4467                            self.layout_elements[current_elem_idx].children_start;
4468                        let children_length =
4469                            self.layout_elements[current_elem_idx].children_length as usize;
4470
4471                        if layout_config.layout_direction == LayoutDirection::LeftToRight {
4472                            if layout_config.wrap {
4473                                let lines = self.compute_wrapped_lines(current_elem_idx, true);
4474                                let content_height = if lines.is_empty() {
4475                                    0.0
4476                                } else {
4477                                    lines.iter().map(|line| line.cross_size).sum::<f32>()
4478                                        + lines.len().saturating_sub(1) as f32
4479                                            * layout_config.wrap_gap as f32
4480                                };
4481
4482                                let mut extra_space = self.layout_elements[current_elem_idx]
4483                                    .dimensions
4484                                    .height
4485                                    - (layout_config.padding.top + layout_config.padding.bottom)
4486                                        as f32
4487                                    - content_height;
4488                                if _scroll_container_data_idx.is_some() {
4489                                    extra_space = extra_space.max(0.0);
4490                                }
4491                                match layout_config.child_alignment.y {
4492                                    AlignY::Top => extra_space = 0.0,
4493                                    AlignY::CenterY => extra_space /= 2.0,
4494                                    AlignY::Bottom => {}
4495                                }
4496                                dfs_buffer[buf_idx].next_child_offset.y += extra_space;
4497                            } else {
4498                                let mut content_width: f32 = 0.0;
4499                                for ci in 0..children_length {
4500                                    let child_idx =
4501                                        self.layout_element_children[children_start + ci] as usize;
4502                                    content_width +=
4503                                        self.layout_elements[child_idx].dimensions.width;
4504                                }
4505                                content_width += children_length.saturating_sub(1) as f32
4506                                    * layout_config.child_gap as f32;
4507                                let mut extra_space = self.layout_elements[current_elem_idx]
4508                                    .dimensions
4509                                    .width
4510                                    - (layout_config.padding.left + layout_config.padding.right)
4511                                        as f32
4512                                    - content_width;
4513                                if _scroll_container_data_idx.is_some() {
4514                                    extra_space = extra_space.max(0.0);
4515                                }
4516                                match layout_config.child_alignment.x {
4517                                    AlignX::Left => extra_space = 0.0,
4518                                    AlignX::CenterX => extra_space /= 2.0,
4519                                    AlignX::Right => {}
4520                                }
4521                                dfs_buffer[buf_idx].next_child_offset.x += extra_space;
4522                            }
4523                        } else if layout_config.wrap {
4524                            let lines = self.compute_wrapped_lines(current_elem_idx, false);
4525                            let content_width = if lines.is_empty() {
4526                                0.0
4527                            } else {
4528                                lines.iter().map(|line| line.cross_size).sum::<f32>()
4529                                    + lines.len().saturating_sub(1) as f32
4530                                        * layout_config.wrap_gap as f32
4531                            };
4532
4533                            let mut extra_space = self.layout_elements[current_elem_idx]
4534                                .dimensions
4535                                .width
4536                                - (layout_config.padding.left + layout_config.padding.right)
4537                                    as f32
4538                                - content_width;
4539                            if _scroll_container_data_idx.is_some() {
4540                                extra_space = extra_space.max(0.0);
4541                            }
4542                            match layout_config.child_alignment.x {
4543                                AlignX::Left => extra_space = 0.0,
4544                                AlignX::CenterX => extra_space /= 2.0,
4545                                AlignX::Right => {}
4546                            }
4547                            dfs_buffer[buf_idx].next_child_offset.x += extra_space;
4548                        } else {
4549                            let mut content_height: f32 = 0.0;
4550                            for ci in 0..children_length {
4551                                let child_idx =
4552                                    self.layout_element_children[children_start + ci] as usize;
4553                                content_height +=
4554                                    self.layout_elements[child_idx].dimensions.height;
4555                            }
4556                            content_height += children_length.saturating_sub(1) as f32
4557                                * layout_config.child_gap as f32;
4558                            let mut extra_space = self.layout_elements[current_elem_idx]
4559                                .dimensions
4560                                .height
4561                                - (layout_config.padding.top + layout_config.padding.bottom) as f32
4562                                - content_height;
4563                            if _scroll_container_data_idx.is_some() {
4564                                extra_space = extra_space.max(0.0);
4565                            }
4566                            match layout_config.child_alignment.y {
4567                                AlignY::Top => extra_space = 0.0,
4568                                AlignY::CenterY => extra_space /= 2.0,
4569                                AlignY::Bottom => {}
4570                            }
4571                            dfs_buffer[buf_idx].next_child_offset.y += extra_space;
4572                        }
4573
4574                        // Update scroll container content size
4575                        if let Some(si) = _scroll_container_data_idx {
4576                            let (content_w, content_h) = if layout_config.wrap {
4577                                let lines = self.compute_wrapped_lines(
4578                                    current_elem_idx,
4579                                    layout_config.layout_direction
4580                                        == LayoutDirection::LeftToRight,
4581                                );
4582                                let content_dims = self.wrapped_content_dimensions(
4583                                    current_elem_idx,
4584                                    layout_config.layout_direction
4585                                        == LayoutDirection::LeftToRight,
4586                                    &lines,
4587                                );
4588                                (content_dims.width, content_dims.height)
4589                            } else {
4590                                let child_gap_total = children_length.saturating_sub(1) as f32
4591                                    * layout_config.child_gap as f32;
4592                                let lr_padding =
4593                                    (layout_config.padding.left + layout_config.padding.right)
4594                                        as f32;
4595                                let tb_padding =
4596                                    (layout_config.padding.top + layout_config.padding.bottom)
4597                                        as f32;
4598
4599                                if layout_config.layout_direction == LayoutDirection::LeftToRight {
4600                                    // LeftToRight: width = sum of children + gap, height = max of children
4601                                    let w: f32 = (0..children_length)
4602                                        .map(|ci| {
4603                                            let idx = self.layout_element_children
4604                                                [children_start + ci]
4605                                                as usize;
4606                                            self.layout_elements[idx].dimensions.width
4607                                        })
4608                                        .sum::<f32>()
4609                                        + lr_padding
4610                                        + child_gap_total;
4611                                    let h: f32 = (0..children_length)
4612                                        .map(|ci| {
4613                                            let idx = self.layout_element_children
4614                                                [children_start + ci]
4615                                                as usize;
4616                                            self.layout_elements[idx].dimensions.height
4617                                        })
4618                                        .fold(0.0_f32, |a, b| a.max(b))
4619                                        + tb_padding;
4620                                    (w, h)
4621                                } else {
4622                                    // TopToBottom: width = max of children, height = sum of children + gap
4623                                    let w: f32 = (0..children_length)
4624                                        .map(|ci| {
4625                                            let idx = self.layout_element_children
4626                                                [children_start + ci]
4627                                                as usize;
4628                                            self.layout_elements[idx].dimensions.width
4629                                        })
4630                                        .fold(0.0_f32, |a, b| a.max(b))
4631                                        + lr_padding;
4632                                    let h: f32 = (0..children_length)
4633                                        .map(|ci| {
4634                                            let idx = self.layout_element_children
4635                                                [children_start + ci]
4636                                                as usize;
4637                                            self.layout_elements[idx].dimensions.height
4638                                        })
4639                                        .sum::<f32>()
4640                                        + tb_padding
4641                                        + child_gap_total;
4642                                    (w, h)
4643                                }
4644                            };
4645                            self.scroll_container_datas[si].content_size =
4646                                Dimensions::new(content_w, content_h);
4647                        }
4648                    }
4649                } else {
4650                    // Returning upward in DFS
4651
4652                    let mut close_clip = false;
4653                    let mut scroll_container_data_idx: Option<usize> = None;
4654
4655                    if self.element_has_config(current_elem_idx, ElementConfigType::Clip) {
4656                        close_clip = true;
4657                        if let Some(clip_cfg_idx) = self
4658                            .find_element_config_index(current_elem_idx, ElementConfigType::Clip)
4659                        {
4660                            let clip_config = self.clip_element_configs[clip_cfg_idx];
4661                            for si in 0..self.scroll_container_datas.len() {
4662                                if self.scroll_container_datas[si].layout_element_index
4663                                    == current_elem_idx as i32
4664                                {
4665                                    scroll_offset = clip_config.child_offset;
4666                                    scroll_container_data_idx = Some(si);
4667                                    break;
4668                                }
4669                            }
4670                        }
4671                    }
4672
4673                    if self.element_has_config(current_elem_idx, ElementConfigType::Border) {
4674                        let border_elem_id = self.layout_elements[current_elem_idx].id;
4675                        if let Some(border_bbox) = self.layout_element_map.get(&border_elem_id).map(|item| item.bounding_box) {
4676                            let bbox = border_bbox;
4677                            if !self.element_is_offscreen(&bbox) {
4678                                let shared = self
4679                                    .find_element_config_index(
4680                                        current_elem_idx,
4681                                        ElementConfigType::Shared,
4682                                    )
4683                                    .map(|idx| self.shared_element_configs[idx])
4684                                    .unwrap_or_default();
4685                                let border_cfg_idx = self
4686                                    .find_element_config_index(
4687                                        current_elem_idx,
4688                                        ElementConfigType::Border,
4689                                    )
4690                                    .unwrap();
4691                                let border_config = self.border_element_configs[border_cfg_idx];
4692
4693                                let children_count =
4694                                    self.layout_elements[current_elem_idx].children_length;
4695
4696                                // between-children borders
4697                                if border_config.width.between_children > 0
4698                                    && border_config.color.a > 0.0
4699                                {
4700                                    let half_gap = layout_config.child_gap as f32 / 2.0;
4701                                    let half_divider =
4702                                        border_config.width.between_children as f32 / 2.0;
4703                                    let children_start =
4704                                        self.layout_elements[current_elem_idx].children_start;
4705                                    let children_length = self.layout_elements[current_elem_idx]
4706                                        .children_length
4707                                        as usize;
4708
4709                                    if layout_config.layout_direction
4710                                        == LayoutDirection::LeftToRight
4711                                    {
4712                                        let mut border_offset_x =
4713                                            layout_config.padding.left as f32 - half_gap;
4714                                        for ci in 0..children_length {
4715                                            let child_idx = self.layout_element_children
4716                                                [children_start + ci]
4717                                                as usize;
4718                                            if ci > 0 {
4719                                                self.add_render_command(InternalRenderCommand {
4720                                                    bounding_box: BoundingBox::new(
4721                                                        bbox.x + border_offset_x
4722                                                            - half_divider
4723                                                            + scroll_offset.x,
4724                                                        bbox.y + scroll_offset.y,
4725                                                        border_config.width.between_children as f32,
4726                                                        self.layout_elements[current_elem_idx]
4727                                                            .dimensions
4728                                                            .height,
4729                                                    ),
4730                                                    command_type: RenderCommandType::Rectangle,
4731                                                    render_data: InternalRenderData::Rectangle {
4732                                                        background_color: border_config.color,
4733                                                        corner_radius: CornerRadius::default(),
4734                                                    },
4735                                                    user_data: shared.user_data,
4736                                                    id: hash_number(
4737                                                        self.layout_elements[current_elem_idx].id,
4738                                                        children_count as u32 + 1 + ci as u32,
4739                                                    )
4740                                                    .id,
4741                                                    z_index: root.z_index,
4742                                                    visual_rotation: None,
4743                                                    shape_rotation: None,
4744                                                    effects: Vec::new(),
4745                                                });
4746                                            }
4747                                            border_offset_x +=
4748                                                self.layout_elements[child_idx].dimensions.width
4749                                                    + layout_config.child_gap as f32;
4750                                        }
4751                                    } else {
4752                                        let mut border_offset_y =
4753                                            layout_config.padding.top as f32 - half_gap;
4754                                        for ci in 0..children_length {
4755                                            let child_idx = self.layout_element_children
4756                                                [children_start + ci]
4757                                                as usize;
4758                                            if ci > 0 {
4759                                                self.add_render_command(InternalRenderCommand {
4760                                                    bounding_box: BoundingBox::new(
4761                                                        bbox.x + scroll_offset.x,
4762                                                        bbox.y + border_offset_y
4763                                                            - half_divider
4764                                                            + scroll_offset.y,
4765                                                        self.layout_elements[current_elem_idx]
4766                                                            .dimensions
4767                                                            .width,
4768                                                        border_config.width.between_children as f32,
4769                                                    ),
4770                                                    command_type: RenderCommandType::Rectangle,
4771                                                    render_data: InternalRenderData::Rectangle {
4772                                                        background_color: border_config.color,
4773                                                        corner_radius: CornerRadius::default(),
4774                                                    },
4775                                                    user_data: shared.user_data,
4776                                                    id: hash_number(
4777                                                        self.layout_elements[current_elem_idx].id,
4778                                                        children_count as u32 + 1 + ci as u32,
4779                                                    )
4780                                                    .id,
4781                                                    z_index: root.z_index,
4782                                                    visual_rotation: None,
4783                                                    shape_rotation: None,
4784                                                    effects: Vec::new(),
4785                                                });
4786                                            }
4787                                            border_offset_y +=
4788                                                self.layout_elements[child_idx].dimensions.height
4789                                                    + layout_config.child_gap as f32;
4790                                        }
4791                                    }
4792                                }
4793                            }
4794                        }
4795                    }
4796
4797                    if let Some(si) = scroll_container_data_idx {
4798                        let scd = self.scroll_container_datas[si];
4799                        if let Some(scrollbar_cfg) = scd.scrollbar {
4800                            let alpha = scrollbar_visibility_alpha(scrollbar_cfg, scd.scrollbar_idle_frames);
4801                            if alpha > 0.0 {
4802                                let vertical = if scd.scroll_y_enabled {
4803                                    compute_vertical_scrollbar_geometry(
4804                                        scd.bounding_box,
4805                                        scd.content_size.height,
4806                                        -scd.scroll_position.y,
4807                                        scrollbar_cfg,
4808                                    )
4809                                } else {
4810                                    None
4811                                };
4812
4813                                let horizontal = if scd.scroll_x_enabled {
4814                                    compute_horizontal_scrollbar_geometry(
4815                                        scd.bounding_box,
4816                                        scd.content_size.width,
4817                                        -scd.scroll_position.x,
4818                                        scrollbar_cfg,
4819                                    )
4820                                } else {
4821                                    None
4822                                };
4823
4824                                if vertical.is_some() || horizontal.is_some() {
4825                                    self.render_scrollbar_geometry(
4826                                        scd.element_id,
4827                                        root.z_index,
4828                                        scrollbar_cfg,
4829                                        alpha,
4830                                        vertical,
4831                                        horizontal,
4832                                    );
4833                                }
4834                            }
4835                        }
4836                    }
4837
4838                    if close_clip {
4839                        let root_elem = &self.layout_elements[root_elem_idx];
4840                        self.add_render_command(InternalRenderCommand {
4841                            command_type: RenderCommandType::ScissorEnd,
4842                            id: hash_number(
4843                                self.layout_elements[current_elem_idx].id,
4844                                root_elem.children_length as u32 + 11,
4845                            )
4846                            .id,
4847                            ..Default::default()
4848                        });
4849                    }
4850
4851                    if self.element_has_config(current_elem_idx, ElementConfigType::Border) {
4852                        let border_elem_id = self.layout_elements[current_elem_idx].id;
4853                        if let Some(border_bbox) = self.layout_element_map.get(&border_elem_id).map(|item| item.bounding_box) {
4854                            let bbox = border_bbox;
4855                            if !self.element_is_offscreen(&bbox) {
4856                                let shared = self
4857                                    .find_element_config_index(
4858                                        current_elem_idx,
4859                                        ElementConfigType::Shared,
4860                                    )
4861                                    .map(|idx| self.shared_element_configs[idx])
4862                                    .unwrap_or_default();
4863                                let border_cfg_idx = self
4864                                    .find_element_config_index(
4865                                        current_elem_idx,
4866                                        ElementConfigType::Border,
4867                                    )
4868                                    .unwrap();
4869                                let border_config = self.border_element_configs[border_cfg_idx];
4870
4871                                let children_count =
4872                                    self.layout_elements[current_elem_idx].children_length;
4873                                self.add_render_command(InternalRenderCommand {
4874                                    bounding_box: bbox,
4875                                    command_type: RenderCommandType::Border,
4876                                    render_data: InternalRenderData::Border {
4877                                        color: border_config.color,
4878                                        corner_radius: shared.corner_radius,
4879                                        width: border_config.width,
4880                                        position: border_config.position,
4881                                    },
4882                                    user_data: shared.user_data,
4883                                    id: hash_number(
4884                                        self.layout_elements[current_elem_idx].id,
4885                                        children_count as u32,
4886                                    )
4887                                    .id,
4888                                    z_index: root.z_index,
4889                                    visual_rotation: None,
4890                                    shape_rotation: None,
4891                                    effects: Vec::new(),
4892                                });
4893                            }
4894                        }
4895                    }
4896
4897                    // Emit GroupEnd commands AFTER border and scissor (innermost first, outermost last)
4898                    let elem_shaders = self.element_shaders.get(current_elem_idx).cloned().unwrap_or_default();
4899                    let elem_visual_rotation = self.element_visual_rotations.get(current_elem_idx).cloned().flatten()
4900                        .filter(|vr| !vr.is_noop());
4901
4902                    // GroupEnd for each shader
4903                    for _shader in elem_shaders.iter() {
4904                        self.add_render_command(InternalRenderCommand {
4905                            command_type: RenderCommandType::GroupEnd,
4906                            id: self.layout_elements[current_elem_idx].id,
4907                            z_index: root.z_index,
4908                            ..Default::default()
4909                        });
4910                    }
4911                    // If no shaders but visual rotation was present, emit its GroupEnd
4912                    if elem_shaders.is_empty() && elem_visual_rotation.is_some() {
4913                        self.add_render_command(InternalRenderCommand {
4914                            command_type: RenderCommandType::GroupEnd,
4915                            id: self.layout_elements[current_elem_idx].id,
4916                            z_index: root.z_index,
4917                            ..Default::default()
4918                        });
4919                    }
4920
4921                    dfs_buffer.pop();
4922                    visited.pop();
4923                    continue;
4924                }
4925
4926                // Add children to DFS buffer (in reverse for correct traversal order)
4927                let is_text =
4928                    self.element_has_config(current_elem_idx, ElementConfigType::Text);
4929                if !is_text {
4930                    let children_start = self.layout_elements[current_elem_idx].children_start;
4931                    let children_length =
4932                        self.layout_elements[current_elem_idx].children_length as usize;
4933
4934                    // Pre-grow dfs_buffer and visited
4935                    let new_len = dfs_buffer.len() + children_length;
4936                    dfs_buffer.resize(new_len, LayoutElementTreeNode::default());
4937                    visited.resize(new_len, false);
4938
4939                    let is_scroll_container = self.scroll_container_datas.iter().any(|scd| {
4940                        scd.layout_element_index == current_elem_idx as i32
4941                    });
4942
4943                    let wrapped_lines = if layout_config.wrap {
4944                        Some(self.compute_wrapped_lines(
4945                            current_elem_idx,
4946                            layout_config.layout_direction == LayoutDirection::LeftToRight,
4947                        ))
4948                    } else {
4949                        None
4950                    };
4951
4952                    let mut wrapped_line_main_cursors: Vec<f32> = Vec::new();
4953                    let mut wrapped_line_cross_starts: Vec<f32> = Vec::new();
4954                    let mut wrapped_child_line_indexes: Vec<usize> =
4955                        vec![0; children_length];
4956
4957                    if let Some(lines) = wrapped_lines.as_ref() {
4958                        if layout_config.layout_direction == LayoutDirection::LeftToRight {
4959                            let mut line_y = dfs_buffer[buf_idx].next_child_offset.y;
4960                            for (line_idx, line) in lines.iter().enumerate() {
4961                                let mut line_extra_space =
4962                                    self.layout_elements[current_elem_idx].dimensions.width
4963                                        - (layout_config.padding.left
4964                                            + layout_config.padding.right)
4965                                            as f32
4966                                        - line.main_size;
4967                                if is_scroll_container {
4968                                    line_extra_space = line_extra_space.max(0.0);
4969                                }
4970                                match layout_config.child_alignment.x {
4971                                    AlignX::Left => line_extra_space = 0.0,
4972                                    AlignX::CenterX => line_extra_space /= 2.0,
4973                                    AlignX::Right => {}
4974                                }
4975
4976                                wrapped_line_main_cursors
4977                                    .push(layout_config.padding.left as f32 + line_extra_space);
4978                                wrapped_line_cross_starts.push(line_y);
4979
4980                                for child_offset in
4981                                    line.start_child_offset..line.end_child_offset
4982                                {
4983                                    wrapped_child_line_indexes[child_offset] = line_idx;
4984                                }
4985
4986                                line_y += line.cross_size + layout_config.wrap_gap as f32;
4987                            }
4988                        } else {
4989                            let mut line_x = dfs_buffer[buf_idx].next_child_offset.x;
4990                            for (line_idx, line) in lines.iter().enumerate() {
4991                                let mut line_extra_space =
4992                                    self.layout_elements[current_elem_idx].dimensions.height
4993                                        - (layout_config.padding.top
4994                                            + layout_config.padding.bottom)
4995                                            as f32
4996                                        - line.main_size;
4997                                if is_scroll_container {
4998                                    line_extra_space = line_extra_space.max(0.0);
4999                                }
5000                                match layout_config.child_alignment.y {
5001                                    AlignY::Top => line_extra_space = 0.0,
5002                                    AlignY::CenterY => line_extra_space /= 2.0,
5003                                    AlignY::Bottom => {}
5004                                }
5005
5006                                wrapped_line_main_cursors
5007                                    .push(layout_config.padding.top as f32 + line_extra_space);
5008                                wrapped_line_cross_starts.push(line_x);
5009
5010                                for child_offset in
5011                                    line.start_child_offset..line.end_child_offset
5012                                {
5013                                    wrapped_child_line_indexes[child_offset] = line_idx;
5014                                }
5015
5016                                line_x += line.cross_size + layout_config.wrap_gap as f32;
5017                            }
5018                        }
5019                    }
5020
5021                    for ci in 0..children_length {
5022                        let child_idx =
5023                            self.layout_element_children[children_start + ci] as usize;
5024                        let child_layout_idx =
5025                            self.layout_elements[child_idx].layout_config_index;
5026
5027                        // Alignment along non-layout axis
5028                        let mut child_offset = dfs_buffer[buf_idx].next_child_offset;
5029                        if layout_config.wrap {
5030                            let Some(lines) = wrapped_lines.as_ref() else {
5031                                continue;
5032                            };
5033                            let line_idx = wrapped_child_line_indexes[ci];
5034                            let line = lines[line_idx];
5035
5036                            if layout_config.layout_direction == LayoutDirection::LeftToRight {
5037                                child_offset.x = wrapped_line_main_cursors[line_idx];
5038                                child_offset.y = wrapped_line_cross_starts[line_idx];
5039
5040                                let whitespace = line.cross_size
5041                                    - self.layout_elements[child_idx].dimensions.height;
5042                                match layout_config.child_alignment.y {
5043                                    AlignY::Top => {}
5044                                    AlignY::CenterY => {
5045                                        child_offset.y += whitespace / 2.0;
5046                                    }
5047                                    AlignY::Bottom => {
5048                                        child_offset.y += whitespace;
5049                                    }
5050                                }
5051
5052                                wrapped_line_main_cursors[line_idx] +=
5053                                    self.layout_elements[child_idx].dimensions.width
5054                                        + layout_config.child_gap as f32;
5055                            } else {
5056                                child_offset.x = wrapped_line_cross_starts[line_idx];
5057                                child_offset.y = wrapped_line_main_cursors[line_idx];
5058
5059                                let whitespace = line.cross_size
5060                                    - self.layout_elements[child_idx].dimensions.width;
5061                                match layout_config.child_alignment.x {
5062                                    AlignX::Left => {}
5063                                    AlignX::CenterX => {
5064                                        child_offset.x += whitespace / 2.0;
5065                                    }
5066                                    AlignX::Right => {
5067                                        child_offset.x += whitespace;
5068                                    }
5069                                }
5070
5071                                wrapped_line_main_cursors[line_idx] +=
5072                                    self.layout_elements[child_idx].dimensions.height
5073                                        + layout_config.child_gap as f32;
5074                            }
5075                        } else if layout_config.layout_direction == LayoutDirection::LeftToRight {
5076                            child_offset.y = layout_config.padding.top as f32;
5077                            let whitespace = self.layout_elements[current_elem_idx].dimensions.height
5078                                - (layout_config.padding.top + layout_config.padding.bottom) as f32
5079                                - self.layout_elements[child_idx].dimensions.height;
5080                            match layout_config.child_alignment.y {
5081                                AlignY::Top => {}
5082                                AlignY::CenterY => {
5083                                    child_offset.y += whitespace / 2.0;
5084                                }
5085                                AlignY::Bottom => {
5086                                    child_offset.y += whitespace;
5087                                }
5088                            }
5089                        } else {
5090                            child_offset.x = layout_config.padding.left as f32;
5091                            let whitespace = self.layout_elements[current_elem_idx].dimensions.width
5092                                - (layout_config.padding.left + layout_config.padding.right) as f32
5093                                - self.layout_elements[child_idx].dimensions.width;
5094                            match layout_config.child_alignment.x {
5095                                AlignX::Left => {}
5096                                AlignX::CenterX => {
5097                                    child_offset.x += whitespace / 2.0;
5098                                }
5099                                AlignX::Right => {
5100                                    child_offset.x += whitespace;
5101                                }
5102                            }
5103                        }
5104
5105                        let child_position = Vector2::new(
5106                            dfs_buffer[buf_idx].position.x + child_offset.x + scroll_offset.x,
5107                            dfs_buffer[buf_idx].position.y + child_offset.y + scroll_offset.y,
5108                        );
5109
5110                        let new_node_index = new_len - 1 - ci;
5111                        let child_padding_left =
5112                            self.layout_configs[child_layout_idx].padding.left as f32;
5113                        let child_padding_top =
5114                            self.layout_configs[child_layout_idx].padding.top as f32;
5115                        dfs_buffer[new_node_index] = LayoutElementTreeNode {
5116                            layout_element_index: child_idx as i32,
5117                            position: child_position,
5118                            next_child_offset: Vector2::new(child_padding_left, child_padding_top),
5119                        };
5120                        visited[new_node_index] = false;
5121
5122                        // Update parent offset
5123                        if !layout_config.wrap {
5124                            if layout_config.layout_direction == LayoutDirection::LeftToRight {
5125                                dfs_buffer[buf_idx].next_child_offset.x +=
5126                                    self.layout_elements[child_idx].dimensions.width
5127                                        + layout_config.child_gap as f32;
5128                            } else {
5129                                dfs_buffer[buf_idx].next_child_offset.y +=
5130                                    self.layout_elements[child_idx].dimensions.height
5131                                        + layout_config.child_gap as f32;
5132                            }
5133                        }
5134                    }
5135                }
5136            }
5137
5138            // End clip
5139            if root.clip_element_id != 0 {
5140                let root_elem = &self.layout_elements[root_elem_idx];
5141                self.add_render_command(InternalRenderCommand {
5142                    command_type: RenderCommandType::ScissorEnd,
5143                    id: hash_number(root_elem.id, root_elem.children_length as u32 + 11).id,
5144                    ..Default::default()
5145                });
5146            }
5147        }
5148
5149        // Focus ring: render a border around the focused element (keyboard focus only)
5150        if self.focused_element_id != 0 && self.focus_from_keyboard {
5151            // Check if the element's accessibility config allows the ring
5152            let a11y = self.accessibility_configs.get(&self.focused_element_id);
5153            let show_ring = a11y.map_or(true, |c| c.show_ring);
5154            if show_ring {
5155                if let Some(item) = self.layout_element_map.get(&self.focused_element_id) {
5156                    let bbox = item.bounding_box;
5157                    if !self.element_is_offscreen(&bbox) {
5158                        let elem_idx = item.layout_element_index as usize;
5159                        let corner_radius = self
5160                            .find_element_config_index(elem_idx, ElementConfigType::Shared)
5161                            .map(|idx| self.shared_element_configs[idx].corner_radius)
5162                            .unwrap_or_default();
5163                        let ring_width = a11y.and_then(|c| c.ring_width).unwrap_or(2);
5164                        let ring_color = a11y.and_then(|c| c.ring_color).unwrap_or(Color::rgba(255.0, 60.0, 40.0, 255.0));
5165                        // Expand bounding box outward by ring width so the ring doesn't overlap content
5166                        let expanded_bbox = BoundingBox::new(
5167                            bbox.x - ring_width as f32,
5168                            bbox.y - ring_width as f32,
5169                            bbox.width + ring_width as f32 * 2.0,
5170                            bbox.height + ring_width as f32 * 2.0,
5171                        );
5172                        self.add_render_command(InternalRenderCommand {
5173                            bounding_box: expanded_bbox,
5174                            command_type: RenderCommandType::Border,
5175                            render_data: InternalRenderData::Border {
5176                                color: ring_color,
5177                                corner_radius: CornerRadius {
5178                                    top_left: corner_radius.top_left + ring_width as f32,
5179                                    top_right: corner_radius.top_right + ring_width as f32,
5180                                    bottom_left: corner_radius.bottom_left + ring_width as f32,
5181                                    bottom_right: corner_radius.bottom_right + ring_width as f32,
5182                                },
5183                                width: BorderWidth {
5184                                    left: ring_width,
5185                                    right: ring_width,
5186                                    top: ring_width,
5187                                    bottom: ring_width,
5188                                    between_children: 0,
5189                                },
5190                                position: BorderPosition::Middle,
5191                            },
5192                            id: hash_number(self.focused_element_id, 0xF0C5).id,
5193                            z_index: 32764, // just below debug panel
5194                            ..Default::default()
5195                        });
5196                    }
5197                }
5198            }
5199        }
5200    }
5201
5202    pub fn set_layout_dimensions(&mut self, dimensions: Dimensions) {
5203        self.layout_dimensions = dimensions;
5204    }
5205
5206    pub fn set_pointer_state(&mut self, position: Vector2, is_down: bool) {
5207        if self.boolean_warnings.max_elements_exceeded {
5208            return;
5209        }
5210        self.pointer_info.position = position;
5211        self.pointer_over_ids.clear();
5212
5213        // Check which elements are under the pointer
5214        for root_index in (0..self.layout_element_tree_roots.len()).rev() {
5215            let root = self.layout_element_tree_roots[root_index];
5216            let mut dfs: Vec<i32> = vec![root.layout_element_index];
5217            let mut vis: Vec<bool> = vec![false];
5218            let mut found = false;
5219
5220            while !dfs.is_empty() {
5221                let idx = dfs.len() - 1;
5222                if vis[idx] {
5223                    dfs.pop();
5224                    vis.pop();
5225                    continue;
5226                }
5227                vis[idx] = true;
5228                let current_idx = dfs[idx] as usize;
5229                let elem_id = self.layout_elements[current_idx].id;
5230
5231                // Copy data from map to avoid borrow issues with mutable access later
5232                let map_data = self.layout_element_map.get(&elem_id).map(|item| {
5233                    (item.bounding_box, item.element_id.clone(), item.on_hover_fn.is_some())
5234                });
5235                if let Some((raw_box, elem_id_copy, has_hover)) = map_data {
5236                    let mut elem_box = raw_box;
5237                    elem_box.x -= root.pointer_offset.x;
5238                    elem_box.y -= root.pointer_offset.y;
5239
5240                    let clip_id =
5241                        self.layout_element_clip_element_ids[current_idx] as u32;
5242                    let clip_ok = clip_id == 0
5243                        || self
5244                            .layout_element_map
5245                            .get(&clip_id)
5246                            .map(|ci| {
5247                                point_is_inside_rect(
5248                                    position,
5249                                    ci.bounding_box,
5250                                )
5251                            })
5252                            .unwrap_or(false);
5253
5254                    if point_is_inside_rect(position, elem_box) && clip_ok {
5255                        // Call hover callbacks
5256                        if has_hover {
5257                            let pointer_data = self.pointer_info;
5258                            if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
5259                                if let Some(ref mut callback) = item.on_hover_fn {
5260                                    callback(elem_id_copy.clone(), pointer_data);
5261                                }
5262                            }
5263                        }
5264                        self.pointer_over_ids.push(elem_id_copy);
5265                        found = true;
5266                    }
5267
5268                    if self.element_has_config(current_idx, ElementConfigType::Text) {
5269                        dfs.pop();
5270                        vis.pop();
5271                        continue;
5272                    }
5273                    let children_start = self.layout_elements[current_idx].children_start;
5274                    let children_length =
5275                        self.layout_elements[current_idx].children_length as usize;
5276                    for ci in (0..children_length).rev() {
5277                        let child = self.layout_element_children[children_start + ci];
5278                        dfs.push(child);
5279                        vis.push(false);
5280                    }
5281                } else {
5282                    dfs.pop();
5283                    vis.pop();
5284                }
5285            }
5286
5287            if found {
5288                let root_elem_idx = root.layout_element_index as usize;
5289                if self.element_has_config(root_elem_idx, ElementConfigType::Floating) {
5290                    if let Some(cfg_idx) = self
5291                        .find_element_config_index(root_elem_idx, ElementConfigType::Floating)
5292                    {
5293                        if self.floating_element_configs[cfg_idx].pointer_capture_mode
5294                            == PointerCaptureMode::Capture
5295                        {
5296                            break;
5297                        }
5298                    }
5299                }
5300            }
5301        }
5302
5303        // Update pointer state
5304        if is_down {
5305            match self.pointer_info.state {
5306                PointerDataInteractionState::PressedThisFrame => {
5307                    self.pointer_info.state = PointerDataInteractionState::Pressed;
5308                }
5309                s if s != PointerDataInteractionState::Pressed => {
5310                    self.pointer_info.state = PointerDataInteractionState::PressedThisFrame;
5311                }
5312                _ => {}
5313            }
5314        } else {
5315            match self.pointer_info.state {
5316                PointerDataInteractionState::ReleasedThisFrame => {
5317                    self.pointer_info.state = PointerDataInteractionState::Released;
5318                }
5319                s if s != PointerDataInteractionState::Released => {
5320                    self.pointer_info.state = PointerDataInteractionState::ReleasedThisFrame;
5321                }
5322                _ => {}
5323            }
5324        }
5325
5326        // Fire on_press / on_release callbacks and track pressed element
5327        match self.pointer_info.state {
5328            PointerDataInteractionState::PressedThisFrame => {
5329                // Check if clicked element is a text input
5330                let clicked_text_input = self.pointer_over_ids.last()
5331                    .and_then(|top| self.layout_element_map.get(&top.id))
5332                    .map(|item| item.is_text_input)
5333                    .unwrap_or(false);
5334
5335                if clicked_text_input {
5336                    // Focus the text input (or keep focus if already focused)
5337                    self.focus_from_keyboard = false;
5338                    if let Some(top) = self.pointer_over_ids.last().cloned() {
5339                        if self.focused_element_id != top.id {
5340                            self.change_focus(top.id);
5341                        }
5342                        // Compute click x,y relative to the element's bounding box
5343                        if let Some(item) = self.layout_element_map.get(&top.id) {
5344                            let click_x = self.pointer_info.position.x - item.bounding_box.x;
5345                            let click_y = self.pointer_info.position.y - item.bounding_box.y;
5346                            // We can't check shift from here (no keyboard state);
5347                            // lib.rs will set shift via a dedicated method if needed.
5348                            self.pending_text_click = Some((top.id, click_x, click_y, false));
5349                        }
5350                        self.pressed_element_ids = self.pointer_over_ids.clone();
5351                    }
5352                } else {
5353                    // Check if any element in the pointer stack preserves focus
5354                    // (e.g. a toolbar button's child text element inherits the parent's preserve_focus)
5355                    let preserves = self.pointer_over_ids.iter().any(|eid| {
5356                        self.layout_element_map.get(&eid.id)
5357                            .map(|item| item.preserve_focus)
5358                            .unwrap_or(false)
5359                    });
5360
5361                    // Clear keyboard focus when the user clicks, unless the element preserves focus
5362                    if !preserves && self.focused_element_id != 0 {
5363                        self.change_focus(0);
5364                    }
5365
5366                    // Mark all hovered elements as pressed and fire on_press callbacks
5367                    self.pressed_element_ids = self.pointer_over_ids.clone();
5368                    for eid in self.pointer_over_ids.clone().iter() {
5369                        if let Some(item) = self.layout_element_map.get_mut(&eid.id) {
5370                            if let Some(ref mut callback) = item.on_press_fn {
5371                                callback(eid.clone(), self.pointer_info);
5372                            }
5373                        }
5374                    }
5375                }
5376            }
5377            PointerDataInteractionState::ReleasedThisFrame => {
5378                // Fire on_release for all elements that were in the pressed chain
5379                let pressed = std::mem::take(&mut self.pressed_element_ids);
5380                self.track_just_released_ids(&pressed);
5381                for eid in pressed.iter() {
5382                    if let Some(item) = self.layout_element_map.get_mut(&eid.id) {
5383                        if let Some(ref mut callback) = item.on_release_fn {
5384                            callback(eid.clone(), self.pointer_info);
5385                        }
5386                    }
5387                }
5388            }
5389            _ => {}
5390        }
5391    }
5392
5393    /// Physics constants for scroll momentum
5394    const SCROLL_DECEL: f32 = 5.0; // Exponential decay rate (reaches ~0.7% after 1s)
5395    const SCROLL_MIN_VELOCITY: f32 = 5.0; // px/s below which momentum stops
5396    const SCROLL_VELOCITY_SMOOTHING: f32 = 0.4; // EMA factor for velocity tracking
5397
5398    pub fn update_scroll_containers(
5399        &mut self,
5400        enable_drag_scrolling: bool,
5401        scroll_delta: Vector2,
5402        delta_time: f32,
5403        touch_input_active: bool,
5404    ) {
5405        let pointer = self.pointer_info.position;
5406        let dt = delta_time.max(0.0001); // Guard against zero/negative dt
5407
5408        // Remove containers that weren't open this frame, reset flag for next frame
5409        let mut i = 0;
5410        while i < self.scroll_container_datas.len() {
5411            if !self.scroll_container_datas[i].open_this_frame {
5412                self.scroll_container_datas.swap_remove(i);
5413                continue;
5414            }
5415            self.scroll_container_datas[i].open_this_frame = false;
5416            i += 1;
5417        }
5418
5419        for scd in &mut self.scroll_container_datas {
5420            scd.scrollbar_activity_this_frame = false;
5421        }
5422
5423        // --- Drag scrolling ---
5424        if enable_drag_scrolling {
5425            let pointer_state = self.pointer_info.state;
5426
5427            match pointer_state {
5428                PointerDataInteractionState::PressedThisFrame => {
5429                    // Find the deepest scroll container under the pointer and start drag
5430                    let mut best: Option<usize> = None;
5431                    for si in 0..self.scroll_container_datas.len() {
5432                        let scd = &self.scroll_container_datas[si];
5433                        let bb = scd.bounding_box;
5434                        if pointer.x >= bb.x
5435                            && pointer.x <= bb.x + bb.width
5436                            && pointer.y >= bb.y
5437                            && pointer.y <= bb.y + bb.height
5438                        {
5439                            best = Some(si);
5440                        }
5441                    }
5442                    if let Some(si) = best {
5443                        let scd = &mut self.scroll_container_datas[si];
5444                        scd.scroll_momentum = Vector2::default();
5445                        scd.previous_delta = Vector2::default();
5446
5447                        scd.pointer_scroll_active = false;
5448                        scd.scrollbar_thumb_drag_active_x = false;
5449                        scd.scrollbar_thumb_drag_active_y = false;
5450
5451                        let mut started_thumb_drag = false;
5452                        if let Some(scrollbar_cfg) = scd.scrollbar {
5453                            let alpha = scrollbar_visibility_alpha(scrollbar_cfg, scd.scrollbar_idle_frames);
5454                            if alpha > 0.0 {
5455                                if scd.scroll_y_enabled {
5456                                    if let Some(geo) = compute_vertical_scrollbar_geometry(
5457                                        scd.bounding_box,
5458                                        scd.content_size.height,
5459                                        -scd.scroll_position.y,
5460                                        scrollbar_cfg,
5461                                    ) {
5462                                        if point_is_inside_rect(pointer, geo.thumb_bbox) {
5463                                            scd.scrollbar_thumb_drag_active_y = true;
5464                                            started_thumb_drag = true;
5465                                        }
5466                                    }
5467                                }
5468
5469                                if !started_thumb_drag && scd.scroll_x_enabled {
5470                                    if let Some(geo) = compute_horizontal_scrollbar_geometry(
5471                                        scd.bounding_box,
5472                                        scd.content_size.width,
5473                                        -scd.scroll_position.x,
5474                                        scrollbar_cfg,
5475                                    ) {
5476                                        if point_is_inside_rect(pointer, geo.thumb_bbox) {
5477                                            scd.scrollbar_thumb_drag_active_x = true;
5478                                            started_thumb_drag = true;
5479                                        }
5480                                    }
5481                                }
5482                            }
5483                        }
5484
5485                        if started_thumb_drag {
5486                            scd.scrollbar_drag_origin = pointer;
5487                            scd.scrollbar_drag_scroll_origin =
5488                                Vector2::new(-scd.scroll_position.x, -scd.scroll_position.y);
5489                            scd.scrollbar_activity_this_frame = true;
5490                        } else if !(scd.no_drag_scroll && !touch_input_active) {
5491                            scd.pointer_scroll_active = true;
5492                            scd.pointer_origin = pointer;
5493                            scd.scroll_origin = scd.scroll_position;
5494                        }
5495                    }
5496                }
5497                PointerDataInteractionState::Pressed => {
5498                    // Update drag: move scroll position to follow pointer
5499                    for si in 0..self.scroll_container_datas.len() {
5500                        let scd = &mut self.scroll_container_datas[si];
5501
5502                        if scd.scrollbar_thumb_drag_active_y {
5503                            if let Some(scrollbar_cfg) = scd.scrollbar {
5504                                if let Some(geo) = compute_vertical_scrollbar_geometry(
5505                                    scd.bounding_box,
5506                                    scd.content_size.height,
5507                                    scd.scrollbar_drag_scroll_origin.y,
5508                                    scrollbar_cfg,
5509                                ) {
5510                                    let delta = pointer.y - scd.scrollbar_drag_origin.y;
5511                                    let new_scroll = if geo.thumb_travel <= 0.0 {
5512                                        0.0
5513                                    } else {
5514                                        scd.scrollbar_drag_scroll_origin.y
5515                                            + delta * (geo.max_scroll / geo.thumb_travel)
5516                                    };
5517                                    scd.scroll_position.y = -new_scroll.clamp(0.0, geo.max_scroll);
5518                                }
5519                            }
5520                            scd.scroll_momentum.y = 0.0;
5521                            scd.scrollbar_activity_this_frame = true;
5522                            continue;
5523                        }
5524
5525                        if scd.scrollbar_thumb_drag_active_x {
5526                            if let Some(scrollbar_cfg) = scd.scrollbar {
5527                                if let Some(geo) = compute_horizontal_scrollbar_geometry(
5528                                    scd.bounding_box,
5529                                    scd.content_size.width,
5530                                    scd.scrollbar_drag_scroll_origin.x,
5531                                    scrollbar_cfg,
5532                                ) {
5533                                    let delta = pointer.x - scd.scrollbar_drag_origin.x;
5534                                    let new_scroll = if geo.thumb_travel <= 0.0 {
5535                                        0.0
5536                                    } else {
5537                                        scd.scrollbar_drag_scroll_origin.x
5538                                            + delta * (geo.max_scroll / geo.thumb_travel)
5539                                    };
5540                                    scd.scroll_position.x = -new_scroll.clamp(0.0, geo.max_scroll);
5541                                }
5542                            }
5543                            scd.scroll_momentum.x = 0.0;
5544                            scd.scrollbar_activity_this_frame = true;
5545                            continue;
5546                        }
5547
5548                        if !scd.pointer_scroll_active {
5549                            continue;
5550                        }
5551
5552                        let drag_delta = Vector2::new(
5553                            pointer.x - scd.pointer_origin.x,
5554                            pointer.y - scd.pointer_origin.y,
5555                        );
5556                        scd.scroll_position = Vector2::new(
5557                            scd.scroll_origin.x + drag_delta.x,
5558                            scd.scroll_origin.y + drag_delta.y,
5559                        );
5560
5561                        // Check if pointer actually moved this frame
5562                        let frame_delta = Vector2::new(
5563                            drag_delta.x - scd.previous_delta.x,
5564                            drag_delta.y - scd.previous_delta.y,
5565                        );
5566                        let moved = frame_delta.x.abs() > 0.5 || frame_delta.y.abs() > 0.5;
5567
5568                        if moved {
5569                            // Pointer moved — update velocity EMA and reset freshness timer
5570                            let instant_velocity = Vector2::new(
5571                                frame_delta.x / dt,
5572                                frame_delta.y / dt,
5573                            );
5574                            let s = Self::SCROLL_VELOCITY_SMOOTHING;
5575                            scd.scroll_momentum = Vector2::new(
5576                                scd.scroll_momentum.x * (1.0 - s) + instant_velocity.x * s,
5577                                scd.scroll_momentum.y * (1.0 - s) + instant_velocity.y * s,
5578                            );
5579                            scd.scrollbar_activity_this_frame = true;
5580                        }
5581                        scd.previous_delta = drag_delta;
5582                    }
5583                }
5584                PointerDataInteractionState::ReleasedThisFrame
5585                | PointerDataInteractionState::Released => {
5586                    for si in 0..self.scroll_container_datas.len() {
5587                        let scd = &mut self.scroll_container_datas[si];
5588                        if scd.scrollbar_thumb_drag_active_x
5589                            || scd.scrollbar_thumb_drag_active_y
5590                        {
5591                            scd.scrollbar_activity_this_frame = true;
5592                        }
5593                        scd.pointer_scroll_active = false;
5594                        scd.scrollbar_thumb_drag_active_x = false;
5595                        scd.scrollbar_thumb_drag_active_y = false;
5596                    }
5597                }
5598            }
5599        }
5600
5601        // --- Momentum scrolling (apply when not actively dragging) ---
5602        for si in 0..self.scroll_container_datas.len() {
5603            let scd = &mut self.scroll_container_datas[si];
5604            if scd.pointer_scroll_active
5605                || scd.scrollbar_thumb_drag_active_x
5606                || scd.scrollbar_thumb_drag_active_y
5607            {
5608                // Still dragging — skip momentum
5609            } else if scd.scroll_momentum.x.abs() > Self::SCROLL_MIN_VELOCITY
5610                || scd.scroll_momentum.y.abs() > Self::SCROLL_MIN_VELOCITY
5611            {
5612                // Apply momentum
5613                scd.scroll_position.x += scd.scroll_momentum.x * dt;
5614                scd.scroll_position.y += scd.scroll_momentum.y * dt;
5615                scd.scrollbar_activity_this_frame = true;
5616
5617                // Exponential decay (frame-rate independent)
5618                let decay = (-Self::SCROLL_DECEL * dt).exp();
5619                scd.scroll_momentum.x *= decay;
5620                scd.scroll_momentum.y *= decay;
5621
5622                // Stop if below threshold
5623                if scd.scroll_momentum.x.abs() < Self::SCROLL_MIN_VELOCITY {
5624                    scd.scroll_momentum.x = 0.0;
5625                }
5626                if scd.scroll_momentum.y.abs() < Self::SCROLL_MIN_VELOCITY {
5627                    scd.scroll_momentum.y = 0.0;
5628                }
5629            }
5630        }
5631
5632        // --- Mouse wheel / external scroll delta ---
5633        if scroll_delta.x != 0.0 || scroll_delta.y != 0.0 {
5634            // Find the deepest (last in list) scroll container the pointer is inside
5635            let mut best: Option<usize> = None;
5636            for si in 0..self.scroll_container_datas.len() {
5637                let bb = self.scroll_container_datas[si].bounding_box;
5638                if pointer.x >= bb.x
5639                    && pointer.x <= bb.x + bb.width
5640                    && pointer.y >= bb.y
5641                    && pointer.y <= bb.y + bb.height
5642                {
5643                    best = Some(si);
5644                }
5645            }
5646            if let Some(si) = best {
5647                let scd = &mut self.scroll_container_datas[si];
5648                scd.scroll_position.y += scroll_delta.y;
5649                scd.scroll_position.x += scroll_delta.x;
5650                // Kill any active momentum when mouse wheel is used
5651                scd.scroll_momentum = Vector2::default();
5652                scd.scrollbar_activity_this_frame = true;
5653            }
5654        }
5655
5656        // --- Clamp all scroll positions ---
5657        for si in 0..self.scroll_container_datas.len() {
5658            let scd = &mut self.scroll_container_datas[si];
5659            let max_scroll_y =
5660                -(scd.content_size.height - scd.bounding_box.height).max(0.0);
5661            let max_scroll_x =
5662                -(scd.content_size.width - scd.bounding_box.width).max(0.0);
5663            scd.scroll_position.y = scd.scroll_position.y.clamp(max_scroll_y, 0.0);
5664            scd.scroll_position.x = scd.scroll_position.x.clamp(max_scroll_x, 0.0);
5665
5666            // Also kill momentum at bounds
5667            if scd.scroll_position.y >= 0.0 || scd.scroll_position.y <= max_scroll_y {
5668                scd.scroll_momentum.y = 0.0;
5669            }
5670            if scd.scroll_position.x >= 0.0 || scd.scroll_position.x <= max_scroll_x {
5671                scd.scroll_momentum.x = 0.0;
5672            }
5673
5674            if scd.scrollbar.is_some() {
5675                if scd.scrollbar_activity_this_frame {
5676                    scd.scrollbar_idle_frames = 0;
5677                } else {
5678                    scd.scrollbar_idle_frames = scd.scrollbar_idle_frames.saturating_add(1);
5679                }
5680            }
5681        }
5682    }
5683
5684    pub fn hovered(&self) -> bool {
5685        let open_idx = self.get_open_layout_element();
5686        let elem_id = self.layout_elements[open_idx].id;
5687        self.pointer_over_ids.iter().any(|eid| eid.id == elem_id)
5688    }
5689
5690    fn release_query_generation(&self) -> u32 {
5691        if self.open_layout_element_stack.len() <= 1 {
5692            self.generation.wrapping_add(1)
5693        } else {
5694            self.generation
5695        }
5696    }
5697
5698    fn track_just_released_ids(&mut self, ids: &[Id]) {
5699        if ids.is_empty() {
5700            return;
5701        }
5702
5703        let target_generation = self.release_query_generation();
5704        if self.released_this_frame_generation != target_generation {
5705            self.released_this_frame_ids.clear();
5706            self.released_this_frame_generation = target_generation;
5707        }
5708
5709        for id in ids {
5710            if !self
5711                .released_this_frame_ids
5712                .iter()
5713                .any(|existing| existing.id == id.id)
5714            {
5715                self.released_this_frame_ids.push(id.clone());
5716            }
5717        }
5718    }
5719
5720    pub fn on_hover(&mut self, callback: Box<dyn FnMut(Id, PointerData)>) {
5721        let open_idx = self.get_open_layout_element();
5722        let elem_id = self.layout_elements[open_idx].id;
5723        if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
5724            item.on_hover_fn = Some(callback);
5725        }
5726    }
5727
5728    pub fn pressed(&self) -> bool {
5729        let open_idx = self.get_open_layout_element();
5730        let elem_id = self.layout_elements[open_idx].id;
5731        self.pressed_element_ids.iter().any(|eid| eid.id == elem_id)
5732    }
5733
5734    pub fn just_pressed(&self) -> bool {
5735        let open_idx = self.get_open_layout_element();
5736        let elem_id = self.layout_elements[open_idx].id;
5737        self.pressed_element_ids.iter().any(|eid| eid.id == elem_id)
5738            && (self.pointer_info.state == PointerDataInteractionState::PressedThisFrame
5739                || self.keyboard_press_this_frame_generation == self.generation)
5740    }
5741
5742    pub fn just_released(&self) -> bool {
5743        if self.released_this_frame_generation != self.generation {
5744            return false;
5745        }
5746
5747        let open_idx = self.get_open_layout_element();
5748        let elem_id = self.layout_elements[open_idx].id;
5749        self.released_this_frame_ids
5750            .iter()
5751            .any(|eid| eid.id == elem_id)
5752    }
5753
5754    pub fn set_press_callbacks(
5755        &mut self,
5756        on_press: Option<Box<dyn FnMut(Id, PointerData)>>,
5757        on_release: Option<Box<dyn FnMut(Id, PointerData)>>,
5758    ) {
5759        let open_idx = self.get_open_layout_element();
5760        let elem_id = self.layout_elements[open_idx].id;
5761        if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
5762            item.on_press_fn = on_press;
5763            item.on_release_fn = on_release;
5764        }
5765    }
5766
5767    /// Returns true if the currently open element has focus.
5768    pub fn focused(&self) -> bool {
5769        let open_idx = self.get_open_layout_element();
5770        let elem_id = self.layout_elements[open_idx].id;
5771        self.focused_element_id == elem_id && elem_id != 0
5772    }
5773
5774    /// Returns the currently focused element's ID, or None.
5775    pub fn focused_element(&self) -> Option<Id> {
5776        if self.focused_element_id != 0 {
5777            self.layout_element_map
5778                .get(&self.focused_element_id)
5779                .map(|item| item.element_id.clone())
5780        } else {
5781            None
5782        }
5783    }
5784
5785    /// Sets focus to the element with the given ID, firing on_unfocus/on_focus callbacks.
5786    pub fn set_focus(&mut self, element_id: u32) {
5787        self.change_focus(element_id);
5788    }
5789
5790    /// Clears focus (no element is focused).
5791    pub fn clear_focus(&mut self) {
5792        self.change_focus(0);
5793    }
5794
5795    /// Internal: changes focus, firing on_unfocus on old and on_focus on new.
5796    pub(crate) fn change_focus(&mut self, new_id: u32) {
5797        let old_id = self.focused_element_id;
5798        if old_id == new_id {
5799            return;
5800        }
5801        self.focused_element_id = new_id;
5802        if new_id == 0 {
5803            self.focus_from_keyboard = false;
5804        }
5805
5806        // Fire on_unfocus on old element
5807        if old_id != 0 {
5808            if let Some(item) = self.layout_element_map.get_mut(&old_id) {
5809                let id_copy = item.element_id.clone();
5810                if let Some(ref mut callback) = item.on_unfocus_fn {
5811                    callback(id_copy);
5812                }
5813            }
5814        }
5815
5816        // Fire on_focus on new element
5817        if new_id != 0 {
5818            if let Some(item) = self.layout_element_map.get_mut(&new_id) {
5819                let id_copy = item.element_id.clone();
5820                if let Some(ref mut callback) = item.on_focus_fn {
5821                    callback(id_copy);
5822                }
5823            }
5824        }
5825    }
5826
5827    /// Fire the on_press callback for the element with the given u32 ID.
5828    /// Used by screen reader action handling.
5829    #[allow(dead_code)]
5830    pub(crate) fn fire_press(&mut self, element_id: u32) {
5831        if let Some(item) = self.layout_element_map.get_mut(&element_id) {
5832            let id_copy = item.element_id.clone();
5833            if let Some(ref mut callback) = item.on_press_fn {
5834                callback(id_copy, PointerData::default());
5835            }
5836        }
5837    }
5838
5839    pub fn set_focus_callbacks(
5840        &mut self,
5841        on_focus: Option<Box<dyn FnMut(Id)>>,
5842        on_unfocus: Option<Box<dyn FnMut(Id)>>,
5843    ) {
5844        let open_idx = self.get_open_layout_element();
5845        let elem_id = self.layout_elements[open_idx].id;
5846        if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
5847            item.on_focus_fn = on_focus;
5848            item.on_unfocus_fn = on_unfocus;
5849        }
5850    }
5851
5852    /// Sets text input callbacks for the currently open element.
5853    pub fn set_text_input_callbacks(
5854        &mut self,
5855        on_changed: Option<Box<dyn FnMut(&str)>>,
5856        on_submit: Option<Box<dyn FnMut(&str)>>,
5857    ) {
5858        let open_idx = self.get_open_layout_element();
5859        let elem_id = self.layout_elements[open_idx].id;
5860        if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
5861            item.on_text_changed_fn = on_changed;
5862            item.on_text_submit_fn = on_submit;
5863        }
5864    }
5865
5866    /// Returns true if the currently focused element is a text input.
5867    pub fn is_text_input_focused(&self) -> bool {
5868        if self.focused_element_id == 0 {
5869            return false;
5870        }
5871        self.text_edit_states.contains_key(&self.focused_element_id)
5872    }
5873
5874    /// Returns true if the currently focused text input is multiline.
5875    pub fn is_focused_text_input_multiline(&self) -> bool {
5876        if self.focused_element_id == 0 {
5877            return false;
5878        }
5879        self.text_input_element_ids.iter()
5880            .position(|&id| id == self.focused_element_id)
5881            .and_then(|idx| self.text_input_configs.get(idx))
5882            .map_or(false, |cfg| cfg.is_multiline)
5883    }
5884
5885    /// Returns the text value for a text input element, or empty string if not found.
5886    pub fn get_text_value(&self, element_id: u32) -> &str {
5887        self.text_edit_states
5888            .get(&element_id)
5889            .map(|state| state.text.as_str())
5890            .unwrap_or("")
5891    }
5892
5893    /// Sets the text value for a text input element.
5894    pub fn set_text_value(&mut self, element_id: u32, value: &str) {
5895        let state = self.text_edit_states
5896            .entry(element_id)
5897            .or_insert_with(crate::text_input::TextEditState::default);
5898        state.text = value.to_string();
5899        #[cfg(feature = "text-styling")]
5900        let max_pos = crate::text_input::styling::cursor_len(&state.text);
5901        #[cfg(not(feature = "text-styling"))]
5902        let max_pos = state.text.chars().count();
5903        if state.cursor_pos > max_pos {
5904            state.cursor_pos = max_pos;
5905        }
5906        state.selection_anchor = None;
5907        state.reset_blink();
5908    }
5909
5910    /// Returns the cursor position for a text input element, or 0 if not found.
5911    /// When text-styling is enabled, this returns the visual position.
5912    pub fn get_cursor_pos(&self, element_id: u32) -> usize {
5913        self.text_edit_states
5914            .get(&element_id)
5915            .map(|state| state.cursor_pos)
5916            .unwrap_or(0)
5917    }
5918
5919    /// Sets the cursor position for a text input element.
5920    /// When text-styling is enabled, `pos` is in visual space.
5921    /// Clamps to the text length and clears any selection.
5922    pub fn set_cursor_pos(&mut self, element_id: u32, pos: usize) {
5923        if let Some(state) = self.text_edit_states.get_mut(&element_id) {
5924            #[cfg(feature = "text-styling")]
5925            let max_pos = crate::text_input::styling::cursor_len(&state.text);
5926            #[cfg(not(feature = "text-styling"))]
5927            let max_pos = state.text.chars().count();
5928            state.cursor_pos = pos.min(max_pos);
5929            state.selection_anchor = None;
5930            state.reset_blink();
5931        }
5932    }
5933
5934    /// Returns the selection range (start, end) for a text input element, or None.
5935    /// When text-styling is enabled, these are visual positions.
5936    pub fn get_selection_range(&self, element_id: u32) -> Option<(usize, usize)> {
5937        self.text_edit_states
5938            .get(&element_id)
5939            .and_then(|state| state.selection_range())
5940    }
5941
5942    /// Sets the selection range for a text input element.
5943    /// `anchor` is where selection started, `cursor` is where it ends.
5944    /// When text-styling is enabled, these are visual positions.
5945    pub fn set_selection(&mut self, element_id: u32, anchor: usize, cursor: usize) {
5946        if let Some(state) = self.text_edit_states.get_mut(&element_id) {
5947            #[cfg(feature = "text-styling")]
5948            let max_pos = crate::text_input::styling::cursor_len(&state.text);
5949            #[cfg(not(feature = "text-styling"))]
5950            let max_pos = state.text.chars().count();
5951            state.selection_anchor = Some(anchor.min(max_pos));
5952            state.cursor_pos = cursor.min(max_pos);
5953            state.reset_blink();
5954        }
5955    }
5956
5957    /// Returns true if the given element ID is currently pressed.
5958    pub fn is_element_pressed(&self, element_id: u32) -> bool {
5959        self.pressed_element_ids.iter().any(|eid| eid.id == element_id)
5960    }
5961
5962    /// Returns true if the given element ID was pressed this frame.
5963    pub fn is_element_just_pressed(&self, element_id: u32) -> bool {
5964        self.pressed_element_ids.iter().any(|eid| eid.id == element_id)
5965            && (self.pointer_info.state == PointerDataInteractionState::PressedThisFrame
5966                || self.keyboard_press_this_frame_generation == self.generation)
5967    }
5968
5969    /// Returns true if the given element ID was released this frame.
5970    pub fn is_element_just_released(&self, element_id: u32) -> bool {
5971        self.released_this_frame_generation == self.generation
5972            && self
5973                .released_this_frame_ids
5974                .iter()
5975                .any(|eid| eid.id == element_id)
5976    }
5977
5978    /// Process a character input event for the focused text input.
5979    /// Returns true if the character was consumed by a text input.
5980    pub fn process_text_input_char(&mut self, ch: char) -> bool {
5981        if !self.is_text_input_focused() {
5982            return false;
5983        }
5984        let elem_id = self.focused_element_id;
5985
5986        // Get max_length from current config (if available this frame)
5987        let max_length = self.text_input_element_ids.iter()
5988            .position(|&id| id == elem_id)
5989            .and_then(|idx| self.text_input_configs.get(idx))
5990            .and_then(|cfg| cfg.max_length);
5991
5992        if let Some(state) = self.text_edit_states.get_mut(&elem_id) {
5993            let old_text = state.text.clone();
5994            state.push_undo(crate::text_input::UndoActionKind::InsertChar);
5995            #[cfg(feature = "text-styling")]
5996            {
5997                state.insert_char_styled(ch, max_length);
5998            }
5999            #[cfg(not(feature = "text-styling"))]
6000            {
6001                state.insert_text(&ch.to_string(), max_length);
6002            }
6003            if state.text != old_text {
6004                let new_text = state.text.clone();
6005                // Fire on_changed callback
6006                if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
6007                    if let Some(ref mut callback) = item.on_text_changed_fn {
6008                        callback(&new_text);
6009                    }
6010                }
6011            }
6012            true
6013        } else {
6014            false
6015        }
6016    }
6017
6018    /// Process a key event for the focused text input.
6019    /// `action` specifies which editing action to perform.
6020    /// Returns true if the key was consumed.
6021    pub fn process_text_input_action(&mut self, action: TextInputAction) -> bool {
6022        if !self.is_text_input_focused() {
6023            return false;
6024        }
6025        let elem_id = self.focused_element_id;
6026
6027        // Get config for the focused element
6028        let config_idx = self.text_input_element_ids.iter()
6029            .position(|&id| id == elem_id);
6030        let (max_length, is_multiline, font_asset, font_size) = config_idx
6031            .and_then(|idx| self.text_input_configs.get(idx))
6032            .map(|cfg| (cfg.max_length, cfg.is_multiline, cfg.font_asset, cfg.font_size))
6033            .unwrap_or((None, false, None, 16));
6034
6035        // For multiline visual navigation, compute visual lines
6036        let visual_lines_opt = if is_multiline {
6037            let visible_width = self.layout_element_map
6038                .get(&elem_id)
6039                .map(|item| item.bounding_box.width)
6040                .unwrap_or(0.0);
6041            if visible_width > 0.0 {
6042                if let Some(state) = self.text_edit_states.get(&elem_id) {
6043                    if let Some(ref measure_fn) = self.measure_text_fn {
6044                        Some(crate::text_input::wrap_lines(
6045                            &state.text,
6046                            visible_width,
6047                            font_asset,
6048                            font_size,
6049                            measure_fn.as_ref(),
6050                        ))
6051                    } else { None }
6052                } else { None }
6053            } else { None }
6054        } else { None };
6055
6056        if let Some(state) = self.text_edit_states.get_mut(&elem_id) {
6057            let old_text = state.text.clone();
6058
6059            // Push undo before text-modifying actions
6060            match &action {
6061                TextInputAction::Backspace => state.push_undo(crate::text_input::UndoActionKind::Backspace),
6062                TextInputAction::Delete => state.push_undo(crate::text_input::UndoActionKind::Delete),
6063                TextInputAction::BackspaceWord => state.push_undo(crate::text_input::UndoActionKind::DeleteWord),
6064                TextInputAction::DeleteWord => state.push_undo(crate::text_input::UndoActionKind::DeleteWord),
6065                TextInputAction::Cut => state.push_undo(crate::text_input::UndoActionKind::Cut),
6066                TextInputAction::Paste { .. } => state.push_undo(crate::text_input::UndoActionKind::Paste),
6067                TextInputAction::Submit if is_multiline => state.push_undo(crate::text_input::UndoActionKind::InsertChar),
6068                _ => {}
6069            }
6070
6071            match action {
6072                TextInputAction::MoveLeft { shift } => {
6073                    #[cfg(feature = "text-styling")]
6074                    { state.move_left_styled(shift); }
6075                    #[cfg(not(feature = "text-styling"))]
6076                    { state.move_left(shift); }
6077                }
6078                TextInputAction::MoveRight { shift } => {
6079                    #[cfg(feature = "text-styling")]
6080                    { state.move_right_styled(shift); }
6081                    #[cfg(not(feature = "text-styling"))]
6082                    { state.move_right(shift); }
6083                }
6084                TextInputAction::MoveWordLeft { shift } => {
6085                    #[cfg(feature = "text-styling")]
6086                    { state.move_word_left_styled(shift); }
6087                    #[cfg(not(feature = "text-styling"))]
6088                    { state.move_word_left(shift); }
6089                }
6090                TextInputAction::MoveWordRight { shift } => {
6091                    #[cfg(feature = "text-styling")]
6092                    { state.move_word_right_styled(shift); }
6093                    #[cfg(not(feature = "text-styling"))]
6094                    { state.move_word_right(shift); }
6095                }
6096                TextInputAction::MoveHome { shift } => {
6097                    // Multiline uses visual line navigation (raw positions)
6098                    #[cfg(not(feature = "text-styling"))]
6099                    {
6100                        if let Some(ref vl) = visual_lines_opt {
6101                            let new_pos = crate::text_input::visual_line_home(vl, state.cursor_pos);
6102                            if shift && state.selection_anchor.is_none() {
6103                                state.selection_anchor = Some(state.cursor_pos);
6104                            }
6105                            state.cursor_pos = new_pos;
6106                            if !shift { state.selection_anchor = None; }
6107                            else if state.selection_anchor == Some(state.cursor_pos) { state.selection_anchor = None; }
6108                            state.reset_blink();
6109                        } else {
6110                            state.move_home(shift);
6111                        }
6112                    }
6113                    #[cfg(feature = "text-styling")]
6114                    {
6115                        state.move_home_styled(shift);
6116                    }
6117                }
6118                TextInputAction::MoveEnd { shift } => {
6119                    #[cfg(not(feature = "text-styling"))]
6120                    {
6121                        if let Some(ref vl) = visual_lines_opt {
6122                            let new_pos = crate::text_input::visual_line_end(vl, state.cursor_pos);
6123                            if shift && state.selection_anchor.is_none() {
6124                                state.selection_anchor = Some(state.cursor_pos);
6125                            }
6126                            state.cursor_pos = new_pos;
6127                            if !shift { state.selection_anchor = None; }
6128                            else if state.selection_anchor == Some(state.cursor_pos) { state.selection_anchor = None; }
6129                            state.reset_blink();
6130                        } else {
6131                            state.move_end(shift);
6132                        }
6133                    }
6134                    #[cfg(feature = "text-styling")]
6135                    {
6136                        state.move_end_styled(shift);
6137                    }
6138                }
6139                TextInputAction::MoveUp { shift } => {
6140                    #[cfg(not(feature = "text-styling"))]
6141                    {
6142                        if let Some(ref vl) = visual_lines_opt {
6143                            let new_pos = crate::text_input::visual_move_up(vl, state.cursor_pos);
6144                            if shift && state.selection_anchor.is_none() {
6145                                state.selection_anchor = Some(state.cursor_pos);
6146                            }
6147                            state.cursor_pos = new_pos;
6148                            if !shift { state.selection_anchor = None; }
6149                            else if state.selection_anchor == Some(state.cursor_pos) { state.selection_anchor = None; }
6150                            state.reset_blink();
6151                        } else {
6152                            state.move_up(shift);
6153                        }
6154                    }
6155                    #[cfg(feature = "text-styling")]
6156                    {
6157                        state.move_up_styled(shift, visual_lines_opt.as_deref());
6158                    }
6159                }
6160                TextInputAction::MoveDown { shift } => {
6161                    #[cfg(not(feature = "text-styling"))]
6162                    {
6163                        if let Some(ref vl) = visual_lines_opt {
6164                            let text_len = state.text.chars().count();
6165                            let new_pos = crate::text_input::visual_move_down(vl, state.cursor_pos, text_len);
6166                            if shift && state.selection_anchor.is_none() {
6167                                state.selection_anchor = Some(state.cursor_pos);
6168                            }
6169                            state.cursor_pos = new_pos;
6170                            if !shift { state.selection_anchor = None; }
6171                            else if state.selection_anchor == Some(state.cursor_pos) { state.selection_anchor = None; }
6172                            state.reset_blink();
6173                        } else {
6174                            state.move_down(shift);
6175                        }
6176                    }
6177                    #[cfg(feature = "text-styling")]
6178                    {
6179                        state.move_down_styled(shift, visual_lines_opt.as_deref());
6180                    }
6181                }
6182                TextInputAction::Backspace => {
6183                    #[cfg(feature = "text-styling")]
6184                    { state.backspace_styled(); }
6185                    #[cfg(not(feature = "text-styling"))]
6186                    { state.backspace(); }
6187                }
6188                TextInputAction::Delete => {
6189                    #[cfg(feature = "text-styling")]
6190                    { state.delete_forward_styled(); }
6191                    #[cfg(not(feature = "text-styling"))]
6192                    { state.delete_forward(); }
6193                }
6194                TextInputAction::BackspaceWord => {
6195                    #[cfg(feature = "text-styling")]
6196                    { state.backspace_word_styled(); }
6197                    #[cfg(not(feature = "text-styling"))]
6198                    { state.backspace_word(); }
6199                }
6200                TextInputAction::DeleteWord => {
6201                    #[cfg(feature = "text-styling")]
6202                    { state.delete_word_forward_styled(); }
6203                    #[cfg(not(feature = "text-styling"))]
6204                    { state.delete_word_forward(); }
6205                }
6206                TextInputAction::SelectAll => {
6207                    #[cfg(feature = "text-styling")]
6208                    { state.select_all_styled(); }
6209                    #[cfg(not(feature = "text-styling"))]
6210                    { state.select_all(); }
6211                }
6212                TextInputAction::Copy => {
6213                    // Copying doesn't modify state; handled by lib.rs
6214                }
6215                TextInputAction::Cut => {
6216                    #[cfg(feature = "text-styling")]
6217                    { state.delete_selection_styled(); }
6218                    #[cfg(not(feature = "text-styling"))]
6219                    { state.delete_selection(); }
6220                }
6221                TextInputAction::Paste { text } => {
6222                    #[cfg(feature = "text-styling")]
6223                    {
6224                        let escaped = crate::text_input::styling::escape_str(&text);
6225                        state.insert_text_styled(&escaped, max_length);
6226                    }
6227                    #[cfg(not(feature = "text-styling"))]
6228                    {
6229                        state.insert_text(&text, max_length);
6230                    }
6231                }
6232                TextInputAction::Submit => {
6233                    if is_multiline {
6234                        #[cfg(feature = "text-styling")]
6235                        { state.insert_text_styled("\n", max_length); }
6236                        #[cfg(not(feature = "text-styling"))]
6237                        { state.insert_text("\n", max_length); }
6238                    } else {
6239                        let text = state.text.clone();
6240                        // Fire on_submit callback
6241                        if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
6242                            if let Some(ref mut callback) = item.on_text_submit_fn {
6243                                callback(&text);
6244                            }
6245                        }
6246                        return true;
6247                    }
6248                }
6249                TextInputAction::Undo => {
6250                    state.undo();
6251                }
6252                TextInputAction::Redo => {
6253                    state.redo();
6254                }
6255            }
6256            if state.text != old_text {
6257                let new_text = state.text.clone();
6258                if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
6259                    if let Some(ref mut callback) = item.on_text_changed_fn {
6260                        callback(&new_text);
6261                    }
6262                }
6263            }
6264            true
6265        } else {
6266            false
6267        }
6268    }
6269
6270    /// Update blink timers for all text input states.
6271    pub fn update_text_input_blink_timers(&mut self) {
6272        let dt = self.frame_delta_time as f64;
6273        for state in self.text_edit_states.values_mut() {
6274            state.cursor_blink_timer += dt;
6275        }
6276    }
6277
6278    /// Update scroll offsets for text inputs to ensure cursor visibility.
6279    pub fn update_text_input_scroll(&mut self) {
6280        let focused = self.focused_element_id;
6281        if focused == 0 {
6282            return;
6283        }
6284        // Get bounding box for the focused text input
6285        let (visible_width, visible_height) = self.layout_element_map
6286            .get(&focused)
6287            .map(|item| (item.bounding_box.width, item.bounding_box.height))
6288            .unwrap_or((0.0, 0.0));
6289        if visible_width <= 0.0 {
6290            return;
6291        }
6292
6293        // Get cursor x-position
6294        if let Some(state) = self.text_edit_states.get(&focused) {
6295            let config_idx = self.text_input_element_ids.iter()
6296                .position(|&id| id == focused);
6297            if let Some(idx) = config_idx {
6298                if let Some(cfg) = self.text_input_configs.get(idx) {
6299                    if let Some(ref measure_fn) = self.measure_text_fn {
6300                        let disp_text = crate::text_input::display_text(
6301                            &state.text,
6302                            &cfg.placeholder,
6303                            cfg.is_password && !cfg.is_multiline,
6304                        );
6305                        if !state.text.is_empty() {
6306                            if cfg.is_multiline {
6307                                // Multiline: use visual lines with word wrapping
6308                                let visual_lines = crate::text_input::wrap_lines(
6309                                    &disp_text,
6310                                    visible_width,
6311                                    cfg.font_asset,
6312                                    cfg.font_size,
6313                                    measure_fn.as_ref(),
6314                                );
6315                                #[cfg(feature = "text-styling")]
6316                                let raw_cursor = state.cursor_pos_raw();
6317                                #[cfg(not(feature = "text-styling"))]
6318                                let raw_cursor = state.cursor_pos;
6319                                let (cursor_line, cursor_col) = crate::text_input::cursor_to_visual_pos(&visual_lines, raw_cursor);
6320                                let vl_text = visual_lines.get(cursor_line).map(|vl| vl.text.as_str()).unwrap_or("");
6321                                let line_positions = crate::text_input::compute_char_x_positions(
6322                                    vl_text,
6323                                    cfg.font_asset,
6324                                    cfg.font_size,
6325                                    measure_fn.as_ref(),
6326                                );
6327                                let cursor_x = line_positions.get(cursor_col).copied().unwrap_or(0.0);
6328                                let cfg_font_asset = cfg.font_asset;
6329                                let cfg_font_size = cfg.font_size;
6330                                let cfg_line_height_val = cfg.line_height;
6331                                let natural_height = self.font_height(cfg_font_asset, cfg_font_size);
6332                                let line_height = if cfg_line_height_val > 0 { cfg_line_height_val as f32 } else { natural_height };
6333                                if let Some(state_mut) = self.text_edit_states.get_mut(&focused) {
6334                                    state_mut.ensure_cursor_visible(cursor_x, visible_width);
6335                                    state_mut.ensure_cursor_visible_vertical(cursor_line, line_height, visible_height);
6336                                }
6337                            } else {
6338                                let char_x_positions = crate::text_input::compute_char_x_positions(
6339                                    &disp_text,
6340                                    cfg.font_asset,
6341                                    cfg.font_size,
6342                                    measure_fn.as_ref(),
6343                                );
6344                                #[cfg(feature = "text-styling")]
6345                                let raw_cursor = state.cursor_pos_raw();
6346                                #[cfg(not(feature = "text-styling"))]
6347                                let raw_cursor = state.cursor_pos;
6348                                let cursor_x = char_x_positions
6349                                    .get(raw_cursor)
6350                                    .copied()
6351                                    .unwrap_or(0.0);
6352                                if let Some(state_mut) = self.text_edit_states.get_mut(&focused) {
6353                                    state_mut.ensure_cursor_visible(cursor_x, visible_width);
6354                                }
6355                            }
6356                        } else if let Some(state_mut) = self.text_edit_states.get_mut(&focused) {
6357                            state_mut.scroll_offset = 0.0;
6358                            state_mut.scroll_offset_y = 0.0;
6359                        }
6360                    }
6361                }
6362            }
6363        }
6364    }
6365
6366    /// Handle pointer-based scrolling for text inputs: scroll wheel and drag-to-scroll.
6367    /// Mobile-first: dragging scrolls the content rather than selecting text.
6368    /// `scroll_delta` contains (x, y) scroll wheel deltas. For single-line, both axes
6369    /// map to horizontal scroll. For multiline, y scrolls vertically.
6370    pub fn update_text_input_pointer_scroll(&mut self, scroll_delta: Vector2, touch_input_active: bool) -> bool {
6371        for idle in self.text_input_scrollbar_idle_frames.values_mut() {
6372            *idle = idle.saturating_add(1);
6373        }
6374
6375        let mut consumed_scroll = false;
6376
6377        let focused = self.focused_element_id;
6378
6379        // --- Scroll wheel: scroll any hovered text input (even if unfocused) ---
6380        let has_scroll = scroll_delta.x.abs() > 0.01 || scroll_delta.y.abs() > 0.01;
6381        if has_scroll {
6382            let p = self.pointer_info.position;
6383            // Find the text input under the pointer
6384            let hovered_ti = self.text_input_element_ids.iter().enumerate().find(|&(_, &id)| {
6385                self.layout_element_map.get(&id)
6386                    .map(|item| {
6387                        let bb = item.bounding_box;
6388                        p.x >= bb.x && p.x <= bb.x + bb.width
6389                            && p.y >= bb.y && p.y <= bb.y + bb.height
6390                    })
6391                    .unwrap_or(false)
6392            });
6393            if let Some((idx, &elem_id)) = hovered_ti {
6394                let is_multiline = self.text_input_configs.get(idx)
6395                    .map(|cfg| cfg.is_multiline)
6396                    .unwrap_or(false);
6397                if let Some(state) = self.text_edit_states.get_mut(&elem_id) {
6398                    if is_multiline {
6399                        if scroll_delta.y.abs() > 0.01 {
6400                            state.scroll_offset_y -= scroll_delta.y;
6401                            if state.scroll_offset_y < 0.0 {
6402                                state.scroll_offset_y = 0.0;
6403                            }
6404                        }
6405                    } else {
6406                        let h_delta = if scroll_delta.x.abs() > scroll_delta.y.abs() {
6407                            scroll_delta.x
6408                        } else {
6409                            scroll_delta.y
6410                        };
6411                        if h_delta.abs() > 0.01 {
6412                            state.scroll_offset -= h_delta;
6413                            if state.scroll_offset < 0.0 {
6414                                state.scroll_offset = 0.0;
6415                            }
6416                        }
6417                    }
6418                    consumed_scroll = true;
6419                    self.text_input_scrollbar_idle_frames.insert(elem_id, 0);
6420                }
6421            }
6422        }
6423
6424        // --- Drag scrolling (focused text input only) ---
6425        if focused == 0 {
6426            if self.text_input_drag_active {
6427                let pointer_state = self.pointer_info.state;
6428                if matches!(pointer_state, PointerDataInteractionState::ReleasedThisFrame | PointerDataInteractionState::Released) {
6429                    self.text_input_drag_active = false;
6430                    self.text_input_drag_from_touch = false;
6431                }
6432            }
6433            self.text_input_scrollbar_drag_active = false;
6434            return consumed_scroll;
6435        }
6436
6437        let ti_index = self
6438            .text_input_element_ids
6439            .iter()
6440            .position(|&id| id == focused);
6441        let Some(ti_index) = ti_index else {
6442            if self.text_input_drag_active {
6443                let pointer_state = self.pointer_info.state;
6444                if matches!(pointer_state, PointerDataInteractionState::ReleasedThisFrame | PointerDataInteractionState::Released) {
6445                    self.text_input_drag_active = false;
6446                    self.text_input_drag_from_touch = false;
6447                }
6448            }
6449            self.text_input_scrollbar_drag_active = false;
6450            return consumed_scroll;
6451        };
6452        let Some(ti_cfg) = self.text_input_configs.get(ti_index).cloned() else {
6453            self.text_input_scrollbar_drag_active = false;
6454            return consumed_scroll;
6455        };
6456        let is_multiline = ti_cfg.is_multiline;
6457
6458        let pointer_over_focused = self.layout_element_map.get(&focused)
6459            .map(|item| {
6460                let bb = item.bounding_box;
6461                let p = self.pointer_info.position;
6462                p.x >= bb.x && p.x <= bb.x + bb.width
6463                    && p.y >= bb.y && p.y <= bb.y + bb.height
6464            })
6465            .unwrap_or(false);
6466
6467        let pointer = self.pointer_info.position;
6468        let pointer_state = self.pointer_info.state;
6469
6470        match pointer_state {
6471            PointerDataInteractionState::PressedThisFrame => {
6472                if pointer_over_focused {
6473                    let mut started_scrollbar_drag = false;
6474
6475                    if let Some(scrollbar_cfg) = ti_cfg.scrollbar {
6476                        if let Some(item) = self.layout_element_map.get(&focused) {
6477                            if let Some(state) = self.text_edit_states.get(&focused).cloned() {
6478                                let bbox = item.bounding_box;
6479                                let (content_width, content_height) =
6480                                    self.text_input_content_size(&state, &ti_cfg, bbox.width);
6481
6482                                let idle_frames = self
6483                                    .text_input_scrollbar_idle_frames
6484                                    .get(&focused)
6485                                    .copied()
6486                                    .unwrap_or(0);
6487                                let alpha = scrollbar_visibility_alpha(scrollbar_cfg, idle_frames);
6488
6489                                if alpha > 0.0 {
6490                                    if ti_cfg.is_multiline {
6491                                        if let Some(geo) = compute_vertical_scrollbar_geometry(
6492                                            bbox,
6493                                            content_height,
6494                                            state.scroll_offset_y,
6495                                            scrollbar_cfg,
6496                                        ) {
6497                                            if point_is_inside_rect(pointer, geo.thumb_bbox) {
6498                                                started_scrollbar_drag = true;
6499                                                self.text_input_scrollbar_drag_active = true;
6500                                                self.text_input_scrollbar_drag_vertical = true;
6501                                                self.text_input_scrollbar_drag_origin = pointer.y;
6502                                                self.text_input_scrollbar_drag_scroll_origin =
6503                                                    state.scroll_offset_y;
6504                                            }
6505                                        }
6506                                    }
6507
6508                                    if !started_scrollbar_drag {
6509                                        if let Some(geo) = compute_horizontal_scrollbar_geometry(
6510                                            bbox,
6511                                            content_width,
6512                                            state.scroll_offset,
6513                                            scrollbar_cfg,
6514                                        ) {
6515                                            if point_is_inside_rect(pointer, geo.thumb_bbox) {
6516                                                started_scrollbar_drag = true;
6517                                                self.text_input_scrollbar_drag_active = true;
6518                                                self.text_input_scrollbar_drag_vertical = false;
6519                                                self.text_input_scrollbar_drag_origin = pointer.x;
6520                                                self.text_input_scrollbar_drag_scroll_origin =
6521                                                    state.scroll_offset;
6522                                            }
6523                                        }
6524                                    }
6525                                }
6526                            }
6527                        }
6528                    }
6529
6530                    if started_scrollbar_drag {
6531                        self.text_input_drag_active = false;
6532                        self.text_input_drag_from_touch = false;
6533                        self.text_input_drag_element_id = focused;
6534                        self.pending_text_click = None;
6535                        self.text_input_scrollbar_idle_frames.insert(focused, 0);
6536                        consumed_scroll = true;
6537                        return consumed_scroll;
6538                    }
6539
6540                    let (scroll_x, scroll_y) = self.text_edit_states.get(&focused)
6541                        .map(|s| (s.scroll_offset, s.scroll_offset_y))
6542                        .unwrap_or((0.0, 0.0));
6543                    self.text_input_drag_active = true;
6544                    self.text_input_drag_origin = pointer;
6545                    self.text_input_drag_scroll_origin = Vector2::new(scroll_x, scroll_y);
6546                    self.text_input_drag_element_id = focused;
6547                    self.text_input_drag_from_touch = touch_input_active;
6548                    self.text_input_scrollbar_drag_active = false;
6549                }
6550            }
6551            PointerDataInteractionState::Pressed => {
6552                if self.text_input_scrollbar_drag_active {
6553                    if let Some(item) = self.layout_element_map.get(&self.text_input_drag_element_id)
6554                    {
6555                        if let Some(state_snapshot) = self
6556                            .text_edit_states
6557                            .get(&self.text_input_drag_element_id)
6558                            .cloned()
6559                        {
6560                            if let Some(scrollbar_cfg) = ti_cfg.scrollbar {
6561                                let bbox = item.bounding_box;
6562                                let (content_width, content_height) = self
6563                                    .text_input_content_size(&state_snapshot, &ti_cfg, bbox.width);
6564
6565                                if let Some(state) =
6566                                    self.text_edit_states.get_mut(&self.text_input_drag_element_id)
6567                                {
6568                                    if self.text_input_scrollbar_drag_vertical {
6569                                        if let Some(geo) = compute_vertical_scrollbar_geometry(
6570                                            bbox,
6571                                            content_height,
6572                                            self.text_input_scrollbar_drag_scroll_origin,
6573                                            scrollbar_cfg,
6574                                        ) {
6575                                            let delta = pointer.y - self.text_input_scrollbar_drag_origin;
6576                                            let new_scroll = if geo.thumb_travel <= 0.0 {
6577                                                0.0
6578                                            } else {
6579                                                self.text_input_scrollbar_drag_scroll_origin
6580                                                    + delta * (geo.max_scroll / geo.thumb_travel)
6581                                            };
6582                                            state.scroll_offset_y = new_scroll.clamp(0.0, geo.max_scroll);
6583                                        }
6584                                    } else if let Some(geo) = compute_horizontal_scrollbar_geometry(
6585                                        bbox,
6586                                        content_width,
6587                                        self.text_input_scrollbar_drag_scroll_origin,
6588                                        scrollbar_cfg,
6589                                    ) {
6590                                        let delta = pointer.x - self.text_input_scrollbar_drag_origin;
6591                                        let new_scroll = if geo.thumb_travel <= 0.0 {
6592                                            0.0
6593                                        } else {
6594                                            self.text_input_scrollbar_drag_scroll_origin
6595                                                + delta * (geo.max_scroll / geo.thumb_travel)
6596                                        };
6597                                        state.scroll_offset = new_scroll.clamp(0.0, geo.max_scroll);
6598                                    }
6599                                }
6600
6601                                self.text_input_scrollbar_idle_frames
6602                                    .insert(self.text_input_drag_element_id, 0);
6603                                consumed_scroll = true;
6604                            }
6605                        }
6606                    }
6607                } else if self.text_input_drag_active {
6608                    let drag_id = self.text_input_drag_element_id;
6609                    if ti_cfg.drag_select && !self.text_input_drag_from_touch {
6610                        consumed_scroll = true;
6611
6612                        if let (Some(item), Some(measure_fn)) = (
6613                            self.layout_element_map.get(&drag_id),
6614                            self.measure_text_fn.as_ref(),
6615                        ) {
6616                            let bbox = item.bounding_box;
6617                            let click_x = pointer.x - bbox.x;
6618                            let click_y = pointer.y - bbox.y;
6619
6620                            if let Some(state) = self.text_edit_states.get_mut(&drag_id) {
6621                                if ti_cfg.is_multiline {
6622                                    if click_y < 0.0 {
6623                                        state.scroll_offset_y = (state.scroll_offset_y + click_y).max(0.0);
6624                                    } else if click_y > bbox.height {
6625                                        state.scroll_offset_y += click_y - bbox.height;
6626                                    }
6627                                } else {
6628                                    if click_x < 0.0 {
6629                                        state.scroll_offset = (state.scroll_offset + click_x).max(0.0);
6630                                    } else if click_x > bbox.width {
6631                                        state.scroll_offset += click_x - bbox.width;
6632                                    }
6633                                }
6634                            }
6635
6636                            if let Some(state_snapshot) = self.text_edit_states.get(&drag_id).cloned() {
6637                                let clamped_x = click_x.clamp(0.0, bbox.width.max(0.0));
6638                                let clamped_y = click_y.clamp(0.0, bbox.height.max(0.0));
6639                                let disp_text = crate::text_input::display_text(
6640                                    &state_snapshot.text,
6641                                    &ti_cfg.placeholder,
6642                                    ti_cfg.is_password,
6643                                );
6644
6645                                if !state_snapshot.text.is_empty() {
6646                                    if ti_cfg.is_multiline {
6647                                        let visual_lines = crate::text_input::wrap_lines(
6648                                            &disp_text,
6649                                            bbox.width,
6650                                            ti_cfg.font_asset,
6651                                            ti_cfg.font_size,
6652                                            measure_fn.as_ref(),
6653                                        );
6654                                        if !visual_lines.is_empty() {
6655                                            let line_height = if ti_cfg.line_height > 0 {
6656                                                ti_cfg.line_height as f32
6657                                            } else {
6658                                                let config = crate::text::TextConfig {
6659                                                    font_asset: ti_cfg.font_asset,
6660                                                    font_size: ti_cfg.font_size,
6661                                                    ..Default::default()
6662                                                };
6663                                                measure_fn("Mg", &config).height
6664                                            };
6665
6666                                            let adjusted_y = clamped_y + state_snapshot.scroll_offset_y;
6667                                            let clicked_line = (adjusted_y / line_height).floor().max(0.0) as usize;
6668                                            let clicked_line = clicked_line.min(visual_lines.len().saturating_sub(1));
6669
6670                                            let vl = &visual_lines[clicked_line];
6671                                            let line_char_x_positions = crate::text_input::compute_char_x_positions(
6672                                                &vl.text,
6673                                                ti_cfg.font_asset,
6674                                                ti_cfg.font_size,
6675                                                measure_fn.as_ref(),
6676                                            );
6677                                            let col = crate::text_input::find_nearest_char_boundary(
6678                                                clamped_x,
6679                                                &line_char_x_positions,
6680                                            );
6681                                            let raw_pos = vl.global_char_start + col;
6682
6683                                            if let Some(state) = self.text_edit_states.get_mut(&drag_id) {
6684                                                #[cfg(feature = "text-styling")]
6685                                                {
6686                                                    let visual_pos = crate::text_input::styling::raw_to_cursor(&state.text, raw_pos);
6687                                                    state.click_to_cursor_styled(visual_pos, true);
6688                                                }
6689                                                #[cfg(not(feature = "text-styling"))]
6690                                                {
6691                                                    if state.selection_anchor.is_none() {
6692                                                        state.selection_anchor = Some(state.cursor_pos);
6693                                                    }
6694                                                    state.cursor_pos = raw_pos;
6695                                                    if state.selection_anchor == Some(state.cursor_pos) {
6696                                                        state.selection_anchor = None;
6697                                                    }
6698                                                    state.reset_blink();
6699                                                }
6700                                            }
6701                                        }
6702                                    } else {
6703                                        let char_x_positions = crate::text_input::compute_char_x_positions(
6704                                            &disp_text,
6705                                            ti_cfg.font_asset,
6706                                            ti_cfg.font_size,
6707                                            measure_fn.as_ref(),
6708                                        );
6709                                        let adjusted_x = clamped_x + state_snapshot.scroll_offset;
6710
6711                                        if let Some(state) = self.text_edit_states.get_mut(&drag_id) {
6712                                            #[cfg(feature = "text-styling")]
6713                                            {
6714                                                let raw_pos = crate::text_input::find_nearest_char_boundary(
6715                                                    adjusted_x,
6716                                                    &char_x_positions,
6717                                                );
6718                                                let visual_pos = crate::text_input::styling::raw_to_cursor(&state.text, raw_pos);
6719                                                state.click_to_cursor_styled(visual_pos, true);
6720                                            }
6721                                            #[cfg(not(feature = "text-styling"))]
6722                                            {
6723                                                state.click_to_cursor(adjusted_x, &char_x_positions, true);
6724                                            }
6725                                        }
6726                                    }
6727                                }
6728                            }
6729
6730                            self.text_input_scrollbar_idle_frames.insert(drag_id, 0);
6731                        }
6732                    } else if let Some(state) = self.text_edit_states.get_mut(&drag_id) {
6733                        if is_multiline {
6734                            let drag_delta_y = self.text_input_drag_origin.y - pointer.y;
6735                            state.scroll_offset_y = (self.text_input_drag_scroll_origin.y + drag_delta_y).max(0.0);
6736                        } else {
6737                            let drag_delta_x = self.text_input_drag_origin.x - pointer.x;
6738                            state.scroll_offset = (self.text_input_drag_scroll_origin.x + drag_delta_x).max(0.0);
6739                        }
6740                        self.text_input_scrollbar_idle_frames
6741                            .insert(drag_id, 0);
6742                    }
6743                }
6744            }
6745            PointerDataInteractionState::ReleasedThisFrame
6746            | PointerDataInteractionState::Released => {
6747                self.text_input_drag_active = false;
6748                self.text_input_drag_from_touch = false;
6749                self.text_input_scrollbar_drag_active = false;
6750            }
6751        }
6752        consumed_scroll
6753    }
6754
6755    /// Clamp text input scroll offsets to valid ranges.
6756    /// For multiline: clamp scroll_offset_y to [0, total_height - visible_height].
6757    /// For single-line: clamp scroll_offset to [0, total_width - visible_width].
6758    pub fn clamp_text_input_scroll(&mut self) {
6759        for i in 0..self.text_input_element_ids.len() {
6760            let elem_id = self.text_input_element_ids[i];
6761            let cfg = match self.text_input_configs.get(i) {
6762                Some(c) => c,
6763                None => continue,
6764            };
6765
6766            let font_asset = cfg.font_asset;
6767            let font_size = cfg.font_size;
6768            let cfg_line_height = cfg.line_height;
6769            let is_multiline = cfg.is_multiline;
6770            let is_password = cfg.is_password;
6771
6772            let (visible_width, visible_height) = self.layout_element_map.get(&elem_id)
6773                .map(|item| (item.bounding_box.width, item.bounding_box.height))
6774                .unwrap_or((200.0, 0.0));
6775
6776            let text_empty = self.text_edit_states.get(&elem_id)
6777                .map(|s| s.text.is_empty())
6778                .unwrap_or(true);
6779
6780            if text_empty {
6781                if let Some(state_mut) = self.text_edit_states.get_mut(&elem_id) {
6782                    state_mut.scroll_offset = 0.0;
6783                    state_mut.scroll_offset_y = 0.0;
6784                }
6785                continue;
6786            }
6787
6788            if let Some(ref measure_fn) = self.measure_text_fn {
6789                let disp_text = self.text_edit_states.get(&elem_id)
6790                    .map(|s| crate::text_input::display_text(&s.text, "", is_password && !is_multiline))
6791                    .unwrap_or_default();
6792
6793                if is_multiline {
6794                    let visual_lines = crate::text_input::wrap_lines(
6795                        &disp_text,
6796                        visible_width,
6797                        font_asset,
6798                        font_size,
6799                        measure_fn.as_ref(),
6800                    );
6801                    let natural_height = self.font_height(font_asset, font_size);
6802                    let font_height = if cfg_line_height > 0 { cfg_line_height as f32 } else { natural_height };
6803                    let total_height = visual_lines.len() as f32 * font_height;
6804                    let max_scroll = (total_height - visible_height).max(0.0);
6805                    if let Some(state_mut) = self.text_edit_states.get_mut(&elem_id) {
6806                        if state_mut.scroll_offset_y > max_scroll {
6807                            state_mut.scroll_offset_y = max_scroll;
6808                        }
6809                    }
6810                } else {
6811                    // Single-line: clamp horizontal scroll
6812                    let char_x_positions = crate::text_input::compute_char_x_positions(
6813                        &disp_text,
6814                        font_asset,
6815                        font_size,
6816                        measure_fn.as_ref(),
6817                    );
6818                    let total_width = char_x_positions.last().copied().unwrap_or(0.0);
6819                    let max_scroll = (total_width - visible_width).max(0.0);
6820                    if let Some(state_mut) = self.text_edit_states.get_mut(&elem_id) {
6821                        if state_mut.scroll_offset > max_scroll {
6822                            state_mut.scroll_offset = max_scroll;
6823                        }
6824                    }
6825                }
6826            }
6827        }
6828    }
6829
6830    /// Cycle focus to the next (or previous, if `reverse` is true) focusable element.
6831    /// This is called when Tab (or Shift+Tab) is pressed.
6832    pub fn cycle_focus(&mut self, reverse: bool) {
6833        if self.focusable_elements.is_empty() {
6834            return;
6835        }
6836        self.focus_from_keyboard = true;
6837
6838        // Sort: explicit tab_index first (ascending), then insertion order
6839        let mut sorted: Vec<FocusableEntry> = self.focusable_elements.clone();
6840        sorted.sort_by(|a, b| {
6841            match (a.tab_index, b.tab_index) {
6842                (Some(ai), Some(bi)) => ai.cmp(&bi).then(a.insertion_order.cmp(&b.insertion_order)),
6843                (Some(_), None) => std::cmp::Ordering::Less,
6844                (None, Some(_)) => std::cmp::Ordering::Greater,
6845                (None, None) => a.insertion_order.cmp(&b.insertion_order),
6846            }
6847        });
6848
6849        // Find current focus position
6850        let current_pos = sorted
6851            .iter()
6852            .position(|e| e.element_id == self.focused_element_id);
6853
6854        let next_pos = match current_pos {
6855            Some(pos) => {
6856                if reverse {
6857                    if pos == 0 { sorted.len() - 1 } else { pos - 1 }
6858                } else {
6859                    if pos + 1 >= sorted.len() { 0 } else { pos + 1 }
6860                }
6861            }
6862            None => {
6863                // No current focus — go to first (or last if reverse)
6864                if reverse { sorted.len() - 1 } else { 0 }
6865            }
6866        };
6867
6868        self.change_focus(sorted[next_pos].element_id);
6869    }
6870
6871    /// Move focus based on arrow key direction, using `focus_left/right/up/down` overrides.
6872    pub fn arrow_focus(&mut self, direction: ArrowDirection) {
6873        if self.focused_element_id == 0 {
6874            return;
6875        }
6876        self.focus_from_keyboard = true;
6877        if let Some(config) = self.accessibility_configs.get(&self.focused_element_id) {
6878            let target = match direction {
6879                ArrowDirection::Left => config.focus_left,
6880                ArrowDirection::Right => config.focus_right,
6881                ArrowDirection::Up => config.focus_up,
6882                ArrowDirection::Down => config.focus_down,
6883            };
6884            if let Some(target_id) = target {
6885                self.change_focus(target_id);
6886            }
6887        }
6888    }
6889
6890    /// Handle keyboard activation (Enter/Space) on the focused element.
6891    pub fn handle_keyboard_activation(&mut self, pressed: bool, released: bool) {
6892        if self.focused_element_id == 0 {
6893            return;
6894        }
6895        if pressed {
6896            let id_copy = self
6897                .layout_element_map
6898                .get(&self.focused_element_id)
6899                .map(|item| item.element_id.clone());
6900            if let Some(id) = id_copy {
6901                self.pressed_element_ids = vec![id.clone()];
6902                self.keyboard_press_this_frame_generation = self.release_query_generation();
6903                if let Some(item) = self.layout_element_map.get_mut(&self.focused_element_id) {
6904                    if let Some(ref mut callback) = item.on_press_fn {
6905                        callback(id, PointerData::default());
6906                    }
6907                }
6908            }
6909        }
6910        if released {
6911            let pressed = std::mem::take(&mut self.pressed_element_ids);
6912            self.track_just_released_ids(&pressed);
6913            for eid in pressed.iter() {
6914                if let Some(item) = self.layout_element_map.get_mut(&eid.id) {
6915                    if let Some(ref mut callback) = item.on_release_fn {
6916                        callback(eid.clone(), PointerData::default());
6917                    }
6918                }
6919            }
6920        }
6921    }
6922
6923    pub fn pointer_over(&self, element_id: Id) -> bool {
6924        self.pointer_over_ids.iter().any(|eid| eid.id == element_id.id)
6925    }
6926
6927    pub fn get_pointer_over_ids(&self) -> &[Id] {
6928        &self.pointer_over_ids
6929    }
6930
6931    pub fn get_element_data(&self, id: Id) -> Option<BoundingBox> {
6932        self.layout_element_map
6933            .get(&id.id)
6934            .map(|item| item.bounding_box)
6935    }
6936
6937    pub fn get_scroll_container_data(&self, id: Id) -> ScrollContainerData {
6938        for scd in &self.scroll_container_datas {
6939            if scd.element_id == id.id {
6940                return ScrollContainerData {
6941                    scroll_position: scd.scroll_position,
6942                    scroll_container_dimensions: Dimensions::new(
6943                        scd.bounding_box.width,
6944                        scd.bounding_box.height,
6945                    ),
6946                    content_dimensions: scd.content_size,
6947                    horizontal: scd.scroll_x_enabled,
6948                    vertical: scd.scroll_y_enabled,
6949                    found: true,
6950                };
6951            }
6952        }
6953        ScrollContainerData::default()
6954    }
6955
6956    pub fn get_scroll_offset(&self) -> Vector2 {
6957        let open_idx = self.get_open_layout_element();
6958        let elem_id = self.layout_elements[open_idx].id;
6959        for scd in &self.scroll_container_datas {
6960            if scd.element_id == elem_id {
6961                return scd.scroll_position;
6962            }
6963        }
6964        Vector2::default()
6965    }
6966
6967    pub fn set_scroll_position(&mut self, id: Id, position: Vector2) {
6968        for scd in &mut self.scroll_container_datas {
6969            if scd.element_id == id.id {
6970                let max_scroll_x = (scd.content_size.width - scd.bounding_box.width).max(0.0);
6971                let max_scroll_y = (scd.content_size.height - scd.bounding_box.height).max(0.0);
6972
6973                let clamped_x = position.x.clamp(0.0, max_scroll_x);
6974                let clamped_y = position.y.clamp(0.0, max_scroll_y);
6975                scd.scroll_position.x = -clamped_x;
6976                scd.scroll_position.y = -clamped_y;
6977                if scd.scrollbar.is_some() {
6978                    scd.scrollbar_idle_frames = 0;
6979                }
6980                return;
6981            }
6982        }
6983
6984        if let Some(ti_idx) = self.text_input_element_ids.iter().position(|&elem_id| elem_id == id.id) {
6985            let Some(config) = self.text_input_configs.get(ti_idx).cloned() else {
6986                return;
6987            };
6988
6989            let Some(state_snapshot) = self.text_edit_states.get(&id.id).cloned() else {
6990                return;
6991            };
6992
6993            let (visible_width, visible_height) = self
6994                .layout_element_map
6995                .get(&id.id)
6996                .map(|item| (item.bounding_box.width, item.bounding_box.height))
6997                .unwrap_or((0.0, 0.0));
6998
6999            let (content_width, content_height) =
7000                self.text_input_content_size(&state_snapshot, &config, visible_width);
7001
7002            let max_scroll_x = (content_width - visible_width).max(0.0);
7003            let max_scroll_y = (content_height - visible_height).max(0.0);
7004
7005            if let Some(state) = self.text_edit_states.get_mut(&id.id) {
7006                state.scroll_offset = position.x.clamp(0.0, max_scroll_x);
7007                state.scroll_offset_y = position.y.clamp(0.0, max_scroll_y);
7008            }
7009
7010            self.text_input_scrollbar_idle_frames.insert(id.id, 0);
7011        }
7012    }
7013
7014    fn render_scrollbar_geometry(
7015        &mut self,
7016        id: u32,
7017        z_index: i16,
7018        config: ScrollbarConfig,
7019        alpha_mul: f32,
7020        vertical: Option<ScrollbarAxisGeometry>,
7021        horizontal: Option<ScrollbarAxisGeometry>,
7022    ) {
7023        if alpha_mul <= 0.0 {
7024            return;
7025        }
7026
7027        let thumb_color = apply_alpha(config.thumb_color, alpha_mul);
7028        let track_color = config.track_color.map(|c| apply_alpha(c, alpha_mul));
7029
7030        if let Some(v) = vertical {
7031            if let Some(track) = track_color {
7032                self.add_render_command(InternalRenderCommand {
7033                    bounding_box: v.track_bbox,
7034                    command_type: RenderCommandType::Rectangle,
7035                    render_data: InternalRenderData::Rectangle {
7036                        background_color: track,
7037                        corner_radius: config.corner_radius.into(),
7038                    },
7039                    id: hash_number(id, 8001).id,
7040                    z_index,
7041                    ..Default::default()
7042                });
7043            }
7044
7045            self.add_render_command(InternalRenderCommand {
7046                bounding_box: v.thumb_bbox,
7047                command_type: RenderCommandType::Rectangle,
7048                render_data: InternalRenderData::Rectangle {
7049                    background_color: thumb_color,
7050                    corner_radius: config.corner_radius.into(),
7051                },
7052                id: hash_number(id, 8002).id,
7053                z_index,
7054                ..Default::default()
7055            });
7056        }
7057
7058        if let Some(h) = horizontal {
7059            if let Some(track) = track_color {
7060                self.add_render_command(InternalRenderCommand {
7061                    bounding_box: h.track_bbox,
7062                    command_type: RenderCommandType::Rectangle,
7063                    render_data: InternalRenderData::Rectangle {
7064                        background_color: track,
7065                        corner_radius: config.corner_radius.into(),
7066                    },
7067                    id: hash_number(id, 8003).id,
7068                    z_index,
7069                    ..Default::default()
7070                });
7071            }
7072
7073            self.add_render_command(InternalRenderCommand {
7074                bounding_box: h.thumb_bbox,
7075                command_type: RenderCommandType::Rectangle,
7076                render_data: InternalRenderData::Rectangle {
7077                    background_color: thumb_color,
7078                    corner_radius: config.corner_radius.into(),
7079                },
7080                id: hash_number(id, 8004).id,
7081                z_index,
7082                ..Default::default()
7083            });
7084        }
7085    }
7086
7087    fn text_input_content_size(
7088        &mut self,
7089        state: &crate::text_input::TextEditState,
7090        config: &crate::text_input::TextInputConfig,
7091        visible_width: f32,
7092    ) -> (f32, f32) {
7093        if state.text.is_empty() || visible_width <= 0.0 {
7094            return (0.0, 0.0);
7095        }
7096
7097        let Some(measure_fn) = self.measure_text_fn.as_ref() else {
7098            return (0.0, 0.0);
7099        };
7100
7101        let display_text = crate::text_input::display_text(
7102            &state.text,
7103            &config.placeholder,
7104            config.is_password && !config.is_multiline,
7105        );
7106
7107        if config.is_multiline {
7108            let visual_lines = crate::text_input::wrap_lines(
7109                &display_text,
7110                visible_width,
7111                config.font_asset,
7112                config.font_size,
7113                measure_fn.as_ref(),
7114            );
7115
7116            let content_width = visual_lines
7117                .iter()
7118                .map(|line| {
7119                    crate::text_input::compute_char_x_positions(
7120                        &line.text,
7121                        config.font_asset,
7122                        config.font_size,
7123                        measure_fn.as_ref(),
7124                    )
7125                    .last()
7126                    .copied()
7127                    .unwrap_or(0.0)
7128                })
7129                .fold(0.0_f32, |a, b| a.max(b));
7130
7131            let natural_height = self.font_height(config.font_asset, config.font_size);
7132            let line_height = if config.line_height > 0 {
7133                config.line_height as f32
7134            } else {
7135                natural_height
7136            };
7137
7138            (content_width, visual_lines.len() as f32 * line_height)
7139        } else {
7140            let positions = crate::text_input::compute_char_x_positions(
7141                &display_text,
7142                config.font_asset,
7143                config.font_size,
7144                measure_fn.as_ref(),
7145            );
7146            (
7147                positions.last().copied().unwrap_or(0.0),
7148                self.font_height(config.font_asset, config.font_size),
7149            )
7150        }
7151    }
7152
7153    const DEBUG_VIEW_DEFAULT_WIDTH: f32 = 400.0;
7154    const DEBUG_VIEW_ROW_HEIGHT: f32 = 30.0;
7155    const DEBUG_VIEW_OUTER_PADDING: u16 = 10;
7156    const DEBUG_VIEW_INDENT_WIDTH: u16 = 16;
7157
7158    const DEBUG_COLOR_1: Color = Color::rgba(58.0, 56.0, 52.0, 255.0);
7159    const DEBUG_COLOR_2: Color = Color::rgba(62.0, 60.0, 58.0, 255.0);
7160    const DEBUG_COLOR_3: Color = Color::rgba(141.0, 133.0, 135.0, 255.0);
7161    const DEBUG_COLOR_4: Color = Color::rgba(238.0, 226.0, 231.0, 255.0);
7162    #[allow(dead_code)]
7163    const DEBUG_COLOR_SELECTED_ROW: Color = Color::rgba(102.0, 80.0, 78.0, 255.0);
7164    const DEBUG_HIGHLIGHT_COLOR: Color = Color::rgba(168.0, 66.0, 28.0, 100.0);
7165
7166    /// Escape text-styling special characters (`{`, `}`, `|`, `\`) so that
7167    /// debug view strings are never interpreted as styling markup.
7168    #[cfg(feature = "text-styling")]
7169    fn debug_escape_str(s: &str) -> String {
7170        let mut result = String::with_capacity(s.len());
7171        for c in s.chars() {
7172            match c {
7173                '{' | '}' | '|' | '\\' => {
7174                    result.push('\\');
7175                    result.push(c);
7176                }
7177                _ => result.push(c),
7178            }
7179        }
7180        result
7181    }
7182
7183    /// Helper: emit a text element with a static string.
7184    /// When `text-styling` is enabled the string is escaped first so that
7185    /// braces and pipes are rendered literally.
7186    fn debug_text(&mut self, text: &'static str, config_index: usize) {
7187        #[cfg(feature = "text-styling")]
7188        {
7189            let escaped = Self::debug_escape_str(text);
7190            self.open_text_element(&escaped, config_index);
7191        }
7192        #[cfg(not(feature = "text-styling"))]
7193        {
7194            self.open_text_element(text, config_index);
7195        }
7196    }
7197
7198    /// Helper: emit a text element from a string (e.g. element IDs
7199    /// or text previews). Escapes text-styling characters when that feature is
7200    /// active.
7201    fn debug_raw_text(&mut self, text: &str, config_index: usize) {
7202        #[cfg(feature = "text-styling")]
7203        {
7204            let escaped = Self::debug_escape_str(text);
7205            self.open_text_element(&escaped, config_index);
7206        }
7207        #[cfg(not(feature = "text-styling"))]
7208        {
7209            self.open_text_element(text, config_index);
7210        }
7211    }
7212
7213    /// Helper: format a number as a string and emit a text element.
7214    fn debug_int_text(&mut self, value: f32, config_index: usize) {
7215        let s = format!("{}", value as i32);
7216        self.open_text_element(&s, config_index);
7217    }
7218
7219    /// Helper: format a float with 2 decimal places and emit a text element.
7220    fn debug_float_text(&mut self, value: f32, config_index: usize) {
7221        let s = format!("{:.2}", value);
7222        self.open_text_element(&s, config_index);
7223    }
7224
7225    /// Helper: open an element, configure, return nothing. Caller must close_element().
7226    fn debug_open(&mut self, decl: &ElementDeclaration<CustomElementData>) {
7227        self.open_element();
7228        self.configure_open_element(decl);
7229    }
7230
7231    /// Helper: open a named element, configure. Caller must close_element().
7232    fn debug_open_id(&mut self, name: &str, decl: &ElementDeclaration<CustomElementData>) {
7233        self.open_element_with_id(&hash_string(name, 0));
7234        self.configure_open_element(decl);
7235    }
7236
7237    /// Helper: open a named+indexed element, configure. Caller must close_element().
7238    fn debug_open_idi(&mut self, name: &str, offset: u32, decl: &ElementDeclaration<CustomElementData>) {
7239        self.open_element_with_id(&hash_string_with_offset(name, offset, 0));
7240        self.configure_open_element(decl);
7241    }
7242
7243    fn debug_get_config_type_label(config_type: ElementConfigType) -> (&'static str, Color) {
7244        match config_type {
7245            ElementConfigType::Shared => ("Shared", Color::rgba(243.0, 134.0, 48.0, 255.0)),
7246            ElementConfigType::Text => ("Text", Color::rgba(105.0, 210.0, 231.0, 255.0)),
7247            ElementConfigType::Aspect => ("Aspect", Color::rgba(101.0, 149.0, 194.0, 255.0)),
7248            ElementConfigType::Image => ("Image", Color::rgba(121.0, 189.0, 154.0, 255.0)),
7249            ElementConfigType::Floating => ("Floating", Color::rgba(250.0, 105.0, 0.0, 255.0)),
7250            ElementConfigType::Clip => ("Overflow", Color::rgba(242.0, 196.0, 90.0, 255.0)),
7251            ElementConfigType::Border => ("Border", Color::rgba(108.0, 91.0, 123.0, 255.0)),
7252            ElementConfigType::Custom => ("Custom", Color::rgba(11.0, 72.0, 107.0, 255.0)),
7253            ElementConfigType::TextInput => ("TextInput", Color::rgba(52.0, 152.0, 219.0, 255.0)),
7254        }
7255    }
7256
7257    /// Render the debug view sizing info for one axis.
7258    fn render_debug_layout_sizing(&mut self, sizing: SizingAxis, config_index: usize) {
7259        let label = match sizing.type_ {
7260            SizingType::Fit => "FIT",
7261            SizingType::Grow => "GROW",
7262            SizingType::Percent => "PERCENT",
7263            SizingType::Fixed => "FIXED",
7264            // Default handled by Grow arm above
7265        };
7266        self.debug_text(label, config_index);
7267        if matches!(sizing.type_, SizingType::Grow | SizingType::Fit | SizingType::Fixed) {
7268            self.debug_text("(", config_index);
7269            let mut wrote_any = false;
7270
7271            if sizing.type_ == SizingType::Grow && !float_equal(sizing.grow_weight, 1.0) {
7272                self.debug_text("weight: ", config_index);
7273                self.debug_float_text(sizing.grow_weight, config_index);
7274                wrote_any = true;
7275            }
7276
7277            if sizing.min_max.min != 0.0 {
7278                if wrote_any {
7279                    self.debug_text(", ", config_index);
7280                }
7281                self.debug_text("min: ", config_index);
7282                self.debug_int_text(sizing.min_max.min, config_index);
7283                wrote_any = true;
7284            }
7285            if sizing.min_max.max != MAXFLOAT {
7286                if wrote_any {
7287                    self.debug_text(", ", config_index);
7288                }
7289                self.debug_text("max: ", config_index);
7290                self.debug_int_text(sizing.min_max.max, config_index);
7291            }
7292            self.debug_text(")", config_index);
7293        } else if sizing.type_ == SizingType::Percent {
7294            self.debug_text("(", config_index);
7295            self.debug_int_text(sizing.percent * 100.0, config_index);
7296            self.debug_text("%)", config_index);
7297        }
7298    }
7299
7300    /// Render a config type header in the selected element detail panel.
7301    fn render_debug_view_element_config_header(
7302        &mut self,
7303        element_id_string: StringId,
7304        config_type: ElementConfigType,
7305        _info_title_config: usize,
7306    ) {
7307        let (label, label_color) = Self::debug_get_config_type_label(config_type);
7308        self.render_debug_view_category_header(label, label_color, element_id_string);
7309    }
7310
7311    /// Render a category header badge with arbitrary label and color.
7312    fn render_debug_view_category_header(
7313        &mut self,
7314        label: &str,
7315        label_color: Color,
7316        element_id_string: StringId,
7317    ) {
7318        let bg = Color::rgba(label_color.r, label_color.g, label_color.b, 90.0);
7319        self.debug_open(&ElementDeclaration {
7320            layout: LayoutConfig {
7321                sizing: SizingConfig {
7322                    width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
7323                    ..Default::default()
7324                },
7325                padding: PaddingConfig {
7326                    left: Self::DEBUG_VIEW_OUTER_PADDING,
7327                    right: Self::DEBUG_VIEW_OUTER_PADDING,
7328                    top: Self::DEBUG_VIEW_OUTER_PADDING,
7329                    bottom: Self::DEBUG_VIEW_OUTER_PADDING,
7330                },
7331                child_alignment: ChildAlignmentConfig { x: AlignX::Left, y: AlignY::CenterY },
7332                ..Default::default()
7333            },
7334            ..Default::default()
7335        });
7336        {
7337            // Badge
7338            self.debug_open(&ElementDeclaration {
7339                layout: LayoutConfig {
7340                    padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
7341                    ..Default::default()
7342                },
7343                background_color: bg,
7344                corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
7345                border: BorderConfig {
7346                    color: label_color,
7347                    width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
7348                    ..Default::default()
7349                },
7350                ..Default::default()
7351            });
7352            {
7353                let tc = self.store_text_element_config(TextConfig {
7354                    color: Self::DEBUG_COLOR_4,
7355                    font_size: 16,
7356                    ..Default::default()
7357                });
7358                self.debug_raw_text(label, tc);
7359            }
7360            self.close_element();
7361            // Spacer
7362            self.debug_open(&ElementDeclaration {
7363                layout: LayoutConfig {
7364                    sizing: SizingConfig {
7365                        width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
7366                        ..Default::default()
7367                    },
7368                    ..Default::default()
7369                },
7370                ..Default::default()
7371            });
7372            self.close_element();
7373            // Element ID string
7374            let tc = self.store_text_element_config(TextConfig {
7375                color: Self::DEBUG_COLOR_3,
7376                font_size: 16,
7377                wrap_mode: WrapMode::None,
7378                ..Default::default()
7379            });
7380            if !element_id_string.is_empty() {
7381                self.debug_raw_text(element_id_string.as_str(), tc);
7382            }
7383        }
7384        self.close_element();
7385    }
7386
7387    /// Render a color value in the debug view.
7388    fn render_debug_view_color(&mut self, color: Color, config_index: usize) {
7389        self.debug_open(&ElementDeclaration {
7390            layout: LayoutConfig {
7391                child_alignment: ChildAlignmentConfig { x: AlignX::Left, y: AlignY::CenterY },
7392                ..Default::default()
7393            },
7394            ..Default::default()
7395        });
7396        {
7397            self.debug_text("{ r: ", config_index);
7398            self.debug_int_text(color.r, config_index);
7399            self.debug_text(", g: ", config_index);
7400            self.debug_int_text(color.g, config_index);
7401            self.debug_text(", b: ", config_index);
7402            self.debug_int_text(color.b, config_index);
7403            self.debug_text(", a: ", config_index);
7404            self.debug_int_text(color.a, config_index);
7405            self.debug_text(" }", config_index);
7406            // Spacer
7407            self.debug_open(&ElementDeclaration {
7408                layout: LayoutConfig {
7409                    sizing: SizingConfig {
7410                        width: SizingAxis {
7411                            type_: SizingType::Fixed,
7412                            min_max: SizingMinMax { min: 10.0, max: 10.0 },
7413                            ..Default::default()
7414                        },
7415                        ..Default::default()
7416                    },
7417                    ..Default::default()
7418                },
7419                ..Default::default()
7420            });
7421            self.close_element();
7422            // Color swatch
7423            let swatch_size = Self::DEBUG_VIEW_ROW_HEIGHT - 8.0;
7424            self.debug_open(&ElementDeclaration {
7425                layout: LayoutConfig {
7426                    sizing: SizingConfig {
7427                        width: SizingAxis {
7428                            type_: SizingType::Fixed,
7429                            min_max: SizingMinMax { min: swatch_size, max: swatch_size },
7430                            ..Default::default()
7431                        },
7432                        height: SizingAxis {
7433                            type_: SizingType::Fixed,
7434                            min_max: SizingMinMax { min: swatch_size, max: swatch_size },
7435                            ..Default::default()
7436                        },
7437                    },
7438                    ..Default::default()
7439                },
7440                background_color: color,
7441                corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
7442                border: BorderConfig {
7443                    color: Self::DEBUG_COLOR_4,
7444                    width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
7445                    ..Default::default()
7446                },
7447                ..Default::default()
7448            });
7449            self.close_element();
7450        }
7451        self.close_element();
7452    }
7453
7454    /// Render corner radius values as simple stacked lines.
7455    fn render_debug_view_corner_radius(
7456        &mut self,
7457        cr: CornerRadius,
7458        info_text_config: usize,
7459    ) {
7460        self.debug_open(&ElementDeclaration::default());
7461        {
7462            self.debug_text("topLeft: ", info_text_config);
7463            self.debug_float_text(cr.top_left, info_text_config);
7464        }
7465        self.close_element();
7466        self.debug_open(&ElementDeclaration::default());
7467        {
7468            self.debug_text("topRight: ", info_text_config);
7469            self.debug_float_text(cr.top_right, info_text_config);
7470        }
7471        self.close_element();
7472        self.debug_open(&ElementDeclaration::default());
7473        {
7474            self.debug_text("bottomLeft: ", info_text_config);
7475            self.debug_float_text(cr.bottom_left, info_text_config);
7476        }
7477        self.close_element();
7478        self.debug_open(&ElementDeclaration::default());
7479        {
7480            self.debug_text("bottomRight: ", info_text_config);
7481            self.debug_float_text(cr.bottom_right, info_text_config);
7482        }
7483        self.close_element();
7484    }
7485
7486    fn debug_border_position_name(position: BorderPosition) -> &'static str {
7487        match position {
7488            BorderPosition::Outside => "OUTSIDE",
7489            BorderPosition::Middle => "MIDDLE",
7490            BorderPosition::Inside => "INSIDE",
7491        }
7492    }
7493
7494    fn render_debug_scrollbar_config(
7495        &mut self,
7496        scrollbar: ScrollbarConfig,
7497        info_text_config: usize,
7498        info_title_config: usize,
7499    ) {
7500        self.debug_text("Width", info_title_config);
7501        self.debug_float_text(scrollbar.width, info_text_config);
7502
7503        self.debug_text("Corner Radius", info_title_config);
7504        self.debug_float_text(scrollbar.corner_radius, info_text_config);
7505
7506        self.debug_text("Min Thumb Size", info_title_config);
7507        self.debug_float_text(scrollbar.min_thumb_size, info_text_config);
7508
7509        self.debug_text("Hide After Frames", info_title_config);
7510        if let Some(frames) = scrollbar.hide_after_frames {
7511            self.debug_int_text(frames as f32, info_text_config);
7512        } else {
7513            self.debug_text("none", info_text_config);
7514        }
7515
7516        self.debug_text("Thumb Color", info_title_config);
7517        self.render_debug_view_color(scrollbar.thumb_color, info_text_config);
7518
7519        self.debug_text("Track Color", info_title_config);
7520        if let Some(track) = scrollbar.track_color {
7521            self.render_debug_view_color(track, info_text_config);
7522        } else {
7523            self.debug_text("none", info_text_config);
7524        }
7525    }
7526
7527    /// Render a shader uniform value in the debug view.
7528    fn render_debug_shader_uniform_value(&mut self, value: &crate::shaders::ShaderUniformValue, config_index: usize) {
7529        use crate::shaders::ShaderUniformValue;
7530        match value {
7531            ShaderUniformValue::Float(v) => {
7532                self.debug_float_text(*v, config_index);
7533            }
7534            ShaderUniformValue::Vec2(v) => {
7535                self.debug_text("(", config_index);
7536                self.debug_float_text(v[0], config_index);
7537                self.debug_text(", ", config_index);
7538                self.debug_float_text(v[1], config_index);
7539                self.debug_text(")", config_index);
7540            }
7541            ShaderUniformValue::Vec3(v) => {
7542                self.debug_text("(", config_index);
7543                self.debug_float_text(v[0], config_index);
7544                self.debug_text(", ", config_index);
7545                self.debug_float_text(v[1], config_index);
7546                self.debug_text(", ", config_index);
7547                self.debug_float_text(v[2], config_index);
7548                self.debug_text(")", config_index);
7549            }
7550            ShaderUniformValue::Vec4(v) => {
7551                self.debug_text("(", config_index);
7552                self.debug_float_text(v[0], config_index);
7553                self.debug_text(", ", config_index);
7554                self.debug_float_text(v[1], config_index);
7555                self.debug_text(", ", config_index);
7556                self.debug_float_text(v[2], config_index);
7557                self.debug_text(", ", config_index);
7558                self.debug_float_text(v[3], config_index);
7559                self.debug_text(")", config_index);
7560            }
7561            ShaderUniformValue::Int(v) => {
7562                self.debug_int_text(*v as f32, config_index);
7563            }
7564            ShaderUniformValue::Mat4(_) => {
7565                self.debug_text("[mat4]", config_index);
7566            }
7567        }
7568    }
7569
7570    /// Render the debug layout elements tree list. Returns (row_count, selected_element_row_index).
7571    fn render_debug_layout_elements_list(
7572        &mut self,
7573        initial_roots_length: usize,
7574        highlighted_row: i32,
7575    ) -> (i32, i32) {
7576        let row_height = Self::DEBUG_VIEW_ROW_HEIGHT;
7577        let indent_width = Self::DEBUG_VIEW_INDENT_WIDTH;
7578        let mut row_count: i32 = 0;
7579        let mut selected_element_row_index: i32 = 0;
7580        let mut highlighted_element_id: u32 = 0;
7581
7582        let scroll_item_layout = LayoutConfig {
7583            sizing: SizingConfig {
7584                height: SizingAxis {
7585                    type_: SizingType::Fixed,
7586                    min_max: SizingMinMax { min: row_height, max: row_height },
7587                    ..Default::default()
7588                },
7589                ..Default::default()
7590            },
7591            child_gap: 6,
7592            child_alignment: ChildAlignmentConfig { x: AlignX::Left, y: AlignY::CenterY },
7593            ..Default::default()
7594        };
7595
7596        let name_text_config = TextConfig {
7597            color: Self::DEBUG_COLOR_4,
7598            font_size: 16,
7599            wrap_mode: WrapMode::None,
7600            ..Default::default()
7601        };
7602
7603        for root_index in 0..initial_roots_length {
7604            let mut dfs_buffer: Vec<i32> = Vec::new();
7605            let root_layout_index = self.layout_element_tree_roots[root_index].layout_element_index;
7606            dfs_buffer.push(root_layout_index);
7607            let mut visited: Vec<bool> = vec![false; self.layout_elements.len()];
7608
7609            // Separator between roots
7610            if root_index > 0 {
7611                self.debug_open_idi("Ply__DebugView_EmptyRowOuter", root_index as u32, &ElementDeclaration {
7612                    layout: LayoutConfig {
7613                        sizing: SizingConfig {
7614                            width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
7615                            ..Default::default()
7616                        },
7617                        padding: PaddingConfig { left: indent_width / 2, right: 0, top: 0, bottom: 0 },
7618                        ..Default::default()
7619                    },
7620                    ..Default::default()
7621                });
7622                {
7623                    self.debug_open_idi("Ply__DebugView_EmptyRow", root_index as u32, &ElementDeclaration {
7624                        layout: LayoutConfig {
7625                            sizing: SizingConfig {
7626                                width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
7627                                height: SizingAxis {
7628                                    type_: SizingType::Fixed,
7629                                    min_max: SizingMinMax { min: row_height, max: row_height },
7630                                    ..Default::default()
7631                                },
7632                            },
7633                            ..Default::default()
7634                        },
7635                        border: BorderConfig {
7636                            color: Self::DEBUG_COLOR_3,
7637                            width: BorderWidth { top: 1, ..Default::default() },
7638                            ..Default::default()
7639                        },
7640                        ..Default::default()
7641                    });
7642                    self.close_element();
7643                }
7644                self.close_element();
7645                row_count += 1;
7646            }
7647
7648            while !dfs_buffer.is_empty() {
7649                let current_element_index = *dfs_buffer.last().unwrap() as usize;
7650                let depth = dfs_buffer.len() - 1;
7651
7652                if visited[depth] {
7653                    // Closing: pop from stack and close containers if non-text with children
7654                    let is_text = self.element_has_config(current_element_index, ElementConfigType::Text);
7655                    let children_len = self.layout_elements[current_element_index].children_length;
7656                    if !is_text && children_len > 0 {
7657                        self.close_element();
7658                        self.close_element();
7659                        self.close_element();
7660                    }
7661                    dfs_buffer.pop();
7662                    continue;
7663                }
7664
7665                // Check if this row is highlighted
7666                if highlighted_row == row_count {
7667                    if self.pointer_info.state == PointerDataInteractionState::PressedThisFrame {
7668                        let elem_id = self.layout_elements[current_element_index].id;
7669                        if self.debug_selected_element_id == elem_id {
7670                            self.debug_selected_element_id = 0; // Deselect on re-click
7671                        } else {
7672                            self.debug_selected_element_id = elem_id;
7673                        }
7674                    }
7675                    highlighted_element_id = self.layout_elements[current_element_index].id;
7676                }
7677
7678                visited[depth] = true;
7679                let current_elem_id = self.layout_elements[current_element_index].id;
7680
7681                // Get bounding box and collision info from hash map
7682                let bounding_box = self.layout_element_map
7683                    .get(&current_elem_id)
7684                    .map(|item| item.bounding_box)
7685                    .unwrap_or_default();
7686                let collision = self.layout_element_map
7687                    .get(&current_elem_id)
7688                    .map(|item| item.collision)
7689                    .unwrap_or(false);
7690                let collapsed = self.layout_element_map
7691                    .get(&current_elem_id)
7692                    .map(|item| item.collapsed)
7693                    .unwrap_or(false);
7694
7695                let offscreen = self.element_is_offscreen(&bounding_box);
7696
7697                if self.debug_selected_element_id == current_elem_id {
7698                    selected_element_row_index = row_count;
7699                }
7700
7701                // Row for this element
7702                let row_bg = if self.debug_selected_element_id == current_elem_id {
7703                    Color::rgba(217.0, 91.0, 67.0, 40.0) // Slight red for selected
7704                } else {
7705                    Color::rgba(0.0, 0.0, 0.0, 0.0)
7706                };
7707                self.debug_open_idi("Ply__DebugView_ElementOuter", current_elem_id, &ElementDeclaration {
7708                    layout: scroll_item_layout,
7709                    background_color: row_bg,
7710                    ..Default::default()
7711                });
7712                {
7713                    let is_text = self.element_has_config(current_element_index, ElementConfigType::Text);
7714                    let children_len = self.layout_elements[current_element_index].children_length;
7715
7716                    // Collapse icon / button or dot
7717                    if !is_text && children_len > 0 {
7718                        // Collapse button
7719                        self.debug_open_idi("Ply__DebugView_CollapseElement", current_elem_id, &ElementDeclaration {
7720                            layout: LayoutConfig {
7721                                sizing: SizingConfig {
7722                                    width: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 16.0, max: 16.0 }, ..Default::default() },
7723                                    height: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 16.0, max: 16.0 }, ..Default::default() },
7724                                },
7725                                child_alignment: ChildAlignmentConfig { x: AlignX::CenterX, y: AlignY::CenterY },
7726                                ..Default::default()
7727                            },
7728                            corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
7729                            border: BorderConfig {
7730                                color: Self::DEBUG_COLOR_3,
7731                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
7732                                ..Default::default()
7733                            },
7734                            ..Default::default()
7735                        });
7736                        {
7737                            let tc = self.store_text_element_config(TextConfig {
7738                                color: Self::DEBUG_COLOR_4,
7739                                font_size: 16,
7740                                ..Default::default()
7741                            });
7742                            if collapsed {
7743                                self.debug_text("+", tc);
7744                            } else {
7745                                self.debug_text("-", tc);
7746                            }
7747                        }
7748                        self.close_element();
7749                    } else {
7750                        // Empty dot for leaf elements
7751                        self.debug_open(&ElementDeclaration {
7752                            layout: LayoutConfig {
7753                                sizing: SizingConfig {
7754                                    width: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 16.0, max: 16.0 }, ..Default::default() },
7755                                    height: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 16.0, max: 16.0 }, ..Default::default() },
7756                                },
7757                                child_alignment: ChildAlignmentConfig { x: AlignX::CenterX, y: AlignY::CenterY },
7758                                ..Default::default()
7759                            },
7760                            ..Default::default()
7761                        });
7762                        {
7763                            self.debug_open(&ElementDeclaration {
7764                                layout: LayoutConfig {
7765                                    sizing: SizingConfig {
7766                                        width: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 8.0, max: 8.0 }, ..Default::default() },
7767                                        height: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 8.0, max: 8.0 }, ..Default::default() },
7768                                    },
7769                                    ..Default::default()
7770                                },
7771                                background_color: Self::DEBUG_COLOR_3,
7772                                corner_radius: CornerRadius { top_left: 2.0, top_right: 2.0, bottom_left: 2.0, bottom_right: 2.0 },
7773                                ..Default::default()
7774                            });
7775                            self.close_element();
7776                        }
7777                        self.close_element();
7778                    }
7779
7780                    // Collision warning badge
7781                    if collision {
7782                        self.debug_open(&ElementDeclaration {
7783                            layout: LayoutConfig {
7784                                padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
7785                                ..Default::default()
7786                            },
7787                            border: BorderConfig {
7788                                color: Color::rgba(177.0, 147.0, 8.0, 255.0),
7789                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
7790                                ..Default::default()
7791                            },
7792                            ..Default::default()
7793                        });
7794                        {
7795                            let tc = self.store_text_element_config(TextConfig {
7796                                color: Self::DEBUG_COLOR_3,
7797                                font_size: 16,
7798                                ..Default::default()
7799                            });
7800                            self.debug_text("Duplicate ID", tc);
7801                        }
7802                        self.close_element();
7803                    }
7804
7805                    // Offscreen badge
7806                    if offscreen {
7807                        self.debug_open(&ElementDeclaration {
7808                            layout: LayoutConfig {
7809                                padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
7810                                ..Default::default()
7811                            },
7812                            border: BorderConfig {
7813                                color: Self::DEBUG_COLOR_3,
7814                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
7815                                ..Default::default()
7816                            },
7817                            ..Default::default()
7818                        });
7819                        {
7820                            let tc = self.store_text_element_config(TextConfig {
7821                                color: Self::DEBUG_COLOR_3,
7822                                font_size: 16,
7823                                ..Default::default()
7824                            });
7825                            self.debug_text("Offscreen", tc);
7826                        }
7827                        self.close_element();
7828                    }
7829
7830                    // Element name
7831                    let id_string = if current_element_index < self.layout_element_id_strings.len() {
7832                        self.layout_element_id_strings[current_element_index].clone()
7833                    } else {
7834                        StringId::empty()
7835                    };
7836                    if !id_string.is_empty() {
7837                        let tc = if offscreen {
7838                            self.store_text_element_config(TextConfig {
7839                                color: Self::DEBUG_COLOR_3,
7840                                font_size: 16,
7841                                ..Default::default()
7842                            })
7843                        } else {
7844                            self.store_text_element_config(name_text_config.clone())
7845                        };
7846                        self.debug_raw_text(id_string.as_str(), tc);
7847                    }
7848
7849                    // Config type badges
7850                    let configs_start = self.layout_elements[current_element_index].element_configs.start;
7851                    let configs_len = self.layout_elements[current_element_index].element_configs.length;
7852                    for ci in 0..configs_len {
7853                        let ec = self.element_configs[configs_start + ci as usize];
7854                        if ec.config_type == ElementConfigType::Shared {
7855                            let shared = self.shared_element_configs[ec.config_index];
7856                            let label_color = Color::rgba(243.0, 134.0, 48.0, 90.0);
7857                            if shared.background_color.a > 0.0 {
7858                                self.debug_open(&ElementDeclaration {
7859                                    layout: LayoutConfig {
7860                                        padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
7861                                        ..Default::default()
7862                                    },
7863                                    background_color: label_color,
7864                                    corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
7865                                    border: BorderConfig {
7866                                        color: label_color,
7867                                        width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
7868                                        ..Default::default()
7869                                    },
7870                                    ..Default::default()
7871                                });
7872                                {
7873                                    let tc = self.store_text_element_config(TextConfig {
7874                                        color: if offscreen { Self::DEBUG_COLOR_3 } else { Self::DEBUG_COLOR_4 },
7875                                        font_size: 16,
7876                                        ..Default::default()
7877                                    });
7878                                    self.debug_text("Color", tc);
7879                                }
7880                                self.close_element();
7881                            }
7882                            if !shared.corner_radius.is_zero() {
7883                                let radius_color = Color::rgba(26.0, 188.0, 156.0, 90.0);
7884                                self.debug_open(&ElementDeclaration {
7885                                    layout: LayoutConfig {
7886                                        padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
7887                                        ..Default::default()
7888                                    },
7889                                    background_color: radius_color,
7890                                    corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
7891                                    border: BorderConfig {
7892                                        color: radius_color,
7893                                        width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
7894                                        ..Default::default()
7895                                    },
7896                                    ..Default::default()
7897                                });
7898                                {
7899                                    let tc = self.store_text_element_config(TextConfig {
7900                                        color: if offscreen { Self::DEBUG_COLOR_3 } else { Self::DEBUG_COLOR_4 },
7901                                        font_size: 16,
7902                                        ..Default::default()
7903                                    });
7904                                    self.debug_text("Radius", tc);
7905                                }
7906                                self.close_element();
7907                            }
7908                            continue;
7909                        }
7910                        let (label, label_color) = Self::debug_get_config_type_label(ec.config_type);
7911                        let bg = Color::rgba(label_color.r, label_color.g, label_color.b, 90.0);
7912                        self.debug_open(&ElementDeclaration {
7913                            layout: LayoutConfig {
7914                                padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
7915                                ..Default::default()
7916                            },
7917                            background_color: bg,
7918                            corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
7919                            border: BorderConfig {
7920                                color: label_color,
7921                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
7922                                ..Default::default()
7923                            },
7924                            ..Default::default()
7925                        });
7926                        {
7927                            let tc = self.store_text_element_config(TextConfig {
7928                                color: if offscreen { Self::DEBUG_COLOR_3 } else { Self::DEBUG_COLOR_4 },
7929                                font_size: 16,
7930                                ..Default::default()
7931                            });
7932                            self.debug_text(label, tc);
7933                        }
7934                        self.close_element();
7935
7936                        let has_scrollbar = match ec.config_type {
7937                            ElementConfigType::Clip => self
7938                                .clip_element_configs
7939                                .get(ec.config_index)
7940                                .and_then(|cfg| cfg.scrollbar)
7941                                .is_some(),
7942                            ElementConfigType::TextInput => self
7943                                .text_input_configs
7944                                .get(ec.config_index)
7945                                .and_then(|cfg| cfg.scrollbar)
7946                                .is_some(),
7947                            _ => false,
7948                        };
7949                        if has_scrollbar {
7950                            let bg = Color::rgba(242.0, 196.0, 90.0, 90.0);
7951                            let border_color = Color::rgba(242.0, 196.0, 90.0, 255.0);
7952                            self.debug_open(&ElementDeclaration {
7953                                layout: LayoutConfig {
7954                                    padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
7955                                    ..Default::default()
7956                                },
7957                                background_color: bg,
7958                                corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
7959                                border: BorderConfig {
7960                                    color: border_color,
7961                                    width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
7962                                    ..Default::default()
7963                                },
7964                                ..Default::default()
7965                            });
7966                            {
7967                                let tc = self.store_text_element_config(TextConfig {
7968                                    color: if offscreen { Self::DEBUG_COLOR_3 } else { Self::DEBUG_COLOR_4 },
7969                                    font_size: 16,
7970                                    ..Default::default()
7971                                });
7972                                self.debug_text("Scrollbar", tc);
7973                            }
7974                            self.close_element();
7975                        }
7976                    }
7977
7978                    // Shader badge
7979                    let has_shaders = self.element_shaders.get(current_element_index)
7980                        .map_or(false, |s| !s.is_empty());
7981                    if has_shaders {
7982                        let badge_color = Color::rgba(155.0, 89.0, 182.0, 90.0);
7983                        self.debug_open(&ElementDeclaration {
7984                            layout: LayoutConfig {
7985                                padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
7986                                ..Default::default()
7987                            },
7988                            background_color: badge_color,
7989                            corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
7990                            border: BorderConfig {
7991                                color: badge_color,
7992                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
7993                                ..Default::default()
7994                            },
7995                            ..Default::default()
7996                        });
7997                        {
7998                            let tc = self.store_text_element_config(TextConfig {
7999                                color: if offscreen { Self::DEBUG_COLOR_3 } else { Self::DEBUG_COLOR_4 },
8000                                font_size: 16,
8001                                ..Default::default()
8002                            });
8003                            self.debug_text("Shader", tc);
8004                        }
8005                        self.close_element();
8006                    }
8007
8008                    // Effect badge
8009                    let has_effects = self.element_effects.get(current_element_index)
8010                        .map_or(false, |e| !e.is_empty());
8011                    if has_effects {
8012                        let badge_color = Color::rgba(155.0, 89.0, 182.0, 90.0);
8013                        self.debug_open(&ElementDeclaration {
8014                            layout: LayoutConfig {
8015                                padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
8016                                ..Default::default()
8017                            },
8018                            background_color: badge_color,
8019                            corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
8020                            border: BorderConfig {
8021                                color: badge_color,
8022                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
8023                                ..Default::default()
8024                            },
8025                            ..Default::default()
8026                        });
8027                        {
8028                            let tc = self.store_text_element_config(TextConfig {
8029                                color: if offscreen { Self::DEBUG_COLOR_3 } else { Self::DEBUG_COLOR_4 },
8030                                font_size: 16,
8031                                ..Default::default()
8032                            });
8033                            self.debug_text("Effect", tc);
8034                        }
8035                        self.close_element();
8036                    }
8037
8038                    // Visual Rotation badge
8039                    let has_visual_rot = self.element_visual_rotations.get(current_element_index)
8040                        .map_or(false, |r| r.is_some());
8041                    if has_visual_rot {
8042                        let badge_color = Color::rgba(155.0, 89.0, 182.0, 90.0);
8043                        self.debug_open(&ElementDeclaration {
8044                            layout: LayoutConfig {
8045                                padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
8046                                ..Default::default()
8047                            },
8048                            background_color: badge_color,
8049                            corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
8050                            border: BorderConfig {
8051                                color: badge_color,
8052                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
8053                                ..Default::default()
8054                            },
8055                            ..Default::default()
8056                        });
8057                        {
8058                            let tc = self.store_text_element_config(TextConfig {
8059                                color: if offscreen { Self::DEBUG_COLOR_3 } else { Self::DEBUG_COLOR_4 },
8060                                font_size: 16,
8061                                ..Default::default()
8062                            });
8063                            self.debug_text("VisualRot", tc);
8064                        }
8065                        self.close_element();
8066                    }
8067
8068                    // Shape Rotation badge
8069                    let has_shape_rot = self.element_shape_rotations.get(current_element_index)
8070                        .map_or(false, |r| r.is_some());
8071                    if has_shape_rot {
8072                        let badge_color = Color::rgba(26.0, 188.0, 156.0, 90.0);
8073                        self.debug_open(&ElementDeclaration {
8074                            layout: LayoutConfig {
8075                                padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
8076                                ..Default::default()
8077                            },
8078                            background_color: badge_color,
8079                            corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
8080                            border: BorderConfig {
8081                                color: badge_color,
8082                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
8083                                ..Default::default()
8084                            },
8085                            ..Default::default()
8086                        });
8087                        {
8088                            let tc = self.store_text_element_config(TextConfig {
8089                                color: if offscreen { Self::DEBUG_COLOR_3 } else { Self::DEBUG_COLOR_4 },
8090                                font_size: 16,
8091                                ..Default::default()
8092                            });
8093                            self.debug_text("ShapeRot", tc);
8094                        }
8095                        self.close_element();
8096                    }
8097                }
8098                self.close_element(); // ElementOuter row
8099
8100                // Text element content row
8101                let is_text = self.element_has_config(current_element_index, ElementConfigType::Text);
8102                let children_len = self.layout_elements[current_element_index].children_length;
8103                if is_text {
8104                    row_count += 1;
8105                    let text_data_idx = self.layout_elements[current_element_index].text_data_index;
8106                    let text_content = if text_data_idx >= 0 {
8107                        self.text_element_data[text_data_idx as usize].text.clone()
8108                    } else {
8109                        String::new()
8110                    };
8111                    let raw_tc_idx = if offscreen {
8112                        self.store_text_element_config(TextConfig {
8113                            color: Self::DEBUG_COLOR_3,
8114                            font_size: 16,
8115                            ..Default::default()
8116                        })
8117                    } else {
8118                        self.store_text_element_config(name_text_config.clone())
8119                    };
8120                    self.debug_open(&ElementDeclaration {
8121                        layout: LayoutConfig {
8122                            sizing: SizingConfig {
8123                                height: SizingAxis {
8124                                    type_: SizingType::Fixed,
8125                                    min_max: SizingMinMax { min: row_height, max: row_height },
8126                                    ..Default::default()
8127                                },
8128                                ..Default::default()
8129                            },
8130                            child_alignment: ChildAlignmentConfig { x: AlignX::Left, y: AlignY::CenterY },
8131                            ..Default::default()
8132                        },
8133                        ..Default::default()
8134                    });
8135                    {
8136                        // Indent spacer
8137                        self.debug_open(&ElementDeclaration {
8138                            layout: LayoutConfig {
8139                                sizing: SizingConfig {
8140                                    width: SizingAxis {
8141                                        type_: SizingType::Fixed,
8142                                        min_max: SizingMinMax {
8143                                            min: (indent_width + 16) as f32,
8144                                            max: (indent_width + 16) as f32,
8145                                        },
8146                                        ..Default::default()
8147                                    },
8148                                    ..Default::default()
8149                                },
8150                                ..Default::default()
8151                            },
8152                            ..Default::default()
8153                        });
8154                        self.close_element();
8155                        self.debug_text("\"", raw_tc_idx);
8156                        if text_content.len() > 40 {
8157                            let mut end = 40;
8158                            while !text_content.is_char_boundary(end) { end -= 1; }
8159                            self.debug_raw_text(&text_content[..end], raw_tc_idx);
8160                            self.debug_text("...", raw_tc_idx);
8161                        } else if !text_content.is_empty() {
8162                            self.debug_raw_text(&text_content, raw_tc_idx);
8163                        }
8164                        self.debug_text("\"", raw_tc_idx);
8165                    }
8166                    self.close_element();
8167                } else if children_len > 0 {
8168                    // Open containers for child indentation
8169                    self.open_element();
8170                    self.configure_open_element(&ElementDeclaration {
8171                        layout: LayoutConfig {
8172                            padding: PaddingConfig { left: 8, ..Default::default() },
8173                            ..Default::default()
8174                        },
8175                        ..Default::default()
8176                    });
8177                    self.open_element();
8178                    self.configure_open_element(&ElementDeclaration {
8179                        layout: LayoutConfig {
8180                            padding: PaddingConfig { left: indent_width, ..Default::default() },
8181                            ..Default::default()
8182                        },
8183                        border: BorderConfig {
8184                            color: Self::DEBUG_COLOR_3,
8185                            width: BorderWidth { left: 1, ..Default::default() },
8186                            ..Default::default()
8187                        },
8188                        ..Default::default()
8189                    });
8190                    self.open_element();
8191                    self.configure_open_element(&ElementDeclaration {
8192                        layout: LayoutConfig {
8193                            layout_direction: LayoutDirection::TopToBottom,
8194                            ..Default::default()
8195                        },
8196                        ..Default::default()
8197                    });
8198                }
8199
8200                row_count += 1;
8201
8202                // Push children in reverse order for DFS (if not text and not collapsed)
8203                if !is_text && !collapsed {
8204                    let children_start = self.layout_elements[current_element_index].children_start;
8205                    let children_length = self.layout_elements[current_element_index].children_length as usize;
8206                    for i in (0..children_length).rev() {
8207                        let child_idx = self.layout_element_children[children_start + i];
8208                        dfs_buffer.push(child_idx);
8209                        // Ensure visited vec is large enough
8210                        while visited.len() <= dfs_buffer.len() {
8211                            visited.push(false);
8212                        }
8213                        visited[dfs_buffer.len() - 1] = false;
8214                    }
8215                }
8216            }
8217        }
8218
8219        // Handle collapse button clicks
8220        if self.pointer_info.state == PointerDataInteractionState::PressedThisFrame {
8221            let collapse_base_id = hash_string("Ply__DebugView_CollapseElement", 0).base_id;
8222            for i in (0..self.pointer_over_ids.len()).rev() {
8223                let element_id = self.pointer_over_ids[i].clone();
8224                if element_id.base_id == collapse_base_id {
8225                    if let Some(item) = self.layout_element_map.get_mut(&element_id.offset) {
8226                        item.collapsed = !item.collapsed;
8227                    }
8228                    break;
8229                }
8230            }
8231        }
8232
8233        // Render highlight on hovered or selected element
8234        // When an element is selected, show its bounding box; otherwise show hovered
8235        let highlight_target = if self.debug_selected_element_id != 0 {
8236            self.debug_selected_element_id
8237        } else {
8238            highlighted_element_id
8239        };
8240        if highlight_target != 0 {
8241            self.debug_open_id("Ply__DebugView_ElementHighlight", &ElementDeclaration {
8242                layout: LayoutConfig {
8243                    sizing: SizingConfig {
8244                        width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
8245                        height: SizingAxis { type_: SizingType::Grow, ..Default::default() },
8246                    },
8247                    ..Default::default()
8248                },
8249                floating: FloatingConfig {
8250                    parent_id: highlight_target,
8251                    z_index: 32767,
8252                    pointer_capture_mode: PointerCaptureMode::Passthrough,
8253                    attach_to: FloatingAttachToElement::ElementWithId,
8254                    ..Default::default()
8255                },
8256                ..Default::default()
8257            });
8258            {
8259                self.debug_open_id("Ply__DebugView_ElementHighlightRectangle", &ElementDeclaration {
8260                    layout: LayoutConfig {
8261                        sizing: SizingConfig {
8262                            width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
8263                            height: SizingAxis { type_: SizingType::Grow, ..Default::default() },
8264                        },
8265                        ..Default::default()
8266                    },
8267                    background_color: Self::DEBUG_HIGHLIGHT_COLOR,
8268                    ..Default::default()
8269                });
8270                self.close_element();
8271            }
8272            self.close_element();
8273        }
8274
8275        (row_count, selected_element_row_index)
8276    }
8277
8278    /// Main debug view rendering. Called from end_layout() when debug mode is enabled.
8279    fn render_debug_view(&mut self) {
8280        let initial_roots_length = self.layout_element_tree_roots.len();
8281        let initial_elements_length = self.layout_elements.len();
8282        let row_height = Self::DEBUG_VIEW_ROW_HEIGHT;
8283        let outer_padding = Self::DEBUG_VIEW_OUTER_PADDING;
8284        let debug_width = self.debug_view_width;
8285
8286        let info_text_config = self.store_text_element_config(TextConfig {
8287            color: Self::DEBUG_COLOR_4,
8288            font_size: 16,
8289            wrap_mode: WrapMode::None,
8290            ..Default::default()
8291        });
8292        let info_title_config = self.store_text_element_config(TextConfig {
8293            color: Self::DEBUG_COLOR_3,
8294            font_size: 16,
8295            wrap_mode: WrapMode::None,
8296            ..Default::default()
8297        });
8298
8299        // Determine scroll offset for the debug scroll pane
8300        let scroll_id = hash_string("Ply__DebugViewOuterScrollPane", 0);
8301        let mut scroll_y_offset: f32 = 0.0;
8302        // Only exclude the bottom 300px from tree interaction when the detail panel is shown
8303        let detail_panel_height = if self.debug_selected_element_id != 0 { 300.0 } else { 0.0 };
8304        let mut pointer_in_debug_view = self.pointer_info.position.y < self.layout_dimensions.height - detail_panel_height;
8305        for scd in &self.scroll_container_datas {
8306            if scd.element_id == scroll_id.id {
8307                if !self.external_scroll_handling_enabled {
8308                    scroll_y_offset = scd.scroll_position.y;
8309                } else {
8310                    pointer_in_debug_view = self.pointer_info.position.y + scd.scroll_position.y
8311                        < self.layout_dimensions.height - detail_panel_height;
8312                }
8313                break;
8314            }
8315        }
8316
8317        let highlighted_row = if pointer_in_debug_view {
8318            ((self.pointer_info.position.y - scroll_y_offset) / row_height) as i32 - 1
8319        } else {
8320            -1
8321        };
8322        let highlighted_row = if self.pointer_info.position.x < self.layout_dimensions.width - debug_width {
8323            -1
8324        } else {
8325            highlighted_row
8326        };
8327
8328        // Main debug view panel (floating)
8329        self.debug_open_id("Ply__DebugView", &ElementDeclaration {
8330            layout: LayoutConfig {
8331                sizing: SizingConfig {
8332                    width: SizingAxis {
8333                        type_: SizingType::Fixed,
8334                        min_max: SizingMinMax { min: debug_width, max: debug_width },
8335                        ..Default::default()
8336                    },
8337                    height: SizingAxis {
8338                        type_: SizingType::Fixed,
8339                        min_max: SizingMinMax { min: self.layout_dimensions.height, max: self.layout_dimensions.height },
8340                        ..Default::default()
8341                    },
8342                },
8343                layout_direction: LayoutDirection::TopToBottom,
8344                ..Default::default()
8345            },
8346            floating: FloatingConfig {
8347                z_index: 32765,
8348                attach_points: FloatingAttachPoints {
8349                    element_x: AlignX::Right,
8350                    element_y: AlignY::CenterY,
8351                    parent_x: AlignX::Right,
8352                    parent_y: AlignY::CenterY,
8353                },
8354                attach_to: FloatingAttachToElement::Root,
8355                clip_to: FloatingClipToElement::AttachedParent,
8356                ..Default::default()
8357            },
8358            border: BorderConfig {
8359                color: Self::DEBUG_COLOR_3,
8360                width: BorderWidth { bottom: 1, ..Default::default() },
8361                ..Default::default()
8362            },
8363            ..Default::default()
8364        });
8365        {
8366            // Header bar
8367            self.debug_open(&ElementDeclaration {
8368                layout: LayoutConfig {
8369                    sizing: SizingConfig {
8370                        width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
8371                        height: SizingAxis {
8372                            type_: SizingType::Fixed,
8373                            min_max: SizingMinMax { min: row_height, max: row_height },
8374                            ..Default::default()
8375                        },
8376                    },
8377                    padding: PaddingConfig { left: outer_padding, right: outer_padding, top: 0, bottom: 0 },
8378                    child_alignment: ChildAlignmentConfig { x: AlignX::Left, y: AlignY::CenterY },
8379                    ..Default::default()
8380                },
8381                background_color: Self::DEBUG_COLOR_2,
8382                ..Default::default()
8383            });
8384            {
8385                self.debug_text("Ply Debug Tools", info_text_config);
8386                // Spacer
8387                self.debug_open(&ElementDeclaration {
8388                    layout: LayoutConfig {
8389                        sizing: SizingConfig {
8390                            width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
8391                            ..Default::default()
8392                        },
8393                        ..Default::default()
8394                    },
8395                    ..Default::default()
8396                });
8397                self.close_element();
8398                // Close button
8399                let close_size = row_height - 10.0;
8400                self.debug_open_id("Ply__DebugView_CloseButton", &ElementDeclaration {
8401                    layout: LayoutConfig {
8402                        sizing: SizingConfig {
8403                            width: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: close_size, max: close_size }, ..Default::default() },
8404                            height: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: close_size, max: close_size }, ..Default::default() },
8405                        },
8406                        child_alignment: ChildAlignmentConfig { x: AlignX::CenterX, y: AlignY::CenterY },
8407                        ..Default::default()
8408                    },
8409                    background_color: Color::rgba(217.0, 91.0, 67.0, 80.0),
8410                    corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
8411                    border: BorderConfig {
8412                        color: Color::rgba(217.0, 91.0, 67.0, 255.0),
8413                        width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
8414                        ..Default::default()
8415                    },
8416                    ..Default::default()
8417                });
8418                {
8419                    let tc = self.store_text_element_config(TextConfig {
8420                        color: Self::DEBUG_COLOR_4,
8421                        font_size: 16,
8422                        ..Default::default()
8423                    });
8424                    self.debug_text("x", tc);
8425                }
8426                self.close_element();
8427            }
8428            self.close_element();
8429
8430            // Separator line
8431            self.debug_open(&ElementDeclaration {
8432                layout: LayoutConfig {
8433                    sizing: SizingConfig {
8434                        width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
8435                        height: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 1.0, max: 1.0 }, ..Default::default() },
8436                    },
8437                    ..Default::default()
8438                },
8439                background_color: Self::DEBUG_COLOR_3,
8440                ..Default::default()
8441            });
8442            self.close_element();
8443
8444            // Scroll pane
8445            self.open_element_with_id(&scroll_id);
8446            self.configure_open_element(&ElementDeclaration {
8447                layout: LayoutConfig {
8448                    sizing: SizingConfig {
8449                        width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
8450                        height: SizingAxis { type_: SizingType::Grow, ..Default::default() },
8451                    },
8452                    ..Default::default()
8453                },
8454                clip: ClipConfig {
8455                    horizontal: true,
8456                    vertical: true,
8457                    scroll_x: true,
8458                    scroll_y: true,
8459                    child_offset: self.get_scroll_offset(),
8460                    ..Default::default()
8461                },
8462                ..Default::default()
8463            });
8464            {
8465                let alt_bg = if (initial_elements_length + initial_roots_length) & 1 == 0 {
8466                    Self::DEBUG_COLOR_2
8467                } else {
8468                    Self::DEBUG_COLOR_1
8469                };
8470                // Content container — Fit height so it extends beyond the scroll pane
8471                self.debug_open(&ElementDeclaration {
8472                    layout: LayoutConfig {
8473                        sizing: SizingConfig {
8474                            width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
8475                            ..Default::default() // height defaults to Fit
8476                        },
8477                        padding: PaddingConfig {
8478                            left: outer_padding,
8479                            right: outer_padding,
8480                            top: 0,
8481                            bottom: 0,
8482                        },
8483                        layout_direction: LayoutDirection::TopToBottom,
8484                        ..Default::default()
8485                    },
8486                    background_color: alt_bg,
8487                    ..Default::default()
8488                });
8489                {
8490                    let _layout_data = self.render_debug_layout_elements_list(
8491                        initial_roots_length,
8492                        highlighted_row,
8493                    );
8494                }
8495                self.close_element(); // content container
8496            }
8497            self.close_element(); // scroll pane
8498
8499            // Separator
8500            self.debug_open(&ElementDeclaration {
8501                layout: LayoutConfig {
8502                    sizing: SizingConfig {
8503                        width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
8504                        height: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 1.0, max: 1.0 }, ..Default::default() },
8505                    },
8506                    ..Default::default()
8507                },
8508                background_color: Self::DEBUG_COLOR_3,
8509                ..Default::default()
8510            });
8511            self.close_element();
8512
8513            // Selected element detail panel
8514            if self.debug_selected_element_id != 0 {
8515                self.render_debug_selected_element_panel(info_text_config, info_title_config);
8516            }
8517        }
8518        self.close_element(); // Ply__DebugView
8519
8520        // Handle close button click
8521        if self.pointer_info.state == PointerDataInteractionState::PressedThisFrame {
8522            let close_base_id = hash_string("Ply__DebugView_CloseButton", 0).id;
8523            let header_base_id = hash_string("Ply__DebugView_LayoutConfigHeader", 0).id;
8524            for i in (0..self.pointer_over_ids.len()).rev() {
8525                let id = self.pointer_over_ids[i].id;
8526                if id == close_base_id {
8527                    self.debug_mode_enabled = false;
8528                    break;
8529                }
8530                if id == header_base_id {
8531                    self.debug_selected_element_id = 0;
8532                    break;
8533                }
8534            }
8535        }
8536    }
8537
8538    /// Render the selected element detail panel in the debug view.
8539    fn render_debug_selected_element_panel(
8540        &mut self,
8541        info_text_config: usize,
8542        info_title_config: usize,
8543    ) {
8544        let row_height = Self::DEBUG_VIEW_ROW_HEIGHT;
8545        let outer_padding = Self::DEBUG_VIEW_OUTER_PADDING;
8546        let attr_padding = PaddingConfig {
8547            left: outer_padding,
8548            right: outer_padding,
8549            top: 8,
8550            bottom: 8,
8551        };
8552
8553        let selected_id = self.debug_selected_element_id;
8554        let selected_item = match self.layout_element_map.get(&selected_id) {
8555            Some(item) => item.clone(),
8556            None => return,
8557        };
8558        let layout_elem_idx = selected_item.layout_element_index as usize;
8559        if layout_elem_idx >= self.layout_elements.len() {
8560            return;
8561        }
8562
8563        let layout_config_index = self.layout_elements[layout_elem_idx].layout_config_index;
8564        let layout_config = self.layout_configs[layout_config_index];
8565
8566        self.debug_open(&ElementDeclaration {
8567            layout: LayoutConfig {
8568                sizing: SizingConfig {
8569                    width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
8570                    height: SizingAxis {
8571                        type_: SizingType::Fixed,
8572                        min_max: SizingMinMax { min: 316.0, max: 316.0 },
8573                        ..Default::default()
8574                    },
8575                },
8576                layout_direction: LayoutDirection::TopToBottom,
8577                ..Default::default()
8578            },
8579            background_color: Self::DEBUG_COLOR_2,
8580            clip: ClipConfig {
8581                vertical: true,
8582                scroll_y: true,
8583                child_offset: self.get_scroll_offset(),
8584                ..Default::default()
8585            },
8586            border: BorderConfig {
8587                color: Self::DEBUG_COLOR_3,
8588                width: BorderWidth { between_children: 1, ..Default::default() },
8589                ..Default::default()
8590            },
8591            ..Default::default()
8592        });
8593        {
8594            // Header: "Layout Config" + element ID
8595            self.debug_open_id("Ply__DebugView_LayoutConfigHeader", &ElementDeclaration {
8596                layout: LayoutConfig {
8597                    sizing: SizingConfig {
8598                        width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
8599                        height: SizingAxis {
8600                            type_: SizingType::Fixed,
8601                            min_max: SizingMinMax { min: row_height + 8.0, max: row_height + 8.0 },
8602                            ..Default::default()
8603                        },
8604                    },
8605                    padding: PaddingConfig { left: outer_padding, right: outer_padding, top: 0, bottom: 0 },
8606                    child_alignment: ChildAlignmentConfig { x: AlignX::Left, y: AlignY::CenterY },
8607                    ..Default::default()
8608                },
8609                ..Default::default()
8610            });
8611            {
8612                self.debug_text("Layout Config", info_text_config);
8613                // Spacer
8614                self.debug_open(&ElementDeclaration {
8615                    layout: LayoutConfig {
8616                        sizing: SizingConfig {
8617                            width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
8618                            ..Default::default()
8619                        },
8620                        ..Default::default()
8621                    },
8622                    ..Default::default()
8623                });
8624                self.close_element();
8625                // Element ID string
8626                let sid = selected_item.element_id.string_id.clone();
8627                if !sid.is_empty() {
8628                    self.debug_raw_text(sid.as_str(), info_title_config);
8629                    if selected_item.element_id.offset != 0 {
8630                        self.debug_text(" (", info_title_config);
8631                        self.debug_int_text(selected_item.element_id.offset as f32, info_title_config);
8632                        self.debug_text(")", info_title_config);
8633                    }
8634                }
8635            }
8636            self.close_element();
8637
8638            // Layout config details
8639            self.debug_open(&ElementDeclaration {
8640                layout: LayoutConfig {
8641                    padding: attr_padding,
8642                    child_gap: 8,
8643                    layout_direction: LayoutDirection::TopToBottom,
8644                    ..Default::default()
8645                },
8646                ..Default::default()
8647            });
8648            {
8649                // Bounding Box
8650                self.debug_text("Bounding Box", info_title_config);
8651                self.debug_open(&ElementDeclaration::default());
8652                {
8653                    self.debug_text("{ x: ", info_text_config);
8654                    self.debug_int_text(selected_item.bounding_box.x, info_text_config);
8655                    self.debug_text(", y: ", info_text_config);
8656                    self.debug_int_text(selected_item.bounding_box.y, info_text_config);
8657                    self.debug_text(", width: ", info_text_config);
8658                    self.debug_int_text(selected_item.bounding_box.width, info_text_config);
8659                    self.debug_text(", height: ", info_text_config);
8660                    self.debug_int_text(selected_item.bounding_box.height, info_text_config);
8661                    self.debug_text(" }", info_text_config);
8662                }
8663                self.close_element();
8664
8665                // Layout Direction
8666                self.debug_text("Layout Direction", info_title_config);
8667                if layout_config.layout_direction == LayoutDirection::TopToBottom {
8668                    self.debug_text("TOP_TO_BOTTOM", info_text_config);
8669                } else {
8670                    self.debug_text("LEFT_TO_RIGHT", info_text_config);
8671                }
8672
8673                // Sizing
8674                self.debug_text("Sizing", info_title_config);
8675                self.debug_open(&ElementDeclaration::default());
8676                {
8677                    self.debug_text("width: ", info_text_config);
8678                    self.render_debug_layout_sizing(layout_config.sizing.width, info_text_config);
8679                }
8680                self.close_element();
8681                self.debug_open(&ElementDeclaration::default());
8682                {
8683                    self.debug_text("height: ", info_text_config);
8684                    self.render_debug_layout_sizing(layout_config.sizing.height, info_text_config);
8685                }
8686                self.close_element();
8687
8688                // Padding
8689                self.debug_text("Padding", info_title_config);
8690                self.debug_open_id("Ply__DebugViewElementInfoPadding", &ElementDeclaration::default());
8691                {
8692                    self.debug_text("{ left: ", info_text_config);
8693                    self.debug_int_text(layout_config.padding.left as f32, info_text_config);
8694                    self.debug_text(", right: ", info_text_config);
8695                    self.debug_int_text(layout_config.padding.right as f32, info_text_config);
8696                    self.debug_text(", top: ", info_text_config);
8697                    self.debug_int_text(layout_config.padding.top as f32, info_text_config);
8698                    self.debug_text(", bottom: ", info_text_config);
8699                    self.debug_int_text(layout_config.padding.bottom as f32, info_text_config);
8700                    self.debug_text(" }", info_text_config);
8701                }
8702                self.close_element();
8703
8704                // Child Gap
8705                self.debug_text("Child Gap", info_title_config);
8706                self.debug_int_text(layout_config.child_gap as f32, info_text_config);
8707
8708                // Wrap
8709                self.debug_text("Wrap", info_title_config);
8710                self.debug_text(
8711                    if layout_config.wrap { "true" } else { "false" },
8712                    info_text_config,
8713                );
8714
8715                // Wrap Gap
8716                self.debug_text("Wrap Gap", info_title_config);
8717                self.debug_int_text(layout_config.wrap_gap as f32, info_text_config);
8718
8719                // Child Alignment
8720                self.debug_text("Child Alignment", info_title_config);
8721                self.debug_open(&ElementDeclaration::default());
8722                {
8723                    self.debug_text("{ x: ", info_text_config);
8724                    let align_x = Self::align_x_name(layout_config.child_alignment.x);
8725                    self.debug_text(align_x, info_text_config);
8726                    self.debug_text(", y: ", info_text_config);
8727                    let align_y = Self::align_y_name(layout_config.child_alignment.y);
8728                    self.debug_text(align_y, info_text_config);
8729                    self.debug_text(" }", info_text_config);
8730                }
8731                self.close_element();
8732            }
8733            self.close_element(); // layout config details
8734
8735            // ── Collect data for grouped categories ──
8736            let configs_start = self.layout_elements[layout_elem_idx].element_configs.start;
8737            let configs_len = self.layout_elements[layout_elem_idx].element_configs.length;
8738            let elem_id_string = selected_item.element_id.string_id.clone();
8739
8740            // Shared data (split into Color + Shape)
8741            let mut shared_bg_color: Option<Color> = None;
8742            let mut shared_corner_radius: Option<CornerRadius> = None;
8743            for ci in 0..configs_len {
8744                let ec = self.element_configs[configs_start + ci as usize];
8745                if ec.config_type == ElementConfigType::Shared {
8746                    let shared = self.shared_element_configs[ec.config_index];
8747                    shared_bg_color = Some(shared.background_color);
8748                    shared_corner_radius = Some(shared.corner_radius);
8749                }
8750            }
8751
8752            // Per-element data (not in element_configs system)
8753            let shape_rot = self.element_shape_rotations.get(layout_elem_idx).copied().flatten();
8754            let visual_rot = self.element_visual_rotations.get(layout_elem_idx).cloned().flatten();
8755            let effects = self.element_effects.get(layout_elem_idx).cloned().unwrap_or_default();
8756            let shaders = self.element_shaders.get(layout_elem_idx).cloned().unwrap_or_default();
8757
8758            // ── [Color] section ──
8759            let has_color = shared_bg_color.map_or(false, |c| c.a > 0.0);
8760            if has_color {
8761                let color_label_color = Color::rgba(243.0, 134.0, 48.0, 255.0);
8762                self.render_debug_view_category_header("Color", color_label_color, elem_id_string.clone());
8763                self.debug_open(&ElementDeclaration {
8764                    layout: LayoutConfig {
8765                        padding: attr_padding,
8766                        child_gap: 8,
8767                        layout_direction: LayoutDirection::TopToBottom,
8768                        ..Default::default()
8769                    },
8770                    ..Default::default()
8771                });
8772                {
8773                    self.debug_text("Background Color", info_title_config);
8774                    self.render_debug_view_color(shared_bg_color.unwrap(), info_text_config);
8775                }
8776                self.close_element();
8777            }
8778
8779            // ── [Shape] section (Corner Radius + Shape Rotation) ──
8780            let has_corner_radius = shared_corner_radius.map_or(false, |cr| !cr.is_zero());
8781            let has_shape_rot = shape_rot.is_some();
8782            if has_corner_radius || has_shape_rot {
8783                let shape_label_color = Color::rgba(26.0, 188.0, 156.0, 255.0);
8784                self.render_debug_view_category_header("Shape", shape_label_color, elem_id_string.clone());
8785                self.debug_open(&ElementDeclaration {
8786                    layout: LayoutConfig {
8787                        padding: attr_padding,
8788                        child_gap: 8,
8789                        layout_direction: LayoutDirection::TopToBottom,
8790                        ..Default::default()
8791                    },
8792                    ..Default::default()
8793                });
8794                {
8795                    if let Some(cr) = shared_corner_radius {
8796                        if !cr.is_zero() {
8797                            self.debug_text("Corner Radius", info_title_config);
8798                            self.render_debug_view_corner_radius(cr, info_text_config);
8799                        }
8800                    }
8801                    if let Some(sr) = shape_rot {
8802                        self.debug_text("Shape Rotation", info_title_config);
8803                        self.debug_open(&ElementDeclaration::default());
8804                        {
8805                            self.debug_text("angle: ", info_text_config);
8806                            self.debug_float_text(sr.rotation_radians, info_text_config);
8807                            self.debug_text(" rad", info_text_config);
8808                        }
8809                        self.close_element();
8810                        self.debug_open(&ElementDeclaration::default());
8811                        {
8812                            self.debug_text("flip_x: ", info_text_config);
8813                            self.debug_text(if sr.flip_x { "true" } else { "false" }, info_text_config);
8814                            self.debug_text(", flip_y: ", info_text_config);
8815                            self.debug_text(if sr.flip_y { "true" } else { "false" }, info_text_config);
8816                        }
8817                        self.close_element();
8818                    }
8819                }
8820                self.close_element();
8821            }
8822
8823            // ── Config-type sections (Text, Image, Floating, Clip, Border, etc.) ──
8824            for ci in 0..configs_len {
8825                let ec = self.element_configs[configs_start + ci as usize];
8826                match ec.config_type {
8827                    ElementConfigType::Shared => {} // handled above as [Color] + [Shape]
8828                    ElementConfigType::Text => {
8829                        self.render_debug_view_element_config_header(elem_id_string.clone(), ec.config_type, info_title_config);
8830                        let text_config = self.text_element_configs[ec.config_index].clone();
8831                        self.debug_open(&ElementDeclaration {
8832                            layout: LayoutConfig {
8833                                padding: attr_padding,
8834                                child_gap: 8,
8835                                layout_direction: LayoutDirection::TopToBottom,
8836                                ..Default::default()
8837                            },
8838                            ..Default::default()
8839                        });
8840                        {
8841                            self.debug_text("Font Size", info_title_config);
8842                            self.debug_int_text(text_config.font_size as f32, info_text_config);
8843                            self.debug_text("Font", info_title_config);
8844                            {
8845                                let label = if let Some(asset) = text_config.font_asset {
8846                                    asset.key().to_string()
8847                                } else {
8848                                    format!("default ({})", self.default_font_key)
8849                                };
8850                                self.open_text_element(&label, info_text_config);
8851                            }
8852                            self.debug_text("Line Height", info_title_config);
8853                            if text_config.line_height == 0 {
8854                                self.debug_text("auto", info_text_config);
8855                            } else {
8856                                self.debug_int_text(text_config.line_height as f32, info_text_config);
8857                            }
8858                            self.debug_text("Letter Spacing", info_title_config);
8859                            self.debug_int_text(text_config.letter_spacing as f32, info_text_config);
8860                            self.debug_text("Wrap Mode", info_title_config);
8861                            let wrap = match text_config.wrap_mode {
8862                                WrapMode::None => "NONE",
8863                                WrapMode::Newline => "NEWLINES",
8864                                _ => "WORDS",
8865                            };
8866                            self.debug_text(wrap, info_text_config);
8867                            self.debug_text("Text Alignment", info_title_config);
8868                            let align = match text_config.alignment {
8869                                AlignX::CenterX => "CENTER",
8870                                AlignX::Right => "RIGHT",
8871                                _ => "LEFT",
8872                            };
8873                            self.debug_text(align, info_text_config);
8874                            self.debug_text("Text Color", info_title_config);
8875                            self.render_debug_view_color(text_config.color, info_text_config);
8876                        }
8877                        self.close_element();
8878                    }
8879                    ElementConfigType::Image => {
8880                        let image_label_color = Color::rgba(121.0, 189.0, 154.0, 255.0);
8881                        self.render_debug_view_category_header("Image", image_label_color, elem_id_string.clone());
8882                        let image_data = self.image_element_configs[ec.config_index].clone();
8883                        self.debug_open(&ElementDeclaration {
8884                            layout: LayoutConfig {
8885                                padding: attr_padding,
8886                                child_gap: 8,
8887                                layout_direction: LayoutDirection::TopToBottom,
8888                                ..Default::default()
8889                            },
8890                            ..Default::default()
8891                        });
8892                        {
8893                            self.debug_text("Source", info_title_config);
8894                            let name = image_data.get_name();
8895                            self.debug_raw_text(name, info_text_config);
8896                        }
8897                        self.close_element();
8898                    }
8899                    ElementConfigType::Aspect => {
8900                        self.render_debug_view_element_config_header(
8901                            elem_id_string.clone(),
8902                            ec.config_type,
8903                            info_title_config,
8904                        );
8905                        let aspect_ratio = self.aspect_ratio_configs[ec.config_index];
8906                        let is_cover = self
8907                            .aspect_ratio_cover_configs
8908                            .get(ec.config_index)
8909                            .copied()
8910                            .unwrap_or(false);
8911                        self.debug_open(&ElementDeclaration {
8912                            layout: LayoutConfig {
8913                                padding: attr_padding,
8914                                child_gap: 8,
8915                                layout_direction: LayoutDirection::TopToBottom,
8916                                ..Default::default()
8917                            },
8918                            ..Default::default()
8919                        });
8920                        {
8921                            self.debug_text("Aspect Ratio", info_title_config);
8922                            self.debug_float_text(aspect_ratio, info_text_config);
8923                            self.debug_text("Mode", info_title_config);
8924                            self.debug_text(
8925                                if is_cover { "COVER" } else { "CONTAIN" },
8926                                info_text_config,
8927                            );
8928                        }
8929                        self.close_element();
8930                    }
8931                    ElementConfigType::Clip => {
8932                        self.render_debug_view_element_config_header(elem_id_string.clone(), ec.config_type, info_title_config);
8933                        let clip_config = self.clip_element_configs[ec.config_index];
8934                        self.debug_open(&ElementDeclaration {
8935                            layout: LayoutConfig {
8936                                padding: attr_padding,
8937                                child_gap: 8,
8938                                layout_direction: LayoutDirection::TopToBottom,
8939                                ..Default::default()
8940                            },
8941                            ..Default::default()
8942                        });
8943                        {
8944                            self.debug_text("Overflow", info_title_config);
8945                            self.debug_open(&ElementDeclaration::default());
8946                            {
8947                                let x_label = if clip_config.scroll_x {
8948                                    "SCROLL"
8949                                } else if clip_config.horizontal {
8950                                    "CLIP"
8951                                } else {
8952                                    "OVERFLOW"
8953                                };
8954                                let y_label = if clip_config.scroll_y {
8955                                    "SCROLL"
8956                                } else if clip_config.vertical {
8957                                    "CLIP"
8958                                } else {
8959                                    "OVERFLOW"
8960                                };
8961                                self.debug_text("{ x: ", info_text_config);
8962                                self.debug_text(x_label, info_text_config);
8963                                self.debug_text(", y: ", info_text_config);
8964                                self.debug_text(y_label, info_text_config);
8965                                self.debug_text(" }", info_text_config);
8966                            }
8967                            self.close_element();
8968
8969                            self.debug_text("No Drag Scroll", info_title_config);
8970                            self.debug_text(
8971                                if clip_config.no_drag_scroll {
8972                                    "true"
8973                                } else {
8974                                    "false"
8975                                },
8976                                info_text_config,
8977                            );
8978                        }
8979                        self.close_element();
8980
8981                        if let Some(scrollbar) = clip_config.scrollbar {
8982                            let scrollbar_label_color = Color::rgba(242.0, 196.0, 90.0, 255.0);
8983                            self.render_debug_view_category_header(
8984                                "Scrollbar",
8985                                scrollbar_label_color,
8986                                elem_id_string.clone(),
8987                            );
8988                            self.debug_open(&ElementDeclaration {
8989                                layout: LayoutConfig {
8990                                    padding: attr_padding,
8991                                    child_gap: 8,
8992                                    layout_direction: LayoutDirection::TopToBottom,
8993                                    ..Default::default()
8994                                },
8995                                ..Default::default()
8996                            });
8997                            {
8998                                self.render_debug_scrollbar_config(
8999                                    scrollbar,
9000                                    info_text_config,
9001                                    info_title_config,
9002                                );
9003                            }
9004                            self.close_element();
9005                        }
9006                    }
9007                    ElementConfigType::Floating => {
9008                        self.render_debug_view_element_config_header(elem_id_string.clone(), ec.config_type, info_title_config);
9009                        let float_config = self.floating_element_configs[ec.config_index];
9010                        self.debug_open(&ElementDeclaration {
9011                            layout: LayoutConfig {
9012                                padding: attr_padding,
9013                                child_gap: 8,
9014                                layout_direction: LayoutDirection::TopToBottom,
9015                                ..Default::default()
9016                            },
9017                            ..Default::default()
9018                        });
9019                        {
9020                            self.debug_text("Offset", info_title_config);
9021                            self.debug_open(&ElementDeclaration::default());
9022                            {
9023                                self.debug_text("{ x: ", info_text_config);
9024                                self.debug_int_text(float_config.offset.x, info_text_config);
9025                                self.debug_text(", y: ", info_text_config);
9026                                self.debug_int_text(float_config.offset.y, info_text_config);
9027                                self.debug_text(" }", info_text_config);
9028                            }
9029                            self.close_element();
9030
9031                            self.debug_text("z-index", info_title_config);
9032                            self.debug_int_text(float_config.z_index as f32, info_text_config);
9033
9034                            self.debug_text("Parent", info_title_config);
9035                            let parent_name = self.layout_element_map
9036                                .get(&float_config.parent_id)
9037                                .map(|item| item.element_id.string_id.clone())
9038                                .unwrap_or(StringId::empty());
9039                            if !parent_name.is_empty() {
9040                                self.debug_raw_text(parent_name.as_str(), info_text_config);
9041                            }
9042
9043                            self.debug_text("Attach Points", info_title_config);
9044                            self.debug_open(&ElementDeclaration::default());
9045                            {
9046                                self.debug_text("{ element: (", info_text_config);
9047                                self.debug_text(Self::align_x_name(float_config.attach_points.element_x), info_text_config);
9048                                self.debug_text(", ", info_text_config);
9049                                self.debug_text(Self::align_y_name(float_config.attach_points.element_y), info_text_config);
9050                                self.debug_text("), parent: (", info_text_config);
9051                                self.debug_text(Self::align_x_name(float_config.attach_points.parent_x), info_text_config);
9052                                self.debug_text(", ", info_text_config);
9053                                self.debug_text(Self::align_y_name(float_config.attach_points.parent_y), info_text_config);
9054                                self.debug_text(") }", info_text_config);
9055                            }
9056                            self.close_element();
9057
9058                            self.debug_text("Pointer Capture Mode", info_title_config);
9059                            let pcm = if float_config.pointer_capture_mode == PointerCaptureMode::Passthrough {
9060                                "PASSTHROUGH"
9061                            } else {
9062                                "NONE"
9063                            };
9064                            self.debug_text(pcm, info_text_config);
9065
9066                            self.debug_text("Attach To", info_title_config);
9067                            let at = match float_config.attach_to {
9068                                FloatingAttachToElement::Parent => "PARENT",
9069                                FloatingAttachToElement::ElementWithId => "ELEMENT_WITH_ID",
9070                                FloatingAttachToElement::Root => "ROOT",
9071                                _ => "NONE",
9072                            };
9073                            self.debug_text(at, info_text_config);
9074
9075                            self.debug_text("Clip To", info_title_config);
9076                            let ct = if float_config.clip_to == FloatingClipToElement::None {
9077                                "NONE"
9078                            } else {
9079                                "ATTACHED_PARENT"
9080                            };
9081                            self.debug_text(ct, info_text_config);
9082                        }
9083                        self.close_element();
9084                    }
9085                    ElementConfigType::Border => {
9086                        self.render_debug_view_element_config_header(elem_id_string.clone(), ec.config_type, info_title_config);
9087                        let border_config = self.border_element_configs[ec.config_index];
9088                        self.debug_open_id("Ply__DebugViewElementInfoBorderBody", &ElementDeclaration {
9089                            layout: LayoutConfig {
9090                                padding: attr_padding,
9091                                child_gap: 8,
9092                                layout_direction: LayoutDirection::TopToBottom,
9093                                ..Default::default()
9094                            },
9095                            ..Default::default()
9096                        });
9097                        {
9098                            self.debug_text("Border Widths", info_title_config);
9099                            self.debug_open(&ElementDeclaration::default());
9100                            {
9101                                self.debug_text("left: ", info_text_config);
9102                                self.debug_int_text(border_config.width.left as f32, info_text_config);
9103                            }
9104                            self.close_element();
9105                            self.debug_open(&ElementDeclaration::default());
9106                            {
9107                                self.debug_text("right: ", info_text_config);
9108                                self.debug_int_text(border_config.width.right as f32, info_text_config);
9109                            }
9110                            self.close_element();
9111                            self.debug_open(&ElementDeclaration::default());
9112                            {
9113                                self.debug_text("top: ", info_text_config);
9114                                self.debug_int_text(border_config.width.top as f32, info_text_config);
9115                            }
9116                            self.close_element();
9117                            self.debug_open(&ElementDeclaration::default());
9118                            {
9119                                self.debug_text("bottom: ", info_text_config);
9120                                self.debug_int_text(border_config.width.bottom as f32, info_text_config);
9121                            }
9122                            self.close_element();
9123                            self.debug_open(&ElementDeclaration::default());
9124                            {
9125                                self.debug_text("betweenChildren: ", info_text_config);
9126                                self.debug_int_text(
9127                                    border_config.width.between_children as f32,
9128                                    info_text_config,
9129                                );
9130                            }
9131                            self.close_element();
9132
9133                            self.debug_text("Border Position", info_title_config);
9134                            self.debug_text(
9135                                Self::debug_border_position_name(border_config.position),
9136                                info_text_config,
9137                            );
9138
9139                            self.debug_text("Border Color", info_title_config);
9140                            self.render_debug_view_color(border_config.color, info_text_config);
9141                        }
9142                        self.close_element();
9143                    }
9144                    ElementConfigType::TextInput => {
9145                        // ── [Input] section for text input config ──
9146                        let input_label_color = Color::rgba(52.0, 152.0, 219.0, 255.0);
9147                        self.render_debug_view_category_header("Input", input_label_color, elem_id_string.clone());
9148                        let ti_cfg = self.text_input_configs[ec.config_index].clone();
9149                        self.debug_open(&ElementDeclaration {
9150                            layout: LayoutConfig {
9151                                padding: attr_padding,
9152                                child_gap: 8,
9153                                layout_direction: LayoutDirection::TopToBottom,
9154                                ..Default::default()
9155                            },
9156                            ..Default::default()
9157                        });
9158                        {
9159                            if !ti_cfg.placeholder.is_empty() {
9160                                self.debug_text("Placeholder", info_title_config);
9161                                self.debug_raw_text(&ti_cfg.placeholder, info_text_config);
9162                            }
9163                            self.debug_text("Placeholder Color", info_title_config);
9164                            self.render_debug_view_color(ti_cfg.placeholder_color, info_text_config);
9165                            self.debug_text("Max Length", info_title_config);
9166                            if let Some(max_len) = ti_cfg.max_length {
9167                                self.debug_int_text(max_len as f32, info_text_config);
9168                            } else {
9169                                self.debug_text("unlimited", info_text_config);
9170                            }
9171                            self.debug_text("Password", info_title_config);
9172                            self.debug_text(if ti_cfg.is_password { "true" } else { "false" }, info_text_config);
9173                            self.debug_text("Multiline", info_title_config);
9174                            self.debug_text(if ti_cfg.is_multiline { "true" } else { "false" }, info_text_config);
9175                            self.debug_text("Drag Select", info_title_config);
9176                            self.debug_text(if ti_cfg.drag_select { "true" } else { "false" }, info_text_config);
9177                            self.debug_text("Line Height", info_title_config);
9178                            if ti_cfg.line_height == 0 {
9179                                self.debug_text("auto", info_text_config);
9180                            } else {
9181                                self.debug_int_text(ti_cfg.line_height as f32, info_text_config);
9182                            }
9183                            self.debug_text("No Styles Movement", info_title_config);
9184                            self.debug_text(
9185                                if ti_cfg.no_styles_movement {
9186                                    "true"
9187                                } else {
9188                                    "false"
9189                                },
9190                                info_text_config,
9191                            );
9192                            self.debug_text("Font", info_title_config);
9193                            self.debug_open(&ElementDeclaration::default());
9194                            {
9195                                let label = if let Some(asset) = ti_cfg.font_asset {
9196                                    asset.key().to_string()
9197                                } else {
9198                                    format!("default ({})", self.default_font_key)
9199                                };
9200                                self.open_text_element(&label, info_text_config);
9201                                self.debug_text(", size: ", info_text_config);
9202                                self.debug_int_text(ti_cfg.font_size as f32, info_text_config);
9203                            }
9204                            self.close_element();
9205                            self.debug_text("Text Color", info_title_config);
9206                            self.render_debug_view_color(ti_cfg.text_color, info_text_config);
9207                            self.debug_text("Cursor Color", info_title_config);
9208                            self.render_debug_view_color(ti_cfg.cursor_color, info_text_config);
9209                            self.debug_text("Selection Color", info_title_config);
9210                            self.render_debug_view_color(ti_cfg.selection_color, info_text_config);
9211                            self.debug_text("Scrollbar", info_title_config);
9212                            if let Some(scrollbar) = ti_cfg.scrollbar {
9213                                self.debug_text("configured", info_text_config);
9214                                self.render_debug_scrollbar_config(
9215                                    scrollbar,
9216                                    info_text_config,
9217                                    info_title_config,
9218                                );
9219                            } else {
9220                                self.debug_text("none", info_text_config);
9221                            }
9222                            // Show current text value
9223                            let state_data = self.text_edit_states.get(&selected_id)
9224                                .map(|s| (s.text.clone(), s.cursor_pos));
9225                            if let Some((text_val, cursor_pos)) = state_data {
9226                                self.debug_text("Value", info_title_config);
9227                                let preview = if text_val.len() > 40 {
9228                                    let mut end = 40;
9229                                    while !text_val.is_char_boundary(end) { end -= 1; }
9230                                    format!("\"{}...\"", &text_val[..end])
9231                                } else {
9232                                    format!("\"{}\"", &text_val)
9233                                };
9234                                self.debug_raw_text(&preview, info_text_config);
9235                                self.debug_text("Cursor Position", info_title_config);
9236                                self.debug_int_text(cursor_pos as f32, info_text_config);
9237                            }
9238                        }
9239                        self.close_element();
9240                    }
9241                    _ => {}
9242                }
9243            }
9244
9245            // ── [Effects] section (Visual Rotation + Shaders + Effects) ──
9246            let has_visual_rot = visual_rot.is_some();
9247            let has_effects = !effects.is_empty();
9248            let has_shaders = !shaders.is_empty();
9249            if has_visual_rot || has_effects || has_shaders {
9250                let effects_label_color = Color::rgba(155.0, 89.0, 182.0, 255.0);
9251                self.render_debug_view_category_header("Effects", effects_label_color, elem_id_string.clone());
9252                self.debug_open(&ElementDeclaration {
9253                    layout: LayoutConfig {
9254                        padding: attr_padding,
9255                        child_gap: 8,
9256                        layout_direction: LayoutDirection::TopToBottom,
9257                        ..Default::default()
9258                    },
9259                    ..Default::default()
9260                });
9261                {
9262                    if let Some(vr) = visual_rot {
9263                        self.debug_text("Visual Rotation", info_title_config);
9264                        self.debug_open(&ElementDeclaration::default());
9265                        {
9266                            self.debug_text("angle: ", info_text_config);
9267                            self.debug_float_text(vr.rotation_radians, info_text_config);
9268                            self.debug_text(" rad", info_text_config);
9269                        }
9270                        self.close_element();
9271                        self.debug_open(&ElementDeclaration::default());
9272                        {
9273                            self.debug_text("pivot: (", info_text_config);
9274                            self.debug_float_text(vr.pivot_x, info_text_config);
9275                            self.debug_text(", ", info_text_config);
9276                            self.debug_float_text(vr.pivot_y, info_text_config);
9277                            self.debug_text(")", info_text_config);
9278                        }
9279                        self.close_element();
9280                        self.debug_open(&ElementDeclaration::default());
9281                        {
9282                            self.debug_text("flip_x: ", info_text_config);
9283                            self.debug_text(if vr.flip_x { "true" } else { "false" }, info_text_config);
9284                            self.debug_text(", flip_y: ", info_text_config);
9285                            self.debug_text(if vr.flip_y { "true" } else { "false" }, info_text_config);
9286                        }
9287                        self.close_element();
9288                    }
9289                    for (i, effect) in effects.iter().enumerate() {
9290                        let label = format!("Effect {}", i + 1);
9291                        self.debug_text("Effect", info_title_config);
9292                        self.debug_open(&ElementDeclaration::default());
9293                        {
9294                            self.debug_raw_text(&label, info_text_config);
9295                            self.debug_text(": ", info_text_config);
9296                            self.debug_raw_text(&effect.name, info_text_config);
9297                        }
9298                        self.close_element();
9299                        for uniform in &effect.uniforms {
9300                            self.debug_open(&ElementDeclaration::default());
9301                            {
9302                                self.debug_text("  ", info_text_config);
9303                                self.debug_raw_text(&uniform.name, info_text_config);
9304                                self.debug_text(": ", info_text_config);
9305                                self.render_debug_shader_uniform_value(&uniform.value, info_text_config);
9306                            }
9307                            self.close_element();
9308                        }
9309                    }
9310                    for (i, shader) in shaders.iter().enumerate() {
9311                        let label = format!("Shader {}", i + 1);
9312                        self.debug_text("Shader", info_title_config);
9313                        self.debug_open(&ElementDeclaration::default());
9314                        {
9315                            self.debug_raw_text(&label, info_text_config);
9316                            self.debug_text(": ", info_text_config);
9317                            self.debug_raw_text(&shader.name, info_text_config);
9318                        }
9319                        self.close_element();
9320                        for uniform in &shader.uniforms {
9321                            self.debug_open(&ElementDeclaration::default());
9322                            {
9323                                self.debug_text("  ", info_text_config);
9324                                self.debug_raw_text(&uniform.name, info_text_config);
9325                                self.debug_text(": ", info_text_config);
9326                                self.render_debug_shader_uniform_value(&uniform.value, info_text_config);
9327                            }
9328                            self.close_element();
9329                        }
9330                    }
9331                }
9332                self.close_element();
9333            }
9334        }
9335        self.close_element(); // detail panel
9336    }
9337
9338    fn align_x_name(value: AlignX) -> &'static str {
9339        match value {
9340            AlignX::Left => "LEFT",
9341            AlignX::CenterX => "CENTER",
9342            AlignX::Right => "RIGHT",
9343        }
9344    }
9345
9346    fn align_y_name(value: AlignY) -> &'static str {
9347        match value {
9348            AlignY::Top => "TOP",
9349            AlignY::CenterY => "CENTER",
9350            AlignY::Bottom => "BOTTOM",
9351        }
9352    }
9353
9354    pub fn set_max_element_count(&mut self, count: i32) {
9355        self.max_element_count = count;
9356    }
9357
9358    pub fn set_max_measure_text_cache_word_count(&mut self, count: i32) {
9359        self.max_measure_text_cache_word_count = count;
9360    }
9361
9362    pub fn set_debug_mode_enabled(&mut self, enabled: bool) {
9363        self.debug_mode_enabled = enabled;
9364    }
9365
9366    pub fn set_debug_view_width(&mut self, width: f32) {
9367        self.debug_view_width = width.max(0.0);
9368    }
9369
9370    pub fn is_debug_mode_enabled(&self) -> bool {
9371        self.debug_mode_enabled
9372    }
9373
9374    pub fn set_culling_enabled(&mut self, enabled: bool) {
9375        self.culling_disabled = !enabled;
9376    }
9377
9378    pub fn set_measure_text_function(
9379        &mut self,
9380        f: Box<dyn Fn(&str, &TextConfig) -> Dimensions>,
9381    ) {
9382        self.measure_text_fn = Some(f);
9383        // Invalidate the font height cache since the measurement function changed.
9384        self.font_height_cache.clear();
9385    }
9386}