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